Question

I'm using NSValueTranformer to encrypt certain Core Data attributes. This all works fine, except I need to be able to use a different encryption key depending on the NSManagedObject. Is there anyway I can access this entity from within my transformer class?

The use case is I have multiple users with different passwords that can access different NSManagedObject entities. If I use the same encryption key for all of the objects, someone could just reassign who owns them in the SQL db and they would still decrypt.

Any ideas on the best way to go about this?

Edit: I should mention I'm doing this in iOS.

Was it helpful?

Solution

Third times the charm? Let me see if I can address your only-transform-when-going-to-disk requirement. Think of this as a hybrid of the other two approaches.

@interface UserSession : NSObject

+ (UserSession*)currentSession;
+ (void)setCurrentSession: (UserSession*)session;
- (id)initWithUserName: (NSString*)username andEncryptionKey: (NSData*)key;

@property (nonatomic, readonly) NSString* userName;
@property (nonatomic, readonly) NSData* encryptionKey;

@end

@implementation UserSession

static UserSession* gCurrentSession = nil;

+ (UserSession*)currentSession
{
    @synchronized(self)
    {
        return gCurrentSession;
    }
}

+ (void)setCurrentSession: (UserSession*)userSession
{
    @synchronized(self)
    {
        gCurrentSession = userSession;
    }
}

- (id)initWithUserName: (NSString*)username andEncryptionKey: (NSData*)key
{
    if (self = [super init])
    {
        _userName = [username copy];
        _encryptionKey = [key copy];
    }
    return self;
}

- (void)dealloc
{
    _userName = nil;
    _encryptionKey = nil;
}

@end

@interface EncryptingValueTransformer : NSValueTransformer
@end

@implementation EncryptingValueTransformer

- (id)transformedValue:(id)value
{    
    UserSession* session = [UserSession currentSession];
    NSAssert(session, @"No user session! Can't decrypt!");

    NSData* key = session.encryptionKey;
    NSData* decryptedData = Decrypt(value, key);
    return decryptedData;
}

- (id)reverseTransformedValue:(id)value
{
    UserSession* session = [UserSession currentSession];
    NSAssert(session, @"No user session! Can't encrypt!");

    NSData* key = session.encryptionKey;
    NSData* encryptedData = Encrypt(value, key);
    return encryptedData;
}

@end

The only tricky part here is that you have to be sure that the current UserSession is set up before you create the managed object context and isn't changed until after the context is saved and deallocated.

Hope this helps.

OTHER TIPS

You can create custom instances of NSValueTransformer subclasses that have state (i.e. the encryption key) and pass them in to -bind:toObject:withKeyPath:options: in the options dictionary using the NSValueTransformerBindingOption key.

You won't be able to set this up in IB directly since IB references value transformers by class name, but you can do it in code. If you're feeling extra ambitious you can set up the bindings in IB and then replace them with different options in code later.

It might look something like this:

@interface EncryptingValueTransformer : NSValueTransformer

@property (nonatomic,readwrite,copy) NSData* encryptionKey;

@end

@implementation EncryptingValueTransformer

- (void)dealloc
{
    _encryptionKey = nil;
}

- (id)transformedValue:(id)value
{
    if (!self.encryptionKey)
        return nil;

    // do the transformation

    return value;
}

- (id)reverseTransformedValue:(id)value
{
    if (!self.encryptionKey)
        return nil;

    // Do the reverse transformation

    return value;
}

@end


@interface MyViewController : NSViewController

@property (nonatomic, readwrite, assign) IBOutlet NSControl* controlBoundToEncryptedValue;

@end

@implementation MyViewController

// Other stuff...

- (void)loadView
{
    [super loadView];

    // Replace IB's value tansformer binding settings (which will be by class and not instance) with specific,
    // stateful instances.
    for (NSString* binding in [self.controlBoundToEncryptedValue exposedBindings])
    {
        NSDictionary* bindingInfo = [self.controlBoundToEncryptedValue infoForBinding: binding];
        NSDictionary* options = bindingInfo[NSOptionsKey];
        if ([options[NSValueTransformerNameBindingOption] isEqual: NSStringFromClass([EncryptingValueTransformer class])])
        {
            // Out with the old
            [self.controlBoundToEncryptedValue unbind: binding];

            // In with the new
            NSMutableDictionary* mutableOptions = [options mutableCopy];
            mutableOptions[NSValueTransformerNameBindingOption] = nil;
            mutableOptions[NSValueTransformerBindingOption] = [[EncryptingValueTransformer alloc] init];
            [self.controlBoundToEncryptedValue bind: binding
                                           toObject: bindingInfo[NSObservedObjectKey]
                                        withKeyPath: bindingInfo[NSObservedKeyPathKey]
                                            options: mutableOptions];
        }
    }
}

