Pergunta

I am working on a product that needs to be able to consume files created with an older product. Some of these files contain content encrypted with RC4 encryption using MS CryptoAPI. I have thus far been unable to successfully decrypt the content using other crypto libraries. After some experimentation, it appears that CryptoAPI's RC4 output is "correct" (i.e. agrees with other libraries) when the key is ASCII, but is "wrong" (differs from other libraries that all agree) when the key is not ASCII (e.g. a hash of a password).

Since all the content I'm interested in consuming was encrypted with a key that was derived from a password via hashing, I'm a bit stuck at the moment. I've written up a small test to show the issue that includes 3 test cases as you can see below in the code. Botan (C++) and CryptoJS (JS) always agree on output. MS CryptoAPI, however, only agrees for the ASCII keys.

Before I go to MS with this issue, is anyone aware of something I'm misunderstanding or doing wrong that could cause this issue?

Also, I apologize for my atrocious javascript.

#pragma pack (push, 1)
struct PlainTextKeyBlob
{
    BLOBHEADER _hdr;
    DWORD      _cbKey;
    BYTE       _key[1];
};
#pragma pack (pop)

void TestBotanAndMSCryptoRC4()
{
    struct TestItem
    {
        std::string key;
        std::string plainText;
    };

    TestItem TestItems[] = {
    { "Secret",               "Attack at dawn" },  // Example taken from Wikipedia RC4 page to verify output.
    { "!\\\"#$%&'()*+",       "Encrypt me" } ,     // Key with various ASCII symbols.
    { "\xF4\xE7\xA8\x74\x0D", "Encrypt me" }       // Key is first 5 bytes of SHA1 hash of "Secret".
    };
    DWORD NumTestItems = _countof(TestItems);

    for( DWORD i = 0; i < NumTestItems; i++ )
    {
        // Botan Encryption
        Botan::SymmetricKey symmKey((BYTE*)TestItems[i].key.c_str(), TestItems[i].key.size());
        Botan::Pipe pipe(Botan::get_cipher("ARC4", symmKey, Botan::ENCRYPTION));
        pipe.process_msg(TestItems[i].plainText);
        SecureByteVector& encryptedBuff = pipe.read_all();

        // MS Crypto API Encryption
        AutoCryptProv CryptProv;
        if( !CryptAcquireContext( CryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
        {
            ASSERT(false);
            return;
        }

        DWORD blobKeySize = TestItems[i].key.size();
        DWORD blobSize    = sizeof(PlainTextKeyBlob) + blobKeySize - 1;
        CByteArray keyBlob;
        keyBlob.SetSize(blobSize);

        PlainTextKeyBlob *pKeyBlob = reinterpret_cast<PlainTextKeyBlob*>(keyBlob.GetData());
        pKeyBlob->_hdr.bType       = PLAINTEXTKEYBLOB;
        pKeyBlob->_hdr.bVersion    = CUR_BLOB_VERSION;
        pKeyBlob->_hdr.reserved    = 0;
        pKeyBlob->_hdr.aiKeyAlg    = CALG_RC4;
        pKeyBlob->_cbKey           = TestItems[i].key.size();
        memcpy_s(pKeyBlob->_key, blobKeySize, TestItems[i].key.c_str(), TestItems[i].key.size());

        AutoCryptKey CryptKey;
        if( !CryptImportKey(CryptProv, reinterpret_cast<BYTE*>(pKeyBlob), blobSize, NULL, 0, CryptKey) )
        {
            ASSERT(false);
            return;
        }

        CByteArray dataBytes;
        dataBytes.SetSize(TestItems[i].plainText.size());
        memcpy_s(dataBytes.GetData(), dataBytes.GetSize(), TestItems[i].plainText.c_str(), TestItems[i].plainText.size());

        DWORD buffSize = dataBytes.GetSize();
        if( !CryptEncrypt(CryptKey, 0, TRUE, 0, dataBytes.GetData(), &buffSize, dataBytes.GetSize()) )
        {
            ASSERT(false);
            return;
        }

        ASSERT(encryptedBuff.size() == dataBytes.GetSize());
        ASSERT( 0 == memcmp(encryptedBuff.begin(), dataBytes.GetData(), dataBytes.GetSize()) );
    }
}

<html>
<body>
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/rc4.js"></script>
<div id="output" />
<script>
    var key1 = CryptoJS.enc.Hex.parse('536563726574'); // Secret
    var message1 = "Attack at dawn";
    var encrypted1 = CryptoJS.RC4.encrypt(message1, key1);

    var key2 = CryptoJS.enc.Hex.parse('215c22232425262728292a2b'); // !\"#$%&'()*+
    var message2 = "Encrypt me";
    var encrypted2 = CryptoJS.RC4.encrypt(message2, key2);

    var key3 = CryptoJS.enc.Hex.parse('f4e7a8740d'); // First 5 bytes of hash of "Secret"
    var message3 = "Encrypt me";
    var encrypted3 = CryptoJS.RC4.encrypt(message3, key3);

    var elem = document.getElementById("output");
    elem.innerHTML = "Key1: " + encrypted1.key + "<br> ciphertext1: " + encrypted1.ciphertext + "<br><br>" +
                     "Key2: " + encrypted2.key + "<br> ciphertext2: " + encrypted2.ciphertext + "<br><br>" +
                     "Key3: " + encrypted3.key + "<br> ciphertext3: " + encrypted3.ciphertext;
</script>
</body>
</html>
Foi útil?

Solução

I just wanted to follow up with the answer I found in case this is useful to someone else in the future.

First and foremost, the answer was to RTFM! As it turns out, by default, CryptDeriveKey adds a "salt" to 40-bit symmetric keys consisting of all 0's. When I looked at our old CryptoAPI code and saw that we weren't passing any flags into it, I assumed this meant nothing special was going on and I didn't read through the details of all possible flags on MSDN. Furthermore, since MS considers this portion of the key a salt it is not included when you export the key so that was also a dead end.

I eventually found my way to http://msdn.microsoft.com/en-us/library/windows/desktop/aa387695(v=vs.85).aspx which explains the salt mechanism for 40-bit keys and the line that caught my eye was that, for compatibility, you should create keys with the CRYPT_NO_SALT flag. In our case, we already have encrypted content with such keys, so we simply modified our Botan/CryptoJS code to append 11 bytes of 0s to the end of the base 40-bit key.

The details for the various CryptDeriveKey flags can be found here: http://msdn.microsoft.com/en-us/library/windows/desktop/aa379916(v=vs.85).aspx

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top