cwindowsencryptioncryptdecrypt

CryptDecrypt works but returns error 0x80090005


I have a problem with the following code (which decrypts a file using a known key):

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <wincrypt.h>

void error(const char *what) {
    fprintf(stderr, "%s failed with last error 0x%x\n", what, GetLastError());
    exit(1);
}

#define AES_KEY_SIZE 32

typedef struct {
    BLOBHEADER hdr;
    DWORD dwKeySize;
    BYTE rgbKeyData[AES_KEY_SIZE];
} AES256KEYBLOB;

BYTE *hex2byte(const char *hex) {
    int len = strlen(hex) / 2;
    BYTE *bytes = malloc(len);
    if (bytes == NULL) {
        error("malloc");
        return NULL;
    }
    unsigned char val[2];

    for (int i = 0; i < len; i++) {
        sscanf(&hex[i * 2], "%2hhx", &val);
        bytes[i] = val[0];
    }
    return bytes;
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <input_file> <output_file>\n", argv[0]);
        return 1;
    }

    BYTE *key = hex2byte("c2ecf7a4f3d1620f18cd38379269948fcd3aaf4fdce6d50d310464ea823a");

    AES256KEYBLOB aes256KeyBlob;
    aes256KeyBlob.hdr.bType = PLAINTEXTKEYBLOB;
    aes256KeyBlob.hdr.bVersion = CUR_BLOB_VERSION;
    aes256KeyBlob.hdr.reserved = 0;
    aes256KeyBlob.hdr.aiKeyAlg = CALG_AES_256;
    aes256KeyBlob.dwKeySize = AES_KEY_SIZE;
    memcpy(aes256KeyBlob.rgbKeyData, key, AES_KEY_SIZE);

    HCRYPTPROV hProv;
    if (!CryptAcquireContextA(&hProv, NULL, MS_ENH_RSA_AES_PROV_A, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
        error("CryptAcquireContext");
    }

    HCRYPTKEY hKey;
    if (!CryptImportKey(hProv, (BYTE *) &aes256KeyBlob, sizeof(AES256KEYBLOB), 0, CRYPT_EXPORTABLE, &hKey)) {
        CryptReleaseContext(hProv, 0);
        error("CryptImportKey");
    }

    DWORD numBytes = 0;
    if (!CryptEncrypt(hKey, 0, TRUE, 0, NULL, &numBytes, 0)) {
        error("CryptEncrypt");
    }

    FILE *input_file = fopen(argv[1], "rb");
    if (!input_file) {
        perror("Error opening input file");
        return 1;
    }

    FILE *output_file = fopen(argv[2], "wb");
    if (!output_file) {
        perror("Error opening output file");
        fclose(input_file);
        return 1;
    }

    BYTE blk[numBytes];
    while (fread(blk, 1, numBytes, input_file) == numBytes) {

        if (!CryptDecrypt(hKey, 0, TRUE, 0, blk, &numBytes)) {
            //error("CryptDecrypt");
        }

        fwrite(blk, 1, numBytes, output_file);
    }

    free(key);
    CryptDestroyKey(hKey);
    CryptReleaseContext(hProv, 0);
    fclose(input_file);
    fclose(output_file);

    return 0;
}

This code WORKS, but I had to comment the error checking line:

//error("CryptDecrypt");

I tried as suggested in other stackoverflow solution to increase the buffer size but it does not help. I don't care much because the code works, but I wish to understand the reason and how to avoid it. Note if I use:

CryptDecrypt(hKey, 0, FALSE, 0, blk, &numBytes)

I get no error but it works only for the first block.

Please don't mark this as a duplicate because none of the solutions work.


Solution

  • Since the encryption code was not posted, it is not clear how the ciphertext was generated. A test shows that a decryption with the posted code is only possible under the described conditions if the ECB mode was used (a decryption of a ciphertext generated with CBC fails).

    The following changes are necessary in the posted code for a proper and error message-free decryption:

    With this, decryption is performed correctly and no more error message is displayed.


    Why did the decryption still work with the code posted in the question (apart from the error message)? After all, the CBC mode with a Zero-IV is used by default, which is actually not compatible with the data generated in ECB mode.

    The reason is: As already stated above, the ciphertext is read and decrypted block by block in the posted code. The final flag is set to TRUE for all blocks. A TRUE sets the IV back to the initial state (i.e. to a Zero-IV). In addition, encrypting a single block in CBC mode with a Zero-IV results in the same ciphertext as encrypting the same block in ECB mode. For this reason, every block is successfully decrypted.
    On the other hand, a TRUE for the final flag also means an unpadding is performed. This generally fails (as only the last block is padded), which leads to an error with the error code 0x80090005 (Error: Bad Data).
    The overall result is an error message with otherwise successful decryption.

    However, if you now try to avoid unpadding by setting the final flag for all blocks to FALSE, the error message is no longer triggered, but the ciphertext is decrypted incorrectly because the IV is no longer reset. In order for the ciphertext to be decrypted correctly again, the mode must be set to ECB. And to ensure that unpadding takes place for the last block, the final flag for the last block must be set to TRUE. This in turn leads to the above solution (with chunk size 16).


    Edit:
    In the comments, it turned out that you are looking for an equivalent to

    openssl enc -aes-256-ecb -d -in "encoded file" -out "decoded file" -nopad -K <hex key>
    

    i.e. a decryption with ECB without padding. Since without padding the final flags of all blocks are to be set to FALSE, the loop can be simplified a little.
    With regard to the performance issues mentioned in the comment: The above 64 bytes were intended for test purposes. For better performance choose a larger chunk size (to avoid unnecessarily many fread() calls), which is at the same time compatible with the available memory, e.g. 64 MB:

    DWORD chunkSize = 64 * 1024 * 1024; // choose a proper chunk size (a multiple of 16 bytes)
    BYTE* buffer = (BYTE*)malloc(chunkSize); 
    DWORD dataLen;
    while ((dataLen = fread(buffer, 1, chunkSize, input_file)) > 0) {
        if (!CryptDecrypt(hKey, 0, FALSE, 0, buffer, &dataLen)) { 
            error("CryptDecrypt");
        }
        fwrite(buffer, 1, dataLen, output_file);
    }
    free(buffer);