I'm continuing my adventures with encryption in Windows. Last time, we looked at public/private key encryption using RSA. It was kind of a pain. Next I got symmetric key encryption working. Spoiler alert: it was much eaiser.
Most of the reason it was probably easier is from the fact that we're generating a symmetric key from scratch, rather than getting a pre-generated key from a certificate. In fact, if I had generated the RSA public/private key pair at runtime in the previous exercise, it probably would have been tantamount in its simplicitly.
Much of the code from the RSA encryption can be carried over wholesale: the calls to CryptEncrypt
and CryptDecrypt
, for instance, are identical. The new calls that are introduced are for CryptGenKey
and CryptDeriveKey
. The former is used to create a new, random key; the latter is used to deterministically create a new key based on a seed value.
The only real hiccup that you may choke up from your programming throat here is the selection of the algorithm. Windows supports a large number of cryptographic hash and key algorithms, which are all outlined on the page for ALG_ID
. The trick is that they require different "cryptographic service providers". I'm not really sure why this concept even exists, but that's how it is.
So if you want to make an RC4 key, you need to use the Base Cryptographic Provider, but if you want an AES key, or, oddly, a SHA1 hash, you'll need an instance of the AES and RSA provider. Maybe it's not that odd; maybe I just don't know enough crypto for it to make sense.
The way you specify the provider to use is to pass it into your call to CryptAcquireContextW
(hey, remember this little guy from last time?). And you'll need to go through the same dance of creating a new "key set" for the generated key to be stored in, too.
When generating or deriving the key, make sure to flag it with CRYPT_EXPORTABLE
, so that you can export the key to a blob later. Well, maybe you don't need to, but if you're going to be sending it to the other end of a secure channel, you'll need to.
When it comes to deriving the key, the way to specify the base data is by using a hash. You have some piece of raw data that is your "seed", and the key is derived from this hash value. Creating a hash is a pretty straightforward two-step process: call CryptCreateHash
to create an empty hash object and CryptHashData
to actually hash data in the object.
It took a few tries to get the right parameters to CryptExportKey
, specifically the dwBlobType
parameter, which indicates the format in which to export the key. I thought SIMPLEBLOB
(export a session key) sounded pretty good, but that actally requires a public key that automatically encrypts the session key for safety. It turns out this actually would suit my purposes here, but I wanted to do all of this "the hard way", so I wanted the raw key value. For that, you pass PLAINTEXTKEYBLOB
. Not too bad.
That's it! Here's the (elided) output from the program. I wrote a few basic tests to validate my assumptions about the randomness or determinism of the types of keys I was generating and their encryption/decryption results. In my code, I'm using AES128 for the symmetric keys and SHA256 for the hashes.
>symenc.exe abc
------------- Testing Random Key Generation ------------
random keys:
D9 99 9D A9 6B 54 37 D1 C1 51 26 F0 10 63 A9 81
E5 90 F3 E6 D6 BB D1 35 6A 65 D5 91 22 4B D6 C6
PASS: random keys are different
------------- Testing Same Fixed Key Generation ------------
hash value:
BA 5E C5 1D 07 A4 AC 0E 95 16 08 70 44 31 D5 9A 02 B2 1A 4E 95 1A CC 10 50 5A 8D C4 07 C5 01 EE
hash value:
BA 5E C5 1D 07 A4 AC 0E 95 16 08 70 44 31 D5 9A 02 B2 1A 4E 95 1A CC 10 50 5A 8D C4 07 C5 01 EE
fixed keys:
BA 5E C5 1D 07 A4 AC 0E 95 16 08 70 44 31 D5 9A
BA 5E C5 1D 07 A4 AC 0E 95 16 08 70 44 31 D5 9A
PASS: fixed keys are the same
------------- Testing Different Fixed Key Generation ------------
hash value:
9D 1E 0E 2D 94 59 D0 65 23 AD 13 E2 8A 40 93 C2 31 6B AA FE 7A EC 5B 25 F3 0E BA 2E 11 35 99 C4
hash value:
4D 7B 3E F7 30 0A CF 70 C8 92 D8 32 7D B8 27 2F 54 43 4A DB C6 1A 4E 13 0A 56 3C B5 9A 0D 0F 47
fixed keys:
9D 1E 0E 2D 94 59 D0 65 23 AD 13 E2 8A 40 93 C2
4D 7B 3E F7 30 0A CF 70 C8 92 D8 32 7D B8 27 2F
PASS: fixed keys are different
--------- random key - encrypt and decrypt ---------------
exported random key:
63 30 4B 6B CF 93 97 A1 53 47 C4 FE 8F 1F 97 2D
cleartext:
61 00 62 00 63 00 64 00
encrypted:
A0 86 DF 42 23 DC 3B 71 90 97 57 1F 75 CC FD 56
decrypted:
61 00 62 00 63 00 64 00
PASS: cleartext and decrypted are the same
--------- fixed key - encrypt and decrypt ---------------
hash value:
CA 97 81 12 CA 1B BD CA FA C2 31 B3 9A 23 DC 4D A7 86 EF F8 14 7C 4E 72 B9 80 77 85 AF EE 48 BB
exported fixed key:
CA 97 81 12 CA 1B BD CA FA C2 31 B3 9A 23 DC 4D
cleartext:
61 00 62 00 63 00 64 00
encrypted:
A0 86 DF 42 23 DC 3B 71 90 97 57 1F 75 CC FD 56
decrypted:
61 00 62 00 63 00 64 00
PASS: cleartext and decrypted are the same
--------- mismatched keys - encrypt and decrypt ---------------
cleartext:
61 00 62 00 63 00 64 00
encrypted:
A0 86 DF 42 23 DC 3B 71 90 97 57 1F 75 CC FD 56
decrypting
PASS: got expected hr 80090005 for bad decryption
Woah, I didn't notice this until reading this output in a web browser, but the AES128 key derived from a SHA256 hash is just a truncation of it. What the...? Is that normal? It could explain why these SHA hashes are provided by the same cryptographic service provider as AES keys.
And I musn't be remiss in providing the code for the project. This time only the Makefile and cpp file are needed! Weeeee!