如何将加密(AES192)方法从nodejs正确移植到java?

How to port encrypt (AES192) method from nodejs to java correctly?

我正在尝试将此 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. encrypt 方法 Java returns 与 nodejs 方法不同的值(对于相同的数据和键)。

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

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

注意:我无法在 nodejs 中更改 encrypt/decrypt 方法,因此我需要将其正确移植到 java。

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

encryptjava脚本函数接受一个密码(参数名称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 has an implementation of EVP_BytesToKey in Java. (The code is originally from this blog entry)

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

最后一条评论:createCipher 已弃用。但是你在你的问题中说你不能改变 javascript encrypt 版本的实现。 (如果您不是 encrypt 的维护者)您应该与维护者讨论弃用问题,以了解他们仍然使用 createCipher(使用 EVP_BytesToKey)的原因。 EVP_BytesToKey 被认为是 and 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;
    }
}