javascript - Java 中 JS 中的 AES/GCM/NoPadding 等效项

标签 javascript java cryptography

我们正在组织中使用第三方的服务,我们必须以加密方式向他们发送一些数据。最近,他们将加密算法更新为AES/GCM/NoPadding。

他们的代码是java的,而我们使用的是javascript。他们与我们分享了他们在 Java 中实现的算法,我们必须在 JS 中复制和实现,因为这就是我们使用的。

我在转换此代码时面临挑战。附上 Java 实现,它的工作方式就像一个魅力,而 JS 代码却没有按预期工作。尽管我尝试了很多事情,但没有一个对我有用。因此,我仅分享我尝试过的最新代码。

我对 Java 或密码学一无所知,因此我们将非常感谢这方面的任何帮助。

JAVA代码-

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * Encryption class for managing all types of AES encryptions
 */
public class EncryptionUtil {

    private final Builder mBuilder;
    private final static String HEX = "0123456789ABCDEF";

    private EncryptionUtil(Builder builder) {
        mBuilder = builder;
    }

    public static EncryptionUtil getDefault(String key, String salt, byte[] iv) {
        try {
            return Builder.getDefaultBuilder(key, salt, iv).build();
        } catch (NoSuchAlgorithmException e) {
        
            return null;
        }
    }

    public String encryptOrNull(String data) {
        try {
            return encrypt(data);
        } catch (Exception e) {
        
            return "";
        }
    }

    private String encrypt(String data) throws Exception {
        if (data == null) return null;
        SecretKey secretKey = getSecretKey(hashTheKey(mBuilder.getKey()));
        return doEncryptAES(data, secretKey, mBuilder.getAlgorithm(), mBuilder.getCharsetName());
    }

    private String decrypt(String data) throws Exception {
        if (data == null) return null;
        SecretKey secretKey = getSecretKey(hashTheKey(mBuilder.getKey()));
        return doDecryptAES(data, secretKey, mBuilder.getAlgorithm());
    }

    private String doEncryptAES(String inputString,
                                SecretKey key, String xForm, String charset) throws Exception {
        byte inpBytes[] = inputString.getBytes(charset);
        Cipher cipher = Cipher.getInstance(xForm);
        switch (xForm) {
            case "AES/ECB/PKCS5Padding":
            case "AES/ECB/NoPadding":
                cipher.init(Cipher.ENCRYPT_MODE, key);
                break;
            case "AES/CBC/PKCS5Padding":
            case "AES/CBC/NoPadding":
                cipher.init(Cipher.ENCRYPT_MODE, key, mBuilder.getIvParameterSpec(), mBuilder.getSecureRandom());
                break;
            case "AES/GCM/NoPadding":
            
                    cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, mBuilder.getIv()));
                    byte[] encryptedData = cipher.doFinal(inpBytes);

