EscrowRequestServer.m   [plain text]

#import "utilities/debugging.h"

#import "keychain/escrowrequest/EscrowRequestController.h"
#import "keychain/escrowrequest/EscrowRequestServer.h"
#import "keychain/escrowrequest/generated_source/SecEscrowPendingRecord.h"
#import "keychain/escrowrequest/SecEscrowPendingRecord+KeychainSupport.h"

#import "keychain/ckks/CKKSLockStateTracker.h"
#import "keychain/ckks/CKKSAnalytics.h"

NSString* ESRPendingSince = @"ERSPending";

@implementation EscrowRequestServer

- (instancetype)initWithLockStateTracker:(CKKSLockStateTracker*)lockStateTracker
{
if((self = [super init])) {
_controller = [[EscrowRequestController alloc] initWithLockStateTracker:lockStateTracker];
}
return self;
}

+ (EscrowRequestServer*)server
{
static EscrowRequestServer* server;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
server = [[EscrowRequestServer alloc] initWithLockStateTracker:[CKKSLockStateTracker globalTracker]];
[server setupAnalytics];
});
return server;
}

+ (id<SecEscrowRequestable> _Nullable)request:(NSError *__autoreleasing _Nullable * _Nullable)error {
return [EscrowRequestServer server];
}

- (BOOL)triggerEscrowUpdate:(nonnull NSString *)reason
error:(NSError * _Nullable __autoreleasing * _Nullable)error
{
// Magical async code style to sync conversion only happens with NSXPC.
// Use a semaphore here, since we don't have any other option.
dispatch_semaphore_t sema = dispatch_semaphore_create(0);

__block NSError* updateError = nil;
[self triggerEscrowUpdate:reason reply:^(NSError * _Nullable operationError) {
updateError = operationError;
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

if(error && updateError) {
*error = updateError;
}
return updateError == nil ? YES : NO;
}

- (NSDictionary *)fetchStatuses:(NSError **)error
{
__block NSDictionary *status = nil;

__block NSError* updateError = nil;
[self fetchRequestStatuses:^(NSDictionary<NSString *,NSString *> * _Nullable requestUUID, NSError * _Nullable blockError) {
status = requestUUID;
updateError = blockError;
}];

if(error && updateError) {
*error = updateError;
}
return status;
}

- (bool)pendingEscrowUpload:(NSError *__autoreleasing _Nullable * _Nullable)error {
__block NSDictionary *result = nil;
__block NSError* updateError = nil;

[self fetchRequestStatuses:^(NSDictionary<NSString *,NSString *> * requestUUID, NSError *blockError) {
result = requestUUID;
updateError = blockError;
}];

if(updateError) {
secnotice("escrow", "failed to fetch escrow statuses: %@", updateError);
if(error) {
*error = updateError;
}
return NO;
}
if(result == nil || (result && [result count] == 0)) {
return NO;
}

BOOL inProgress = NO;
for(NSString* status in result.allValues) {
if([status isEqualToString:SecEscrowRequestHavePrecord] ||
[status isEqualToString:SecEscrowRequestPendingPasscode] ||
[status isEqualToString:SecEscrowRequestPendingCertificate]) {
inProgress = YES;
}
}

return inProgress;
}

- (void)cachePrerecord:(NSString*)uuid
serializedPrerecord:(nonnull NSData *)prerecord
reply:(nonnull void (^)(NSError * _Nullable))reply
{
NSError* error = nil;
SecEscrowPendingRecord* record = [SecEscrowPendingRecord loadFromKeychain:uuid error:&error];

if(error) {
secerror("escrowrequest: unable to load uuid %@: %@", uuid, error);
reply(error);
return;
}

record.serializedPrerecord = prerecord;

[record saveToKeychain:&error];
if(error) {
secerror("escrowrequest: unable to save new prerecord for uuid %@: %@", uuid, error);
reply(error);
return;
}

secnotice("escrowrequest", "saved new prerecord for uuid %@", uuid);

// Poke the EscrowRequestController, so it will upload the record
[self.controller.stateMachine pokeStateMachine];

reply(nil);
}

- (void)fetchPrerecord:(nonnull NSString *)prerecordUUID
reply:(nonnull void (^)(NSData * _Nullable, NSError * _Nullable))reply
{
NSError* error = nil;
SecEscrowPendingRecord* record = [SecEscrowPendingRecord loadFromKeychain:prerecordUUID error:&error];

if(error) {
secerror("escrowrequest: unable to load prerecord with uuid %@: %@", prerecordUUID, error);
reply(nil, error);
return;
}

if(record.hasSerializedPrerecord) {
secnotice("escrowrequest", "fetched prerecord for uuid %@", prerecordUUID);
reply(record.serializedPrerecord, nil);
} else {
secerror("escrowrequest: no prerecord for uuid %@: %@", prerecordUUID, error);
// TODO: fill in error
reply(nil, error);
}
}

- (void)fetchRequestWaitingOnPasscode:(nonnull void (^)(NSString * _Nullable, NSError * _Nullable))reply
{
NSError* error = nil;

NSArray<SecEscrowPendingRecord*>* records = [SecEscrowPendingRecord loadAllFromKeychain:&error];

if(error && [error.domain isEqualToString:NSOSStatusErrorDomain] && error.code == errSecItemNotFound) {
// Fair enough! There are no requests waiting for a passcode.
[[CKKSAnalytics logger] setDateProperty:nil forKey:ESRPendingSince];
reply(nil, nil);
return;
}

if(!records || error) {
reply(nil, error);
return;
}

// Are any of these requests pending?
for(SecEscrowPendingRecord* record in records) {
if(!record.certCached) {
secnotice("escrowrequest", "Escrow request %@ doesn't yet have a certificate cached", record.uuid);
continue;
}

if(record.hasSerializedPrerecord) {
secnotice("escrowrequest", "Escrow request %@ already has a prerecord; no passcode needed", record.uuid);
continue;
}

secnotice("escrowrequest", "Escrow request %@ is pending a passcode", record.uuid);
reply(record.uuid, nil);
return;
}

secnotice("escrowrequest", "No escrow requests need a passcode");
reply(nil, nil);
}

- (void)triggerEscrowUpdate:(nonnull NSString *)reason
reply:(nonnull void (^)(NSError * _Nullable))reply
{
secnotice("escrowrequest", "Triggering an escrow update request due to '%@'", reason);

[self.controller triggerEscrowUpdateRPC:reason
reply:reply];
}

- (void)fetchRequestStatuses:(void (^)(NSDictionary<NSString*, NSString*>* _Nullable requestUUID, NSError* _Nullable error))reply
{
NSError* error = nil;
NSArray<SecEscrowPendingRecord*>* records = [SecEscrowPendingRecord loadAllFromKeychain:&error];

if(error && [error.domain isEqualToString:NSOSStatusErrorDomain] && error.code == errSecItemNotFound) {
// Fair enough! There are no requests waiting for a passcode.
secnotice("escrowrequest", "no extant requests");
reply(@{}, nil);
return;
}

if(error) {
secerror("escrowrequest: failed to load requests: %@", error);
reply(nil, error);
}

secnotice("escrowrequest", "found requests: %@", records);

NSMutableDictionary<NSString*, NSString*>* d = [NSMutableDictionary dictionary];
for(SecEscrowPendingRecord* record in records) {
if(record.uploadCompleted) {
d[record.uuid] = @"complete";
} else if(record.hasSerializedPrerecord) {
d[record.uuid] = SecEscrowRequestHavePrecord;
} else if(record.certCached) {
d[record.uuid] = SecEscrowRequestPendingPasscode;
} else {
d[record.uuid] = SecEscrowRequestPendingCertificate;
}
}

reply(d, nil);
}

- (void)resetAllRequests:(void (^)(NSError* _Nullable error))reply
{
secnotice("escrowrequest", "deleting all requests");

NSError* error = nil;
NSArray<SecEscrowPendingRecord*>* records = [SecEscrowPendingRecord loadAllFromKeychain:&error];

if(error && [error.domain isEqualToString:NSOSStatusErrorDomain] && error.code == errSecItemNotFound) {
// Fair enough! There are no requests waiting for a passcode.
secnotice("escrowrequest", "no extant requests; nothing to delete");
reply(nil);
return;
}

if(error) {
secnotice("escrowrequest", "error fetching records: %@", error);
reply(error);
return;
}

for(SecEscrowPendingRecord* record in records) {
[record deleteFromKeychain:&error];
if(error) {
secnotice("escrowrequest", "Unable to delete %@: %@", record, error);
}
}

// Report the last error, if any.
reply(error);
}

- (void)storePrerecordsInEscrow:(void (^)(uint64_t count, NSError* _Nullable error))reply
{
secnotice("escrowrequest", "attempting to store a prerecord in escrow");

[self.controller storePrerecordsInEscrowRPC:reply];
}

- (void)setupAnalytics
{
[[CKKSAnalytics logger] AddMultiSamplerForName:@"escorwrequest-healthSummary" withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
NSMutableDictionary<NSString *,NSNumber*>* values = [NSMutableDictionary dictionary];

NSDate *date = [[CKKSAnalytics logger] datePropertyForKey:ESRPendingSince];
if (date) {
values[ESRPendingSince] = @([CKKSAnalytics fuzzyDaysSinceDate:date]);
}

return values;
}];
}

@end