Skip to content

Commit

Permalink
Merge pull request #27 from deeje/feature/EncryptedFields
Browse files Browse the repository at this point in the history
EncryptedValues
  • Loading branch information
deeje committed Apr 12, 2022
2 parents 2f771f7 + eb611c7 commit 7c51b88
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 55 deletions.
2 changes: 1 addition & 1 deletion CloudCore.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "CloudCore"
s.summary = "Framework that enables synchronization between CloudKit and Core Data."
s.version = "4.0.2"
s.version = "4.1"
s.homepage = "https://github.com/deeje/CloudCore"
s.license = 'MIT'
s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" }
Expand Down
12 changes: 4 additions & 8 deletions CloudCore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
570D8D23280631F900E6836A /* DeleteCloudCoreZoneOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570D8D22280631F900E6836A /* DeleteCloudCoreZoneOperation.swift */; };
57505AB021A7591500D9CF8F /* PullResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57505AAF21A7591500D9CF8F /* PullResult.swift */; };
575ADF462655AB7C0050D693 /* PullRecordOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575ADF442655AB7C0050D693 /* PullRecordOperation.swift */; };
575ADF472655AB7C0050D693 /* PullChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575ADF452655AB7C0050D693 /* PullChangesOperation.swift */; };
Expand Down Expand Up @@ -42,11 +43,9 @@
E22A53DA1E4A8743009286C0 /* CloudKitAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22A53D91E4A8743009286C0 /* CloudKitAttribute.swift */; };
E22C40461E42956C009469A1 /* CoreDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22C40451E42956C009469A1 /* CoreDataObserver.swift */; };
E23C478C1E48A404004310F9 /* PushOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23C478B1E48A404004310F9 /* PushOperationQueue.swift */; };
E247EF8D1E67775500EBD75E /* ErrorBlockProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */; };
E247EF971E67873E00EBD75E /* DeleteFromCoreDataOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF951E67873900EBD75E /* DeleteFromCoreDataOperationTests.swift */; };
E247EF9A1E678EAC00EBD75E /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF981E678EA200EBD75E /* CustomFunctions.swift */; };
E24F44A61E4595B900F78819 /* CoreDataRelationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F44A51E4595B900F78819 /* CoreDataRelationship.swift */; };
E2564BFF1E5061BC002E518B /* ErrorBlockProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2564BFE1E5061BC002E518B /* ErrorBlockProxy.swift */; };
E28F0B931E671E7400BF532A /* CKRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0B911E671E6500BF532A /* CKRecordTests.swift */; };
E28F0BA21E67260900BF532A /* NSEntityDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0BA01E6725E700BF532A /* NSEntityDescriptionTests.swift */; };
E28F0BA31E67280100BF532A /* NSManagedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F44A81E459E3E00F78819 /* NSManagedObjectTests.swift */; };
Expand Down Expand Up @@ -90,6 +89,7 @@

