Java NIO+AES从客户端到服务器的加密-ByteBuffer问题



我是加密和NIO方面的新手,我有以下客户端代码:

String key1 = "1234567812345678";
byte[] key2 = key1.getBytes();
SecretKeySpec secret = new SecretKeySpec(key2, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secret);
byte[] encrypted = cipher.doFinal(msg.getBytes());
System.out.println("Encrypted info: " + encrypted);
String send = encrypted.toString();
bytebuf = ByteBuffer.allocate(48);
bytebuf.clear();
bytebuf.put(send.getBytes());
bytebuf.flip();
while(bytebuf.hasRemaining()) {
nBytes += client.write(bytebuf);
}

以及服务器的以下代码:

// Server receives data and decrypts
SocketChannel socket = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
nBytes = socket.read(buf);
String data = new String(buf.array()).trim();
String key1 = "1234567812345678";
byte[] key2 = key1.getBytes();
SecretKeySpec secret = new SecretKeySpec(key2, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, secret);
byte[] decrypted = cipher.doFinal(data.getBytes());
System.out.println("Decrypted Info: " + new String(decrypted));

当消息从客户端发送到服务器时,例如"HELLO"被加密为[B@34d74aa5,在服务器端,我得到*数据包,发现为[B@34d 74aa5

直到这里,一切看起来都很好,但我得到了以下例外:

javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher

我怀疑我对数据从服务器端缓冲区出来的方式有问题?对此有什么想法吗?

更新:

**根据Erickson的回答,这是的最终解决方案

javax.crypto.BadPaddingException: Given final block not properly padded

客户代码:

String key1 = "1234567812345678";
byte[] key2 = key1.getBytes();
byte[] iv = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
IvParameterSpec ivspec = new IvParameterSpec(iv);
SecretKeySpec secret = new SecretKeySpec(key2, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secret, ivspec);
byte[] encrypted = cipher.doFinal(msg.getBytes(StandardCharsets.UTF_8));
String text = DatatypeConverter.printBase64Binary(encrypted);
System.out.println("Encrypted info: " + text);
bytebuf = ByteBuffer.allocate(32);
bytebuf.clear();
bytebuf.put(text.getBytes());
bytebuf.flip();
while(bytebuf.hasRemaining()) {
nBytes += client.write(bytebuf);
}

服务器代码:

LOGGER.info("Confirming write");
String data = new String(buf.array());
LOGGER.info("Data packet found as {}", data);
/*******************************************************/
byte[] iv = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
IvParameterSpec ivspec = new IvParameterSpec(iv);
String key1 = "1234567812345678";
byte[] key2 = key1.getBytes();
SecretKeySpec secret = new SecretKeySpec(key2, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secret, ivspec);
byte[] encrypted = DatatypeConverter.parseBase64Binary(data);
byte[] decrypted = cipher.doFinal(encrypted);
System.out.println("Decrypted Info: " + new String(decrypted, StandardCharsets.UTF_8));

您的密文encryptedbyte[],在数组上调用toString()不会呈现数组内容,它会返回类型([B)和哈希码(@34d74aa5)信息,如Object.toString()所述。

你也不能只使用new String(encrypted)。当字节数组被解码为文本时,解码器将用替换字符uFFFD(�;)替换任何无效的字节序列。因此,信息丢失,随后的解密将失败。

使用类似base-64的编码将字节序列转换为可打印字符。不要为此将你的代码与第三方库进行垃圾处理;您可以使用javax.xml.bind.DatatypeConverter

/* Client: */
byte[] encrypted = cipher.doFinal(msg.getBytes(StandardCharsets.UTF_8));
String text = DatatypeConverter.printBase64Binary(encrypted);
…
/* Server: */
byte[] encrypted = DatatypeConverter.parseBase64Binary(data);
byte[] decrypted = Cipher.doFinal(encrypted);
System.out.println(new String(decrypted, StandardCharsets.UTF_8);

您还应该明确选择您的模式和填充(如"AES/CCBC/PKCS5Padding"),因为无法保证收件人将使用相同的提供程序,也无法保证同一个提供程序将随着时间的推移使用相同的默认值。指定字符编码也是如此,比如UTF-8。

AES方案是一种"分组密码",适用于固定大小的数据块。您正在创建一个"原始"密码实例,它将要求您确保传递给密码的每个字节数组都与密码的"本地"块长度对齐。这通常不是你想做的。

在使用"原始"密码时,你会遇到另一个问题,尽管它不会导致实际错误,但如果你在不同的场合向它传递相同的数据块,每次都会对该数据块进行相同的加密,从而为攻击者提供有关数据结构的线索。同样,在实际应用中,这通常不是您想要做的。

因此,通常需要指定两个额外的东西:填充方案,它确定当数据段与块大小不完全对齐时会发生什么,以及块模式。它确定密码将使用什么方案来避免将相同的输入块加密为相同的输出块。块模式通常需要使用名为初始化向量的"启动状态"进行初始化(您可以使用"全零"的默认状态,但这不太安全)。

所以你需要做两件事:

  • 您需要使用填充方案和块初始化密码模式,例如"AES/CCBC/PKCS5PADDING">

  • 为了增加安全性,您通常还会设置(并传输在数据之前)随机初始化矢量。请参阅此示例了解更多信息信息

您正在将密文byte[]转换为String,此处为:

byte[] encrypted = cipher.doFinal(msg.getBytes());
String send = encrypted.toString();

这是不正确的。您也不能执行new String(byte[]),因为byte[]是随机的,而不是new String(byte[])假定的平台默认编码中的字符数据流。您应该使用十六进制或base64编码(我建议使用Apache Commons编解码器)将byte[]数据转换为String,例如

hexEncodedCipherText = new String(Hex.encodeHex(binaryCipherText))

在服务器端,使用相反的操作将十六进制或base64编码的数据在解密前转换回byte[],例如

binaryCipherText = Hex.decodeHex(hexEncodedCipherText.toCharArray());

更新:

由于初始化向量的使用不正确,更新后的问题在解密过程中不起作用。在加密过程中,您没有指定IV,这意味着Java将生成一个随机的IV。您需要在加密后通过调用cipher.getIV()从密码中获得这个随机IV(或者明确指定它,尽管生成随机IV更安全)。然后,在解密期间,使用在加密期间创建的IV创建IvParameterSpec。此外,您需要以与密文相同的方式对IV进行编码/解码,因为它也是二进制数据。

更新2:

我看到你已经用IV更新了你的问题,但你使用的是空IV。通常,只有当你发送的每条消息都有一个唯一的密钥时,这才是"安全的"。如果您的密钥是固定的或重复使用了很长一段时间,您应该为每次加密/解密生成一个唯一的IV。否则,您将面临基于用同一密钥和IV加密的多个密文的密码分析。

最新更新