CloudKit error: Couldn't get a PCS object to unwrap encrypted data for field encryptedPublicSharingKey

CloudKit error: Couldn't get a PCS object to unwrap encrypted data for field encryptedPublicSharingKey

在 iOS 10 后,我的应用程序的 CloudKit 功能不再可用。完全相同的应用程序在 iOS 9 上运行良好。我尝试在 XCode 8 上构建,但它仍然无法运行。

不起作用的代码及其生成的错误如下所示。我们所做的是从 public 云数据库中获取一条记录。我已确认该设备上有一个新的 iCloud 帐户。同一设备与 iOS 下的应用程序完美配合 9. 我尝试重新启动设备并登录和退出 iCloud,但仍然出现相同的错误。

请指教...

CKDatabase *publicDatabase = [[CKContainer defaultContainer] publicCloudDatabase];
CKRecordID *myRecordID = [[CKRecordID alloc] initWithRecordName:@"myRecord"];
[publicDatabase fetchRecordWithID:myRecordID completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
    if(error != nil) {
        CLS_LOG(@"<ERROR> Error fetching record. Error: %@",error);
        return;
    }
    //rest of code
}];

结果:

Error: <CKError 0x15e7c2b0: "Internal Error" (1/5001); "Couldn't get a
PCS object to unwrap encrypted data for field
encryptedPublicSharingKey: (null)">

更新(找到解决方案):

None 我原来回答的观点是正确的。真正的罪魁祸首是我从我的服务器到服务器脚本创建了一条记录,该脚本将 "createShortGUID"(我从 here 复制)设置为 true。不知道为什么会这样。但只要我清除所有服务器创建的记录,并将我的脚本更改为:

var json = {
    operations: [{
        operationType: 'forceUpdate',
        record: {
            recordType : "heartBeat",
            //createShortGUID: true, // Comment this away!!!!
            recordName : "heartBeatRecordName",
            fields: {
                dummyField: { value: "dummyValue" },
            },
        },
    }],
};

并再次重新填充我的服务器记录,然后 iOS 应用程序再次开始工作。 createShortGUID 对应于 CloudKit Dashboard:

中记录右上角的复选框

原答案(猜错):

试试这些,其中一个解决了我的问题。 (但我不知道是哪一个。)

  1. 稍等片刻。自创建容器以来,我花了大约 3 个小时。
  2. 尝试切换到生产容器。在您的授权文件中添加键 "com.apple.developer.icloud-container-environment" 和值 "Production"(区分大小写)。您需要在键和值上都准确才能构建。查看更多 https://developer.apple.com/library/content/technotes/tn2415/_index.html
  3. 尝试从该设备中创建一条记录。我使用服务器到服务器脚本在 public 数据库中创建记录。在我从那个设备中添加一条记录后,iOS 似乎有 "fixed" 内部的东西。

我的整个经历...

  • 3 小时前创建了一个全新的容器。可以获取服务器到服务器脚本将记录抽取到 public 数据库中。现在我需要从客户端 iOS app.
  • 读取记录
  • 如果我查询(唯一记录类型的)记录,即使使用简单的 TRUE 谓词,我也会得到 <CKError 0x174247470: "Internal Error" (1/1000); "Encountered an error fetching records">
  • 谷歌了一下。运气不好。
  • 如果我获取记录(使用我的服务器到服务器脚本创建的记录的 recordID,通过 CloudKit 仪表板检索)我得到 <CKError 0x174253590: "Internal Error" (1/5001); "Couldn't get a PCS object to unwrap encrypted data for field encryptedPublicSharingKey: (null)">
  • 谷歌了一下。运气不好。
  • 意识到这可能是设备上的问题,因为设备日志显示:
    • 默认 00:09:03.190715 +0800 cloudd [操作 0x1018916d0] 操作正在回调队列上完成,但出现错误
    • 默认 00:09:03.190989 +0800 cloudd [操作 0x1018916d0] 由于本地错误,操作正在接受流量控制
  • 已尝试 enable/disable iCloud/iCloud 设备上的钥匙串。没用。
  • 尝试切换到生产容器,似乎没有帮助。 (我忘了我是不是用了entitlements中的"APS Environment"键做错了,是给APNs的,不是给CloudKit的)
  • 尝试从设备内部创建记录,写入正常。
  • 尝试再次切换到生产容器(这次可能使用了正确的密钥)
  • 奇迹般地,从那以后一切正常。

