CoreData中Transformable类型加密字符串属性重加密时无法触发更新的问题
问题描述
我在iOS应用中使用Core Data,其中一个字符串属性通过ValueTransformer加密:
class EncryptedStringTransformer: ValueTransformer { private var encryptionKey: String init(encryptionKey: String) { self.encryptionKey = encryptionKey super.init() } override class func allowsReverseTransformation() -> Bool { return true } override class func transformedValueClass() -> AnyClass { return NSData.self } override func transformedValue(_ value: Any?) -> Any? { guard let string = value as? String, let data = string.data(using: .utf8), let encryptedData = EncryptionUtils.aes256EncryptData(data, withKey: encryptionKey) else { return nil } return encryptedData } override func reverseTransformedValue(_ value: Any?) -> Any? { guard let encryptedData = value as? Data, let decryptedData = EncryptionUtils.aes256DecryptData(encryptedData, withKey: encryptionKey) else { return nil } return String(data: decryptedData, encoding: .utf8) } func updateEncryptionKey(newKey: String) { self.encryptionKey = newKey } }
加密属性定义如下:
@NSManaged public var message: String?
并在Core Data模型中设置为Transformable类型。
加密基于用户密码,而用户可能会修改密码。当用户更新密码时,我需要重新加密该属性,于是写了如下函数:
func reEncryptDataWithNewPassword(oldPassword: String, newPassword: String) -> ReEncryptDBStatus { let fetchRequest: NSFetchRequest<Message> = Message.fetchRequest() guard let messages = try? context.fetch(fetchRequest) else { return .failed } guard !messages.isEmpty else { return .success } for message in messages { if let _ = message.message { message.message = message.message } } ValueTransformer.setValueTransformer(EncryptedStringTransformer(encryptionKey: newPassword), forName: .encryptedStringTransformer) do { try context.save() } catch { return .failed } return .success }
问题在于调用context.save()时,并没有触发数据库更新,EncryptedStringTransformer中的transformedValue方法也没有被调用。请问问题出在哪里?
问题分析与解决方案
我来帮你拆解下问题根源,再给出几个靠谱的解决思路:
核心问题
你现在的写法message.message = message.message不会触发Core Data的属性变更检测——因为Core Data会认为属性值没有变化(前后是同一个字符串对象,或者值内容完全一致),自然不会调用Transformer重新加密,也不会把变更写入数据库。另外,你替换Transformer的时机也有问题,应该先完成旧数据的解密/重新加密,再替换全局Transformer,否则可能导致读取数据时密钥不匹配。
方案一:强制标记属性为脏数据
既然Core Data没检测到变化,那我们手动告诉它这个属性需要被重新保存。修改循环里的代码:
for message in messages { if let originalMessage = message.message { // 先获取解密后的原始字符串 message.message = originalMessage // 手动标记属性为已修改,触发Core Data的变更跟踪 message.willChangeValue(forKey: "message") message.didChangeValue(forKey: "message") } }
注意:一定要在替换新Transformer之前完成这个操作,否则读取message.message时会用新密钥解密,直接导致数据错误。不过这种方式有点“hack”,属于强制触发更新,不是最稳妥的做法。
方案二:先解密再用新密钥重新加密(推荐)
更可靠的方式是先明确拿到未加密的原始字符串,再用新密钥重新加密:
func reEncryptDataWithNewPassword(oldPassword: String, newPassword: String) -> ReEncryptDBStatus { let fetchRequest: NSFetchRequest<Message> = Message.fetchRequest() guard let messages = try? context.fetch(fetchRequest) else { return .failed } guard !messages.isEmpty else { return .success } // 1. 先用旧密钥的Transformer解密所有数据 let oldTransformer = EncryptedStringTransformer(encryptionKey: oldPassword) ValueTransformer.setValueTransformer(oldTransformer, forName: .encryptedStringTransformer) // 缓存所有原始未加密的字符串 let messageCache = messages.reduce(into: [Message: String?]()) { dict, message in dict[message] = message.message } // 2. 替换为新密钥的Transformer let newTransformer = EncryptedStringTransformer(encryptionKey: newPassword) ValueTransformer.setValueTransformer(newTransformer, forName: .encryptedStringTransformer) // 3. 重新设置属性,触发新的加密逻辑 for (message, originalText) in messageCache { message.message = originalText } // 4. 保存变更,出错时回滚Transformer避免后续问题 do { try context.save() } catch { ValueTransformer.setValueTransformer(oldTransformer, forName: .encryptedStringTransformer) return .failed } return .success }
这个流程逻辑清晰,先确保拿到正确的原始数据,再用新密钥加密,能最大程度避免数据损坏的风险。
额外建议:避免全局Transformer
你当前用全局注册Transformer的方式,在密钥变化场景下很容易出问题(比如多用户切换时)。更优的做法是给每个ManagedObject实例单独关联Transformer,或者把密钥的获取逻辑内聚到Transformer中(比如从安全存储中读取,注意不要硬编码或存在不安全的存储里)。
另外,操作前一定要做好数据备份,加密操作一旦出错,数据可能无法恢复!
备注:内容来源于stack exchange,提问作者YosiFZ

