从 NSValueTransformer 内部引用 NSManagedObject 实体

Posted

技术标签:

【中文标题】从 NSValueTransformer 内部引用 NSManagedObject 实体【英文标题】:Reference NSManagedObject entity from inside NSValueTransformer 【发布时间】:2013-09-20 12:03:48 【问题描述】:

我正在使用 NSValueTranformer 加密某些核心数据属性。这一切都很好,除了我需要能够根据 NSManagedObject 使用不同的加密密钥。无论如何我可以从我的转换器类中访问这个实体吗?

用例是我有多个具有不同密码的用户可以访问不同的 NSManagedObject 实体。如果我对所有对象使用相同的加密密钥,那么有人可以在 SQL 数据库中重新分配拥有它们的人,他们仍然会解密。

关于解决此问题的最佳方法有什么想法吗?

编辑: 我应该提到我在 ios 中这样做。

【问题讨论】:

我可以假设一次只有一个用户登录吗?注销后可以清除 CoreData 堆栈吗?此外,用户是否在同一商店共享不受限制的数据,或者您可以为每个用户开设不同的商店? 【参考方案1】:

三倍魅力?让我看看我是否可以解决您的 only-transform-when-going-to-disk 要求。将此视为其他两种方法的混合体。

@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

这里唯一棘手的部分是您必须确保当前的UserSession 是在创建托管对象上下文之前设置的,并且直到之后才更改 em> 上下文被保存和释放。

希望这会有所帮助。

【讨论】:

不幸的是,这个解决方案对我不起作用。如果用户没有登录,应用程序仍然需要为他们下载信息并存储它,所以我不能有一个单例来获取密钥。另外,我并不是在此实现中看到与之相关的问题,而是该应用程序在多个线程中使用了多个上下文。 如果您是代表用户下载内容然后存储,那么在开始保存操作时将密钥放入threadDictionary,然后再将其取出如何?然后价值转换器可以得到它。这也将允许您同时在不同线程上代表不同用户执行操作。 想过这个,但是某些线程是为用户共享的,并且持久化到磁盘只发生在主线程上。这让我发疯了,必须有一种方法可以在创建时将一些信息传递给这个转换器。 很明显,CoreData API 在设计时并没有考虑到这样的用例。我怀疑您将不得不稍微调整您的要求才能启用解决方案。 (即每个线程一个用户,以另一种方式联锁保存操作(除了编组到主线程)等) 这是一个想法。 MD5 对字符串进行哈希处理,将其存储在具有与之关联的用户 ID 的单例中。从转换器中,请求与字符串的 MD5 哈希关联的加密密钥并对其进行加密。然后将加密数据的 MD5 哈希传递给单例,并将其与密钥相关联。解密后再次传入 md5 哈希,仅用于加密字符串,并获得解密密钥。我认为这可能真的有效......【参考方案2】:

您可以创建具有状态(即加密密钥)的NSValueTransformer 子类的自定义实例,并使用NSValueTransformerBindingOption 密钥将它们传递给选项字典中的-bind:toObject:withKeyPath:options:

您将无法直接在 IB 中进行设置,因为 IB 按类名引用值转换器,但您可以在代码中进行。如果您觉得雄心勃勃,您可以在 IB 中设置绑定,然后稍后在代码中用不同的选项替换它们。

它可能看起来像这样:

@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

【讨论】:

看起来 NSValueTransformerBindOption 在 iOS 上不可用,我错了吗? 哦,是的……iOS 上根本没有 Cocoa 绑定。如果你在 iOS 上,那么你根本不关心绑定的东西。只需创建一个带有状态的 NSValueTransformer 子类,在用户更改时设置状态,就完成了。 CoreData 没有相同的工具来为托管对象模型提供实例——只有类名。那好吧。太糟糕了。鉴于此,我的建议是让托管对象上的属性提供加密数据,然后让使用对象的代码根据需要使用正确的密钥转换数据。我不认为没有一些非常老套的东西就可以内生地做到这一点。 性能问题。我只能在进出磁盘时加密和解密这个值。我想我将不得不尝试一些 hacky objc 运行时的东西,看看我是否能以某种方式完成这项工作。 我已经发布了另外两个答案以及另外两个选项,其中一个希望对您有用。【参考方案3】:

好的。这让我很烦恼,所以我想了更多......我认为最简单的方法是拥有某种“会话”对象,然后在您的托管对象上拥有一个“派生属性”。假设您有一个名为 UserData 的实体,其属性名为 encryptedData,我编写了一些可能有助于说明的代码:

@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

这里的想法是,当给定用户登录时,您创建一个新的 UserSession 对象并调用+[UserSession setCurrentSession: [[UserSession alloc] initWithUserName: @"foo" andEncryptionKey: <whatever>]]。派生属性 (unencryptedData) 访问器和修改器获取当前会话并使用密钥将值来回转换为“真实”属性。 (另外,不要跳过 +keyPathsForValuesAffectingUnencryptedData 方法。这会告诉运行时两个属性之间的关系,并有助于更无缝地工作。)

【讨论】:

我在其他 cmets 中看到您添加了仅在持久性时间完成转换的要求。我添加了第三个答案,希望能满足这一新要求。

以上是关于从 NSValueTransformer 内部引用 NSManagedObject 实体的主要内容,如果未能解决你的问题,请参考以下文章

NSValueTransformer,如何使用?

如何将上下文对象传递给 NSValueTransformer

无法在 NSValueTransformer 中调用transformedValue

NSValueTransformer 加密数据

iOS 核心数据加密使用 NSValueTransformer

用于核心数据中 C 结构的 NSValueTransformer