I mentioned before that the SecTransform
API can be confusing. Needless to say, that is still true. Today I'm offering you a second lesson. For free. Free as in, you are required to stand and salute whenever you see me.
There are two broad categories of encryption algorithm, symmetric and asymmetric. With symmetric-key algorithms, the same key is used to both encrypt and decrypt the data. With asymmetric-key algorithms, on the other hand, the data is encrypted with one key, the public key, and decrypted with a different key, the private key. The public key is actually mathematically derived from the private key (but the private key cannot be derived from the public key, otherwise the whole scheme would collapse). I'm going to talk about symmetric-key encryption here, because … of reasons. I could tell you why, but you'd have to sign an NDA first. In blood.
My code sample will focus on the AES-128 encryption algorithm. AES-128 uses a 128-bit encryption key — or 16 bytes, whichever is bigger. The block size is also 16 bytes. The key is applied to each block separately, so if the length of the data to be encrypted is not a multiple of 16 bytes, it needs to be padded until it is a multiple of 16 bytes. In code, we first have to create a key (SecKeyRef
), then with the key we create a transform (SecTransformRef
) to encrypt or decrypt the data. Below is the beginning of a sample command-line tool:
#include <Security/Security.h>
int main(int argc, const char *argv[])
{
int keySizeInBits = kSecAES128;
int rounds = 33333;
UInt8 saltBytes[32];
if (SecRandomCopyBytes(kSecRandomDefault, 32, saltBytes ) != 0)
{
perror( "SecRandomCopyBytes" );
return EXIT_FAILURE;
}
CFNumberRef keySize = CFNumberCreate(NULL, kCFNumberIntType, &keySizeInBits);
CFNumberRef numberOfRounds = CFNumberCreate(NULL, kCFNumberIntType, &rounds);
CFDataRef salt = CFDataCreate(NULL, saltBytes, 32);
CFMutableDictionaryRef parameters = CFDictionaryCreateMutable(NULL, 3, NULL, NULL);
CFDictionaryAddValue(parameters, kSecAttrKeyType, kSecAttrKeyTypeAES);
CFDictionaryAddValue(parameters, kSecAttrKeySizeInBits, keySize);
CFDictionaryAddValue(parameters, kSecAttrPRF, kSecAttrPRFHmacAlgSHA256);
CFDictionaryAddValue(parameters, kSecAttrRounds, numberOfRounds);
CFDictionaryAddValue(parameters, kSecAttrSalt, salt);
CFErrorRef error = NULL;
SecKeyRef key = SecKeyDeriveFromPassword(CFSTR("Joshua"), parameters, &error);
if (key == NULL)
{
CFShow(error);
return EXIT_FAILURE;
}
We're going to use a password to encrypt and decrypt the data. Or to blow up the world. Or to prevent the world from blowing up. Your choice. We make sure to specify all the parameters for SecKeyDeriveFromPassword
in the code, even though some parameters are documented as optional, because once we encrypt the data, someone is going to have to decrypt it, and if there's any kind of mismatch in the algorithms, the decryption will fail. In fact, the documentation seems to lie, for it says that kSecAttrPRFHmacAlgSHA1
is the default value of kSecAttrPRF
, but Apple's open source suggests otherwise:
algorithmDictValue = (CFDataRef) utilGetStringFromCFDict(parameters, kSecAttrPRF, kSecAttrPRFHmacAlgSHA256);
We also borrow the value of kSecAttrRounds
from the open source. The number of rounds determines how long it takes, computationally, to derive the encryption key from the password. The slower the better, as far as password safety is concerned, although not of course as far as user interaction is concerned. The length of the salt was chosen to match the salting algorithm SHA-256, which was not strictly necessary, but it's a good idea. I won't explain cryptographic salt in this blog post, except to say that without it, your passwords may seem quite bland.
Now that we've created the key, let's create the transform and encrypt some data with it. In the sample code, I'm not going to do anything with the encrypted data except display it:
static void EncryptBytes(const UInt8 *bytes, CFIndex length, SecKeyRef key, CFDataRef iv, CFStringRef mode, CFStringRef padding)
{
CFErrorRef error = NULL;
SecTransformRef transform = SecEncryptTransformCreate(key, &error);
if ( transform == NULL )
{
CFShow(error);
CFRelease(error);
return;
}
if (mode != NULL && !SecTransformSetAttribute(transform, kSecEncryptionMode, mode, &error))
goto cleanup;
if (padding != NULL && !SecTransformSetAttribute(transform, kSecPaddingKey, padding, &error))
goto cleanup;
if (iv != NULL && !SecTransformSetAttribute(transform, kSecIVKey, iv, &error))
goto cleanup;
CFDataRef data = CFDataCreate(NULL, bytes, length);
if (SecTransformSetAttribute(transform, kSecTransformInputAttributeName, data, &error))
{
CFDataRef result = SecTransformExecute(transform, &error);
if (result != NULL)
{
CFShow(result);
CFRelease(result);
}
}
CFRelease(data);
cleanup:
if (error != NULL)
{
CFShow(error);
CFRelease(error);
}
CFRelease(transform);
}
The decryption process is identical to encryption, except that you would use SecDecryptTransformCreate
in place of SecEncryptTransformCreate
. It is important to note that if an initialization vector (iv
) is used for encryption, then decryption requires the same initialization vector. And indeed the same salt is required if both sides need to hash the key from the password. Again, an exposition of the cryptographic concepts of password salts and initialization vectors would fall outside the scope of this blog post. I'll simply warn you never to reuse an initialization vector! If you don't understand what you're doing, I would recommend paying someone more knowledgeable to do it for you. Preferably me, in large sums.
Except for kSecTransformInputAttributeName
, the transform attributes are documented as optional. Still, it's best to specify them all explicitly, as we did with the encryption key parameters. The default values can be quite unsuitable. For example, the documentation for kSecIVKey
says, "If you do not supply a value for this key, an appropriate value will be supplied for you." This is somewhat surprising, if you know how initialization vectors are supposed to work. Another look at the source reveals the "appropriate" value:
static uint8 iv[16] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
The most important of the optional attributes is kSecEncryptionMode
, because it determines the relevance of the other attributes. I'm only going to discuss two of the modes here, kSecModeECBKey
and kSecModeCBCKey
, corresponding to Electronic Codebook and Cipher Block Chaining. There's a tradeoff between them, because on the one hand, CBC requires padding. If you try to use kSecModeCBCKey
with kSecPaddingNoneKey
, you get an invalid attribute error. On the other hand, ECB does not use an initialization vector. No matter what you do with kSecIVKey
, it will be ignored if you specify kSecModeECBKey
.
You'll probably want to use kSecModeCBCKey
. In most applications, padding will be considered an advantage rather than a disadvantage, because without padding, you would need to provide all of your unencrypted data in block-size increments. If you use kSecModeECBKey
with kSecPaddingNoneKey
, and your input data length is not a multiple of 16 bytes, then any extra unpadded data will simply get dropped rather than encrypted, as the output will always be in blocks. Moreover, some form of input data randomization is advisable for security. You could try to hack something into kSecModeECBKey
by manually XORing the input data, but kSecModeCBCKey
already handles this automatically in a secure way, and implementing your own encryption scheme is perilous at best.
What about padding? For padding, you'll want to use kSecPaddingPKCS7Key
, though it actually doesn't seem to matter whether you use kSecPaddingPKCS1Key
, kSecPaddingPKCS5Key
, or kSecPaddingPKCS7Key
. Technically, kSecPaddingPKCS5Key
is just a very limited subset of kSecPaddingPKCS7Key
; the two are interchangeable in most implementations. And under the hood, kSecPaddingPKCS1Key
is swapped with kSecPaddingPKCS7Key
anyway:
else if (CFEqual(paddingStr, kSecPaddingPKCS1Key))
{
result = CSSM_PADDING_PKCS7; //CSSM_PADDING_PKCS1 ois not supported
}
Now that the attributes are all sorted out, we can call our encryption function:
UInt8 ivBytes[16];
if (SecRandomCopyBytes(kSecRandomDefault, 16, ivBytes ) != 0)
{
perror( "SecRandomCopyBytes" );
return EXIT_FAILURE;
}
CFDataRef iv = CFDataCreate(NULL, ivBytes, 16);
const UInt8 unencryptedBytes[30] = {
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF
};
EncryptBytes(unencryptedBytes, sizeof(unencryptedBytes), key, iv, kSecModeCBCKey, kSecPaddingPKCS7Key);
The unencrypted bytes could be anything. There's nothing special about my sample, other than the ease with which you can compare the unencrypted and encrypted bytes. The length of the initialization vector was chosen to match the encryption key size.
One more thing: as far as I can tell, you can't reuse a SecTransformRef
. If you try to set its kSecTransformInputAttributeName
to some new data after calling SecTransformExecute
, you get an error saying you can't do that while it's executing. Thus, you have to create a new transform each time you need to encrypt more data, even if you're using the same encryption key the whole time. This is inconvenient, but so it goes. And so I go.