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: 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 reply(error);
return;
}
record.serializedPrerecord = prerecord;
[record saveToKeychain:&error];
if(error) {
secerror("escrowrequest: unable to save new prerecord for uuid reply(error);
return;
}
secnotice("escrowrequest", "saved new prerecord for 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 reply(nil, error);
return;
}
if(record.hasSerializedPrerecord) {
secnotice("escrowrequest", "fetched prerecord for uuid reply(record.serializedPrerecord, nil);
} else {
secerror("escrowrequest: no prerecord for uuid // 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 continue;
}
if(record.hasSerializedPrerecord) {
secnotice("escrowrequest", "Escrow request continue;
}
secnotice("escrowrequest", "Escrow request 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: reply(nil, error);
}
secnotice("escrowrequest", "found requests:
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: reply(error);
return;
}
for(SecEscrowPendingRecord* record in records) {
[record deleteFromKeychain:&error];
if(error) {
secnotice("escrowrequest", "Unable to delete }
}
// 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