CryptoAPI C++使用 AES 与 Java 互操作

发布于 2024-12-25 11:38:00 字数 3081 浏览 2 评论 0原文

我正在尝试使用 CryptoAPI 在 C++ 中加密并使用 SunJCE 对 Java 进行解密。我已经让 RSA 密钥可以工作——并在测试字符串上进行了验证。但是,我的 AES 密钥无法正常工作 - 我收到 javax.crypto.BadPaddingException: 鉴于最终块未正确填充

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++ 进行加密和解密。

问题:

  1. 我应该使用 CBC/PKCS5PADDING 吗?这是 MS_ENH_RSA_AES_PROV 的默认设置吗?
  2. 归零的 IV 确实是 MS_ENH_RSA_AES_PROV 的默认设置吗?
  3. 有什么方法可以诊断密钥行为的具体情况吗?
  4. 我想坚持使用标准 Java 包而不是安装 BouncyCastle,但是是否有任何差异可以使第 3 方包工作得更好?

I am trying to encrypt in C++ using CryptoAPI and decrypt Java using SunJCE. I have gotten the RSA key to work -- and verified on a test string. However, my AES key is not working -- I get javax.crypto.BadPaddingException: Given final block not properly padded.

C++ Encryption:

// 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 Decryption:

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();
}

In a similar example I have tried permutations of not reversing the bytes of the key and not reversing bytes in the message. If I encrypt and decrypt with the imported key in java, I get valid results. I can also encrypt and decrypt exclusively in C++.

Questions:

  1. Should I use CBC/PKCS5PADDING? Is this the default for MS_ENH_RSA_AES_PROV?
  2. Is a zeroed IV indeed the default for MS_ENH_RSA_AES_PROV?
  3. Are there any ways to diagnose the specifics of how the key is behaving?
  4. I'd like to stick with standard Java packages instead of installing BouncyCastle, but are there any differences that would make a 3rd party package work better?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

苏璃陌 2025-01-01 11:38:00

我必须做几件事才能正确获取消息:

  1. KP_MODE 显式设置为 CRYPT_MODE_CBC,并将 KP_IV 设置为 0
  2. 在 Java 解密中使用 NoPadding
  3. 不要反转密钥或消息的字节

在诊断问题方面,最有用的建议是在 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();
}

I had to do several things to get the message correctly:

  1. Explicitly set KP_MODE to CRYPT_MODE_CBC, and KP_IV to 0
  2. Use NoPadding in Java decryption
  3. Don't reverse the bytes for the key or the message

In terms of diagnosing the problem the most useful piece of advice was to set NoPadding in Java which prevents the BadPaddingException. This allowed me to see the results -- even if wrong.

Strangely, the RSA Java/CryptoAPI interop solution requires the message to be completely byte reversed in order to work with Java, but AES does not expect the key or the message to be byte reversed.

CryptSetKeyParam would not let me use ZERO_PADDING, but when looking at the decrypted bytes, it is clear that CryptoAPI fills with the number of unused bytes. For instance, with a block size of 16, if the last block only uses 9 bytes, then the remaining 5 bytes get the value of 0x05. Does this present a potential security leak? Should I pad all other bytes with random bytes and use only the last byte to signify how much padding is used?

The working code (using the CryptoAPI convention of last byte being pad count) is below (checking of return values from Crypt have been removed for simplicity):

// 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 Decryption:

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();
}
追我者格杀勿论 2025-01-01 11:38:00

您在 Windows 下对 AES 密钥进行了太多旋转。使用 CryptImportKey 将其设置为已知值 - 例如,参见 WinAES :C++ AES 类

您应该在 Windows 上使用 CryptSetKeyParamKP_MODECRYPT_MODE_CBC 设置 CBC 模式。否则,您正在使用 ECB 模式(如果我没记错的话)再次参见 WinAES: A C++ AES类

默认情况下,对称密码使用 PKCS5 填充。我什至不记得如何更改它(如果可能的话)。我怀疑你唯一的选择就是“无填充”。

Microsoft 默认使用由 0 组成的字符串作为 IV。您需要通过 CryptSetKeyParamKP_IV 设置 IV。

You are doing too many gyrations for the AES key under Windows. Set it to a known value with CryptImportKey - see for example, WinAES: A C++ AES Class.

You should set CBC mode on Windows using CryptSetKeyParam, KP_MODE, and CRYPT_MODE_CBC. Otherwise, you are using ECB mode (if I recall correctly) Again, see WinAES: A C++ AES Class.

PKCS5 padding is used for symmetric ciphers by default. I don't even recall how to change it (if its possible). I suspect you only other choice is 'no padding'.

Microsoft defaults to a string of 0's for an IV. You will need to set the IV via CryptSetKeyParam and KP_IV.

番薯 2025-01-01 11:38:00

Q1& Q2:不要依赖默认值。为了可维护性,您可以选择三个选项:让每个人了解默认值是什么(我认为这不是最好的选项)、使用注释或简单地设置所有可能的参数。就我个人而言,我总是会选择第三种选择——其他选择都太脆弱了。

Q3 不,如果密钥的位错误或顺序不正确(见下文),您将得到错误的填充异常垃圾输出。您可以做的是在解密期间在 Java 中使用“/NoPadding”(或在 C++ 中类似)。这样,您可以通过查看输出来了解是否存在填充问题。如果您的纯文本在那里,那么您可能会遇到填充问题。如果只有第一个块是错误的,那么您的 IV 就会遇到问题。

Q4 不,不是。如果您想继续使用 Java,Java JCE 工作得非常好。 Bouncy Castle 具有更多功能,并且可能具有不同的性能特征。您可以使用其他提供程序来使用不同的密钥存储(例如,依赖于操作系统或智能卡)、使用性能增强(本机)实现等。

可能您需要相反的密钥,因为Java 使用大端字节序,而 C++ 可能使用小端字节序。不过,我无法想象 C++ 会反转输入/输出的字节。通常它们都不代表数字,因此两个平台的顺序应该相同。

删除字节反转,指定所有参数并报告回来?

Q1 & Q2: simply don't rely on defaults. For maintainability, you can choose three options: let everybody find out what the defaults are (not the best option, I think), use comments or simply set all the possible parameters. Personally, I would always opt for the third choice - the other options are too brittle.

Q3 No, if the bits of the key are wrong or in the incorrect order (see below), you will get either a bad padding exception or trash output. What you can do is to use "/NoPadding" in Java during decrypt (or similar in C++). In that way you can see if you have a padding issue by looking at the output. If your plain text is there then you are likely having a padding issue. If only the first block is wrong, you are having trouble with the IV.

Q4 No, not really. Java JCE is working pretty well if you want to stay within Java. Bouncy Castle has (way) more functionality and may have different performance characteristics. You can use other providers to use different key stores (OS dependent or smartcard, for instance), use a performance enhanced (native) implementation etc.

It could be possible that you need the reverse of the key because Java is using big endian and C++ may use little endian. I cannot fanthom that C++ would reverse the bytes of the input/output though. Normally neither of them represent a number, so the order should be identical for both platforms.

Remove the reversal of the bytes, specify all parameters and report back?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文