// Assuming you're using the standard representedObject pattern, this will get set every time you want
// your view to expose new model data. This is a good place to update the encryption key in the transformers'
// state...

- (void)setRepresentedObject:(id)representedObject
{
    for (NSString* binding in [self.controlBoundToEncryptedValue exposedBindings])
    {
        id transformer = [self.controlBoundToEncryptedValue infoForBinding: NSValueBinding][NSOptionsKey][NSValueTransformerBindingOption];
        EncryptingValueTransformer* encryptingTransformer = [transformer isKindOfClass: [EncryptingValueTransformer class]] ? (EncryptingValueTransformer*)transformer : nil;
        encryptingTransformer.encryptionKey = nil;
    }

    [super setRepresentedObject:representedObject];

    // Get key from model however...
    NSData* encryptionKeySpecificToThisUser = /* Whatever it is... */ nil;

    for (NSString* binding in [self.controlBoundToEncryptedValue exposedBindings])
    {
        id transformer = [self.controlBoundToEncryptedValue infoForBinding: NSValueBinding][NSOptionsKey][NSValueTransformerBindingOption];
        EncryptingValueTransformer* encryptingTransformer = [transformer isKindOfClass: [EncryptingValueTransformer class]] ? (EncryptingValueTransformer*)transformer : nil;
        encryptingTransformer.encryptionKey = encryptionKeySpecificToThisUser;
    }
}

// ...Other stuff

@end

OK. This was bugging me so I thought about it some more... I think the easiest way is to have some sort of "session" object and then have a "derived property" on your managed object. Assuming you have an entity called UserData with a property called encryptedData, I whipped up some code that might help illustrate:

@interface UserData : NSManagedObject
@property (nonatomic, retain) NSData * unencryptedData;
@end

@interface UserData () // Private
@property (nonatomic, retain) NSData * encryptedData;
@end

// These functions defined elsewhere
NSData* Encrypt(NSData* clearData, NSData* key);
NSData* Decrypt(NSData* cipherData, NSData* key);

@interface UserSession : NSObject

+ (UserSession*)currentSession;

- (id)initWithUserName: (NSString*)username andEncryptionKey: (NSData*)key;

@property (nonatomic, readonly) NSString* userName;
@property (nonatomic, readonly) NSData* encryptionKey;

@end

@implementation UserData

@dynamic encryptedData;
@dynamic unencryptedData;

+ (NSSet*)keyPathsForValuesAffectingUnencryptedData
{
    return [NSSet setWithObject: NSStringFromSelector(@selector(encryptedData))];
}

- (NSData*)unencryptedData
{
    UserSession* session = [UserSession currentSession];
    if (nil == session)
        return nil;

    NSData* key = session.encryptionKey;
    NSData* encryptedData = self.encryptedData;
    NSData* decryptedData = Decrypt(encryptedData, key);
    return decryptedData;
}

- (void)setUnencryptedData:(NSData *)unencryptedData
{
    UserSession* session = [UserSession currentSession];
    NSAssert(session, @"No user session! Can't encrypt!");

    NSData* key = session.encryptionKey;
    NSData* encryptedData = Encrypt(unencryptedData, key);

    self.encryptedData = encryptedData;
}

@end

@implementation UserSession

static UserSession* gCurrentSession = nil;

+ (UserSession*)currentSession
{
    @synchronized(self)
    {
        return gCurrentSession;
    }
}

+ (void)setCurrentSession: (UserSession*)userSession
{
    @synchronized(self)
    {
        gCurrentSession = userSession;
    }
}

- (id)initWithUserName: (NSString*)username andEncryptionKey: (NSData*)key
{
    if (self = [super init])
    {
        _userName = [username copy];
        _encryptionKey = [key copy];
    }
    return self;
}

-(void)dealloc
{
    _userName = nil;
    _encryptionKey = nil;
}

@end

The idea here is that when a given user logs in you create a new UserSession object and call +[UserSession setCurrentSession: [[UserSession alloc] initWithUserName: @"foo" andEncryptionKey: <whatever>]]. The derived property (unencryptedData) accessor and mutator get the current session and use the key to transform the values back and forth to the "real" property. (Also, don't skip over the +keyPathsForValuesAffectingUnencryptedData method. This tells the runtime about the relationship between the two properties, and will help things work more seamlessly.)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top