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



我正在尝试使用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++中独家加密和解密。

问题:

  1. 我应该使用CBC/PKCS5PADDING吗?这是MS_ENH_RSA_AES_PROV的默认值吗
  2. 零IV确实是MS_ENH_RSA_AES_PROV的默认值吗
  3. 有什么方法可以诊断钥匙的具体表现吗
  4. 我想坚持使用标准的Java包,而不是安装BouncyCastle,但有什么不同之处可以让第三方包更好地工作吗

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

  1. 显式设置KP_MODECRYPT_MODE_CBCKP_IV0
  2. NoPadding在Java解密中的应用
  3. 不要反转密钥或消息的字节数

就诊断问题而言,最有用的建议是在Java中设置NoPadding,以防止BadPaddingException。这让我看到了结果——即使是错误的。

奇怪的是,为了使用Java,RSA Java/CryptoAPI互操作解决方案要求消息完全反转字节,但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类。

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

PKCS5填充默认用于对称密码。我甚至不记得该怎么改(如果可能的话)。我怀疑你唯一的选择就是"不填充"。

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

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

Q3否,如果密钥的位错误或顺序不正确(见下文),您将得到错误的填充异常垃圾输出。您可以在Java中使用"/NoPPadding"进行解密(或在C++中使用类似的方法)。通过这种方式,您可以通过查看输出来查看是否存在填充问题。如果你的纯文本在那里,那么你很可能有填充问题。如果只有第一个区块是错误的,那么你的IV 就有问题了

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

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

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

最新更新