I recently wondered how to encrypt the attributes of a Core Data
application. The encryption should be transparent to the application,
which means the entity objects should do all of the encryption/
decryption work and the rest of the application doesn’t even know
that there is something happening. This way, you can continue using
bindings with Interface Builder and all the other convenient stuff
which Core Data offers without worrying about your encryption/
decryption.
You can download the Demo Project (created with Xcode 3.1 beta).
Note: The Demo Project includes the SSCrypto Framework. Please
respect it’s copyright.
But how do you actually encrypt stuff using Cocoa? Obviously Cocoa
doesn’t offer any suitable objects – but Mac OS X does. Mac OS X
ships with OpenSSL, a powerful and robust tool which offers many
important cryptographic functions. OpenSSL is also a full-featured
library (libcrypto). This library is exactly what we need to perform
encryption within a Core Data application. If you take a closer look
at the OpenSSL library functions, you may find them a bit difficult
to use (especially if you have only a basic knowledge of all the
cryptographic stuff), but furtunatly there is SSCrypto – an easy-to-use
Cocoa Framework wich wraps around OpenSSL and does all the
tricky stuff for us.
This sounds pretty good, does it? So let’s see, how to put all this
together in a Core Data application.
Core Data doesn’t support encryption as well. If you’re coding for
Leopard only, maybe you can use an Atomic Store to encrypt/decrypt
your whole Persistent Store (I haven’t tried this yet, since my apps
have to be Tiger compatible).
Instead we will use Transient Attributes to transparently encrypt the attributes of our entities.
Each of the attributes we want to encrypt must be transient and
must have a corresponding “Data Attribute” of type “Binary Data”
which is persistent and actually holds the encrypted data. In XCode
your model could look like this:
Now we need to create a NSManagedObject subclass which represents
the entity. If you create this class with the XCode wizard, don’t let it
create Objective-C 2.0 Properties since we need to add custom
accessor methods. The class interface looks as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| @interface Item : NSManagedObject
{
}
- (NSString *)itemName;
- (void)setItemName:(NSString *)value;
- (NSData *)creationDateData;
- (void)setCreationDateData:(NSData *)value;
- (NSData *)itemNameData;
- (void)setItemNameData:(NSData *)value;
- (NSNumber *)itemId;
- (void)setItemId:(NSNumber *)value;
- (NSData *)itemIdData;
- (void)setItemIdData:(NSData *)value;
- (NSDate *)creationDate;
- (void)setCreationDate:(NSDate *)value;
@end |
We don’t have to touch the accessors for the “Data Attributs” but the
accessors for the transient attributes need to be customized in order
to perform encryption/decryption.
Let’s take a look at -(NSString *)itemName which performs
decryption of the attribute “itemName”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| - (NSString *)itemName
{
NSString * tmpValue;
[self willAccessValueForKey:@"itemName"];
tmpValue = [self primitiveValueForKey:@"itemName"];
[self didAccessValueForKey:@"itemName"];
if(!tmpValue || [tmpValue isEqualToString:@""]) {
NSData *encryptedData = [self valueForKey:@"itemNameData"];
/* if there are any encrypted data, decrypt it */
if(encryptedData) {
/* init SSCrypto with the stored key from the PasswordController */
SSCrypto *crypto = [[SSCrypto alloc] initWithSymmetricKey:[[PasswordController sharedInstance] keyData]];
[crypto setCipherText:[NSKeyedUnarchiver unarchiveObjectWithData:encryptedData]];
NSData *decryptedData = [crypto decrypt:@"aes256"];
/* some debugging output. maybe useful. you can wrap them with #ifdef/#endif directives to enable them only in debug configuration */
NSLog(@"Using symmetric key: %@", [[crypto symmetricKey] hexval]);
NSLog(@"Archived itemName: %@", [encryptedData hexval]);
NSLog(@"Encrypted itemName: %@", [[crypto cipherTextAsData] hexval]);
NSLog(@"Decrypted itemName: %@", [crypto clearTextAsString]);
tmpValue = [[NSString alloc] initWithData:decryptedData encoding:NSUTF8StringEncoding];
[self setPrimitiveValue:tmpValue forKey:@"itemName"];
[crypto release];
}
else {
/* insert the defalt value here */
tmpValue = @"Default Value";
[self setItemName:tmpValue];
}
}
return tmpValue;
} |
Basically, this method loads the encrypted data (if it exists) and
decrypts it. The decrypted data is stored in the primitive value so
it doesn’t need to be decrypted each time it is accessed – only when
it is read for the first time. If there is no encrypted data (a new
entity is inserted), then the default value is used.
The set accessor for the itemName attribute works in a
similar way. It performs the following steps:
If the new value equals the old one, return. This avoids any
unnecessary encryption which dramatically reduces cpu load, since
Core Data acesses the attributes quite heavily
Store the new value in the primitive value
Encrypt the new value and store it in the “Data Attribute”
Here’s the code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| - (void)setItemName:(NSString *)value
{
NSString * tmpValue;
[self willAccessValueForKey:@"itemName"];
tmpValue = [self primitiveValueForKey:@"itemName"];
[self didAccessValueForKey:@"itemName"];
/* if the new value is equal to the old value, return. this improves the performance a lot, since core data
accesses the attributes quite heavily and encrypting them would stress the cpu when it's not really necessary*/
if([tmpValue isEqualToString:value])
return;
[self willChangeValueForKey:@"itemName"];
[self setPrimitiveValue:value forKey:@"itemName"];
[self didChangeValueForKey:@"itemName"];
SSCrypto *crypto = [[SSCrypto alloc] initWithSymmetricKey:[[PasswordController sharedInstance] keyData]];
[crypto setClearTextWithData:[value dataUsingEncoding:NSUTF8StringEncoding]];
NSData *encryptedData = [crypto encrypt:@"aes256"];
NSLog(@"Encrypting itemName: %@", value);
NSLog(@"Encrypting itemName: %@ with key: %@", value, [[crypto symmetricKey] hexval]);
NSLog(@"Encrypted itemName: %@", [encryptedData hexval]);
NSLog(@"Archived itemName: %@", [[NSKeyedArchiver archivedDataWithRootObject:encryptedData] hexval]);
/* archiving the encrypted value ensures, that it can be saven as plain-text xml */
[self setPrimitiveValue:[NSKeyedArchiver archivedDataWithRootObject:encryptedData] forKey:@"itemNameData"];
[crypto release];
} |
In prinziple, this is how the encryption works. But what key is
used to encrypt the data and where does it come from? In our
example the key is entered by the user. I have created a class
called PasswordController which stores the key data.
This class is designed to be a singleton because only one instance
of it is needed within the whole application.
When the application is launched, I use the
-applicationDidFinishLaunching method to request the
key from the user and store the data in the PasswordController.
When you take a look at the console, while you launch the app,
you will notice, that there is happening a lot of stuff before
the user has entered the key. This is because Core Data accesses
the attributes right after the Nib-Files have been loaded. But the
-applicationDidFinishLaunching method is called after that.
I haven’t found any issues with this yet. If you find any problems,
please let me know.
Another aspect I’d like to mention is, that the demo application
cannot determine if the user has entered the wrong key. In that
case, the decryption simply does not work and any newly created
items are encrypted with the other key, which produces a
currupted Persistent Store (it’s not really “damaged”, but unusable.
Try it, you’ll see what I mean).
This issue coud be addressed by including a digest (sha1, or
something) of the key in the Persistent Store and compare it each
time the user has entered the key. The Metadata Dictionary of the
persistent Store would be a good place to put it in. This is not yet
included in the demo application since I didn’t find the time yet.
Maybe I will do it in some future version. Sorry for that!