]> git.saurik.com Git - apple/security.git/blobdiff - keychain/escrowrequest/EscrowRequestController.m
Security-59306.11.20.tar.gz
[apple/security.git] / keychain / escrowrequest / EscrowRequestController.m
diff --git a/keychain/escrowrequest/EscrowRequestController.m b/keychain/escrowrequest/EscrowRequestController.m
new file mode 100644 (file)
index 0000000..f985e50
--- /dev/null
@@ -0,0 +1,227 @@
+
+#import "utilities/debugging.h"
+
+#import "keychain/ot/OctagonStateMachine.h"
+#import "keychain/ot/OTStates.h"
+#import "keychain/escrowrequest/EscrowRequestController.h"
+#import "keychain/escrowrequest/EscrowRequestServer.h"
+
+#import "keychain/ckks/CKKSLockStateTracker.h"
+
+#import "keychain/ot/ObjCImprovements.h"
+
+#import "keychain/escrowrequest/operations/EscrowRequestInformCloudServicesOperation.h"
+#import "keychain/escrowrequest/operations/EscrowRequestPerformEscrowEnrollOperation.h"
+
+#import "keychain/escrowrequest/generated_source/SecEscrowPendingRecord.h"
+#import "keychain/escrowrequest/SecEscrowPendingRecord+KeychainSupport.h"
+
+OctagonState* const EscrowRequestStateNothingToDo = (OctagonState*)@"nothing_to_do";
+OctagonState* const EscrowRequestStateTriggerCloudServices = (OctagonState*)@"trigger_cloudservices";
+
+OctagonState* const EscrowRequestStateAttemptEscrowUpload = (OctagonState*)@"trigger_escrow_upload";
+OctagonState* const EscrowRequestStateWaitForUnlock = (OctagonState*)@"wait_for_unlock";
+
+@interface EscrowRequestController ()
+@property dispatch_queue_t queue;
+@property CKKSLockStateTracker* lockStateTracker;
+@property bool haveRecordedDate;
+@end
+
+@implementation EscrowRequestController
+
+- (instancetype)initWithLockStateTracker:(CKKSLockStateTracker*)lockStateTracker
+{
+    if((self = [super init])) {
+        _queue = dispatch_queue_create("EscrowRequestControllerQueue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
+        _lockStateTracker = lockStateTracker;
+
+        _stateMachine = [[OctagonStateMachine alloc] initWithName:@"escrowrequest"
+                                                           states:[NSSet setWithArray:@[EscrowRequestStateNothingToDo,
+                                                                                          EscrowRequestStateTriggerCloudServices,
+                                                                                          EscrowRequestStateAttemptEscrowUpload,
+                                                                                          EscrowRequestStateWaitForUnlock]]
+                                                     initialState:EscrowRequestStateNothingToDo
+                                                            queue:_queue
+                                                      stateEngine:self
+                                                 lockStateTracker:lockStateTracker];
+
+        _forceIgnoreCloudServicesRateLimiting = false;
+    }
+
+    return self;
+}
+
+- (CKKSResultOperation<OctagonStateTransitionOperationProtocol> * _Nullable)_onqueueNextStateMachineTransition:(nonnull OctagonState *)currentState
+                                                                                                         flags:(nonnull OctagonFlags *)flags
+                                                                                                  pendingFlags:(nonnull id<OctagonStateOnqueuePendingFlagHandler>)pendingFlagHandler
+{
+    if([flags _onqueueContains:OctagonFlagEscrowRequestInformCloudServicesOperation]) {
+        [flags _onqueueRemoveFlag:OctagonFlagEscrowRequestInformCloudServicesOperation];
+        return [[EscrowRequestInformCloudServicesOperation alloc] initWithIntendedState:EscrowRequestStateNothingToDo
+                                                                             errorState:EscrowRequestStateNothingToDo
+                                                                       lockStateTracker:self.lockStateTracker];
+    }
+
+    if([currentState isEqualToString:EscrowRequestStateTriggerCloudServices]) {
+        return [[EscrowRequestInformCloudServicesOperation alloc] initWithIntendedState:EscrowRequestStateNothingToDo
+                                                                             errorState:EscrowRequestStateNothingToDo
+                                                                       lockStateTracker:self.lockStateTracker];
+    }
+
+    if([currentState isEqualToString:EscrowRequestStateAttemptEscrowUpload]) {
+        return [[EscrowRequestPerformEscrowEnrollOperation alloc] initWithIntendedState:EscrowRequestStateNothingToDo
+                                                                             errorState:EscrowRequestStateNothingToDo
+                                                                    enforceRateLimiting:true
+                                                                       lockStateTracker:self.lockStateTracker];
+    }
+
+    if([currentState isEqualToString:EscrowRequestStateWaitForUnlock]) {
+        secnotice("escrowrequest", "waiting for unlock before continuing with state machine");
+        OctagonStateTransitionOperation* op = [OctagonStateTransitionOperation named:@"wait-for-unlock"
+                                                                            entering:EscrowRequestStateNothingToDo];
+        [op addNullableDependency:self.lockStateTracker.unlockDependency];
+        return op;
+    }
+
+    NSError* error = nil;
+    NSArray<SecEscrowPendingRecord*>* records = [SecEscrowPendingRecord loadAllFromKeychain:&error];
+    if(error) {
+        if([self.lockStateTracker isLockedError:error]) {
+            return [OctagonStateTransitionOperation named:@"wait-for-unlock"
+                                                 entering:EscrowRequestStateWaitForUnlock];
+        }
+        secnotice("escrowrequest", "failed to fetch records from keychain, nothing to do: %@", error);
+        return nil;
+    }
+
+    // First, do we need to poke CloudServices?
+    for(SecEscrowPendingRecord* record in records) {
+        // Completed records don't need anything.
+        if(record.hasUploadCompleted && record.uploadCompleted) {
+            continue;
+        }
+
+        if (!self.haveRecordedDate) {
+            NSDate *date = [[CKKSAnalytics logger] datePropertyForKey:ESRPendingSince];
+            if (date == NULL) {
+                [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:ESRPendingSince];
+            }
+            self.haveRecordedDate = true;
+        }
+
+        uint64_t fiveMinutesAgo = ((uint64_t)[[NSDate date] timeIntervalSince1970] * 1000) - (1000*60*5);
+
+        if(!record.certCached) {
+            if(!self.forceIgnoreCloudServicesRateLimiting && (record.hasLastCloudServicesTriggerTime && record.lastCloudServicesTriggerTime >= fiveMinutesAgo)) {
+                secnotice("escrowrequest", "Request %@ needs to cache a certificate, but that has been attempted recently. Holding off...", record.uuid);
+                continue;
+            }
+
+            secnotice("escrowrequest", "Request %@ needs a cached certififcate", record.uuid);
+
+            return [OctagonStateTransitionOperation named:@"escrow-request-cache-cert"
+                                                 entering:EscrowRequestStateTriggerCloudServices];
+        }
+
+        if(record.hasSerializedPrerecord) {
+            if([record escrowAttemptedWithinLastSeconds:5*60]) {
+                secnotice("escrowrequest", "Request %@ needs to be stored, but has been attempted recently. Holding off...", record.uuid);
+                continue;
+            }
+
+            secnotice("escrowrequest", "Request %@ needs to be stored!", record.uuid);
+
+            return [OctagonStateTransitionOperation named:@"escrow-request-attempt-escrow-upload"
+                                                 entering:EscrowRequestStateAttemptEscrowUpload];
+        }
+    }
+
+
+    return nil;
+}
+
+- (void)triggerEscrowUpdateRPC:(nonnull NSString *)reason
+                         reply:(nonnull void (^)(NSError * _Nullable))reply
+{
+    [self.stateMachine startOperation];
+
+    NSError* error = nil;
+    NSArray<SecEscrowPendingRecord*>* records = [SecEscrowPendingRecord loadAllFromKeychain:&error];
+    if(error && !([error.domain isEqualToString:NSOSStatusErrorDomain] && error.code == errSecItemNotFound)) {
+        secnotice("escrowrequest", "failed to fetch records from keychain: %@", error);
+        reply(error);
+        return;
+    }
+    error = nil;
+
+    secnotice("escrowrequest", "Investigating a new escrow request");
+
+    BOOL escrowRequestExists = NO;
+    for(SecEscrowPendingRecord* existingRecord in records) {
+        if(existingRecord.uploadCompleted) {
+            continue;
+        }
+
+        if (existingRecord.hasAltDSID) {
+            continue;
+        }
+
+        secnotice("escrowrequest", "Retriggering an existing escrow request: %@", existingRecord);
+        existingRecord.hasCertCached = false;
+        existingRecord.serializedPrerecord = nil;
+
+        [existingRecord saveToKeychain:&error];
+        if(error) {
+            secerror("escrowrequest: Unable to save modified request to keychain: %@", error);
+            reply(error);
+            return;
+        }
+
+        secnotice("escrowrequest", "Retriggering an existing escrow request complete");
+        escrowRequestExists = YES;
+    }
+
+    if(escrowRequestExists == NO){
+        secnotice("escrowrequest", "Creating a new escrow request");
+
+        SecEscrowPendingRecord* record = [[SecEscrowPendingRecord alloc] init];
+        record.uuid = [[NSUUID UUID] UUIDString];
+        record.altDSID = nil;
+        record.triggerRequestTime = ((uint64_t)[[NSDate date] timeIntervalSince1970] * 1000);
+        secnotice("escrowrequest", "beginning a new escrow request (%@)", record.uuid);
+
+        [record saveToKeychain:&error];
+
+        if(error) {
+            secerror("escrowrequest: unable to save escrow update request: %@", error);
+            reply(error);
+            return;
+        }
+    }
+
+    [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:ESRPendingSince];
+    self.haveRecordedDate = true;
+
+    [self.stateMachine handleFlag:OctagonFlagEscrowRequestInformCloudServicesOperation];
+
+    reply(nil);
+}
+
+- (void)storePrerecordsInEscrowRPC:(void (^)(uint64_t count, NSError* _Nullable error))reply
+{
+    EscrowRequestPerformEscrowEnrollOperation* op = [[EscrowRequestPerformEscrowEnrollOperation alloc] initWithIntendedState:EscrowRequestStateNothingToDo
+                                                                                                                  errorState:EscrowRequestStateNothingToDo
+                                                                                                         enforceRateLimiting:false
+                                                                                                            lockStateTracker:self.lockStateTracker];
+    [self.stateMachine startOperation];
+    [self.stateMachine doSimpleStateMachineRPC:@"trigger-escrow-store"
+                                            op:op
+                                  sourceStates:[NSSet setWithObject:EscrowRequestStateNothingToDo]
+                                         reply:^(NSError * _Nullable error) {
+                                             secnotice("escrowrequest", "Uploaded %d records with error %@", (int)op.numberOfRecordsUploaded, error);
+                                             reply(op.numberOfRecordsUploaded, error);
+                                         }];
+}
+
+@end