by John S. Reid
February 19, 2005
Years ago I was tasked with a simple but fast implementation of encrypting strings
for placement in a database. The database involved had no native support for encrypting
strings, unlike many of the databases of today.
We mulled over a couple of different solutions and ended up going with a simple
symmetric cipher. A symmetric cipher uses the same password to both encrypt and
decrypt the data, while an assymmetric cipher uses one password to encrypt and another
to decrypt the message, like the public/private key systems in common use today
such as SSL encryption for web sites.
Symmetric ciphers have two advantages over the public key system: 1. they are much
faster, and 2. they don't require a third party to act as a repository and authority
over the keys, making them cheaper and easier to implement. The downside is that
there is no authentication capability and the secret is now spread across two or
more parties, potentially making them less secure.
In our solution we had a component that read and wrote encrypted strings and was
the sole holder of the password, so we went the easy route and simply compiled the
password into the application. For someone to get access to it they would need to
get a hold of the binary and look through it. To make things more difficult we used
a random string of bytes as the password, including NULL characters, instead of
a human readable string.
We weren't really fooling ourselves into thinking this was completely secure, so
the real security came from locking down access to the binary itself. Anyone who
had administrative access to the box could potentially discover the password but
that was considered an acceptable risk.
On to the code!
BOOL bSuccess = CryptAcquireContext(
&hProvider,
NULL,
MS_ENHANCED_PROV,
PROV_RSA_FULL,
CRYPT_VERIFYCONTEXT);
The call to CryptAcquireContext grabs a handle into the first parameter that represents
the handle to a key container for the cryptographic service provider you specify.
This handle is then used to create the hash and key you will use to encrypt and
decrypt the data.
The second parameter specifies the name of the key container to use. Here we leave
this parameter NULL to grab the default key container of the user process.
The third through fifth parameters are the interesting ones for this example. Here
we tell the function to grab the "Microsoft Enhanced Cryptographic Provider" which
supports 128 bit block encryption unlike the base provider which supports only 40
bit. I've chosen to use the RSA implementation in the fourth parameter, and the
fifth parameter I'm using here tells the system we don't need access to private
keys since we will be doing public key symmetric encryption and decryption
Setting up the encryption key.
After we have the context we can create the hash we are going to use for the password
and then add the password to the hash data. From there its a simple call to CryptDeriveKey
to create the key we need to encrypt and decrypt our data.
bSuccess = CryptCreateHash(
hProvider,
CALG_MD5,
0,
0,
&hHash);
bSuccess = CryptHashData(
hHash,
pPassword,
dwPasswordLen,
0);
bSuccess = CryptDeriveKey(
hProvider,
CALG_RC2,
hHash,
0x00800000 | CRYPT_CREATE_SALT,
&hKey);
DWORD dwData = 128;
bSuccess = CryptSetKeyParam(
hKey,
KP_EFFECTIVE_KEYLEN,
(LPBYTE)&dwData,
0);
Items to note are the MD5 algorithm in the call to CryptCreateHash, the use of the
block cipher RC2 in the call to CryptDeriveKey as well as the 128 bit salt.
Tip: If you are not supporting Windows NT4 you
won't need the call to CryptSetKeyParam. It is needed in Windows NT4 to make the effective
keylength 128 bits to overcome the 40 bit default in that implementation.
As long as both the encryptor and the decryptor set up the key to use in the same
manner with the same password they will be able to communicate.
The Encryption/Decryption.
bSuccess = CryptEncrypt(
hKey,
0,
TRUE,
0,
pData,
pdwLength,
dwBuffer);
bSuccess = CryptDecrypt(
hKey,
0,
TRUE,
0,
pData,
pdwLength);
CryptEncrypt and CryptDecrypt are pretty straightforward now that you have the key
to use. In fact, setting up the key is a lot more challenging than actually making
the encryption/decryption calls. To encrypt your data call CryptEncrypt and pass
in the key as the first parameter, the data to encrypt in a buffer large enough
to hold the encrypted data as parameter number four and a couple of length parameters.
Decrypting is practically the reverse.
The class I've written reduces all the code above to just three lines if you don't
count the necessary setup that needs to be done in either case.
C6Crypto crypto;
LPSTR szPassword = "myPassw0rd!";
char szData[1000];
strcpy(szData, "This string will be encrypted");
DWORD dwLength = strlen(szData);
crypto.SetPassword((LPBYTE)szPassword, strlen(szPassword));
crypto.Encrypt(&dwLength, sizeof(szData), szData);
crypto.Decrypt(&dwLength, szData);
Sound appealing? You can download the source
here.
After you set the password I recommend you call the IsInitialized() method to verify
that the key was created correctly. Any errors can be retrieved with the GetLastError()
method.
bool bInit = crypto.IsInitialized();
DWORD dwError = crypto.GetLastError();
The C6Crypto class is really only a helper class that makes it much easier to implement
symmetric ciphers in your code. I hope you can get as much good use out of this
class as I have.