我想从 Java 中加密一个字符串并在 Python 中解密该加密值。使用 AEC GCM 算法。下面是我的java代码
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AESEncryptionUtil {
public static void main(String[] args) {
String encString = "Hello, World!";
String secKey = "hellow world";
String encrypted = encrypt(encString, secKey);
System.out.println("Encrypted (Java): " + encrypted);
String decrypted = decrypt(encrypted, secKey);
System.out.println("Decrypted (Java): " + decrypted);
}
private static final Logger logger = LoggerFactory.getLogger(AESEncryption.class);
private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding";
private static final int TAG_LENGTH_BIT = 128;
private static final int IV_LENGTH_BYTE = 12;
private static final int SALT_LENGTH_BYTE = 16;
private static final Charset UTF_8 = StandardCharsets.UTF_8;
public static String encrypt(String pText, String secKey) {
try {
if (pText == null || pText.equals("null")) {
return null;
}
byte[] salt = getRandomNonce(SALT_LENGTH_BYTE);
byte[] iv = getRandomNonce(IV_LENGTH_BYTE);
byte[] keyBytes = secKey.getBytes(StandardCharsets.UTF_16);
SecretKeySpec skeySpec = new SecretKeySpec(Arrays.copyOf(keyBytes, 16), "AES");
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, new GCMParameterSpec(TAG_LENGTH_BIT, iv));
byte[] cipherText = cipher.doFinal(pText.getBytes());
byte[] cipherTextWithIvSalt =
ByteBuffer.allocate(iv.length + salt.length + cipherText.length)
.put(iv)
.put(salt)
.put(cipherText)
.array();
return Base64.getEncoder().encodeToString(cipherTextWithIvSalt);
} catch (Exception ex) {
logger.error("Error while encrypting:", ex);
}
return null;
}
public static String decrypt(String cText, String secKey) {
try {
if (cText == null || cText.equals("null")) {
return null;
}
byte[] decode = Base64.getDecoder().decode(cText.getBytes(UTF_8));
ByteBuffer bb = ByteBuffer.wrap(decode);
byte[] iv = new byte[IV_LENGTH_BYTE];
bb.get(iv);
byte[] salt = new byte[SALT_LENGTH_BYTE];
bb.get(salt);
byte[] cipherText = new byte[bb.remaining()];
bb.get(cipherText);
byte[] keyBytes = secKey.getBytes(StandardCharsets.UTF_16);
SecretKeySpec skeySpec = new SecretKeySpec(Arrays.copyOf(keyBytes, 16), "AES");
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
cipher.init(Cipher.DECRYPT_MODE, skeySpec, new GCMParameterSpec(TAG_LENGTH_BIT, iv));
byte[] plainText = cipher.doFinal(cipherText);
return new String(plainText, UTF_8);
} catch (Exception ex) {
logger.error("Error while decrypting:", ex);
}
return null;
}
public static byte[] getRandomNonce(int numBytes) {
byte[] nonce = new byte[numBytes];
new SecureRandom().nextBytes(nonce);
return nonce;
}
}
我无法更改我的 Java 代码;我在Python中尝试了很多方法但未能实现。 大多数时候我都会收到 SecretKey 解码错误 和来自 Python 端的 cryptography.exceptions.InvalidTag 。 感谢您的建议。
Python代码:
import base64
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
def decrypt(cipher_text_base64, secret_key):
cipher_text_with_iv_salt = base64.b64decode(cipher_text_base64)
iv = cipher_text_with_iv_salt[:12]
salt = cipher_text_with_iv_salt[12:28]
tag = cipher_text_with_iv_salt[-16:] # The last 16 bytes are the tag
ciphertext = cipher_text_with_iv_salt[28:-16] # Ciphertext excluding tag
key = secret_key.encode('utf-16')
key = key[:16].ljust(16, b'\0')
decryptor = Cipher(algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend()).decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
return plaintext.decode('utf-8')
if __name__ == "__main__":
text_to_encrypt = "Hello, World!"
secret_key = "hellow world"
# Decrypt in Python
decrypted_text = decrypt("encrypted_text", secret_key)
print(f"Decrypted (Python): {decrypted_text}")
该问题是由于密钥的编码不同造成的。
Java 代码中的secret_key.encode('utf-16')
根据定义表示带有 BOM(字节顺序标记)的 big 字节序,请参阅 Charset,秒。 标准字符集:
解码时,UTF-16和UTF-32字符集...;编码时使用big-endian字节顺序,并写入big-endian字节顺序标记。
相比之下,Python 代码中的
encode('utf-16')
是带有 BOM 的 little 字节序(至少在我的机器上;这可能取决于平台的本机字节顺序)。
在Python代码中,可以可靠地实现带有BOM的big endian,例如与:
import codecs
...
key = codecs.BOM_UTF16_BE + secret_key.encode('utf-16be')
key = key[:16].ljust(16, b'\0')
...
通过此修复,可以使用 Python 代码进行解密。
虽然 Java 代码无法更改,但未来的读者应该意识到一个漏洞,即从具有字符集编码的字符串派生密钥。
相反,密钥导出函数(例如 Argon2 或至少 PBKDF2)应与随机盐结合使用。
有趣的是,Java 代码中会生成(然后连接)随机盐,但它不会应用于任何地方。有可能考虑过使用专用的密钥导出函数,但后来由于某种原因没有实现。
此外,对于文本编码(例如
pText.getBytes()
),应始终显式指定特定编码,否则将使用特定于平台的默认编码(至少对于较旧的 Java 版本)。