本文主旨
Rc4
是一种对称加密方式,且属于流加密,本文讲解如何使用Java
做Rc4
的加密解密操作,并且讲解Shadowsocks
是如何使用Rc4
加密和Rc4md5
加密的。
Rc4
加密解密例子
Rc4
需要一个密钥来加密。
下面这段代码演示了,如何使用Java
自带的Cipher
做Rc4
加密解密。密钥使用的是随机生成的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 测试java自带的rc4加密解密
*/
@Test
public void testRc4Origin() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
//随机生成20位密码
String password = randomString(20);
//需要加密的字符串
String content = randomString(50);
//用于加密的
Cipher encoder = Cipher.getInstance("RC4");
//初始化成加密模式
encoder.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(password.getBytes(), "RC4"));
//用于解密的
Cipher decoder = Cipher.getInstance("RC4");
//初始化成解密模式
decoder.init(Cipher.DECRYPT_MODE, new SecretKeySpec(password.getBytes(), "RC4"));
//密文
byte[] update = encoder.update(content.getBytes());
//解密后
byte[] update1 = decoder.update(update);
//加密在解密后和原文一致
Assert.assertEquals(content, new String(update1, StandardCharsets.UTF_8));
//因为是流加密,只要不关闭,encoder decoder对象可以一直使用
String content2 = randomString(50);
//密文
byte[] en = encoder.update(content2.getBytes());
//解密后
byte[] de = decoder.update(en);
//加密在解密后和原文一致
Assert.assertEquals(content2, new String(de, StandardCharsets.UTF_8));
}
private static String randomString(int length) {
StringBuilder sb = new StringBuilder(length);
Random random = new Random();
for (int i = 0; i < length; i++) {
int rand = random.nextInt('z' - 'a' + 1) + 'a';
sb.append((char) rand);
}
return sb.toString();
}
Shadowsocks
使用的Rc4
上面示例可以看出,Rc4
是流式加密,且初始化Cipher
时只需要传递密钥即可。Shadowsocks
里面的Rc4
算法和上面是一样的,使用上面的代码就能解析Shadowsocks
发出的Rc4
算法加密的数据。
但在新版本里面已经移除了Rc4
算法,因为用户经常为了方便使用弱密码,导致Rc4
强度不足。
Shadowsocks
改进Rc4
为Rc4Md5
算法
之所以抛弃Rc4
并不是Rc4
本身加密强度不够,而是因为用户使用弱密码,所以只要增加密码位数就能得到安全的加密。
加强密钥
既然用户会提供弱密码,那Shadowsocks
就想办法将密钥加强,它的做法是:
1
realPassWord = md5(password)
因为md5
算法得到的128bit
摘要数据,用作密钥要比用户自己输入的要强一些。
避免特征
上面解决了弱密码的问题,但又有另一个问题出现了,因为密码是固定的,得到的md5
也是固定的,也就是说每次连接使用的都是相同的密钥,这样时间长了可能被发现特征。于是有了下面的办法让每个连接使用的密码都不一样。
解决办法是每个连接在数据流前先发送16B
的随机值称为IV值
,与上面得到的密钥进行混合,这样每个连接流使用的密钥就不同了。
1
2
3
4
5
6
7
8
9
10
public static byte[] md5IvKey(byte[] originPassword, byte[] iv) throws NoSuchAlgorithmException {
MessageDigest md5 = MessageDigest.getInstance("md5");
//对原始密钥取md5
byte[] md5Password = md5.digest(originPassword);
md5.reset();
//重新加密密文 和 iv值得到真实密钥
md5.update(md5Password);
md5.update(iv);
return md5.digest();
}
上面代码抽象就是:
1
realPassWord = md5(md5(password),iv);
由上是Shadowsocks
内部的Rc4md5
的工作原理。他是使用的Rc4
做加密解密手段,并通过md5
算法混合用户输入的密码+IV,每次使用的密钥都不相同,降低流量特征。
在我们程序中借鉴此方案
在我写的内网穿透软件中rmp,客户端和服务端通信需要实现加密传输,并且服务端和客户端配置相同的Token
来验证。就可以参考上面的方式来加密数据。
写一个Rc4Md5Handler
继承ByteToMessageCodec
来将输出数据加密,输入数据解密。`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package com.fys.cmd.handler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.List;
import static com.fys.cmd.util.CodeUtil.md5;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* hcy 2020/4/3
*/
public class Rc4Md5Handler extends ByteToMessageCodec<ByteBuf> {
private final static SecureRandom random = new SecureRandom();
private boolean firstDecode = true;
private boolean firstEncode = true;
private Cipher decoderCipher;
private Cipher encoderCipher;
private String password;
public Rc4Md5Handler(String password) {
this.password = password;
}
/*
* 第一次写数据时,生成随机IV 构建cipher
* 使用cipher加密数据
* */
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
if (firstEncode) {
byte[] iv = randomIv();
encoderCipher = Cipher.getInstance("RC4");
encoderCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(md5(md5(password.getBytes(UTF_8)), iv), "RC4"));
firstEncode = false;
out.writeBytes(iv);
}
out.writeBytes(encoderCipher.update(readByte(msg, msg.readableBytes())));
}
/*
* 初次读时,前16位作为iv使用,构建cipher,以后再使用则不需要构建
* */
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (firstDecode) {
byte[] iv = readByte(in, 16);
decoderCipher = Cipher.getInstance("RC4");
byte[] realPassWord = md5(md5(password.getBytes(UTF_8)), iv);
decoderCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(realPassWord, "RC4"));
firstDecode = false;
}
out.add(Unpooled.wrappedBuffer(decoderCipher.update(readByte(in, in.readableBytes()))));
}
private byte[] readByte(ByteBuf in, int length) {
byte[] bytes = ByteBufUtil.getBytes(in, in.readerIndex(), length);
in.skipBytes(bytes.length);
return bytes;
}
private byte[] randomIv() {
return random.generateSeed(16);
}
}
将删改你的Handler
添加到server
和 client
的handlerChain
第一位。
上面这个Handler
完全符合Shadowsocks
的Rc4Md5
加密方式,即使用户使用弱密码也能得到满足条件的加密强度。
优缺点
无法识别数据流是否正确,即错误的数据流也能解密出错误的数据,但不能在第一时间发现数据是错误的。
这个问题会在Shadowsocks
后缀为GCM
的算法中解决。
但是这个Rc4
算法简单,在很多设备上都能拥有较快的处理速度。
更新2020-04-13
上面写的handler
有一点没考虑清除。真实使用场景下,接收到的io数据一般是存储在直接内存里的,上面读取到数组再解密的方式需要从直接内存拷贝到堆内存,这样会影响性能的。
更多情况下应该使用下面这个方法方法:
1
public final int update(ByteBuffer var1, ByteBuffer var2) throws ShortBufferException {
直接操作Bytebuf
,提高性能减少GC。