我已经就这个问题与 Apple 的开发者技术支持团队进行了数周的沟通。正如@ewcy 的回答所暗示的,问题与短 GUID 属性 有关。我会把它设置为正确的答案,但是,还有更多的东西。

从 iOS 10.0.2 开始,如果记录在 public 数据库中,它选中了 Short GUID 选项,然后当 iOS 10 设备尝试获取记录时将遇到以下错误:

CKError 0x15e7c2b0: "Internal Error" (1/5001); "Couldn't get a PCS object to unwrap encrypted data for field encryptedPublicSharingKey: (null)"

然而,如果记录在 private 数据库中(无论它是在自定义区域还是默认区域),那么它可以有一个短 GUID 并且仍然可以被获取iOS 10.

就好了

Apple 告诉我无法从 public 数据库中获取具有短 GUID 的记录是 iOS10 中的一个错误,我相信他们会在未来的更新中修复。然而,他们无法告诉我,要等多久才能解决这个问题。

解决方法: 所以,至少现在,解决方法(@ewcy 也提到了)是重新创建 public 数据库中的所有记录,而不使用 Short已选中 GUID 选项。我的做法是直接在 CloudKit 仪表板中创建记录。 @ewcy 的做法是使用 JavaScript。您也可以在 Objective C 中执行此操作,如我在下面的代码示例中所示。最后,你可以在 Swift 中完成(特别是如果你非常喜欢感叹号和问号!!!??!!?!1)。

短 GUID 属性 与 iOS 10 中引入的 CloudKit 的新共享功能相关。如果您以编程方式将 CKShare 添加到 CKRecord(它只能对私有数据库中自定义区域中的记录进行操作,除非您想使您的应用程序崩溃...呵呵)然后该记录将自动获得一个短 GUID。您可以使用下面的示例代码对此进行测试。

该错误可能来自 public 数据库记录无法共享的事实,因此它们的 encryptedPublicSharingKey 为空。但由于无法看到 Apple API 的幕后情况,因此很难确定确切原因是什么。

帮助我解决这个问题的 Apple 开发人员技术支持人员告诉我,他们甚至不应该选择在任何无法共享的记录上设置短 GUID,例如 public 数据库中的那些记录.即使在那里有那个选项也毫无意义。

当然,当我们大多数开发人员看到 "Short GUID" 时,我们会想,"GUIDs are cool, I better check that box! Might need that GUID later!" 我的意思是,谁不喜欢 GUID,amirite? ¯_(ツ)_/¯


另请注意,iOS10 上存在一个不同的错误,如果 iCloud Drive 未启用,或者没有人在设备上登录 iCloud,则基本上在 CloudKit 中执行任何操作都会失败(但有不同的错误信息)。然而,所有这些东西在 iOS 9 上运行良好。

也许我们会在 iOS 10.1 中看到所有这些问题的一些更新?事实是 CloudKit 在 iOS 10 上部分损坏,现在已经有一个多月了。我意识到我不应该在此评论中发表社论,所以我不会,但我只是说我表现出了很多克制。


用于隔离问题实际发生的时间和地点的示例代码(如果您关闭了 iCloud Drive 或未关闭,还揭示了我提到的其他 iOS 10 CloudKit 个错误在设备上登录 iCloud):

//
//  AppDelegate.m
//

#import "AppDelegate.h"
#import <CloudKit/CloudKit.h>

#define LOG(__FORMAT__, ...) NSLog(@"\n\n" __FORMAT__ @"\n\n", ##__VA_ARGS__)

