2012-01-06 74 views
1

我想使用CryptoAPI在C++中进行加密,并使用SunJCE对Java进行解密。我已经获得了RSA密钥的工作 - 并在一个测试字符串上进行了验证。但是,我的AES密钥无法使用 - 我收到javax.crypto.BadPaddingException: Given final block not properly paddedCryptoAPI C++使用AES与Java互操作

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

爪哇解密:

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的,但在那里,这将使第三方包工作做得更好什么不同吗?
+0

如果使用加密,它可能是高度完整的软件(或至少某些部分)。停止忽略来自CryptoAPI的返回值。 – jww 2012-01-06 22:34:26

+0

缺少的返回值只是为了简化这里的代码。实际的单元测试检查所有返回代码。 – RunHolt 2012-01-09 11:26:17

回答

2

我必须做一些事情来获得正确的信息:

  1. 明确设置KP_MODECRYPT_MODE_CBC,并KP_IV0
  2. 使用NoPadding在Java中解密
  3. 不要扭转密钥或消息的字节

在诊断问题方面,最有用的一条建议是在Java中设置阻止BadPaddingException的NoPadding。这让我看到了结果 - 即使是错误的。

奇怪的是,RSA Java/CryptoAPI互操作解决方案需要将消息完全字节反转以便与Java一起工作,但AES并不认为密钥或消息被字节反转。

CryptSetKeyParam不让我使用ZERO_PADDING,但是当查看解密的字节时,很明显CryptoAPI会填充未使用字节的数量。例如,如果块大小为16,如果最后一个块仅使用9个字节,则其余5个字节的值将为0x05。这是否会带来潜在的安全漏洞?我应该使用随机字节填充所有其他字节,并只使用最后一个字节来表示填充多少?

工作代码(使用最后一个字节是平板计的CryptoAPI的约定)低于(从地穴返回值的检查已经为简单起见删除):

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

不要忘记'CryptAcquireContext','CryptGenKey','CryptSetKeyParam'和朋友返回'TRUE' /'FALSE'。你应该断言并检查返回值。 – jww 2012-01-09 20:36:33

+0

“2.在Java解密中使用NoPadding” - 听起来不对。尝试对以下长度的消息进行加密/解密以验证正确性:0,1,15,16,17,31,32,33。 – jww 2012-01-09 20:38:02

+0

“例如,如果块大小为16,如果最后一个块仅使用9个字节,那么剩下的11个字节的值就是0x05“ - 由于CryptoAPI使用PKCS5,所以这是不正确的。在这个例子中,由于剩下五个字节,会有五个0x05字节。如果有11个剩余字节,则该缓冲区将是11个字节的0x11。唯一的角落案例是0字节,它获得16个字节的0x16以确保消除歧义。 – jww 2012-01-09 20:41:52

1

你做得太多回旋的Windows下的AES密钥。使用CryptImportKey将其设置为已知值 - 请参阅例如WinAES: A C++ AES Class

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

PKCS5填充用于通过缺省对称密码。我什至不记得如何改变它(如果可能的话)。我怀疑你只有其他选择是'没有填充'。

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

+0

在Java中,您将至少具有XML加密兼容填充(仅填充填充八比特组的最后一个字节)。我有时会使用'/ NoPadding'来查看问题是否与填充或键/ IV本身有关。例如,如果安装Bouncy Castle(一位设置为1,其余为零),则可以使用“/ ISO7816Padding”。 – 2012-01-07 12:10:47

0

Q1 & Q2:根本不依赖于默认。为了可维护性,您可以选择三种选择:让每个人都找出默认设置(不是我认为的最佳选项),使用注释或简单地设置所有可能的参数。就个人而言,我会一直选择第三种选择 - 其他选项太脆弱。

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

Q4不,不是。如果你想留在Java中,Java JCE工作得很好。 Bouncy Castle有(方式)更多的功能,并可能有不同的性能特点。您可以使用其他提供程序使用不同的密钥存储区(例如操作系统相关或智能卡),使用性能增强(本机)实现等。

可能有可能您需要密钥的反转,因为Java使用的是big endian,而C++可能使用little endian。我不能幻想C++会颠倒输入/输出的字节。通常他们都不代表数字,所以两个平台的顺序应该是相同的。

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

+0

明确设置的好主意 - – RunHolt 2012-01-09 15:42:45

+0

“你可能需要密钥的反转,因为Java使用的是big endian,而C++可能使用的是little endian。我不能幻想C++会颠倒输入/输出的字节“ - 微软在时间的CryptoAPI上做了它。例如,我相信PUBLICKEYBLOB中的字节数组是字节交换的(即,Microsoft是小尾数)。 – jww 2012-01-09 20:33:22

+0

关键数据本身并不需要交换任何字节(尽管RSA加密需要交换消息的所有字节)。 PUBLICKEYBLOB的字段确实需要交换(只是AlgId和keyLength - 因为其他字段是单个字节)。 – RunHolt 2012-01-09 20:56:06