CryptoAPI C++ 使用 AES 与 Java 互操作
Posted
技术标签:
【中文标题】CryptoAPI C++ 使用 AES 与 Java 互操作【英文标题】:CryptoAPI C++ interop with Java using AES 【发布时间】:2012-01-06 19:50:25 【问题描述】:我正在尝试使用 CryptoAPI 在 C++ 中加密并使用 SunJCE 解密 Java。我已经获得了 RSA 密钥,并在测试字符串上进行了验证。但是,我的 AES 密钥不起作用 - 我收到 javax.crypto.BadPaddingException: Given final block not properly padded
。
C++ 加密:
// init and gen key
HCRYPTPROV provider;
CryptAcquireContext(&provider, NULL, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);
// Use symmetric key encryption
HCRYPTKEY sessionKey;
DWORD exportKeyLen;
CryptGenKey(provider, CALG_AES_128, CRYPT_EXPORTABLE, &sessionKey);
// Export key
BYTE exportKey[1024];
CryptExportKey(sessionKey, NULL, PLAINTEXTKEYBLOB, 0, exportKey, &exportKeyLen);
// skip PLAINTEXTKEYBLOB header
// uint8_t bType, uint8_t version, uint16_t reserved, uint32_t aiKey, uint32_t keySize
DWORD keySize = *((DWORD*)(exportKey + 8));
BYTE * rawKey = exportKey + 12;
// reverse bytes for java
for (unsigned i=0; i<keySize/2; i++)
BYTE temp = rawKey[i];
rawKey[i] = rawKey[keySize-i-1];
rawKey[keySize-i-1] = temp;
// Encrypt message
BYTE encryptedMessage[1024];
const char * message = "Decryption Works";
BYTE messageLen = (BYTE)strlen(message);
memcpy(encryptedMessage, message, messageLen);
DWORD encryptedMessageLen = messageLen;
CryptEncrypt(sessionKey, NULL, TRUE, 0, encryptedMessage, &encryptedMessageLen, sizeof(encryptedMessage));
// reverse bytes for java
for (unsigned i=0; i<encryptedMessageLen/2; i++)
BYTE temp = encryptedMessage[i];
encryptedMessage[i] = encryptedMessage[encryptedMessageLen - i - 1];
encryptedMessage[encryptedMessageLen - i - 1] = temp;
BYTE byteEncryptedMessageLen = (BYTE)encryptedMessageLen;
FILE * f = fopen("test.aes", "wb");
fwrite(rawKey, 1, keySize, f);
fwrite(&byteEncryptedMessageLen, 1, sizeof(byteEncryptedMessageLen), f);
fwrite(encryptedMessage, 1, encryptedMessageLen, f);
fclose(f);
// destroy session key
CryptDestroyKey(sessionKey);
CryptReleaseContext(provider, 0);
Java解密:
try
FileInputStream in = new FileInputStream("test.aes");
DataInputStream dataIn = new DataInputStream(in);
// stream key and message
byte[] rawKey = new byte[16];
dataIn.read(rawKey);
byte encryptedMessageLen = dataIn.readByte();
byte[] encryptedMessage = new byte[encryptedMessageLen];
dataIn.read(encryptedMessage);
// use CBC/PKCS5PADDING, with 0 IV -- default for Microsoft Base Cryptographic Provider
SecretKeySpec sessionKey = new SecretKeySpec(rawKey, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, sessionKey, new IvParameterSpec(new byte[16]));
cipher.doFinal(encryptedMessage);
catch (Exception e)
e.printStackTrace();
在一个类似的例子中,我尝试了不反转密钥字节和不反转消息中的字节的排列。如果我在 java 中使用导入的密钥进行加密和解密,我会得到有效的结果。我也可以专门用 C++ 加密和解密。
问题:
-
我应该使用 CBC/PKCS5PADDING 吗?这是
MS_ENH_RSA_AES_PROV
的默认值吗?
归零的 IV 确实是 MS_ENH_RSA_AES_PROV
的默认值吗?
是否有任何方法可以诊断密钥的具体行为方式?
我想坚持使用标准 Java 包而不是安装 BouncyCastle,但是否有任何差异可以让第 3 方包更好地工作?
【问题讨论】:
如果它使用加密,它可能是高完整性软件(或至少一部分)。停止忽略来自 CryptoAPI 的返回值。 缺少的返回值只是为了简化这里的代码。对此的实际单元测试会检查所有返回码。 【参考方案1】:我必须做几件事才能正确获取消息:
-
将
KP_MODE
显式设置为CRYPT_MODE_CBC
,并将KP_IV
设置为0
在Java解密中使用NoPadding
不要颠倒密钥或消息的字节
就诊断问题而言,最有用的建议是在 Java 中设置 NoPadding 以防止出现BadPaddingException
。这让我看到了结果——即使是错误的。
奇怪的是,RSA Java/CryptoAPI 互操作解决方案要求消息完全字节反转才能与 Java 一起使用,但 AES 不希望密钥或消息被字节反转。
CryptSetKeyParam 不允许我使用 ZERO_PADDING,但是在查看解密的字节时,很明显 CryptoAPI 填充了未使用的字节数。例如,对于一个大小为 16 的块,如果最后一个块只使用 9 个字节,那么剩余的 5 个字节的值就是 0x05。这是否存在潜在的安全漏洞?我应该用随机字节填充所有其他字节并只使用最后一个字节来表示使用了多少填充?
工作代码(使用 CryptoAPI 约定的最后一个字节为填充计数)如下(为简单起见,已删除检查来自 Crypt 的返回值):
// init and gen key
HCRYPTPROV provider;
CryptAcquireContext(&provider, NULL, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);
// Use symmetric key encryption
HCRYPTKEY sessionKey;
DWORD exportKeyLen;
BYTE iv[32];
memset(iv, 0, sizeof(iv));
DWORD padding = PKCS5_PADDING;
DWORD mode = CRYPT_MODE_CBC;
CryptGenKey(provider, CALG_AES_128, CRYPT_EXPORTABLE, &sessionKey);
CryptSetKeyParam(sessionKey, KP_IV, iv, 0);
CryptSetKeyParam(sessionKey, KP_PADDING, (BYTE*)&padding, 0);
CryptSetKeyParam(sessionKey, KP_MODE, (BYTE*)&mode, 0);
// Export key
BYTE exportKey[1024];
CryptExportKey(sessionKey, NULL, PLAINTEXTKEYBLOB, 0, exportKey, &exportKeyLen);
// skip PLAINTEXTKEYBLOB header
// uint8_t bType, uint8_t version, uint16_t reserved, uint32_t aiKey, uint32_t keySize
DWORD keySize = *((DWORD*)(exportKey + 8));
BYTE * rawKey = exportKey + 12;
// Encrypt message
BYTE encryptedMessage[1024];
const char * message = "Decryption Works -- using multiple blocks";
BYTE messageLen = (BYTE)strlen(message);
memcpy(encryptedMessage, message, messageLen);
DWORD encryptedMessageLen = messageLen;
CryptEncrypt(sessionKey, NULL, TRUE, 0, encryptedMessage, &encryptedMessageLen, sizeof(encryptedMessage));
BYTE byteEncryptedMessageLen = (BYTE)encryptedMessageLen;
FILE * f = fopen("test.aes", "wb");
fwrite(rawKey, 1, keySize, f);
fwrite(&byteEncryptedMessageLen, 1, sizeof(byteEncryptedMessageLen), f);
fwrite(encryptedMessage, 1, encryptedMessageLen, f);
fclose(f);
// destroy session key
CryptDestroyKey(sessionKey);
CryptReleaseContext(provider, 0);
Java解密:
try
FileInputStream in = new FileInputStream("test.aes");
DataInputStream dataIn = new DataInputStream(in);
// stream key and message
byte[] rawKey = new byte[16];
dataIn.read(rawKey);
byte encryptedMessageLen = dataIn.readByte();
byte[] encryptedMessage = new byte[encryptedMessageLen];
dataIn.read(encryptedMessage);
// use CBC/NoPadding, with 0 IV -- (each message is creating it's own session key, so zero IV is ok)
SecretKeySpec sessionKey = new SecretKeySpec(rawKey, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, sessionKey, new IvParameterSpec(new byte[16]));
byte[] decryptedBlocks = cipher.doFinal(encryptedMessage);
// check versus expected message
byte[] expectedBytes = "Decryption Works -- using multiple blocks".getBytes();
Assert.assertTrue("Incorrect Message" + new String(message), Arrays.equals(message, expectedBytes));
catch (Exception e)
e.printStackTrace();
【讨论】:
别忘了CryptAcquireContext
、CryptGenKey
、CryptSetKeyParam
,还有朋友回TRUE
/FALSE
。您应该断言并检查返回值。
"2. 在 Java 解密中使用 NoPadding" - 这听起来不对。尝试加密/解密以下长度的消息以验证正确性:0、1、15、16、17、31、32、33。
"例如,块大小为 16,如果最后一个块只使用 9 个字节,那么剩余的 11 个字节的值是 0x05" - 这是不正确的,因为 CryptoAPI 使用 PKCS5。在此示例中,由于剩余 5 个字节,将有 5 个 0x05 字节。如果有 11 个剩余字节,填充将是 0x11 的 11 个字节。唯一的极端情况是 0 字节,它得到 16 字节的 0x16 以确保消除歧义。
是的,我尝试了几种不同的消息长度,并且总是在最后一个字节(以及所有其他需要填充的字节)中得到预期的填充。
我不明白。这真的很像PKCS5Padding,但如果我相信你的cmets,纯文本+填充不是块长度* N。这应该是不可能的。【参考方案2】:
您在 Windows 下对 AES 密钥进行了太多的旋转操作。使用 CryptImportKey
将其设置为已知值 - 参见例如 WinAES: A C++ AES Class。
您应该在 Windows 上使用 CryptSetKeyParam
、KP_MODE
和 CRYPT_MODE_CBC
设置 CBC 模式。否则,您正在使用 ECB 模式(如果我没记错的话)再次,请参阅 WinAES: A C++ AES Class。
PKCS5 填充默认用于对称密码。我什至不记得如何改变它(如果可能的话)。我怀疑你唯一的选择是“无填充”。
Microsoft 默认使用 0 字符串作为 IV。您需要通过CryptSetKeyParam
和KP_IV
设置IV。
【讨论】:
在 Java 中,您将至少拥有与 XML 加密兼容的填充(只有最后一个字节设置为填充八位字节的数量)。我有时使用/NoPadding
来查看问题是出在填充还是密钥/IV 本身。例如,/ISO7816Padding
在您安装 Bouncy Castle 时可用(一位设置为 1,其余为 0)。【参考方案3】:
Q1 & Q2:不要依赖默认值。为了可维护性,您可以选择三个选项:让每个人找出默认值是什么(我认为不是最佳选项)、使用 cmets 或简单地设置所有可能的参数。就个人而言,我总是会选择第三种选择——其他选择太脆弱了。
Q3 否,如果密钥的位错误或顺序不正确(见下文),您将得到错误的填充异常或垃圾输出。您可以做的是在解密期间在 Java 中使用“/NoPadding”(或在 C++ 中类似)。这样,您可以通过查看输出来查看是否有填充问题。如果您的纯文本在那里,那么您可能会遇到填充问题。如果只有第一个块是错误的,那么您的 IV 就有问题。
Q4 不,不是。如果您想留在 Java 中,Java JCE 工作得很好。 Bouncy Castle 具有(方式)更多功能,并且可能具有不同的性能特征。您可以使用其他提供程序来使用不同的密钥存储(例如依赖于操作系统或智能卡),使用性能增强(本机)实现等。
可能您可能需要密钥的反转,因为 Java 使用的是大端序,而 C++ 可能使用的是小端序。我无法想象 C++ 会反转输入/输出的字节。通常它们都不代表数字,因此两个平台的顺序应该相同。
去除字节的反转,指定所有参数并报告回来?
【讨论】:
明确设置的好主意 -- “您可能需要反转密钥,因为 Java 使用大端序,而 C++ 可能使用小端序。我无法想象 C++ 会反转输入/输出的字节” - 微软偶尔会在 CryptoAPI 中做到这一点。例如,我相信 PUBLICKEYBLOB 中的字节数组是字节交换的(即微软是小端的)。 密钥数据本身不需要交换任何字节(尽管 RSA 加密需要交换消息的所有字节)。 PUBLICKEYBLOB 的字段确实需要交换(只是 AlgId 和 keyLength -- 因为其他字段是单字节)。以上是关于CryptoAPI C++ 使用 AES 与 Java 互操作的主要内容,如果未能解决你的问题,请参考以下文章
将 PEM 编码的 X.509 证书加载到 Windows CryptoAPI