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/CDPAccount.h>
37 NSString* OTCKContainerName = @"com.apple.security.keychain";
38 NSString* OTCKZoneName = @"OctagonTrust";
39 static NSString* const kOTRampZoneName = @"metadata_zone";
41 @interface OTContext (lockstateTracker) <CKKSLockStateNotification>
44 @interface OTContext ()
46 @property (nonatomic, strong) NSString* contextID;
47 @property (nonatomic, strong) NSString* contextName;
48 @property (nonatomic, strong) NSString* dsid;
50 @property (nonatomic, strong) OTLocalStore* localStore;
51 @property (nonatomic, strong) OTCloudStore* cloudStore;
52 @property (nonatomic, strong) NSData* changeToken;
53 @property (nonatomic, strong) NSString* egoPeerID;
54 @property (nonatomic, strong) NSDate* egoPeerCreationDate;
55 @property (nonatomic, strong) dispatch_queue_t queue;
56 @property (nonatomic, weak) id <OTContextIdentityProvider> identityProvider;
58 @property (nonatomic, strong) CKKSCKAccountStateTracker* accountTracker;
59 @property (nonatomic, strong) CKKSLockStateTracker* lockStateTracker;
60 @property (nonatomic, strong) CKKSReachabilityTracker *reachabilityTracker;
64 @implementation OTContext
66 -(CKContainer*)makeCKContainer:(NSString*)containerName {
67 CKContainer* container = [CKContainer containerWithIdentifier:containerName];
68 container = [[CKContainer alloc] initWithContainerID: container.containerID];
72 -(BOOL) isPrequeliteEnabled
75 if([PQLConnection class] == nil) {
76 secerror("OT: prequelite appears to not be linked. Can't create OT objects.");
82 - (nullable instancetype) initWithContextID:(NSString*)contextID
84 localStore:(OTLocalStore*)localStore
85 cloudStore:(nullable OTCloudStore*)cloudStore
86 identityProvider:(id <OTContextIdentityProvider>)identityProvider
87 error:(NSError**)error
89 if(![self isPrequeliteEnabled]){
90 // We're running in the base build environment, which lacks a bunch of libraries.
91 // We don't support doing anything in this environment. Bye.
97 NSError* localError = nil;
98 _contextID = contextID;
100 _identityProvider = identityProvider;
101 _localStore = localStore;
103 NSString* contextAndDSID = [NSString stringWithFormat:@"%@-%@", contextID, dsid];
105 CKContainer* container = [self makeCKContainer:OTCKContainerName];
107 _accountTracker = [CKKSViewManager manager].accountTracker;
108 _lockStateTracker = [CKKSViewManager manager].lockStateTracker;
109 _reachabilityTracker = [CKKSViewManager manager].reachabilityTracker;
112 _cloudStore = [[OTCloudStore alloc]initWithContainer:container
113 zoneName:OTCKZoneName
114 accountTracker:_accountTracker
115 reachabilityTracker:_reachabilityTracker
116 localStore:_localStore
119 fetchRecordZoneChangesOperationClass:[CKFetchRecordZoneChangesOperation class]
120 fetchRecordsOperationClass:[CKFetchRecordsOperation class]
121 queryOperationClass:[CKQueryOperation class]
122 modifySubscriptionsOperationClass:[CKModifySubscriptionsOperation class]
123 modifyRecordZonesOperationClass:[CKModifyRecordZonesOperation class]
124 apsConnectionClass:[APSConnection class]
127 _cloudStore = cloudStore;
130 OTContextRecord* localContextRecord = [_localStore readLocalContextRecordForContextIDAndDSID:contextAndDSID error:&localError];
132 if(localContextRecord == nil || localContextRecord.contextID == nil){
134 BOOL result = [_localStore initializeContextTable:contextID dsid:dsid error:&localError];
135 if(!result || localError != nil){
136 secerror("octagon: reading from database failed with error: %@", localError);
142 localContextRecord = [_localStore readLocalContextRecordForContextIDAndDSID:contextAndDSID error:&localError];
143 if(localContextRecord == nil || localError !=nil){
144 secerror("octagon: reading from database failed with error: %@", localError);
152 _contextID = localContextRecord.contextID;
153 _contextName = localContextRecord.contextName;
154 _changeToken = localContextRecord.changeToken;
155 _egoPeerID = localContextRecord.egoPeerID;
156 _egoPeerCreationDate = localContextRecord.egoPeerCreationDate;
158 _queue = dispatch_queue_create("com.apple.security.otcontext", DISPATCH_QUEUE_SERIAL);
163 - (nullable OTBottledPeerSigned *) createBottledPeerRecordForIdentity:(OTIdentity *)identity
164 secret:(NSData*)secret
165 error:(NSError**)error
167 NSError* localError = nil;
168 if(self.lockStateTracker.isLocked){
169 secnotice("octagon", "device is locked");
171 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
176 OTEscrowKeys *escrowKeys = [[OTEscrowKeys alloc] initWithSecret:secret dsid:self.dsid error:&localError];
177 if (!escrowKeys || localError != nil) {
178 secerror("octagon: unable to derive escrow keys: %@", localError);
185 OTBottledPeer *bp = [[OTBottledPeer alloc] initWithPeerID:identity.peerID
187 peerSigningKey:identity.peerSigningKey
188 peerEncryptionKey:identity.peerEncryptionKey
189 escrowKeys:escrowKeys
191 if (!bp || localError !=nil) {
192 secerror("octagon: unable to create a bottled peer: %@", localError);
198 return [[OTBottledPeerSigned alloc] initWithBottledPeer:bp
199 escrowedSigningKey:escrowKeys.signingKey
200 peerSigningKey:identity.peerSigningKey
204 - (NSData* _Nullable) makeMeSomeEntropy:(int)requiredLength
206 NSMutableData* salt = [NSMutableData dataWithLength:requiredLength];
210 if (SecRandomCopyBytes(kSecRandomDefault, [salt length], [salt mutableBytes]) != 0){
216 - (nullable OTPreflightInfo*) preflightBottledPeer:(NSString*)contextID
217 entropy:(NSData*)entropy
218 error:(NSError**)error
220 NSError* localError = nil;
221 if(self.lockStateTracker.isLocked){
222 secnotice("octagon", "device is locked");
224 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
229 OTIdentity *identity = [self.identityProvider currentIdentity:&localError];
230 if (!identity || localError != nil) {
231 secerror("octagon: unable to get current identity:%@", localError);
238 OTBottledPeerSigned* bps = [self createBottledPeerRecordForIdentity:identity
241 if (!bps || localError != nil) {
242 secerror("octagon: failed to create bottled peer record: %@", localError);
248 secnotice("octagon", "created bottled peer:%@", bps);
250 OTBottledPeerRecord *bprec = [bps asRecord:identity.spID];
252 if (!identity.spID) {
253 secerror("octagon: cannot enroll without a spID");
255 *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorNoIdentity userInfo:@{NSLocalizedDescriptionKey: @"OTIdentity does not have an SOS peer id"}];
260 OTPreflightInfo* info = [[OTPreflightInfo alloc]init];
261 info.escrowedSigningSPKI = bprec.escrowedSigningSPKI;
263 if(!info.escrowedSigningSPKI){
265 *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorEscrowSigningSPKI userInfo:@{NSLocalizedDescriptionKey: @"Escrowed spinging SPKI is nil"}];
267 secerror("octagon: Escrowed spinging SPKI is nil");
271 info.bottleID = bprec.recordName;
274 *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorBottleID userInfo:@{NSLocalizedDescriptionKey: @"BottleID is nil"}];
276 secerror("octagon: BottleID is nil");
280 //store record in localStore
281 BOOL result = [self.localStore insertBottledPeerRecord:bprec escrowRecordID:identity.spID error:&localError];
282 if(!result || localError){
283 secerror("octagon: could not persist the bottle record: %@", localError);
293 - (BOOL)scrubBottledPeer:(NSString*)contextID
294 bottleID:(NSString*)bottleID
295 error:(NSError**)error
297 secnotice("octagon", "scrubBottledPeer");
298 NSError* localError = nil;
299 if(self.lockStateTracker.isLocked){
300 secnotice("octagon", "device is locked");
302 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
307 BOOL result = [self.localStore deleteBottledPeer:bottleID error:&localError];
308 if(!result || localError != nil){
309 secerror("octagon: could not remove record for bottleID %@, error:%@", bottleID, localError);
317 - (OTBottledPeerSigned *) restoreFromEscrowRecordID:(NSString*)escrowRecordID
318 secret:(NSData*)secret
319 error:(NSError**)error
321 NSError *localError = nil;
323 if(self.lockStateTracker.isLocked){
325 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
330 OTEscrowKeys *escrowKeys = [[OTEscrowKeys alloc] initWithSecret:secret dsid:self.dsid error:&localError];
331 if (!escrowKeys || localError != nil) {
332 secerror("unable to derive escrow keys: %@", localError);
339 BOOL result = [self.cloudStore downloadBottledPeerRecord:&localError];
340 if(!result || localError){
341 secerror("octagon: could not download bottled peer record:%@", localError);
346 NSString* recordName = [OTBottledPeerRecord constructRecordID:escrowRecordID
347 escrowSigningSPKI:[escrowKeys.signingKey.publicKey asSPKI]];
348 OTBottledPeerRecord* rec = [self.localStore readLocalBottledPeerRecordWithRecordID:recordName error:&localError];
351 secerror("octagon: could not read bottled peer record:%@", localError);
358 OTBottledPeerSigned *bps = [[OTBottledPeerSigned alloc] initWithBottledPeerRecord:rec
359 escrowKeys:escrowKeys
362 secerror("octagon: could not unpack bottled peer:%@", localError);
372 -(BOOL)bottleExistsLocallyForIdentity:(OTIdentity*)identity logger:(CKKSAnalytics*)logger error:(NSError**)error
374 NSError* localError = nil;
375 //read all the local bp records
376 NSArray<OTBottledPeerRecord*>* bottles = [self.localStore readLocalBottledPeerRecordsWithMatchingPeerID:identity.spID error:&localError];
377 if(!bottles || [bottles count] == 0 || localError != nil){
378 secerror("octagon: there are no eligible bottle peer records: %@", localError);
379 [logger logRecoverableError:localError
380 forEvent:OctagonEventBottleCheck
381 zoneName:kOTRampZoneName
382 withAttributes:NULL];
390 //if check all the records if the peer signing public key matches the bottled one!
391 for(OTBottledPeerRecord* bottle in bottles){
392 NSData* bottledSigningSPKIData = [[SFECPublicKey fromSPKI:bottle.peerSigningSPKI] keyData];
393 NSData* currentIdentitySPKIData = [identity.peerSigningKey.publicKey keyData];
395 //spIDs are the same AND check bottle signature
396 if([currentIdentitySPKIData isEqualToData:bottledSigningSPKIData] &&
397 [OTBottledPeerSigned verifyBottleSignature:bottle.bottle
398 signature:bottle.signatureUsingPeerKey
399 key:identity.peerSigningKey.publicKey
410 -(BOOL)queryCloudKitForBottle:(OTIdentity*)identity logger:(CKKSAnalytics*)logger error:(NSError**)error
412 NSError* localError = nil;
414 //attempt to pull down all the records, but continue checking local store even if this fails.
415 BOOL fetched = [self.cloudStore downloadBottledPeerRecord:&localError];
416 if(fetched == NO || localError != nil){ //couldn't download bottles
417 secerror("octagon: 0 bottled peers downloaded: %@", localError);
418 [logger logRecoverableError:localError
419 forEvent:OctagonEventBottleCheck
420 zoneName:kOTRampZoneName
421 withAttributes:NULL];
426 }else{ //downloaded bottles, let's check local store
427 hasBottle = [self bottleExistsLocallyForIdentity:identity logger:logger error:&localError];
436 -(OctagonBottleCheckState) doesThisDeviceHaveABottle:(NSError**)error
438 secnotice("octagon", "checking if device has enrolled a bottle");
440 if(self.lockStateTracker.isLocked){
441 secnotice("octagon", "device locked, not checking for bottle");
443 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
448 if(self.accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable){
450 *error = [NSError errorWithDomain:octagonErrorDomain
451 code:OTErrorNotSignedIn
452 userInfo:@{NSLocalizedDescriptionKey: @"iCloud account is logged out"}];
454 secnotice("octagon", "not logged into an account");
458 NSError* localError = nil;
459 OctagonBottleCheckState bottleStatus = NOBOTTLE;
460 CKKSAnalytics* logger = [CKKSAnalytics logger];
461 SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityBottleCheck withAction:nil];
464 //get our current identity
465 OTIdentity* identity = [self.identityProvider currentIdentity:&localError];
467 //if we get the locked error, return true so we don't prompt the user
468 if(localError && [_lockStateTracker isLockedError:localError]){
469 secnotice("octagon", "attempting to perform bottle check while locked: %@", localError);
473 if(!identity && localError != nil){
474 secerror("octagon: do not have an identity: %@", localError);
475 [logger logRecoverableError:localError
476 forEvent:OctagonEventBottleCheck
477 zoneName:kOTRampZoneName
478 withAttributes:NULL];
486 //check locally first
487 BOOL bottleExistsLocally = [self bottleExistsLocallyForIdentity:identity logger:logger error:&localError];
489 //no bottle and we have no network
490 if(!bottleExistsLocally && !self.reachabilityTracker.currentReachability){
491 secnotice("octagon", "no network, can't query");
492 localError = [NSError errorWithDomain:octagonErrorDomain
493 code:OTErrorNoNetwork
494 userInfo:@{NSLocalizedDescriptionKey: @"no network"}];
501 else if(!bottleExistsLocally){
502 if([self queryCloudKitForBottle:identity logger:logger error:&localError]){
503 bottleStatus = BOTTLE;
505 }else if(bottleExistsLocally){
506 bottleStatus = BOTTLE;
509 if(bottleStatus == NOBOTTLE){
510 localError = [NSError errorWithDomain:octagonErrorDomain code:OTErrorNoBottlePeerRecords userInfo:@{NSLocalizedDescriptionKey: @"Peer %@ does not have any bottled records"}];
511 secerror("octagon: this device does not have any bottled peers: %@", localError);
512 [logger logRecoverableError:localError
513 forEvent:OctagonEventBottleCheck
514 zoneName:kOTRampZoneName
515 withAttributes:@{ OctagonEventAttributeFailureReason : @"does not have bottle"}];
521 [logger logSuccessForEventNamed:OctagonEventBottleCheck];