【问题标题】:How to port encrypt (AES192) method from nodejs to java correctly?如何正确地将加密(AES192)方法从nodejs移植到java?
【发布时间】:2021-09-25 19:00:02
【问题描述】:

我正在尝试将此 nodejs 代码移植到 java

const crypto = require("crypto");

const encrypt = (data, key) => {
    const cipher = crypto.createCipher('aes192', key)

    let crypted = cipher.update(data, 'utf8', 'hex')
    crypted += cipher.final('hex')

    return crypted;
}

我已尝试使用此解决方案:

import org.springframework.security.crypto.codec.Hex;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public String encrypt(String data, String key) {
    try {
        var cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes(), "AES"));

        var cipherText = cipher.update(data.getBytes());
        cipherText = ArrayUtils.addAll(cipherText, cipher.doFinal());

        return new String(Hex.encode(cipherText));
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

缺点:

  1. Java 的encrypt 方法返回的值与 nodejs 的方法不同(对于相同的数据和键)。

  2. 在 nodejs 中,我可以放一个短键(5 个字符长),同时在 java 中我正在捕获异常,例如“java.security.InvalidKeyException:无效的 AES 密钥长度:5 个字节”

您能否提出正确的解决方案或指出现有解决方案中的错误?提前谢谢!

注意:我无法更改 nodejs 中的加密/解密方法,因此我需要将其正确移植到 java。

【问题讨论】:

  • createCipher() 已弃用。密文不同,因为createCipher() 使用(不安全的)基于密码的密钥派生函数从密码中派生密钥(即key 被解释为密码)。应用需要密钥的createCipheriv()(如果需要结合可靠的密钥派生,如 PBKDF2)。否则,您必须在 Java 中实现(不安全的)密钥派生(Web 上有多种 Java 实现,例如 here)。

标签: java node.js encryption aes


【解决方案1】:

您的encrypt java 版本使用的逻辑与 javascript 版本不同。

encrypt javascript 函数接受一个密码(参数名称key 具有误导性)然后将其传递给createCiphercreateCipher 不直接使用密码,而是从中派生出一个密钥。它是用于加密消息的派生密钥。 From NodeJs documentation

The password is used to derive the cipher key and initialization vector (IV). The value must be either a 'latin1' encoded string, a Buffer, a TypedArray, or a DataView.

另一方面,encrypt java 函数需要一个随时可用的密钥,因此您必须自己从密码中导出密钥。

AES 密钥具有固定大小。 They can only be 128, 192 or 256 bits long。 (以字节 8、16、32 字节长)。如果您使用不同大小的密钥,您将收到异常InvalidKeyException。 NodeJS 没有抱怨“无效的密钥长度”,因为实际上您使用的是密码,而不是密钥。 NodeJS 在加密数据之前从密码中派生出一个密钥。

(如文档中所述)NodeJs 使用 OpenSSL 加密数据并使用特定于 OpenSSL 的函数派生密钥:EVP_BytesToKey

幸运的是,SO answer 在 Java 中实现了 EVP_BytesToKey。 (代码原来自this blog entry

我修改了您的代码以使用它。我将最终结果添加到答案的末尾。我很少编写安全代码,在这种情况下,我只是调整了一个现有的解决方案,因此如果您决定使用该代码(或者如果您的公司有安全团队,请让其他人进行审查)。

最后一条评论:createCipher 已弃用。但是您在问题中说您无法更改 javascript encrypt 版本的实现。 (如果您不是encrypt 的维护者)您应该与维护者讨论弃用问题,以了解他们仍然使用createCipher(使用EVP_BytesToKey)的原因。 EVP_BytesToKey 被认为是 weak key derivation function 和 OpenSSL recommends using more modern functions for newer applications

import org.apache.commons.lang3.ArrayUtils;
import org.springframework.security.crypto.codec.Hex;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

public class Main {
    public static void main(String[] args){
        System.out.println("Result : " + encrypt("my secret message","pass"));
    }

    public static byte[][] EVP_BytesToKey(int key_len, int iv_len, MessageDigest md, byte[] salt, byte[] data, int count) {
        byte[][] both = new byte[2][];
        byte[] key = new byte[key_len];
        int key_ix = 0;
        byte[]  iv = new byte[iv_len];
        int iv_ix = 0;
        both[0] = key;
        both[1] = iv;
        byte[] md_buf = null;
        int nkey = key_len;
        int niv = iv_len;
        int i = 0;
        if(data == null) {
            return both;
        }
        int addmd = 0;
        for(;;) {
            md.reset();
            if(addmd++ > 0) {
                md.update(md_buf);
            }
            md.update(data);
            if(null != salt) {
                md.update(salt,0,8);
            }
            md_buf = md.digest();
            for(i=1;i<count;i++) {
                md.reset();
                md.update(md_buf);
                md_buf = md.digest();
            }
            i=0;
            if(nkey > 0) {
                for(;;) {
                    if(nkey == 0) break;
                    if(i == md_buf.length) break;
                    key[key_ix++] = md_buf[i];
                    nkey--;
                    i++;
                }
            }
            if(niv > 0 && i != md_buf.length) {
                for(;;) {
                    if(niv == 0) break;
                    if(i == md_buf.length) break;
                    iv[iv_ix++] = md_buf[i];
                    niv--;
                    i++;
                }
            }
            if(nkey == 0 && niv == 0) {
                break;
            }
        }
        for(i=0;i<md_buf.length;i++) {
            md_buf[i] = 0;
        }
        return both;
    }

    public static String encrypt(String data, String password) {
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            var keySizeBits = 192 / Byte.SIZE; //AES with 192 bits key = 16 bytes
            var ivSize = cipher.getBlockSize();
            final byte[][] keyAndIV = EVP_BytesToKey(keySizeBits, ivSize, md5, null, password.getBytes(StandardCharsets.US_ASCII), 1);
            SecretKeySpec key = new SecretKeySpec(keyAndIV[0], "AES");
            IvParameterSpec iv = new IvParameterSpec(keyAndIV[1]);

            cipher.init(Cipher.ENCRYPT_MODE, key, iv);

            var cipherText = cipher.update(data.getBytes());
            cipherText = ArrayUtils.addAll(cipherText, cipher.doFinal());

            return new String(Hex.encode(cipherText));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-27
    • 1970-01-01
    • 1970-01-01
    • 2012-06-18
    • 1970-01-01
    • 1970-01-01
    • 2013-10-15
    相关资源
    最近更新 更多