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

我正在尝试使用CryptoAPI在C ++中加密并使用SunJCE解密Java。 我已经获得了RSA密钥 – 并在测试字符串上进行了validation。 但是,我的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 ++专门加密和解密。

问题:

  1. 我应该使用CBC / PKCS5PADDING吗? 这是MS_ENH_RSA_AES_PROV的默认值吗?
  2. 归零IV确实是MS_ENH_RSA_AES_PROV的默认值吗?
  3. 有没有办法诊断密钥行为的具体细节?
  4. 我想坚持使用标准的Java软件包而不是安装BouncyCastle,但有没有任何差异可以使第三方软件包更好地工作?

我必须做几件事来正确地得到消息:

  1. KP_MODE明确设置为CRYPT_MODE_CBC ,将KP_MODE设置为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(); } 

你在Windows下为AES密钥做了太多的旋转。 使用CryptImportKey将其设置为已知值 – 例如,参见WinAES:A C ++ AES Class 。

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

默认情况下,PKCS5填充用于对称密码。 我甚至不记得如何改变它(如果可能的话)。 我怀疑你只有其他选择是’没有填充’。

对于IV,Microsoft默认为0的字符串。 您需要通过CryptSetKeyParamKP_IV设置IV。

Q1和Q2:根本不依赖于默认值。 为了可维护性,您可以选择三个选项:让每个人都知道默认值(我认为不是最佳选项),使用注释或只是设置所有可能的参数。 就个人而言,我总是选择第三种选择 – 其他选择太脆弱了。

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

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

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

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