问题描述
首先是做了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()