                    ByteBuffer byteBuffer = ByteBuffer.allocate(4 + mBuilder.getIv().length + encryptedData.length);
                    byteBuffer.putInt(mBuilder.getIv().length);
                    byteBuffer.put(mBuilder.getIv());
                    byteBuffer.put(encryptedData);
                    return toHex(byteBuffer.array());
        
        }
        return toHex(cipher.doFinal(inpBytes));
    }

    /**
     * for AES in GCM mode kitkat version is required
     *
     * @param inputString is String we want to decrypt
     * @param key         is symmetric key use for decryption and it similar to key used for encryption (128,192,256)
     * @param xForm       is the transformation form in which form we want to transform
     *                    (AES/ECB/PKCS5Padding,AES/ECB/NoPadding,AES/CBC/PKCS5Padding,AES/CBC/NoPadding,AES/GCM/NoPadding)
     * @return it reurn decrypted string
     * @throws Exception NOSuchAlgorithmEXception,NoSuchPaddingEXception
     */
    private String doDecryptAES(String inputString,
                                SecretKey key, String xForm) throws Exception {
        byte[] inpBytes = toByte(inputString);
        Cipher cipher = Cipher.getInstance(xForm);
        switch (xForm) {
            case "AES/ECB/PKCS5Padding":
            case "AES/ECB/NoPadding":
                cipher.init(Cipher.DECRYPT_MODE, key);
                break;
            case "AES/CBC/PKCS5Padding":
            case "AES/CBC/NoPadding":
                cipher.init(Cipher.DECRYPT_MODE, key, mBuilder.getIvParameterSpec(), mBuilder.getSecureRandom());
                break;
            case "AES/GCM/NoPadding":
            
                    ByteBuffer byteBuffer = ByteBuffer.wrap(inpBytes);
                    int noonceSize = byteBuffer.getInt();

                    if (noonceSize < 12 || noonceSize >= 16)
                        throw new IllegalArgumentException("Nonce size is incorrect. Make sure that the incoming data is an AES encrypted file.");

                    byte[] iv = new byte[noonceSize];
                    byteBuffer.get(iv);

                    byte[] cipherBytes = new byte[byteBuffer.remaining()];
                    byteBuffer.get(cipherBytes);
                    cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
                    return new String(cipher.doFinal(cipherBytes), mBuilder.getCharsetName());
               
        }
        return new String(cipher.doFinal(inpBytes), mBuilder.getCharsetName());
    }

    public String decryptOrNull(String data) {
        try {
            return decrypt(data);
        } catch (Exception e) {
            return "";
        }
    }

    private SecretKey getSecretKey(char[] key) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeySpecException {
        SecretKeyFactory factory = SecretKeyFactory.getInstance(mBuilder.getSecretKeyType());
        KeySpec spec = new PBEKeySpec(key, mBuilder.getSalt().getBytes(mBuilder.getCharsetName()), mBuilder.getIterationCount(), mBuilder.getKeyLength());
        SecretKey tmp = factory.generateSecret(spec);
        return new SecretKeySpec(tmp.getEncoded(), mBuilder.getKeyAlgorithm());
    }

    private char[] hashTheKey(String key) throws UnsupportedEncodingException, NoSuchAlgorithmException {
        MessageDigest messageDigest = MessageDigest.getInstance(mBuilder.getDigestAlgorithm());
        messageDigest.update(key.getBytes(mBuilder.getCharsetName()));
        return toHex(messageDigest.digest()).toCharArray();
    }

    private byte[] toByte(String hexString) {
        int len = hexString.length() / 2;

        byte[] result = new byte[len];

        for (int i = 0; i < len; i++)
            result[i] = Integer.valueOf(hexString.substring(2 * i, 2 * i + 2), 16).byteValue();
        return result;
    }

    public String toHex(byte[] stringBytes) {
        StringBuffer result = new StringBuffer(2 * stringBytes.length);

        for (int i = 0; i < stringBytes.length; i++) {
            result.append(HEX.charAt((stringBytes[i] >> 4) & 0x0f)).append(HEX.charAt(stringBytes[i] & 0x0f));
        }

        return result.toString();
    }

    private static class Builder {

        private byte[] mIv;
        private int mKeyLength;
        private int mIterationCount;
        private String mSalt;
        private String mKey;
        private String mAlgorithm;
        private String mKeyAlgorithm;
        private String mCharsetName;
        private String mSecretKeyType;
        private String mDigestAlgorithm;
        private String mSecureRandomAlgorithm;
        private SecureRandom mSecureRandom;
        private IvParameterSpec mIvParameterSpec;

        static Builder getDefaultBuilder(String key, String salt, byte[] iv) {
            return new Builder()
                    .setIv(iv)
                    .setKey(key)
                    .setSalt(salt)
                    .setKeyLength(128)
                    .setKeyAlgorithm("AES")
                    .setCharsetName("UTF8")
                    .setIterationCount(1)
                    .setDigestAlgorithm("SHA-256")
                    .setAlgorithm("AES/GCM/NoPadding")
                    .setSecureRandomAlgorithm("SHA1PRNG")
                    .setSecretKeyType("PBKDF2WithHmacSHA1");
        }

        private EncryptionUtil build() throws NoSuchAlgorithmException {
            setSecureRandom(SecureRandom.getInstance(getSecureRandomAlgorithm()));
            SecureRandom secureRandom = new SecureRandom();
            byte[] iv = getIv();
            secureRandom.nextBytes(iv);
            setIvParameterSpec(new IvParameterSpec(iv));
            return new EncryptionUtil(this);
        }

        private String getCharsetName() {
            return mCharsetName;
        }

        private Builder setCharsetName(String charsetName) {
            mCharsetName = charsetName;
            return this;
        }

        private String getAlgorithm() {
            return mAlgorithm;
        }

        private Builder setAlgorithm(String algorithm) {
            mAlgorithm = algorithm;
            return this;
        }

        private String getKeyAlgorithm() {
            return mKeyAlgorithm;
        }

        private Builder setKeyAlgorithm(String keyAlgorithm) {
            mKeyAlgorithm = keyAlgorithm;
            return this;
        }

        private String getSecretKeyType() {
            return mSecretKeyType;
        }

        private Builder setSecretKeyType(String secretKeyType) {
            mSecretKeyType = secretKeyType;
            return this;
        }

        private String getSalt() {
            return mSalt;
        }

        private Builder setSalt(String salt) {
            mSalt = salt;
            return this;
        }

        private String getKey() {
            return mKey;
        }

        private Builder setKey(String key) {
            mKey = key;
            return this;
        }

        private int getKeyLength() {
            return mKeyLength;
        }

        Builder setKeyLength(int keyLength) {
            mKeyLength = keyLength;
            return this;
        }

        private int getIterationCount() {
            return mIterationCount;
        }

        Builder setIterationCount(int iterationCount) {
            mIterationCount = iterationCount;
            return this;
        }

        private String getSecureRandomAlgorithm() {
            return mSecureRandomAlgorithm;
        }

        Builder setSecureRandomAlgorithm(String secureRandomAlgorithm) {
            mSecureRandomAlgorithm = secureRandomAlgorithm;
            return this;
        }

        private byte[] getIv() {
            return mIv;
        }

        Builder setIv(byte[] iv) {
            mIv = iv;
            return this;
        }

        private SecureRandom getSecureRandom() {
            return mSecureRandom;
        }

        Builder setSecureRandom(SecureRandom secureRandom) {
            mSecureRandom = secureRandom;
            return this;
        }

        private IvParameterSpec getIvParameterSpec() {
            return mIvParameterSpec;
        }

        Builder setIvParameterSpec(IvParameterSpec ivParameterSpec) {
            mIvParameterSpec = ivParameterSpec;
            return this;
        }

        private String getDigestAlgorithm() {
            return mDigestAlgorithm;
        }

        Builder setDigestAlgorithm(String digestAlgorithm) {
            mDigestAlgorithm = digestAlgorithm;
            return this;
        }

    }

    public static void main(String[] args) {
        String secretKey = "some_secret_key";
        String salt = "some_secret_salt";
        EncryptionUtil encryptionUtil = EncryptionUtil.getDefault(secretKey, salt, new byte[12]);
        String data = "Data to encrypt";
        System.out.println("Encrypted:");
        String encrypted = encryptionUtil.encryptOrNull(data);
        System.out.println(encrypted);
        System.out.println("Decrypted:");
        System.out.println(encryptionUtil.decryptOrNull(encrypted));
    }
}

