我看了slowAes,我认为你是对的。它坏了。
代码打算在 CBC 模式下运行时应用 PKCS#7 填充,但没有成功。 (PKCS#7 只是将 PKCS#5 扩展为 16 字节块加密算法。我认为 java 使用 术语 PKCS#5 和 AES 是一个错误 - 他们应该称之为 PKCS#7,因为他们正在做 16 字节的填充)。
我修改了 slowAes 以正确地进行填充,并且通过该修改,我在 slowAes 和您的 Java 代码之间获得了良好的互操作性。但是您需要修改您的 Java 代码。稍后再谈。
这是我使用的 java 代码:(仅用于演示;不适合在实际应用中使用)
private static MessageDigest md;
static {
try {
md = MessageDigest.getInstance("MD5");
}
catch(Exception e) {
md = null;
}
}
private static byte[] md5(String source) {
byte[] bytes = source.getBytes();
byte[] digest = md.digest(bytes);
return digest;
}
public static String encrypt(String input, String key){
byte[] ivbytes = "sixteenbyteslong".getBytes(); // <- NO NO NO NO !!!!
IvParameterSpec ips = new IvParameterSpec(ivbytes);
System.out.println("plaintext: " + input);
byte[] keybytes = md5(key); // <- NO NO NO NO !!!!
System.out.println("key : " + Hex.encodeHexString(keybytes));
System.out.println("iv : " + Hex.encodeHexString(ivbytes));
byte[] crypted = null;
try{
SecretKeySpec skey = new SecretKeySpec(keybytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, skey, ips);
byte[] ptext = input.getBytes();
crypted = cipher.doFinal(ptext);
}catch(Exception e){
System.out.println(e.toString());
}
return new String(Hex.encodeHexString(crypted));
}
public static String decrypt(String input, String key){
IvParameterSpec ips = new IvParameterSpec("sixteenbyteslong".getBytes()); // <- NO !!!
byte[] keybytes = md5(key); // <- BAD BAD BAD!!!
System.out.println("key : " + Hex.encodeHexString(keybytes));
byte[] output = null;
try{
SecretKeySpec skey = new SecretKeySpec(keybytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skey, ips);
output = cipher.doFinal(Hex.decodeHex(input.toCharArray()));
}catch(Exception e){
System.out.println(e.toString());
}
return new String(output);
}
public void Run() {
String plaintext = CommandLineArgs.get("pt");
String keystring = CommandLineArgs.get("k");
if (plaintext == null || keystring == null) {
Usage();
return;
}
System.out.println("encrypting...");
String crypto = encrypt(plaintext, keystring);
System.out.println("crypto : " + crypto);
System.out.println("decrypting...");
String decrypted = decrypt(crypto, keystring);
System.out.println("decrypted: " + decrypted);
}
(上面代码中的 Hex 类是 org.apache.commons.codec.binary.Hex;它与您使用的 Base64 编码器在同一个 jar 中。我换掉了您的 Base64,因为我想实际查看字节。 )
这是输出:
encrypting...
plaintext: AbbaDabbaDo_Once_upon_a_time....
key : 2e0160e078aa4b925e62b20610378253
iv : 7369787465656e62797465736c6f6e67
crypto : f353e4dd6fb11ea13254dfef670ad88f8fbebcd24217374c06daefbbfe152df504035ae2d82537392c9ab1f719993ec1
decrypting...
key : 2e0160e078aa4b925e62b20610378253
decrypted: AbbaDabbaDo_Once_upon_a_time....
JS 模块的输出:
key : 2e0160e078aa4b925e62b20610378253
iv : 7369787465656e62797465736c6f6e67
plaintext : AbbaDabbaDo_Once_upon_a_time....
ciphertext: f353e4dd6fb11ea13254dfef670ad88f8fbebcd24217374c06daefbbfe152df504035ae2d82537392c9ab1f719993ec1
decrypted : AbbaDabbaDo_Once_upon_a_time....
还有JS代码:
var keystring = "keystring",
md5String = MD5.getDigest(keystring),
keybytes = cryptoHelpers.toNumbers(md5String), // <- NO NO NO!
iv = "sixteenbyteslong".getBytes(), // <- NO NO NO
keysize, key = cryptoHelpers.toHex(keybytes),
plaintext, bytesToEncrypt, mode, result,
decrypted, recoveredText;
say("key : " + key);
keysize = slowAES.aes.keySize.SIZE_128;
say("iv : " + cryptoHelpers.toHex(iv));
plaintext = "AbbaDabbaDo_Once_upon_a_time....";
bytesToEncrypt = cryptoHelpers.convertStringToByteArray(plaintext);
mode = slowAES.modeOfOperation.CBC;
result = slowAES.encrypt(bytesToEncrypt,
mode,
keybytes,
keysize,
iv);
say( "plaintext : " + plaintext);
say( "ciphertext: " + cryptoHelpers.toHex(result.cipher));
decrypted = slowAES.decrypt(result.cipher,
result.mode,
keybytes,
keysize,
iv) ;
recoveredText = cryptoHelpers.convertByteArrayToString(decrypted);
say( "decrypted : " + recoveredText);
我对 slowAes 所做的修改是在 encrypt() 函数中。我添加了一个名为 padLength 的新变量,
if (mode == this.modeOfOperation.CBC) {
padLength = 16 - (bytesIn.length % 16);
}
// the AES input/output
if (bytesIn !== null)
{
for (var j = 0;j < Math.ceil((bytesIn.length + padLength)/16); j++)
{
....
您可以获取我使用的修改后的 AES 源 here,以及 my test program。
重要提示:您不应该使用密码的 MD5 来获取密钥字节。使用 PBKDF2。那里有一个 PBKDF2 的 java 版本,它可以工作,还有a Javascript PBKDF2 that works。此外,一些 J2EE 服务器包括一个 PBKDF2 类。对于 IV 字节也是如此。这些也应该来自密码短语。如果您对此表示怀疑,请read IETF RFC 2898 了解原因。
不要将我上面发布的代码用于真正的应用程序。修改它以使用 PBKDF2。
编辑
关于填充...
在解密返回的消息时,我将如何删除额外的填充,因为我不一定知道未加密的长度。我认为填充字节应该等于填充长度,但似乎并非如此。
AES 是一种块加密器;它加密正好 16 个字节长的块。如果你输入 32 字节的明文,你会得到正好 32 字节的密文作为回报。如果你输入 1024 个字节,你会得到 1024 个字节。 (不完全正确,稍后您会明白为什么。现在假设这是正确的。)
如您所见,当明文不是 16 字节的偶数倍时,由于 AES 需要 16 字节块,问题就出现了 - 我应该输入什么作为“额外”制作一个完整的 16 字节块的东西? 答案是填充。
有不同的是要垫。在 CBC 模式下,典型的方式是 PKCS#7(Java 将其称为 PKCS#5,正如我所说,我认为这是用词不当)。如果您发送 25 个字节的明文,填充意味着 AES 实际上将加密 32 个字节:25 个字节的实际数据和 7 个字节的填充。好的,但是 7 个字节的填充有什么值呢?
PKCS#7 表示填充字节是值16-len,其中len 是最终块中实际数据字节的长度。换句话说,该值与填充字节数相同,也就是您所说的。在上面的例子中,如果你加密 25 个字节,你需要 7 个填充字节,每个字节取值 7。这些填充字节在加密之前被添加到明文的末尾。 结果在一个由整数个 16 字节块组成的加密流中。
这很好,因为在解密时,解密器可以简单地查看解密流中的最后一个字节,它现在知道要从解密流中删除多少填充字节。使用 PKCS#7 填充,应用层无需担心在解密时删除填充或在加密时添加填充。 AES 库应该处理所有这些。假设解密器解密 32 个字节的密文,得到的明文的最后一个字节的值为 7。使用 PKCS#7 填充,解密器知道从最后一个块的末尾切掉 7 个字节,并传递部分块最后一个字节为 9 个字节,发送给应用程序,总共 25 个字节的明文。
Java 正确地做到了这一点。除了明文长度是 16 字节的倍数的情况外,slowAES 做得正确。 PKCS#7 说在这种情况下,您需要添加 16 个字节的填充,所有值都为 16。如果您想加密 32 个字节,PKCS#7 for AES 说,您需要添加 16 个字节的填充,对于共加密 48 个字节。这样解密器就可以做正确的事情了。想一想:如果不添加 16 字节的填充,解密器无法“告诉”明文的最后一个字节不是填充字节。
在这种情况下,SlowAES 没有填充,这是您无法使其与 Java 互操作的部分原因。我注意到了这一点,因为对于 32 字节的明文,加密流正好是 32 字节,这意味着没有填充。当我查看代码时,逻辑错误就在那里。 (提醒我:我需要验证在解密端的填充方面,slowaes 中没有逻辑错误)
因此,您的应用不知道未加密的长度是正确的。但是解密器确实知道要切掉多少字节,如果使用了 PKCS#7 填充,并且正确的解密器将始终向您返回正确的明文字节数。为了使其正常工作,解密时必须使用与加密时相同的填充约定。您通常需要告诉加密库要使用什么填充,尽管有些(如 slowAES)不给您选择。
如果您在某些库中使用“无填充”选项,但在 CBC 模式下在 slowAES 中没有,那么,是的,您的应用程序必须以某种方式“知道”未加密数据的大小,以便它可以丢弃最后一个N字节的明文。这在某些数据格式和协议中是可以的。但通常使用 PKCS#7 填充更容易。
编辑
又看了一遍,是的,slowAES 中的解密逻辑也有填充问题。它希望您传递“解密的长度”-我现在明白了,我明白这就是您提出问题的原因。如果正确执行 PKCS#7 填充,则这是不必要的。现在不是。应该是一个简单的修复。稍后会在这里更新。
编辑
好的,更新的 AES 文件现在可用here;更新后的测试代码为here。它在加密和解密时正确执行 PKCS#7 填充。我可能应该将这些更改发回给 slowAES 的所有者。
编辑
哦,还有一件事:Performing encryption within Javascript is considered harmful。