c++encryptionrsapublic-keycryptoapi

Crypto API RSA public key can decrypt data, is not asymmetric as expected


The problem I am encountering is that I am able to decrypt data using the same RSA 2048-bit public key that was used to encrypt the data. It seems to me that this defeats the entire purpose of encrypting the data in the first place, if a public key can decrypt it. The only thing I can consider at this time is that I'm generating symmetric key exchange pairs when I think I'm generating asymmetric pairs.

The end-user purpose of this is to use it later for transmitting user credentials to be authenticated when using an application away from the office, when I am unable to use their cached credentials from their workstations on the domain. I would theoretically be able to then decrypt these credentials using only the private key.

I have produced a simple test class and code to reproduce my problem. The steps I'm taking are as follows:

  1. Acquire a context to Microsoft Enhanced Cryptographic Provider v1.0
  2. Generate a public / private key pair.
  3. Export the public and private key BLOBs to separate files.
  4. Load up the public key and encrypt some simple text.
  5. Attempt to decrypt the same encrypted text using the public key (I expected it to fail here except for when I'm using the private key - yet both work).

TestEncryptDecrypt helper class: TestEncryptDecrypt.h

#pragma once
#include <Windows.h>
#include <wincrypt.h>

class TestEncryptDecrypt
{
public:
    TestEncryptDecrypt()
    {
    }
    ~TestEncryptDecrypt()
    {
        if (hKey != NULL)
            CryptDestroyKey(hKey);

        if (hProvider != NULL)
            CryptReleaseContext(hProvider, 0);
    }

    BOOL InitializeProvider(LPCTSTR pszProvider, DWORD dwProvType)
    {
        if (hProvider != NULL)
        {
            if (!CryptReleaseContext(hProvider, 0))
                return 0;
        }

        return CryptAcquireContext(&hProvider, NULL, pszProvider, dwProvType, 0);
    }

    BOOL Generate2048BitKeys(ALG_ID Algid)
    {
        DWORD dwFlags = (0x800 << 16) | CRYPT_EXPORTABLE;
        return CryptGenKey(hProvider, Algid, dwFlags, &hKey);
    }

    VOID ExportPrivatePublicKey(LPTSTR lpFileName)
    {
        if (hKey == NULL)
            return;

        DWORD dwDataLen = 0;
        BOOL exportResult = CryptExportKey(hKey, NULL, PRIVATEKEYBLOB, 0, NULL, &dwDataLen);
        LPBYTE lpKeyBlob = (LPBYTE)malloc(dwDataLen);
        exportResult = CryptExportKey(hKey, NULL, PRIVATEKEYBLOB, 0, lpKeyBlob, &dwDataLen);
        WriteBytesFile(lpFileName, lpKeyBlob, dwDataLen);
        free(lpKeyBlob);
    }

    VOID ExportPublicKey(LPTSTR lpFileName)
    {
        if (hKey == NULL)
            return;

        DWORD dwDataLen = 0;
        BOOL exportResult = CryptExportKey(hKey, NULL, PUBLICKEYBLOB, 0, NULL, &dwDataLen);
        LPBYTE lpKeyBlob = (LPBYTE)malloc(dwDataLen);
        exportResult = CryptExportKey(hKey, NULL, PUBLICKEYBLOB, 0, lpKeyBlob, &dwDataLen);
        WriteBytesFile(lpFileName, lpKeyBlob, dwDataLen);
        free(lpKeyBlob);
    }

    BOOL ImportKey(LPTSTR lpFileName)
    {
        if (hProvider == NULL)
            return 0;

        if (hKey != NULL)
            CryptDestroyKey(hKey);

        LPBYTE lpKeyContent = NULL;
        DWORD dwDataLen = 0;
        ReadBytesFile(lpFileName, &lpKeyContent, &dwDataLen);
        BOOL importResult = CryptImportKey(hProvider, lpKeyContent, dwDataLen, 0, 0, &hKey);

        delete[] lpKeyContent;

        return importResult;
    }

    BOOL EncryptDataWriteToFile(LPTSTR lpSimpleDataToEncrypt, LPTSTR lpFileName)
    {
        DWORD SimpleDataToEncryptLength = _tcslen(lpSimpleDataToEncrypt)*sizeof(TCHAR);
        DWORD BufferLength = SimpleDataToEncryptLength * 10;
        BYTE *EncryptedBuffer = new BYTE[BufferLength];
        SecureZeroMemory(EncryptedBuffer, BufferLength);
        CopyMemory(EncryptedBuffer, lpSimpleDataToEncrypt, SimpleDataToEncryptLength);

        BOOL cryptResult = CryptEncrypt(hKey, NULL, TRUE, 0, EncryptedBuffer, &SimpleDataToEncryptLength, BufferLength);
        DWORD dwGetLastError = GetLastError();

        WriteBytesFile(lpFileName, EncryptedBuffer, SimpleDataToEncryptLength);

        delete[] EncryptedBuffer;

        return cryptResult;
    }

    BOOL DecryptDataFromFile(LPBYTE *lpDecryptedData, LPTSTR lpFileName, DWORD *dwDecryptedLen)
    {
        if (hKey == NULL)
            return 0;

        LPBYTE lpEncryptedData = NULL;
        DWORD dwDataLen = 0;
        ReadBytesFile(lpFileName, &lpEncryptedData, &dwDataLen);
        BOOL decryptResult = CryptDecrypt(hKey, NULL, TRUE, 0, lpEncryptedData, &dwDataLen);
        *dwDecryptedLen = dwDataLen;
        //WriteBytesFile(L"decryptedtest.txt", lpEncryptedData, dwDataLen);
        *lpDecryptedData = new BYTE[dwDataLen + 1];
        SecureZeroMemory(*lpDecryptedData, dwDataLen + 1);
        CopyMemory(*lpDecryptedData, lpEncryptedData, dwDataLen);

        delete[]lpEncryptedData;

        return decryptResult;
    }

    VOID WriteBytesFile(LPTSTR lpFileName, BYTE *content, DWORD dwDataLen)
    {
        HANDLE hFile = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE, 0x7, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        DWORD dwBytesWritten = 0;
        WriteFile(hFile, content, dwDataLen, &dwBytesWritten, NULL);
        CloseHandle(hFile);
    }

private:
    HCRYPTPROV hProvider = NULL;
    HCRYPTKEY hKey = NULL;

    VOID ReadBytesFile(LPTSTR lpFileName, BYTE **content, DWORD *dwDataLen)
    {
        HANDLE hFile = CreateFile(lpFileName, GENERIC_READ, 0x7, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        DWORD dwFileLength = 0;
        DWORD dwBytesToRead = GetFileSize(hFile, NULL);
        DWORD dwBytesRead = 0;

        *content = new BYTE[dwBytesToRead + 1];
        SecureZeroMemory(*content, dwBytesToRead + 1);

        ReadFile(hFile, *content, dwBytesToRead, &dwBytesRead, NULL);

        *dwDataLen = dwBytesRead;

        CloseHandle(hFile);
    }
};

Test Code: Main .cpp file

#include "stdafx.h"
#include "TestEncryptDecrypt.h"
#include <Windows.h>
#include <wincrypt.h>

int main()
{
    TestEncryptDecrypt *edc = new TestEncryptDecrypt();
    //Initialize the provider
    edc->InitializeProvider(MS_ENHANCED_PROV, PROV_RSA_FULL);

    //Generate a 2048-bit asymmetric key pair
    edc->Generate2048BitKeys(CALG_RSA_KEYX);

    //Export the private / public key pair
    edc->ExportPrivatePublicKey(L"privpubkey.txt");

    //Export only the public key
    edc->ExportPublicKey(L"pubkey.txt");

    //Import the public key (destroys the private/public key pair already set)
    edc->ImportKey(L"pubkey.txt");

    //Encrypt and write some test data to file
    edc->EncryptDataWriteToFile(TEXT("Hello World!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"), L"encrypteddata.txt");

    //Decrypt the data from file using the same public key (this should fail but it doesn't)
    DWORD dwDataLen = 0;
    LPBYTE lpDecryptedData = NULL;
    edc->DecryptDataFromFile(&lpDecryptedData, L"encrypteddata.txt", &dwDataLen);

    //Write the supposedly decrypted data to another file
    edc->WriteBytesFile(L"decrypteddata.txt", lpDecryptedData, dwDataLen);

    //Clear data
    delete[] lpDecryptedData;
    delete edc;

    return 0;
}

Unfortunately I don't get the opportunity to work with C++ very often so you may notice some problems. Feel free to constructively criticize.

Does anyone know why I am able to decrypt data using the same public key? My goal is to be able to irreversibly encrypt something on the client side where it can only be decrypted on the server, where the private key will hide.

Edit: I had considered that the hKey wasn't being destroyed properly by the ImportKey method, so I wrote this test case instead (same results - public key can encrypt and decrypt the data):

// CPPTests.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "TestEncryptDecrypt.h"
#include <Windows.h>
#include <wincrypt.h>

int main()
{
    TestEncryptDecrypt *edc = new TestEncryptDecrypt();
    //Initialize the provider
    edc->InitializeProvider(MS_ENHANCED_PROV, PROV_RSA_FULL);

    //Generate a 2048-bit asymmetric key pair
    edc->Generate2048BitKeys(CALG_RSA_KEYX);

    //Export the private / public key pair
    edc->ExportPrivatePublicKey(L"privpubkey.txt");

    //Export only the public key
    edc->ExportPublicKey(L"pubkey.txt");

    //Destroy everything and load up only the public key to write some encrypted data
    delete edc;
    edc = new TestEncryptDecrypt();
    edc->InitializeProvider(MS_ENHANCED_PROV, PROV_RSA_FULL);
    edc->ImportKey(L"pubkey.txt");

    //Encrypt and write some test data to file
    edc->EncryptDataWriteToFile(TEXT("Hello World!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"), L"encrypteddata.txt");

    //Destroy everything and load up only the public key to read some encrypted data
    delete edc;
    edc = new TestEncryptDecrypt();
    edc->InitializeProvider(MS_ENHANCED_PROV, PROV_RSA_FULL);
    edc->ImportKey(L"pubkey.txt");

    //Decrypt the data from file using the same public key (this should fail but it doesn't)
    DWORD dwDataLen = 0;
    LPBYTE lpDecryptedData = NULL;
    edc->DecryptDataFromFile(&lpDecryptedData, L"encrypteddata.txt", &dwDataLen);

    //Write the supposedly decrypted data to another file
    edc->WriteBytesFile(L"decrypteddata.txt", lpDecryptedData, dwDataLen);

    //Clear data
    delete[] lpDecryptedData;
    delete edc;

    return 0;
}

Solution

  • This API is deprecated according to Microsoft, so if you came here looking for a native cryptography API, you may want to look elsewhere.

    After some fighting with the same problem I realized where the error was.

    In your first code you were acquiring your context with the last flag set to zero:

    CryptAcquireContext(&hProvider, NULL, pszProvider, dwProvType, 0);
    

    But in your solution you changed it into CRYPT_VERIFYCONTEXT.

    CryptAcquireContext(&hProvider, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
    

    You solved your problem by changing this flag, not by importing the keys from OpenSSL. I am pretty sure that if you test this in your initial code, it will work as expected.

    This CRYPT_VERIFYCONTEXT flag is responsible for not allowing a key to achieve persistence in the system, a persistence which turned the public RSA able to encrypt and decrypt.