从 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 中调用transformedValue