@interface AppDelegate()

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    CKDatabase *publicDatabase = [[CKContainer defaultContainer] publicCloudDatabase];

    /* First create public testRecord_noGuid with no Short GUID, and testRecord_withGuid with a Short GUID, in the CloudKit Dashboard. Then run this code. */

    /* Try to fetch the one without a Short GUID. */

    CKRecordID *testRecordID_noGuid = [[CKRecordID alloc] initWithRecordName:@"testRecord_noGuid"];
    [publicDatabase fetchRecordWithID:testRecordID_noGuid completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
        if(error != nil) {
            LOG(@"<ERROR> Error fetching testRecord_noGuid from public default DB. Error: %@",error);
            return;
        }
        /* This is where we end up on iOS 9 or 10. */
        NSDictionary *middle = [record dictionaryWithValuesForKeys:record.allKeys];
        LOG(@"<NOTICE> testRecord_noGuid fetched from public default zone: %@",middle);
    }];

    /* Try to fetch the one with a Short GUID. */

    CKRecordID *testRecordID_withGuid = [[CKRecordID alloc] initWithRecordName:@"testRecord_withGuid"];
    [publicDatabase fetchRecordWithID:testRecordID_withGuid completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
        if(error != nil) {
            /* This is where we always end up on iOS 10. */
            LOG(@"<ERROR> Error fetching testRecord_withGuid from public default zone. Error: %@",error);
            return;
        }
        /* This is where we end up on iOS 9. */
        NSDictionary *middle = [record dictionaryWithValuesForKeys:record.allKeys];
        LOG(@"<NOTICE> testRecord_withGuid fetched from public default zone: %@",middle);
    }];

    /* The below code demonstrates the issue does not affect private records in custom zones.
        First time you run this, do it while logged into your developer AppleID with permissions to create a new zone. 
        That way you can login to CloudKit dashboard and verify whether the created records have a Short GUID or not.
     */

    CKDatabase *privateDB = [[CKContainer defaultContainer] privateCloudDatabase];    
    CKRecordZone *testRecordZone = [[CKRecordZone alloc] initWithZoneName:@"TestRecordZone"];

    /* Create a new zone */

    [privateDB saveRecordZone:testRecordZone completionHandler:^(CKRecordZone * _Nullable zone, NSError * _Nullable error) {
        if(error != nil) {
            LOG(@"<ERROR> Error saving private custom zone. Error: %@", error);
            [self checkPrivateRecordCreationInZone:testRecordZone]; // This will only work if the zone was already created.
            return;
        }
        LOG(@"<NOTICE> Saving of TestRecordZone succeeded. Proceeding to create records in it.");
        /* Now that the new zone is created, create a test record without a Short GUID */
        [self checkPrivateRecordCreationInZone:zone];
    }];


    /* Uncomment this and run it if the testRecordZone is already created and now you are testing from an
       AppleID without any perms. */
    [self checkPrivateRecordCreationInZone:testRecordZone];

    /* Run this to see how it works int he default zone */
    //[self checkPrivateRecordCreationInZone:[CKRecordZone defaultRecordZone]];

    return YES;
}

