]> git.saurik.com Git - apple/security.git/blob - keychain/ot/OTContext.m
Security-58286.60.28.tar.gz
[apple/security.git] / keychain / ot / OTContext.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23 #if OCTAGON
24
25 #import "OTContext.h"
26 #import "SFPublicKey+SPKI.h"
27
28 #include <utilities/SecFileLocations.h>
29 #include <Security/SecRandomP.h>
30
31 #import "keychain/ckks/CKKS.h"
32 #import "keychain/ckks/CKKSViewManager.h"
33 #import "keychain/ckks/CKKSAnalytics.h"
34
35 #import "CoreCDP/CDPFollowUpController.h"
36 #import "CoreCDP/CDPFollowUpContext.h"
37 #import <CoreCDP/CDPAccount.h>
38
39 NSString* OTCKContainerName = @"com.apple.security.keychain";
40 NSString* OTCKZoneName = @"OctagonTrust";
41 static NSString* const kOTRampZoneName = @"metadata_zone";
42
43 @interface OTContext (lockstateTracker) <CKKSLockStateNotification>
44 @end
45
46 @interface OTContext ()
47
48 @property (nonatomic, strong) NSString* contextID;
49 @property (nonatomic, strong) NSString* contextName;
50 @property (nonatomic, strong) NSString* dsid;
51
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;
59
60 @property (nonatomic, strong) CKKSCKAccountStateTracker* accountTracker;
61 @property (nonatomic, strong) CKKSLockStateTracker* lockStateTracker;
62 @property (nonatomic, strong) CKKSReachabilityTracker *reachabilityTracker;
63
64 @end
65
66 @implementation OTContext
67
68 -(CKContainer*)makeCKContainer:(NSString*)containerName {
69 CKContainer* container = [CKContainer containerWithIdentifier:containerName];
70 container = [[CKContainer alloc] initWithContainerID: container.containerID];
71 return container;
72 }
73
74 -(BOOL) isPrequeliteEnabled
75 {
76 BOOL result = YES;
77 if([PQLConnection class] == nil) {
78 secerror("OT: prequelite appears to not be linked. Can't create OT objects.");
79 result = NO;
80 }
81 return result;
82 }
83
84 - (nullable instancetype) initWithContextID:(NSString*)contextID
85 dsid:(NSString*)dsid
86 localStore:(OTLocalStore*)localStore
87 cloudStore:(nullable OTCloudStore*)cloudStore
88 identityProvider:(id <OTContextIdentityProvider>)identityProvider
89 error:(NSError**)error
90 {
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.
94 return nil;
95 }
96
97 self = [super init];
98 if (self) {
99 NSError* localError = nil;
100 _contextID = contextID;
101 _dsid = dsid;
102 _identityProvider = identityProvider;
103 _localStore = localStore;
104
105 NSString* contextAndDSID = [NSString stringWithFormat:@"%@-%@", contextID, dsid];
106
107 CKContainer* container = [self makeCKContainer:OTCKContainerName];
108
109 _accountTracker = [CKKSViewManager manager].accountTracker;
110 _lockStateTracker = [CKKSViewManager manager].lockStateTracker;
111 _reachabilityTracker = [CKKSViewManager manager].reachabilityTracker;
112
113 if(!cloudStore) {
114 _cloudStore = [[OTCloudStore alloc]initWithContainer:container
115 zoneName:OTCKZoneName
116 accountTracker:_accountTracker
117 reachabilityTracker:_reachabilityTracker
118 localStore:_localStore
119 contextID:contextID
120 dsid:dsid
121 fetchRecordZoneChangesOperationClass:[CKFetchRecordZoneChangesOperation class]
122 fetchRecordsOperationClass:[CKFetchRecordsOperation class]
123 queryOperationClass:[CKQueryOperation class]
124 modifySubscriptionsOperationClass:[CKModifySubscriptionsOperation class]
125 modifyRecordZonesOperationClass:[CKModifyRecordZonesOperation class]
126 apsConnectionClass:[APSConnection class]
127 operationQueue:nil];
128 } else{
129 _cloudStore = cloudStore;
130 }
131
132 OTContextRecord* localContextRecord = [_localStore readLocalContextRecordForContextIDAndDSID:contextAndDSID error:&localError];
133
134 if(localContextRecord == nil || localContextRecord.contextID == nil){
135 localError = 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);
139 if (error) {
140 *error = localError;
141 }
142 return nil;
143 }
144 localContextRecord = [_localStore readLocalContextRecordForContextIDAndDSID:contextAndDSID error:&localError];
145 if(localContextRecord == nil || localError !=nil){
146 secerror("octagon: reading from database failed with error: %@", localError);
147 if (error) {
148 *error = localError;
149 }
150 return nil;
151 }
152 }
153
154 _contextID = localContextRecord.contextID;
155 _contextName = localContextRecord.contextName;
156 _changeToken = localContextRecord.changeToken;
157 _egoPeerID = localContextRecord.egoPeerID;
158 _egoPeerCreationDate = localContextRecord.egoPeerCreationDate;
159
160 _queue = dispatch_queue_create("com.apple.security.otcontext", DISPATCH_QUEUE_SERIAL);
161 }
162 return self;
163 }
164
165 - (nullable OTBottledPeerSigned *) createBottledPeerRecordForIdentity:(OTIdentity *)identity
166 secret:(NSData*)secret
167 error:(NSError**)error
168 {
169 NSError* localError = nil;
170 if(self.lockStateTracker.isLocked){
171 secnotice("octagon", "device is locked");
172 if(error){
173 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
174 }
175 return nil;
176 }
177
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);
181 if (error) {
182 *error = localError;
183 }
184 return nil;
185 }
186
187 OTBottledPeer *bp = [[OTBottledPeer alloc] initWithPeerID:identity.peerID
188 spID:identity.spID
189 peerSigningKey:identity.peerSigningKey
190 peerEncryptionKey:identity.peerEncryptionKey
191 escrowKeys:escrowKeys
192 error:&localError];
193 if (!bp || localError !=nil) {
194 secerror("octagon: unable to create a bottled peer: %@", localError);
195 if (error) {
196 *error = localError;
197 }
198 return nil;
199 }
200 return [[OTBottledPeerSigned alloc] initWithBottledPeer:bp
201 escrowedSigningKey:escrowKeys.signingKey
202 peerSigningKey:identity.peerSigningKey
203 error:error];
204 }
205
206 - (NSData* _Nullable) makeMeSomeEntropy:(int)requiredLength
207 {
208 NSMutableData* salt = [NSMutableData dataWithLength:requiredLength];
209 if (salt == nil){
210 return nil;
211 }
212 if (SecRandomCopyBytes(kSecRandomDefault, [salt length], [salt mutableBytes]) != 0){
213 return nil;
214 }
215 return salt;
216 }
217
218 - (nullable OTPreflightInfo*) preflightBottledPeer:(NSString*)contextID
219 entropy:(NSData*)entropy
220 error:(NSError**)error
221 {
222 NSError* localError = nil;
223 if(self.lockStateTracker.isLocked){
224 secnotice("octagon", "device is locked");
225 if(error){
226 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
227 }
228 return nil;
229 }
230
231 OTIdentity *identity = [self.identityProvider currentIdentity:&localError];
232 if (!identity || localError != nil) {
233 secerror("octagon: unable to get current identity:%@", localError);
234 if (error) {
235 *error = localError;
236 }
237 return nil;
238 }
239
240 OTBottledPeerSigned* bps = [self createBottledPeerRecordForIdentity:identity
241 secret:entropy
242 error:&localError];
243 if (!bps || localError != nil) {
244 secerror("octagon: failed to create bottled peer record: %@", localError);
245 if (error) {
246 *error = localError;
247 }
248 return nil;
249 }
250 secnotice("octagon", "created bottled peer:%@", bps);
251
252 OTBottledPeerRecord *bprec = [bps asRecord:identity.spID];
253
254 if (!identity.spID) {
255 secerror("octagon: cannot enroll without a spID");
256 if(error){
257 *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorNoIdentity userInfo:@{NSLocalizedDescriptionKey: @"OTIdentity does not have an SOS peer id"}];
258 }
259 return nil;
260 }
261
262 OTPreflightInfo* info = [[OTPreflightInfo alloc]init];
263 info.escrowedSigningSPKI = bprec.escrowedSigningSPKI;
264
265 if(!info.escrowedSigningSPKI){
266 if(error){
267 *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorEscrowSigningSPKI userInfo:@{NSLocalizedDescriptionKey: @"Escrowed spinging SPKI is nil"}];
268 }
269 secerror("octagon: Escrowed spinging SPKI is nil");
270 return nil;
271 }
272
273 info.bottleID = bprec.recordName;
274 if(!info.bottleID){
275 if(error){
276 *error = [NSError errorWithDomain:octagonErrorDomain code:OTErrorBottleID userInfo:@{NSLocalizedDescriptionKey: @"BottleID is nil"}];
277 }
278 secerror("octagon: BottleID is nil");
279 return nil;
280 }
281
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);
286 if (error) {
287 *error = localError;
288 }
289 return nil;
290 }
291
292 return info;
293 }
294
295 - (BOOL)scrubBottledPeer:(NSString*)contextID
296 bottleID:(NSString*)bottleID
297 error:(NSError**)error
298 {
299 secnotice("octagon", "scrubBottledPeer");
300 NSError* localError = nil;
301 if(self.lockStateTracker.isLocked){
302 secnotice("octagon", "device is locked");
303 if(error){
304 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
305 }
306 return YES;
307 }
308
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);
312 if (error) {
313 *error = localError;
314 }
315 }
316 return result;
317 }
318
319 - (OTBottledPeerSigned *) restoreFromEscrowRecordID:(NSString*)escrowRecordID
320 secret:(NSData*)secret
321 error:(NSError**)error
322 {
323 NSError *localError = nil;
324
325 if(self.lockStateTracker.isLocked){
326 if(error){
327 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
328 }
329 return nil;
330 }
331
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);
335 if (error) {
336 *error = localError;
337 }
338 return nil;
339 }
340
341 BOOL result = [self.cloudStore downloadBottledPeerRecord:&localError];
342 if(!result || localError){
343 secerror("octagon: could not download bottled peer record:%@", localError);
344 if(error){
345 *error = localError;
346 }
347 }
348 NSString* recordName = [OTBottledPeerRecord constructRecordID:escrowRecordID
349 escrowSigningSPKI:[escrowKeys.signingKey.publicKey asSPKI]];
350 OTBottledPeerRecord* rec = [self.localStore readLocalBottledPeerRecordWithRecordID:recordName error:&localError];
351
352 if (!rec) {
353 secerror("octagon: could not read bottled peer record:%@", localError);
354 if (error) {
355 *error = localError;
356 }
357 return nil;
358 }
359
360 OTBottledPeerSigned *bps = [[OTBottledPeerSigned alloc] initWithBottledPeerRecord:rec
361 escrowKeys:escrowKeys
362 error:&localError];
363 if (!bps) {
364 secerror("octagon: could not unpack bottled peer:%@", localError);
365 if (error) {
366 *error = localError;
367 }
368 return nil;
369 }
370
371 return bps;
372 }
373
374 -(BOOL)bottleExistsLocallyForIdentity:(OTIdentity*)identity logger:(CKKSAnalytics*)logger error:(NSError**)error
375 {
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];
385 if(error){
386 *error = localError;
387 }
388 return NO;
389 }
390
391 BOOL hasBottle = NO;
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];
396
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
402 error:error]){
403 hasBottle = YES;
404 }
405 }
406
407
408
409 return hasBottle;
410 }
411
412 -(BOOL)queryCloudKitForBottle:(OTIdentity*)identity logger:(CKKSAnalytics*)logger error:(NSError**)error
413 {
414 NSError* localError = nil;
415 BOOL hasBottle = NO;
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];
424 if(error){
425 *error = localError;
426 }
427 return NO;
428 }else{ //downloaded bottles, let's check local store
429 hasBottle = [self bottleExistsLocallyForIdentity:identity logger:logger error:&localError];
430 }
431
432 if(error){
433 *error = localError;
434 }
435 return hasBottle;
436 }
437
438 -(OctagonBottleCheckState) doesThisDeviceHaveABottle:(NSError**)error
439 {
440 secnotice("octagon", "checking if device has enrolled a bottle");
441
442 if(self.lockStateTracker.isLocked){
443 secnotice("octagon", "device locked, not checking for bottle");
444 if(error){
445 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
446 }
447 return UNCLEAR;
448 }
449
450 if(self.accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable){
451 if(error){
452 *error = [NSError errorWithDomain:octagonErrorDomain
453 code:OTErrorNotSignedIn
454 userInfo:@{NSLocalizedDescriptionKey: @"iCloud account is logged out"}];
455 }
456 secnotice("octagon", "not logged into an account");
457 return UNCLEAR;
458 }
459
460 NSError* localError = nil;
461 OctagonBottleCheckState bottleStatus = NOBOTTLE;
462 CKKSAnalytics* logger = [CKKSAnalytics logger];
463 SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityBottleCheck withAction:nil];
464 [tracker start];
465
466 //get our current identity
467 OTIdentity* identity = [self.identityProvider currentIdentity:&localError];
468
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);
472 return UNCLEAR;
473 }
474
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];
481 [tracker stop];
482 if(error){
483 *error = localError;
484 }
485 return NOBOTTLE;
486 }
487
488 //check locally first
489 BOOL bottleExistsLocally = [self bottleExistsLocallyForIdentity:identity logger:logger error:&localError];
490
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"}];
497 [tracker stop];
498 if(error){
499 *error = localError;
500 }
501 return UNCLEAR;
502 }
503 else if(!bottleExistsLocally){
504 if([self queryCloudKitForBottle:identity logger:logger error:&localError]){
505 bottleStatus = BOTTLE;
506 }
507 }else if(bottleExistsLocally){
508 bottleStatus = BOTTLE;
509 }
510
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"}];
518 if(error){
519 *error = localError;
520 }
521 }
522 else{
523 [logger logSuccessForEventNamed:OctagonEventBottleCheck];
524 }
525
526 [tracker stop];
527
528 return bottleStatus;
529 }
530
531 -(void) postFollowUp
532 {
533 NSError* error = nil;
534
535 CKKSAnalytics* logger = [CKKSAnalytics logger];
536 SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityBottleCheck withAction:nil];
537
538 [tracker start];
539 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
540 CDPFollowUpContext *context = [CDPFollowUpContext contextForOfflinePasscodeChange];
541
542 [cdpd postFollowUpWithContext:context error:&error];
543 if(error){
544 [logger logUnrecoverableError:error forEvent:OctagonEventCoreFollowUp withAttributes:@{
545 OctagonEventAttributeFailureReason : @"core follow up failed"}];
546
547 secerror("request to CoreCDP to follow up failed: %@", error);
548 }
549 else{
550 [logger logSuccessForEventNamed:OctagonEventCoreFollowUp];
551 }
552 [tracker stop];
553 }
554
555
556 @end
557 #endif