【问题标题】:Javascript <-> Java AESJavascript <-> Java AES
【发布时间】:2011-08-23 19:12:39
【问题描述】:

我正在尝试编写一个使用 AES 加密而不是 AJAX 与 Java 后端交互的 Web 应用程序。

我花了一些时间寻找和测试库,但没有一个被证明是有效的。

我有 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”,但即使存在这种情况,实际的加密也可能无法正常工作。

【问题讨论】:

  • 我正在创建一个 API,它可以通过 HTTP 轻松访问,并且需要快速。 SSL 会减慢请求速度并使客户端的设置变得复杂。与使用 AES 设置的 SSL 相比,我拥有的一个固有优势是有一个共享密钥,客户端和服务器都知道密钥,因此请求是一个简单的调用->响应,而不是执行握手、证书检查,然后最终传输.对于长期连接,SSL 是一个明智的选择,但我重视快速响应时间。 AES 是有道理的,但我想我可能会被说服。
  • 我会先按照KISS,然后优化if有性能问题,if已经证明是来自SSL 层。此外,您将如何分配您的密钥?如果密钥被泄露怎么办?客户将如何保护他们?如果他们必须在自己的 HTML 页面中硬编码,他们很快就会公开。
  • API 是基于用户名/密码的。密码永远不会在加载时存储在页面中,但在 javascript 的情况下将由用户输入。 SSL 对于短请求来说是一个很大的开销,只是操作原理决定了这一点。有人对问题中描述的 AES 情况有任何建议吗?

标签: java javascript aes


【解决方案1】:

我看了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

【讨论】:

  • 太棒了。你救了我,可能还有很多其他人经常发现这个问题。我现在有了 Java Javascript,但 PHP 加密不再起作用。我认为这只是我需要更改的填充?它与以前的 Java 实现一起工作。
  • 嗯嗯,这很糟糕。我没有更改 Java 程序中的填充;我保持原样。但我确实更改了字符串的编码 - 在 my Java 代码中,它使用 Hex 进行编码,而您使用的是 Base64。因此,您可能希望将其切换回 Java 程序,并将 JS 程序中的 toHex() 替换为 base64 等效项。您需要为 Javascript 获取 Base64 编码器。 webtoolkit.info/javascript-base64.html 有一个。并确保获得 PKBDF2 密钥生成器。
  • 到目前为止,你帮了我这么多,问别的问题似乎很不礼貌,但我不认为这有那么复杂。在解密返回的消息时,我将如何删除额外的填充,因为我不一定知道未加密的长度。我认为填充字节应该等于填充长度,但似乎并非如此。谢谢。
  • 我的回复太长了,这里放不下,所以我编辑了上面的答案。祝你好运。问这个问题并不失礼。如果我不想回答,我不会。我有没有提到你应该使用 PBKDF2?
  • @Cheeso - 我无法打开您共享的测试 URL。我遇到了一个类似的问题,Java 向我发送了一个加密文本,我需要在 UI 上使用 javascript 对其进行解密。你能分享一些你有完整的Javascript代码的地方吗?我可以用它来解密。除了 JS 代码之外,您还使用任何库
猜你喜欢
  • 2019-05-30
  • 2012-02-04
  • 1970-01-01
  • 1970-01-01
  • 2014-06-12
  • 1970-01-01
  • 1970-01-01
  • 2013-12-17
  • 1970-01-01
相关资源
最近更新 更多