簡體   English   中英

JS 中 Java 的 AES/GCM/NoPadding 等價物

[英]Java's AES/GCM/NoPadding equivalent in JS

我們在我們的組織中使用第三方的服務,我們必須以加密的方式向他們發送一些數據。 最近,他們將加密算法更新為 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 function 傳遞給 Java decrypt function 來解密我生成的編碼字符串。

在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 代碼解密的結果。


請注意,static 鹽是不安全的。 相反,鹽應該像每次加密的 IV 一樣隨機生成,並與密文一起傳遞。
此外,迭代計數為 1 是不安全的,該值應盡可能高且性能可接受。
在派生 PBKDF2 之前使用 SHA256 散列密鑰實際上是不必要的(至少如果 PBKDF2 被正確應用)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM