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