/* Begin PBXFileReference section */
245F765CC7CBF0507158B4A9 /* Pods_CloudCoreTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCoreTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
570D8D22280631F900E6836A /* DeleteCloudCoreZoneOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteCloudCoreZoneOperation.swift; sourceTree = "<group>"; };
57505AAF21A7591500D9CF8F /* PullResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullResult.swift; sourceTree = "<group>"; };
575ADF442655AB7C0050D693 /* PullRecordOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullRecordOperation.swift; sourceTree = "<group>"; };
575ADF452655AB7C0050D693 /* PullChangesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullChangesOperation.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -131,12 +131,10 @@
E22C40441E4291FB009469A1 /* CloudCore.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = CloudCore.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
E22C40451E42956C009469A1 /* CoreDataObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
E23C478B1E48A404004310F9 /* PushOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushOperationQueue.swift; sourceTree = "<group>"; };
E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorBlockProxyTests.swift; sourceTree = "<group>"; };
E247EF951E67873900EBD75E /* DeleteFromCoreDataOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteFromCoreDataOperationTests.swift; sourceTree = "<group>"; };
E247EF981E678EA200EBD75E /* CustomFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctions.swift; sourceTree = "<group>"; };
E24F44A51E4595B900F78819 /* CoreDataRelationship.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataRelationship.swift; sourceTree = "<group>"; };
E24F44A81E459E3E00F78819 /* NSManagedObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObjectTests.swift; sourceTree = "<group>"; };
E2564BFE1E5061BC002E518B /* ErrorBlockProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorBlockProxy.swift; sourceTree = "<group>"; };
E277DB061E7726FB00DC334A /* PublicDatabaseSubscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicDatabaseSubscriptions.swift; sourceTree = "<group>"; };
E277DB0C1E77F96400DC334A /* FetchPublicSubscriptionsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchPublicSubscriptionsOperation.swift; sourceTree = "<group>"; };
E28F0B911E671E6500BF532A /* CKRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CKRecordTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -253,6 +251,7 @@
D985DEA31FE026D400236870 /* CreateCloudCoreZoneOperation.swift */,
D985DEAA1FE0335800236870 /* PushAllLocalDataOperation.swift */,
D985DEA71FE0292000236870 /* SubscribeOperation.swift */,
570D8D22280631F900E6836A /* DeleteCloudCoreZoneOperation.swift */,
);
path = Setup;
sourceTree = "<group>";
Expand Down Expand Up @@ -320,7 +319,6 @@
E2075FF31E4BB70D00E31F1F /* Pull */,
E2075FF21E4BB6F700E31F1F /* Push */,
E200D44C1E48E13200B707D4 /* CloudCore.swift */,
E2564BFE1E5061BC002E518B /* ErrorBlockProxy.swift */,
);
path = Classes;
sourceTree = "<group>";
Expand Down Expand Up @@ -369,7 +367,6 @@
children = (
E247EF8E1E677D1400EBD75E /* Fetch */,
E29D11771E69808800E3DCBF /* Upload */,
E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */,
);
path = Classes;
sourceTree = "<group>";
Expand Down Expand Up @@ -699,6 +696,7 @@
E2E4D8411E76D5A600550CBE /* PullOperation.swift in Sources */,
E2C02A141E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift in Sources */,
E2C02A191E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift in Sources */,
570D8D23280631F900E6836A /* DeleteCloudCoreZoneOperation.swift in Sources */,
E29BB21A1E4334590020F5B6 /* CloudCoreConfig.swift in Sources */,
E2EE20071E4E6DCE0060F769 /* ServiceAttributeName.swift in Sources */,
D985DEAE1FE034A900236870 /* NSManagedObjectModel.swift in Sources */,
Expand All @@ -715,7 +713,6 @@
E2E296CA1E49DA0800E7D6ED /* Tokens.swift in Sources */,
575ADF462655AB7C0050D693 /* PullRecordOperation.swift in Sources */,
E2075FFF1E4BCD7E00E31F1F /* ObjectToRecordOperation.swift in Sources */,
E2564BFF1E5061BC002E518B /* ErrorBlockProxy.swift in Sources */,
D985DEA41FE026D400236870 /* CreateCloudCoreZoneOperation.swift in Sources */,
D985DEAB1FE0335800236870 /* PushAllLocalDataOperation.swift in Sources */,
E2C02A0E1E4C99AD001B2871 /* ObjectToRecordConverter.swift in Sources */,
Expand Down Expand Up @@ -770,7 +767,6 @@
E29D117D1E69A47700E3DCBF /* CoreDataRelationshipTests.swift in Sources */,
D9B3C7391FCF0C9E00CDB7FF /* CorrectObject.swift in Sources */,
E247EF9A1E678EAC00EBD75E /* CustomFunctions.swift in Sources */,
E247EF8D1E67775500EBD75E /* ErrorBlockProxyTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
7 changes: 6 additions & 1 deletion Example/Sources/Class/ModelFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ class ModelFactory {
org.name = faker.company.name()
org.bs = faker.company.bs()
org.founded = Date(timeIntervalSince1970: faker.number.randomDouble(min: 1292250324, max: 1513175137))


org.secretString = "This is a secret"
org.secretInteger = 42
org.secretDouble = 3.14
org.secretBoolean = true

return org
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20F71" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21E230" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Employee" representedClassName="Employee" syncable="YES" codeGenerationType="class">
<attribute name="department" attributeType="String"/>
<attribute name="name" attributeType="String"/>
Expand All @@ -26,6 +26,10 @@
<attribute name="privateRecordData" optional="YES" attributeType="Binary" preserveAfterDeletion="YES"/>
<attribute name="publicRecordData" optional="YES" attributeType="Binary" preserveAfterDeletion="YES"/>
<attribute name="recordName" optional="YES" attributeType="String"/>
<attribute name="secretBoolean" optional="YES" attributeType="Boolean" allowsCloudEncryption="YES" usesScalarValueType="YES"/>
<attribute name="secretDouble" optional="YES" attributeType="Double" defaultValueString="0.0" allowsCloudEncryption="YES" usesScalarValueType="YES"/>
<attribute name="secretInteger" optional="YES" attributeType="Integer 32" defaultValueString="0" allowsCloudEncryption="YES" usesScalarValueType="YES"/>
<attribute name="secretString" optional="YES" attributeType="String" allowsCloudEncryption="YES"/>
<attribute name="shareRecordData" optional="YES" attributeType="Binary"/>
<attribute name="sort" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
<relationship name="employees" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Employee" inverseName="organization" inverseEntity="Employee"/>
Expand All @@ -38,6 +42,6 @@
</entity>
<elements>
<element name="Employee" positionX="261" positionY="216" width="128" height="180"/>
<element name="Organization" positionX="261" positionY="189" width="128" height="179"/>
<element name="Organization" positionX="261" positionY="189" width="128" height="239"/>
</elements>
</model>
2 changes: 1 addition & 1 deletion Example/Sources/Model/Organization+CloudCoreSharing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension Organization: CloudCoreSharing {
}

public var sharingType: String? {
return "com.deeje.cloudcore.example.organization"
return "com.deeje.sample.CloudCore.organization"
}

public var sharingImage: Data? {
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* **public database** push is supported
* Parent-Child relationships can be defined for CloudKit Sharing
* Respects Core Data options (cascade deletions, external storage).
* Support for 'Allows Cloud Encryption' for attributes in Core Data with automatic encoding to and from encryptedValues[] in CloudKit.
* Knows and manages CloudKit errors like `userDeletedZone`, `zoneNotFound`, `changeTokenExpired`, `isMore`.
* Available on iOS and iPadOS (watchOS and tvOS haven't been tested)
* Sharing can be extended to your NSManagedObject classes, and native SharingUI is implemented
Expand Down
31 changes: 25 additions & 6 deletions Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ public class RecordToCoreDataOperation: AsynchronousOperation {
/// - entityName: entity name of `object`
/// - recordDataAttributeName: attribute name containing recordData
private func fill(object: NSManagedObject, entityName: String, serviceAttributeNames: ServiceAttributeNames, context: NSManagedObjectContext) throws {
for key in record.allKeys() {
let recordValue = record.value(forKey: key)

let ckAttribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context)

func storeValue(_ recordValue: Any?, for key: String) {
let ckAttribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context)
if let coreDataValue = try? ckAttribute.makeCoreDataValue() {
if let cdAttribute = object.entity.attributesByName[key], cdAttribute.attributeType == .transformableAttributeType,
let data = coreDataValue as? Data {
Expand All @@ -111,8 +110,28 @@ public class RecordToCoreDataOperation: AsynchronousOperation {
missingObjectsPerEntities[object] = ckAttribute.notFoundRecordNamesForAttribute
}
}
}

}

if #available(iOS 15, *) {
let allKeys = record.allKeys()
let encryptedKeys = record.encryptedValues.allKeys()

for key in allKeys {
let recordValue: Any?
if encryptedKeys.contains(key) {
recordValue = record.encryptedValues[key]
} else {
recordValue = record.value(forKey: key)
}
storeValue(recordValue, for: key)
}
} else {
for key in record.allKeys() {
let recordValue = record.value(forKey: key)
storeValue(recordValue, for: key)
}
}

// Set system headers
object.setValue(record.recordID.recordName, forKey: serviceAttributeNames.recordName)
object.setValue(record.recordID.zoneID.ownerName, forKey: serviceAttributeNames.ownerName)
Expand Down
23 changes: 20 additions & 3 deletions Source/Classes/Push/CoreDataObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -276,26 +276,43 @@ class CoreDataObserver {
case .zoneNotFound:
pushOperationQueue.cancelAllOperations()

var resetZoneOperations: [Operation] = []

var deleteZoneOperation: Operation? = nil
if let _ = cloudError.userInfo["CKErrorUserDidResetEncryptedDataKey"] {
// per https://developer.apple.com/documentation/cloudkit/encrypting_user_data
// see also https://github.com/apple/cloudkit-sample-encryption

let deleteOp = DeleteCloudCoreZoneOperation()
resetZoneOperations.append(deleteOp)

deleteZoneOperation = deleteOp
}

// Create CloudCore Zone
let createZoneOperation = CreateCloudCoreZoneOperation()
createZoneOperation.errorBlock = {
self.delegate?.error(error: $0, module: .some(.pushToCloud))
self.pushOperationQueue.cancelAllOperations()
}
if let deleteZoneOperation = deleteZoneOperation {
createZoneOperation.addDependency(deleteZoneOperation)
}
resetZoneOperations.append(createZoneOperation)

// Subscribe operation
let subscribeOperation = SubscribeOperation()
subscribeOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) }
subscribeOperation.addDependency(createZoneOperation)
resetZoneOperations.append(subscribeOperation)

pushOperationQueue.addOperation(subscribeOperation)

// Upload all local data
let uploadOperation = PushAllLocalDataOperation(parentContext: parentContext, managedObjectModel: container.managedObjectModel)
uploadOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) }
uploadOperation.addDependency(createZoneOperation)
resetZoneOperations.append(uploadOperation)

pushOperationQueue.addOperations([createZoneOperation, uploadOperation], waitUntilFinished: true)
pushOperationQueue.addOperations(resetZoneOperations, waitUntilFinished: true)
case .operationCancelled: return
default: delegate?.error(error: cloudError, module: .some(.pushToCloud))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,15 @@ class ObjectToRecordOperation: Operation {

if let attribute = CoreDataAttribute(value: value, attributeName: attributeName, entity: managedObject.entity) {
let recordValue = try attribute.makeRecordValue()
record.setValue(recordValue, forKey: attributeName)
if #available(iOS 15, *) {
if attribute.description.allowsCloudEncryption {
record.encryptedValues[attributeName] = (recordValue as! __CKRecordObjCValue)
} else {
record.setValue(recordValue, forKey: attributeName)
}
} else {
record.setValue(recordValue, forKey: attributeName)
}
} else if let relationship = CoreDataRelationship(scope: scope, value: value, relationshipName: attributeName, entity: managedObject.entity) {
let references = try relationship.makeRecordValue()
record.setValue(references, forKey: attributeName)
Expand Down
41 changes: 41 additions & 0 deletions Source/Classes/Setup/DeleteCloudCoreZoneOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// DeleteCloudCoreZoneOperation.swift
// CloudCore
//
// Created by deeje cooley on 4/3/22.
//

import Foundation
import CloudKit

class DeleteCloudCoreZoneOperation: AsynchronousOperation {

var errorBlock: ErrorBlock?
private var deleteZoneOperation: CKModifyRecordZonesOperation?

public override init() {
super.init()

name = "CreateCloudCoreZoneOperation"
qualityOfService = .userInitiated
}

override func main() {
super.main()

let cloudCoreZone = CKRecordZone(zoneName: CloudCore.config.zoneName)
let recordZoneOperation = CKModifyRecordZonesOperation(recordZonesToSave: nil, recordZoneIDsToDelete: [cloudCoreZone.zoneID])
recordZoneOperation.qualityOfService = .userInitiated
recordZoneOperation.modifyRecordZonesCompletionBlock = {
if let error = $2 {
self.errorBlock?(error)
}

self.state = .finished
}

CloudCore.config.container.privateCloudDatabase.add(recordZoneOperation)
self.deleteZoneOperation = recordZoneOperation
}

}
32 changes: 0 additions & 32 deletions Tests/CloudCoreTests/Classes/ErrorBlockProxyTests.swift

This file was deleted.

0 comments on commit 7c51b88

Please sign in to comment.