EscrowRequestController.m [plain text]
#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]]
flags: [NSSet setWithArray:@[OctagonFlagEscrowRequestInformCloudServicesOperation]]
initialState:EscrowRequestStateNothingToDo
queue:_queue
stateEngine:self
lockStateTracker:lockStateTracker
reachabilityTracker:nil];
_forceIgnoreCloudServicesRateLimiting = false;
}
return self;
}
- (CKKSResultOperation<OctagonStateTransitionOperationProtocol> * _Nullable)_onqueueNextStateMachineTransition:(nonnull OctagonState *)currentState
flags:(nonnull OctagonFlags *)flags
pendingFlags:(nonnull id<OctagonStateOnqueuePendingFlagHandler>)pendingFlagHandler
{
dispatch_assert_queue(self.queue);
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: 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 continue;
}
secnotice("escrowrequest", "Request
return [OctagonStateTransitionOperation named:@"escrow-request-cache-cert"
entering:EscrowRequestStateTriggerCloudServices];
}
if(record.hasSerializedPrerecord) {
if([record escrowAttemptedWithinLastSeconds:5*60]) {
secnotice("escrowrequest", "Request continue;
}
secnotice("escrowrequest", "Request
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: 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.hasCertCached = false;
existingRecord.serializedPrerecord = nil;
[existingRecord saveToKeychain:&error];
if(error) {
secerror("escrowrequest: Unable to save modified request to keychain: 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 saveToKeychain:&error];
if(error) {
secerror("escrowrequest: unable to save escrow update request: 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 reply(op.numberOfRecordsUploaded, error);
}];
}
@end