请注意,我只需要帮助来加密数据

JS 代码 -

import * as crypto from 'crypto';

export const encData = () => {
    const data = 'Data to encrypt';
    const secretKey = 'some_secret_key';
    const salt = 'some_secret_salt';
    let key = '';

    const keyHash = key => {
        const hash = crypto.createHash('sha256');
        const hashedKey = hash.update(key, 'utf-8');
        return hashedKey.digest('hex').toUpperCase();
    };

    const getSecretKey = key => {
        return crypto.pbkdf2Sync(key, salt, 1, 16, 'sha1');
    };

    key = getSecretKey(keyHash(secretKey));

    const iv = crypto.randomBytes(12);
    const cipher = crypto.createCipheriv('aes-128-gcm', key, iv);
    const buffer = Buffer.from(_.isPlainObject(data) ? JSON.stringify(data) : data);

    // Updating text
    let encrypted = cipher.update(buffer);

    // Using concatenation
    encrypted = Buffer.concat([encrypted, cipher.final()]);

    return encrypted.toString('base64');
};

console.log(encData());

为了确保我的代码正常工作,我通过将 JS 函数生成的编码字符串传递给 Java 解密函数来解密它。

最佳答案

在Java代码中,加密的结果组成如下:

iv-length (4 bytes, BE) | IV | ciphertext | authentication tag 

相比之下,在 NodeJS 代码中,结果仅包含密文,即缺少 IV 长度、IV 和标记,必须添加。
这里必须考虑到 Java 的 SunJCE 提供程序会自动连接密文和标记,而这必须在 NodeJS 代码中显式发生。

此外,密文在 Java 代码中以十六进制编码返回,而在 NodeJS 代码中以 Base64 编码返回。这也需要在 NodeJS 代码中进行更改。

修复方法是在 NodeJS 代码中替换以下行:

// Using concatenation
encrypted = Buffer.concat([encrypted, cipher.final()]);

return encrypted.toString('base64');

与:

const length = Buffer.allocUnsafe(4);
length.writeUInt32BE(iv.length);

// Using concatenation
encrypted = Buffer.concat([length, iv, encrypted, cipher.final(), cipher.getAuthTag()]);

return encrypted.toString('hex');

这样,NodeJS 代码就会返回一个可以由 Java 代码解密的结果。


请注意,静态盐是不安全的。相反,盐应该像每次加密的 IV 一样随机生成,并与密文一起传递。
此外,迭代计数为 1 并不安全,该值应该尽可能高,并且性能可接受。
在 PBKDF2 派生之前使用 SHA256 对 key 进行哈希处理实际上是没有必要的(至少在正确应用 PBKDF2 的情况下)。

关于javascript - Java 中 JS 中的 AES/GCM/NoPadding 等效项,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71075350/

相关文章:

javascript - `${}` 在 Angular 中有什么用?

javascript - 有 Angular 。如何访问 ng-repeat 项目范围

javascript - jquery 在密码和文本字段之间切换 IE bug

java - 获取CPLEX中线性规划的所有极值点

java - Android AES 无填充解密,字符串末尾有未知字符 'NUL'

.net-2.0 - 使用 RSA 公钥解密数据

javascript - 当两个字段包含数据时如何创建警报

JavaFX - MVC - 如何从另一个包加载图像

java - Cucumber:无法将 DataTable 转换为 List<java.lang.String>。有一个表格单元格转换器,但表格太宽而无法使用

javascript - 如何在 javascript 中生成 CMAC-AES