CloudKit错误:无法获取PCS对象来解包字段encryptedPublicSharingKey的加密数据

时间:2016-09-15 07:08:51

标签: cloudkit ios10

在iOS 10中,我的应用程序的CloudKit功能不再起作用。完全相同的应用程序在iOS 9上工作正常。我尝试在XCode 8中构建它仍然无法正常工作。

不起作用的代码及其生成的错误如下所示。我们所做的是从公共云数据库中获取记录。我已经确认该设备上有一个新的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)">

2 个答案:

答案 0 :(得分:2)

更新(找到解决方案):

我原来的答案中没有一点是正确的。真正的罪魁祸首是我有一个从我的服务器到服务器脚本创建的记录,其中“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仪表板中记录右上角的复选框:

enter image description here

原始答案(错误的猜测):

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

  1. 等一段时间。自从我创建容器以来,我花了大约3个小时。
  2. 尝试切换到生产容器。在权利文件中添加密钥“com.apple.developer.icloud-container-environment”和值“Production”(区分大小写)。您需要在密钥和值中都准确无误才能构建。请点击https://developer.apple.com/library/content/technotes/tn2415/_index.html
  3. 了解更多信息
  4. 尝试从该设备中创建记录。我使用Server-to-Server脚本在公共数据库中创建记录。在我从该设备中添加了一条记录之后,iOS似乎已经“固定”了一些内部。
  5. 我的整个经历......

    • 3小时前创建了一个全新的容器。可以获取服务器到服务器脚本将记录泵入公共数据库。现在我需要从客户端iOS应用程序中读取记录。
    • 如果我查询(唯一的记录类型)记录,即使使用普通的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]由于本地错误导致操作流量受控
    • 尝试在设备上启用/禁用iCloud / iCloud Keychain。没有帮助。
    • 试图切换到Production容器,似乎没有帮助。 (我忘了是否通过在权利中使用“APS环境”键错误地做错了,这是针对APN而不是CloudKit)
    • 尝试从设备中创建记录,写入没问题。
    • 尝试再次切换到生产容器(这次可能使用正确的密钥)
    • 从那时起,一切都很有效。

答案 1 :(得分:2)

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

从iOS 10.0.2开始,如果记录在公共数据库中,,则选中了短GUID选项,则会遇到以下错误: iOS 10设备尝试获取记录:

  

CKError 0x15e7c2b0:“内部错误”(1/5001); “无法获得PCS   用于解包字段encryptedPublicSharingKey的加密数据的对象:   (空)“

但是,如果记录位于私有数据库中(无论它是在自定义区域还是默认区域中),那么它可以有一个短GUID,并且在iOS 10上仍然可以正常获取

Apple告诉我,无法从公共数据库获取具有短GUID的记录是iOS 10中的一个错误,我相信他们将在未来的更新中修复。然而,他们无法告诉我,在修复之前需要多长时间。

解决方法:因此,至少目前,解决方法(@ewcy还提到)是在没有选中Short GUID选项的情况下重新创建公共数据库中的所有记录。我这样做的方法就是直接在CloudKit仪表板中创建记录。 @ewcy的做法是使用JavaScript。您也可以在Objective C中执行此操作,如下面的代码示例所示。最后,你可以在Swift中做到这一点(特别是如果你喜欢感叹号和问号很多!!! ?? !!?!1)。

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

该错误可能来自公共数据库记录无法共享的事实,因此他们的encryptedPublicSharingKey为空的原因。但是,如果不能看到苹果API背后的幕后,很难确切地知道究竟是什么原因。

Apple帮助我处理此案例的开发者技术支持人员告诉我,他们甚至不应该选择在任何无法共享的记录上设置Short GUID,例如公共数据库中的那些记录。即使在那里有那个选择也没有意义。

当然,当我们大多数开发人员看到“短GUID”时,我们认为,“GUID很酷,我最好检查一下这个盒子!以后可能需要GUID!”我的意思是,谁不爱GUID,amirite? _(ツ)_ /¯

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

也许我们会在iOS 10.1中看到所有这些问题的一些更新?事实上,CloudKit在iOS 10上部分被破坏,现在已经持续了一个多月。我意识到我不应该在这篇评论中进行编辑,所以我不会,但我只想说我表现出很多克制。

用于隔离问题实际发生的时间和地点的示例代码(还显示我提到的其他iOS 10 CloudKit错误,如果您关闭了iCloud Drive或者您未在设备上登录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