我试图编写一个Web应用程序,该应用程序通过AJAX使用AES加密来与Java后端进行交互。
我花了一些时间寻找和测试库,但都没有一个能证明卓有成效。
我的Java <-> PHP可与以下Java代码一起正常工作:
public static String encrypt(String input, String key){
IvParameterSpec ips = new IvParameterSpec("sixteenbyteslong".getBytes());
try {
key = md5(key);
} catch (NoSuchAlgorithmException e1) {
e1.printStackTrace();
}
byte[] crypted = null;
try{
SecretKeySpec skey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, skey, ips);
crypted = cipher.doFinal(input.getBytes());
}catch(Exception e){
System.out.println(e.toString());
}
return new String(Base64.encodeBase64(crypted));
}
public static String decrypt(String input, String key){
IvParameterSpec ips = new IvParameterSpec("sixteenbyteslong".getBytes());
try {
key = md5(key);
} catch (NoSuchAlgorithmException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
byte[] output = null;
try{
SecretKeySpec skey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skey,ips);
output = cipher.doFinal(Base64.decodeBase64(input));
}catch(Exception e){
System.out.println(e.toString());
}
return new String(output);
}
Base64是
org.apache.commons.codec.binary.Base64
。我尝试了SlowAES,但它不支持“ PKCS5Padding”,但是即使存在,实际的加密也可能无法正常工作。
最佳答案
我看过slowAes,我认为你是对的。坏了
该代码打算在CBC模式下运行时应用PKCS#7填充,但不会成功。 (PKCS#7只是扩展到16字节块加密算法的PKCS#5。我认为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。有一个Java版本的PBKDF2,它可以工作,并且有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可以正确做到这一点。 slowAES正确执行了此操作,但明文长度是16字节的倍数的情况除外。 PKCS#7表示,在这种情况下,您需要添加16个字节的填充(全部为16)。如果要精确加密32个字节,则AES的PKCS#7说,您需要添加16字节的pad。共加密48个字节。这样解密器才能做正确的事。考虑一下:如果不添加16个字节的填充,解密器将无法“告诉”明文的最后一个字节不是填充字节。
在这种情况下,SlowAES没有填充,这是您无法使其与Java互操作的部分原因。我注意到了这一点,因为对于32字节的明文,密码流恰好是32字节,这意味着没有填充。当我查看代码时,逻辑错误就在那里。 (提醒我:我需要验证在加密方面,在加密方面是否存在逻辑错误)
因此,您的应用程序不知道未加密的长度是正确的。但是,如果使用PKCS#7填充,则解密程序确实知道要分割多少字节,并且正确的解密程序将始终向您返回正确的明文字节数。为了使其正常工作,解密时必须使用与加密时相同的填充约定。您通常需要告诉加密库使用什么填充,尽管有些填充(例如slowAES)没有选择的余地。
如果在某些库中使用“ no padding”选项(但在CBC模式下不是在slowAES中使用),那么,是的,您的应用程序必须以某种方式“知道”未加密数据的大小,以便可以丢弃最后N个字节。纯文本。在某些数据格式和协议中可以。但是通常使用PKCS#7填充会更容易。
编辑
只是再看一遍,是的,slowAES中的解密逻辑也存在填充问题。它希望您传递“解密的长度”-我现在看到了,我看到这是您提出问题的原因。如果正确执行PKCS#7填充,则没有必要。不是现在应该是一个简单的解决方法。稍后将更新回这里。
编辑
好的,更新的AES文件现在可用here;更新的测试代码为here。它在加密和解密时正确执行PKCS#7填充。我可能应该将这些更改发送回slowAES的所有者。
编辑
哦,还有一件事:Performing encryption within Javascript is considered harmful。
关于java - Javascript <-> Java AES,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7166275/