- (void)checkPrivateRecordCreationInZone:(CKRecordZone *)zone {
    CKDatabase *privateDB = [[CKContainer defaultContainer] privateCloudDatabase];    
    CKRecordID *testRecordID_noGuid_private = [[CKRecordID alloc] initWithRecordName:@"testRecord_noGuid" zoneID:zone.zoneID];
    CKRecord *testRecord_noGuid_private = [[CKRecord alloc] initWithRecordType:@"TestRecordType" recordID:testRecordID_noGuid_private];
    [testRecord_noGuid_private setValue:@"foo" forKey:@"bar"];
    [privateDB saveRecord:testRecord_noGuid_private completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
        if(error != nil) {
            if([error.userInfo[@"ServerErrorDescription"] containsString:@"already exists"]) {
                record = error.userInfo[@"ServerRecord"];
                LOG(@"<NOTICE> Record with no Short GUID already existed in private zone: %@",record);
            }
            else {
                if(error.userInfo[@"CKRetryAfter"] != nil) {
                    /* Retry after three seconds :D */
                    double delay = [error.userInfo[@"CKRetryAfter"] doubleValue] + 0.1;
                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                        [self checkPrivateRecordCreationInZone:zone];
                    });
                }
                LOG(@"<ERROR> Error saving record with no Short GUID to private custom zone. Error: %@", error);
                return; 
            }
        }
        else {
            LOG(@"<NOTICE> Created new record with no Short GUID in private custom zone: %@",record);
        }

        /* Now that we successfully created a record without a short GUID to the private store, fetch it. */

        [privateDB fetchRecordWithID:testRecordID_noGuid_private completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
            if(error != nil) {
                LOG(@"<ERROR> Error fetching testRecord_noGuid from private custom zone: %@",error);
                return;
            }
            LOG(@"<NOTICE> Successfully fetched testRecord_noGuid from private custom zone: %@",record);
        }];

    }];

    /* On iOS 10 or later we can create a private record with a share to force it to have a Short GUID.
       On iOS 9 or earlier you have to manually create the private record with Short GUID after running 
       the test once to create the TestCustomZone and the private record with no Short GUID. */

    CKRecordID *testRecordID_withGuid_private = [[CKRecordID alloc] initWithRecordName:@"testRecord_withGuid" zoneID:zone.zoneID];
    CKRecord *testRecord_withGuid_private = [[CKRecord alloc] initWithRecordType:@"TestRecordType" recordID:testRecordID_withGuid_private];

    if([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 10 || [zone isEqual:[CKRecordZone defaultRecordZone]]) {
        [privateDB fetchRecordWithID:testRecordID_withGuid_private completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
            if(error != nil) {
                LOG(@"<ERROR> Error fetching testRecord_withGuid from private custom zone: %@",error);
                return;
            }
            LOG(@"<NOTICE> Successfully fetched testRecord_withGuid from private custom zone: %@",record);
        }];

        return;
    }

    /* Create a test record with a Short GUID */

    [testRecord_withGuid_private setValue:@"foo" forKey:@"bar"];

    CKShare *meForceGuidToExistMuhahaha = [[CKShare alloc] initWithRootRecord:testRecord_withGuid_private];
    //meForceGuidToExistMuhahaha.publicPermission = CKShareParticipantPermissionNone;
    CKModifyRecordsOperation *save = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:@[meForceGuidToExistMuhahaha,testRecord_withGuid_private] 
                                                                           recordIDsToDelete:nil];
    save.savePolicy = CKRecordSaveAllKeys;

    [save setModifyRecordsCompletionBlock: ^(
        NSArray <CKRecord   *> * _Nullable savedRecordArray, 
        NSArray <CKRecordID *> * _Nullable deletedRecordArray, 
        NSError                * _Nullable modifyError
    ){
        CKShare *savedShare;
        CKRecord *savedRecord;
        BOOL operationDidFail = NO;
        if(modifyError != nil) {
            NSArray *errorsToIgnore = @[@"record to insert already exists",@"Atomic failure"];
            NSArray *partialErrors = modifyError.userInfo[@"CKPartialErrors"];
            if(partialErrors == nil) {
                operationDidFail = YES;
            }
            else {
                for(id item in partialErrors) {
                    if([item isKindOfClass:[NSError class]]) {
                        NSError *partialError = item;
                        NSString *errorDesc = partialError.userInfo[@"ServerErrorDescription"];
                        if(errorDesc == nil) {
                            operationDidFail = YES;
                            break;
                        }
                        if(NO == [errorsToIgnore containsObject:errorDesc]) {
                            operationDidFail = YES;
                            break;
                        }
                        savedRecord = partialError.userInfo[@"ServerRecord"];
                    }
                    else {
                        LOG(@"<ERROR> Unexpected %@ encountered in CKPartialErrrors: %@",NSStringFromClass([item class]),item);
                    }
                } 
            }
            if(savedRecord == nil) {
                operationDidFail = YES;
            }
        }
        if(operationDidFail) {
            LOG(@"<ERROR> Error saving testRecord_withGuid to private custom zone. Error: %@",modifyError);
            return;
        }
        if(savedRecord != nil) {
            // This could happen if the save policy is not CKRecordSaveAllKeys.
            LOG(@"<NOTICE> Record already existed on server. Proceeding with fetch. Record: %@",savedRecord);
        }
        else {
            LOG(@"savedRecords: %@",savedRecordArray);
            savedShare = (CKShare *)savedRecordArray[0];
            savedRecord = (CKShare *)savedRecordArray[1];
            LOG(@"<NOTICE> Successfully upserted record to private custom zone with share URL: %@",[savedShare.URL absoluteString]);
        }
        /* Now that we successfully created a record with a short GUID to the private store, fetch it. */

        [privateDB fetchRecordWithID:testRecordID_withGuid_private completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
            if(error != nil) {
                LOG(@"<ERROR> Error fetching testRecord_withGuid from private custom zone: %@",error);
                return;
            }
            LOG(@"<NOTICE> Successfully fetched testRecord_withGuid from private custom zone: %@",record);
        }];
    }];
    [save start];
}

@end