java rc4加密

  • TOC
    {:toc}

本文主旨

Rc4是一种对称加密方式,且属于流加密,本文讲解如何使用JavaRc4的加密解密操作,并且讲解Shadowsocks是如何使用Rc4加密和Rc4md5加密的。

Rc4加密解密例子

Rc4需要一个密钥来加密。

下面这段代码演示了,如何使用Java自带的CipherRc4加密解密。密钥使用的是随机生成的

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改进Rc4Rc4Md5算法

​ 之所以抛弃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添加到serverclienthandlerChain第一位。

​ 上面这个Handler完全符合ShadowsocksRc4Md5加密方式,即使用户使用弱密码也能得到满足条件的加密强度。

优缺点

​ 无法识别数据流是否正确,即错误的数据流也能解密出错误的数据,但不能在第一时间发现数据是错误的。

这个问题会在Shadowsocks后缀为GCM的算法中解决。

​ 但是这个Rc4算法简单,在很多设备上都能拥有较快的处理速度。

更新2020-04-13

​ 上面写的handler有一点没考虑清除。真实使用场景下,接收到的io数据一般是存储在直接内存里的,上面读取到数组再解密的方式需要从直接内存拷贝到堆内存,这样会影响性能的。

​ 更多情况下应该使用下面这个方法方法:

1
public final int update(ByteBuffer var1, ByteBuffer var2) throws ShortBufferException {

​ 直接操作Bytebuf,提高性能减少GC。


java rc4加密
https://www.huangchaoyu.com/3883934631.html
作者
hcy
发布于
2020年4月3日
更新于
2024年8月17日
许可协议