2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
26 #import "SFPublicKey+SPKI.h"
28 #include <utilities/SecFileLocations.h>
29 #include <Security/SecRandomP.h>
31 #import "keychain/ckks/CKKS.h"
32 #import "keychain/ckks/CKKSViewManager.h"
33 #import "keychain/ckks/CKKSAnalytics.h"
35 #import "CoreCDP/CDPFollowUpController.h"
36 #import "CoreCDP/CDPFollowUpContext.h"
37 #import <CoreCDP/CDPAccount.h>
39 NSString* OTCKContainerName = @"com.apple.security.keychain";
40 NSString* OTCKZoneName = @"OctagonTrust";
41 static NSString* const kOTRampZoneName = @"metadata_zone";
43 @interface OTContext (lockstateTracker) <CKKSLockStateNotification>
46 @interface OTContext ()
48 @property (nonatomic, strong) NSString* contextID;
49 @property (nonatomic, strong) NSString* contextName;
50 @property (nonatomic, strong) NSString* dsid;
52 @property (nonatomic, strong) OTLocalStore* localStore;
53 @property (nonatomic, strong) OTCloudStore* cloudStore;
54 @property (nonatomic, strong) NSData* changeToken;
55 @property (nonatomic, strong) NSString* egoPeerID;
56 @property (nonatomic, strong) NSDate* egoPeerCreationDate;
57 @property (nonatomic, strong) dispatch_queue_t queue;
58 @property (nonatomic, weak) id <OTContextIdentityProvider> identityProvider;
60 @property (nonatomic, strong) CKKSCKAccountStateTracker* accountTracker;
61 @property (nonatomic, strong) CKKSLockStateTracker* lockStateTracker;
62 @property (nonatomic, strong) CKKSReachabilityTracker *reachabilityTracker;
66 @implementation OTContext
68 -(CKContainer*)makeCKContainer:(NSString*)containerName {
69 CKContainer* container = [CKContainer containerWithIdentifier:containerName];
70 container = [[CKContainer alloc] initWithContainerID: container.containerID];
74 -(BOOL) isPrequeliteEnabled
77 if([PQLConnection class] == nil) {
78 secerror("OT: prequelite appears to not be linked. Can't create OT objects.");
84 - (nullable instancetype) initWithContextID:(NSString*)contextID
86 localStore:(OTLocalStore*)localStore
87 cloudStore:(nullable OTCloudStore*)cloudStore
88 identityProvider:(id <OTContextIdentityProvider>)identityProvider
89 error:(NSError**)error
91 if(![self isPrequeliteEnabled]){
92 // We're running in the base build environment, which lacks a bunch of libraries.
93 // We don't support doing anything in this environment. Bye.
99 NSError* localError = nil;
100 _contextID = contextID;
102 _identityProvider = identityProvider;
103 _localStore = localStore;
105 NSString* contextAndDSID = [NSString stringWithFormat:@"%@-%@", contextID, dsid];
107 CKContainer* container = [self makeCKContainer:OTCKContainerName];
109 _accountTracker = [CKKSViewManager manager].accountTracker;
110 _lockStateTracker = [CKKSViewManager manager].lockStateTracker;
111 _reachabilityTracker = [CKKSViewManager manager].reachabilityTracker;
114 _cloudStore = [[OTCloudStore alloc]initWithContainer:container
115 zoneName:OTCKZoneName
116 accountTracker:_accountTracker
117 reachabilityTracker:_reachabilityTracker
118 localStore:_localStore
121 fetchRecordZoneChangesOperationClass:[CKFetchRecordZoneChangesOperation class]
122 fetchRecordsOperationClass:[CKFetchRecordsOperation class]
123 queryOperationClass:[CKQueryOperation class]
124 modifySubscriptionsOperationClass:[CKModifySubscriptionsOperation class]
125 modifyRecordZonesOperationClass:[CKModifyRecordZonesOperation class]
126 apsConnectionClass:[APSConnection class]
129 _cloudStore = cloudStore;
132 OTContextRecord* localContextRecord = [_localStore readLocalContextRecordForContextIDAndDSID:contextAndDSID error:&localError];
134 if(localContextRecord == nil || localContextRecord.contextID == nil){
136 BOOL result = [_localStore initializeContextTable:contextID dsid:dsid error:&localError];
137 if(!result || localError != nil){
138 secerror("octagon: reading from database failed with error: %@", localError);
144 localContextRecord = [_localStore readLocalContextRecordForContextIDAndDSID:contextAndDSID error:&localError];
145 if(localContextRecord == nil || localError !=nil){
146 secerror("octagon: reading from database failed with error: %@", localError);
154 _contextID = localContextRecord.contextID;
155 _contextName = localContextRecord.contextName;
156 _changeToken = localContextRecord.changeToken;
157 _egoPeerID = localContextRecord.egoPeerID;
158 _egoPeerCreationDate = localContextRecord.egoPeerCreationDate;
160 _queue = dispatch_queue_create("com.apple.security.otcontext", DISPATCH_QUEUE_SERIAL);
165 - (nullable OTBottledPeerSigned *) createBottledPeerRecordForIdentity:(OTIdentity *)identity
166 secret:(NSData*)secret
167 error:(NSError**)error
169 NSError* localError = nil;
170 if(self.lockStateTracker.isLocked){
171 secnotice("octagon", "device is locked");
173 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
178 OTEscrowKeys *escrowKeys = [[OTEscrowKeys alloc] initWithSecret:secret dsid:self.dsid error:&localError];
179 if (!escrowKeys || localError != nil) {
180 secerror("octagon: unable to derive escrow keys: %@", localError);
187 OTBottledPeer *bp = [[OTBottledPeer alloc] initWithPeerID:identity.peerID
189 peerSigningKey:identity.peerSigningKey
190 peerEncryptionKey:identity.peerEncryptionKey
191 escrowKeys:escrowKeys
193 if (!bp || localError !=nil) {
194 secerror("octagon: unable to create a bottled peer: %@", localError);
200 return [[OTBottledPeerSigned alloc] initWithBottledPeer:bp
201 escrowedSigningKey:escrowKeys.signingKey
202 peerSigningKey:identity.peerSigningKey
206 - (NSData* _Nullable) makeMeSomeEntropy:(int)requiredLength
208 NSMutableData* salt = [NSMutableData dataWithLength:requiredLength];
212 if (SecRandomCopyBytes(kSecRandomDefault, [salt length], [salt mutableBytes]) != 0){
218 - (nullable OTPreflightInfo*) preflightBottledPeer:(NSString*)contextID
219 entropy:(NSData*)entropy
220 error:(NSError**)error
222 NSError* localError = nil;
223 if(self.lockStateTracker.isLocked){
224 secnotice("octagon", "device is locked");
226 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
231 OTIdentity *identity = [self.identityProvider currentIdentity:&localError];
232 if (!identity || localError != nil) {
233 secerror("octagon: unable to get current identity:%@", localError);
240 OTBottledPeerSigned* bps = [self createBottledPeerRecordForIdentity:identity
243 if (!bps || localError != nil) {
244 secerror("octagon: failed to create bottled peer record: %@", localError);
250 secnotice("octagon", "created bottled peer:%@", bps);
252 OTBottledPeerRecord *bprec = [bps asRecord:identity.spID];
254 if (!identity.spID) {
255 secerror("octagon: cannot enroll without a spID");
257 *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorNoIdentity userInfo:@{NSLocalizedDescriptionKey: @"OTIdentity does not have an SOS peer id"}];
262 OTPreflightInfo* info = [[OTPreflightInfo alloc]init];
263 info.escrowedSigningSPKI = bprec.escrowedSigningSPKI;
265 if(!info.escrowedSigningSPKI){
267 *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorEscrowSigningSPKI userInfo:@{NSLocalizedDescriptionKey: @"Escrowed spinging SPKI is nil"}];
269 secerror("octagon: Escrowed spinging SPKI is nil");
273 info.bottleID = bprec.recordName;
276 *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorBottleID userInfo:@{NSLocalizedDescriptionKey: @"BottleID is nil"}];
278 secerror("octagon: BottleID is nil");
282 //store record in localStore
283 BOOL result = [self.localStore insertBottledPeerRecord:bprec escrowRecordID:identity.spID error:&localError];
284 if(!result || localError){
285 secerror("octagon: could not persist the bottle record: %@", localError);
295 - (BOOL)scrubBottledPeer:(NSString*)contextID
296 bottleID:(NSString*)bottleID
297 error:(NSError**)error
299 secnotice("octagon", "scrubBottledPeer");
300 NSError* localError = nil;
301 if(self.lockStateTracker.isLocked){
302 secnotice("octagon", "device is locked");
304 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
309 BOOL result = [self.localStore deleteBottledPeer:bottleID error:&localError];
310 if(!result || localError != nil){
311 secerror("octagon: could not remove record for bottleID %@, error:%@", bottleID, localError);
319 - (OTBottledPeerSigned *) restoreFromEscrowRecordID:(NSString*)escrowRecordID
320 secret:(NSData*)secret
321 error:(NSError**)error
323 NSError *localError = nil;
325 if(self.lockStateTracker.isLocked){
327 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
332 OTEscrowKeys *escrowKeys = [[OTEscrowKeys alloc] initWithSecret:secret dsid:self.dsid error:&localError];
333 if (!escrowKeys || localError != nil) {
334 secerror("unable to derive escrow keys: %@", localError);
341 BOOL result = [self.cloudStore downloadBottledPeerRecord:&localError];
342 if(!result || localError){
343 secerror("octagon: could not download bottled peer record:%@", localError);
348 NSString* recordName = [OTBottledPeerRecord constructRecordID:escrowRecordID
349 escrowSigningSPKI:[escrowKeys.signingKey.publicKey asSPKI]];
350 OTBottledPeerRecord* rec = [self.localStore readLocalBottledPeerRecordWithRecordID:recordName error:&localError];
353 secerror("octagon: could not read bottled peer record:%@", localError);
360 OTBottledPeerSigned *bps = [[OTBottledPeerSigned alloc] initWithBottledPeerRecord:rec
361 escrowKeys:escrowKeys
364 secerror("octagon: could not unpack bottled peer:%@", localError);
374 -(BOOL)bottleExistsLocallyForIdentity:(OTIdentity*)identity logger:(CKKSAnalytics*)logger error:(NSError**)error
376 NSError* localError = nil;
377 //read all the local bp records
378 NSArray<OTBottledPeerRecord*>* bottles = [self.localStore readLocalBottledPeerRecordsWithMatchingPeerID:identity.spID error:&localError];
379 if(!bottles || [bottles count] == 0 || localError != nil){
380 secerror("octagon: there are no eligible bottle peer records: %@", localError);
381 [logger logRecoverableError:localError
382 forEvent:OctagonEventBottleCheck
383 zoneName:kOTRampZoneName
384 withAttributes:NULL];
392 //if check all the records if the peer signing public key matches the bottled one!
393 for(OTBottledPeerRecord* bottle in bottles){
394 NSData* bottledSigningSPKIData = [[SFECPublicKey fromSPKI:bottle.peerSigningSPKI] keyData];
395 NSData* currentIdentitySPKIData = [identity.peerSigningKey.publicKey keyData];
397 //spIDs are the same AND check bottle signature
398 if([currentIdentitySPKIData isEqualToData:bottledSigningSPKIData] &&
399 [OTBottledPeerSigned verifyBottleSignature:bottle.bottle
400 signature:bottle.signatureUsingPeerKey
401 key:identity.peerSigningKey.publicKey
412 -(BOOL)queryCloudKitForBottle:(OTIdentity*)identity logger:(CKKSAnalytics*)logger error:(NSError**)error
414 NSError* localError = nil;
416 //attempt to pull down all the records, but continue checking local store even if this fails.
417 BOOL fetched = [self.cloudStore downloadBottledPeerRecord:&localError];
418 if(fetched == NO || localError != nil){ //couldn't download bottles
419 secerror("octagon: 0 bottled peers downloaded: %@", localError);
420 [logger logRecoverableError:localError
421 forEvent:OctagonEventBottleCheck
422 zoneName:kOTRampZoneName
423 withAttributes:NULL];
428 }else{ //downloaded bottles, let's check local store
429 hasBottle = [self bottleExistsLocallyForIdentity:identity logger:logger error:&localError];
438 -(OctagonBottleCheckState) doesThisDeviceHaveABottle:(NSError**)error
440 secnotice("octagon", "checking if device has enrolled a bottle");
442 if(self.lockStateTracker.isLocked){
443 secnotice("octagon", "device locked, not checking for bottle");
445 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
450 if(self.accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable){
452 *error = [NSError errorWithDomain:octagonErrorDomain
453 code:OTErrorNotSignedIn
454 userInfo:@{NSLocalizedDescriptionKey: @"iCloud account is logged out"}];
456 secnotice("octagon", "not logged into an account");
460 NSError* localError = nil;
461 OctagonBottleCheckState bottleStatus = NOBOTTLE;
462 CKKSAnalytics* logger = [CKKSAnalytics logger];
463 SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityBottleCheck withAction:nil];
466 //get our current identity
467 OTIdentity* identity = [self.identityProvider currentIdentity:&localError];
469 //if we get the locked error, return true so we don't prompt the user
470 if(localError && [_lockStateTracker isLockedError:localError]){
471 secnotice("octagon", "attempting to perform bottle check while locked: %@", localError);
475 if(!identity && localError != nil){
476 secerror("octagon: do not have an identity: %@", localError);
477 [logger logRecoverableError:localError
478 forEvent:OctagonEventBottleCheck
479 zoneName:kOTRampZoneName
480 withAttributes:NULL];
488 //check locally first
489 BOOL bottleExistsLocally = [self bottleExistsLocallyForIdentity:identity logger:logger error:&localError];
491 //no bottle and we have no network
492 if(!bottleExistsLocally && !self.reachabilityTracker.currentReachability){
493 secnotice("octagon", "no network, can't query");
494 localError = [NSError errorWithDomain:octagonErrorDomain
495 code:OTErrorNoNetwork
496 userInfo:@{NSLocalizedDescriptionKey: @"no network"}];
503 else if(!bottleExistsLocally){
504 if([self queryCloudKitForBottle:identity logger:logger error:&localError]){
505 bottleStatus = BOTTLE;
507 }else if(bottleExistsLocally){
508 bottleStatus = BOTTLE;
511 if(bottleStatus == NOBOTTLE){
512 localError = [NSError errorWithDomain:octagonErrorDomain code:OTErrorNoBottlePeerRecords userInfo:@{NSLocalizedDescriptionKey: @"Peer %@ does not have any bottled records"}];
513 secerror("octagon: this device does not have any bottled peers: %@", localError);
514 [logger logRecoverableError:localError
515 forEvent:OctagonEventBottleCheck
516 zoneName:kOTRampZoneName
517 withAttributes:@{ OctagonEventAttributeFailureReason : @"does not have bottle"}];
523 [logger logSuccessForEventNamed:OctagonEventBottleCheck];
533 NSError* error = nil;
535 CKKSAnalytics* logger = [CKKSAnalytics logger];
536 SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityBottleCheck withAction:nil];
539 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
540 CDPFollowUpContext *context = [CDPFollowUpContext contextForOfflinePasscodeChange];
542 [cdpd postFollowUpWithContext:context error:&error];
544 [logger logUnrecoverableError:error forEvent:OctagonEventCoreFollowUp withAttributes:@{
545 OctagonEventAttributeFailureReason : @"core follow up failed"}];
547 secerror("request to CoreCDP to follow up failed: %@", error);
550 [logger logSuccessForEventNamed:OctagonEventCoreFollowUp];