记录一次处理Node 和 Java 互相加解密的问题处理

问题描述

首先是做了NodeJS平台的加解密工具,后来因需求需要在Java(JDK8) 端也实现一套可以跟NodeJS这边互相加解密的工具。之前也做过一个Python 端的加解密工具,整理一下三种语言的实现。

NodeJS 这边的实现

注意一点,这里加解密的KEY 以及密文都是HEX编码的字符串。

const crypto = require('crypto');

const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32;
const CHARACTER_ENCODING = 'hex';

function encrypt(value, key) {
	// 此处用了lodash 的方法,可以替换其他判断方法
    if (_.isEmpty(value) || _.isEmpty(key)) {
        return value;
    }

    let iv = crypto.randomBytes(16);
    let cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(key, CHARACTER_ENCODING), iv);
    
    let encrypted = cipher.update(value);
    encrypted = Buffer.concat([encrypted, cipher.final()]);

    let authTag = cipher.getAuthTag();
    encrypted = Buffer.concat([encrypted, authTag]);
    encrypted = Buffer.concat([iv, encrypted]);

    return encrypted.toString(CHARACTER_ENCODING);
}

function decrypt(value, key) {
    // 此处用了lodash 的方法,可以替换其他判断方法
    if (_.isEmpty(value) || _.isEmpty(key)) {
        return value;
    }

    let encrypted = Buffer.from(value, CHARACTER_ENCODING);
    let iv = encrypted.subarray(0, 16);
    let authTag = encrypted.subarray(encrypted.length - 16, encrypted.length);
    let encryptedContent = encrypted.subarray(16, encrypted.length - 16)

    const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(key, CHARACTER_ENCODING), iv);
    decipher.setAuthTag(authTag);
    let decrypted = decipher.update(encryptedContent);
    decrypted += decipher.final('utf8');
    return decrypted.toString("utf8");
}

生成加解密的KEY代码如下:

function generateRandomKey() {
    let password = crypto.randomBytes(16);
    let salt = crypto.randomBytes(16);
    let key = crypto.scryptSync(password, salt, KEY_LENGTH);
    return key.toString(CHARACTER_ENCODING);
}

Java 最后的实现版本如下

private static final String KEY_ALGORITHM = "AES";
private static final String DEFAULT_CIPHER_ALGORITHM = "AES/GCM/PKCS5Padding";
private static final int IV_LEN = 16;

这里注意IV需要自己创建,为了跟NodeJS版本的长度保持一致。如果不指定,而是使用cipher.getIV(),那么至少AES/GCM/PKCS5Padding算法获取到的长度是12位的,与NodeJS版本的实现不一致。

Java版本的16 个byte(128bit)的authTag默认拼接到了密文的最后,跟NodeJS需要自己处理authTag不一样。不过,之前NodeJS的实现就是放到了最后,不受影响。IV 最终拼接到哪个位置,可以自行决定,这里放在最前面,跟NodeJS的实现保持一致。

public static String aesEncrypt(String content, String encryptPass) {
    try {
        byte[] iv = generateRandomIv(IV_LEN);
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);

        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(encryptPass), gcmParameterSpec);

        byte[] encryptData = cipher.doFinal(content.getBytes());
        byte[] message = new byte[IV_LEN + encryptData.length];
        System.arraycopy(iv, 0, message, 0, IV_LEN);
        System.arraycopy(encryptData, 0, message, IV_LEN, encryptData.length);
        return encodeHex(message);
    } catch (Exception e) {
      // 异常处理
    }
    return null;
}

private static byte[] generateRandomIv(int size) {
    byte[] iv = new byte[size];
    SecureRandom randomSecureRandom = new SecureRandom();
    randomSecureRandom.nextBytes(iv);
    return iv;
}

Java版本的解密操作

public static String aesDecrypt(String base64Content, String encryptPass) {
    try {
        byte[] content = decodeHex(base64Content);
        if (content.length < IV_LEN + 16) {
            throw new IllegalArgumentException();
        }
        GCMParameterSpec params = new GCMParameterSpec(128, content, 0, IV_LEN);
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, getSecretKey(encryptPass), params);
        byte[] decryptData = cipher.doFinal(content, IV_LEN, content.length - IV_LEN);
        return new String(decryptData);
    } catch (Exception e) {
        // 异常处理
    }
    return null;
}

上面需要注意的是,JDK 8版本没有提供默认HEX 编码和解码,这里用的是org.apache.commons.codec.binary.Hex

private static byte[] decodeHex(String hexStr) throws DecoderException {
    if (StringUtils.isEmpty(hexStr)) {
        return new byte[]{};
    }
    return Hex.decodeHex(hexStr.toCharArray());
}

private static String encodeHex(byte[] content) {
    if (content == null || content.length == 0) {
        return Symbol.EMPTY;
    }
    return Hex.encodeHexString(content);
}

为了保持跟NodeJs的实现保持一致,对key的处理是直接以hex编码获取到byte数组。

private static Key getSecretKey(String encryptPass) throws DecoderException, NoSuchAlgorithmException {
    return new SecretKeySpec(decodeHex(encryptPass), KEY_ALGORITHM);
}

如果只是Java端自行加解密,推荐如下方法处理Key:

private static Key getSecretKey(String encryptPass) throws DecoderException, NoSuchAlgorithmException {
    KeyGenerator instance = KeyGenerator.getInstance(KEY_ALGORITHM);
    instance.init(256, new SecureRandom(decodeHex(encryptPass)));
    SecretKey secretKey = instance.generateKey();
    return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);
}

Python 端实现一致的加解密算法

主要依赖pycryptodome==3.15.0来实现加解密算法,没有使用python默认的库。

Python 中的IV 和authTag 也是需要自己决定位置:

def encrypt(origin_str_value, key_str):
    from Crypto.Cipher import AES
    from Crypto.Random import get_random_bytes

    nonce = get_random_bytes(16)
    key = bytes.fromhex(key_str)

    cipher = AES.new(key, AES.MODE_GCM, nonce)
    ciphertext, tag = cipher.encrypt_and_digest(
        bytes(origin_str_value, encoding='utf-8'))
    return (nonce + ciphertext + tag).hex()
def decrypt(encrypted_value_str, key_str):
    from Crypto.Cipher import AES

    values = bytes.fromhex(encrypted_value_str)

    nonce = values[0:16]
    encrypted = values[16:-16]
    authTag = values[-16:]

    key = bytes.fromhex(key_str)

    cipher = AES.new(key, AES.MODE_GCM, nonce)
    return cipher.decrypt_and_verify(encrypted, authTag).decode(encoding='utf-8')

def generate_key():
    from Crypto.Random import get_random_bytes
    return get_random_bytes(32).hex()
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值