2012-01-06 108 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