session-ios/SignalServiceKit/src/Account/TSPreKeyManager.m

285 lines
10 KiB
Objective-C

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "TSPreKeyManager.h"
#import "AppContext.h"
#import "NSURLSessionDataTask+StatusCode.h"
#import "OWSIdentityManager.h"
#import "OWSPrimaryStorage+SignedPreKeyStore.h"
#import "TSNetworkManager.h"
#import "TSStorageHeaders.h"
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
// Time before deletion of signed prekeys (measured in seconds)
#define kSignedPreKeysDeletionTime (7 * kDayInterval)
// Time before rotation of signed prekeys (measured in seconds)
#define kSignedPreKeyRotationTime (2 * kDayInterval)
// How often we check prekey state on app activation.
#define kPreKeyCheckFrequencySeconds (12 * kHourInterval)
// This global should only be accessed on prekeyQueue.
static NSDate *lastPreKeyCheckTimestamp = nil;
// Maximum number of failures while updating signed prekeys
// before the message sending is disabled.
static const NSUInteger kMaxPrekeyUpdateFailureCount = 5;
// Maximum amount of time that can elapse without updating signed prekeys
// before the message sending is disabled.
#define kSignedPreKeyUpdateFailureMaxFailureDuration (10 * kDayInterval)
#pragma mark -
@implementation TSPreKeyManager
#pragma mark - State Tracking
+ (BOOL)isAppLockedDueToPreKeyUpdateFailures
{
// Only disable message sending if we have failed more than N times
// over a period of at least M days.
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
return ([primaryStorage prekeyUpdateFailureCount] >= kMaxPrekeyUpdateFailureCount &&
[primaryStorage firstPrekeyUpdateFailureDate] != nil
&& fabs([[primaryStorage firstPrekeyUpdateFailureDate] timeIntervalSinceNow])
>= kSignedPreKeyUpdateFailureMaxFailureDuration);
}
+ (void)incrementPreKeyUpdateFailureCount
{
// Record a prekey update failure.
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
int failureCount = [primaryStorage incrementPrekeyUpdateFailureCount];
OWSLogInfo(@"new failureCount: %d", failureCount);
if (failureCount == 1 || ![primaryStorage firstPrekeyUpdateFailureDate]) {
// If this is the "first" failure, record the timestamp of that
// failure.
[primaryStorage setFirstPrekeyUpdateFailureDate:[NSDate new]];
}
}
+ (void)clearPreKeyUpdateFailureCount
{
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
[primaryStorage clearFirstPrekeyUpdateFailureDate];
[primaryStorage clearPrekeyUpdateFailureCount];
}
+ (void)refreshPreKeysDidSucceed
{
lastPreKeyCheckTimestamp = [NSDate new];
}
#pragma mark - Check/Request Initiation
+ (NSOperationQueue *)operationQueue
{
static dispatch_once_t onceToken;
static NSOperationQueue *operationQueue;
// PreKey state lives in two places - on the client and on the service.
// Some of our pre-key operations depend on the service state, e.g. we need to check our one-time-prekey count
// before we decide to upload new ones. This potentially entails multiple async operations, all of which should
// complete before starting any other pre-key operation. That's why a dispatch_queue is insufficient for
// coordinating PreKey operations and instead we use NSOperation's on a serial NSOperationQueue.
dispatch_once(&onceToken, ^{
operationQueue = [NSOperationQueue new];
operationQueue.name = @"TSPreKeyManager";
operationQueue.maxConcurrentOperationCount = 1;
});
return operationQueue;
}
+ (void)checkPreKeysIfNecessary
{
if (!CurrentAppContext().isMainApp) {
return;
}
OWSAssertDebug(CurrentAppContext().isMainAppAndActive);
if (!TSAccountManager.isRegistered) {
return;
}
SSKRefreshPreKeysOperation *refreshOperation = [SSKRefreshPreKeysOperation new];
__weak SSKRefreshPreKeysOperation *weakRefreshOperation = refreshOperation;
NSBlockOperation *checkIfRefreshNecessaryOperation = [NSBlockOperation blockOperationWithBlock:^{
BOOL shouldCheck = (lastPreKeyCheckTimestamp == nil
|| fabs([lastPreKeyCheckTimestamp timeIntervalSinceNow]) >= kPreKeyCheckFrequencySeconds);
if (!shouldCheck) {
[weakRefreshOperation cancel];
}
}];
[refreshOperation addDependency:checkIfRefreshNecessaryOperation];
SSKRotateSignedPreKeyOperation *rotationOperation = [SSKRotateSignedPreKeyOperation new];
__weak SSKRotateSignedPreKeyOperation *weakRotationOperation = rotationOperation;
NSBlockOperation *checkIfRotationNecessaryOperation = [NSBlockOperation blockOperationWithBlock:^{
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
SignedPreKeyRecord *_Nullable signedPreKey = [primaryStorage currentSignedPreKey];
BOOL shouldCheck
= !signedPreKey || fabs(signedPreKey.generatedAt.timeIntervalSinceNow) >= kSignedPreKeyRotationTime;
if (!shouldCheck) {
[weakRotationOperation cancel];
}
}];
[rotationOperation addDependency:checkIfRotationNecessaryOperation];
// Order matters here - if we rotated *before* refreshing, we'd risk uploading
// two SPK's in a row since RefreshPreKeysOperation can also upload a new SPK.
[checkIfRotationNecessaryOperation addDependency:refreshOperation];
NSArray<NSOperation *> *operations =
@[ checkIfRefreshNecessaryOperation, refreshOperation, checkIfRotationNecessaryOperation, rotationOperation ];
[self.operationQueue addOperations:operations waitUntilFinished:NO];
}
+ (void)createPreKeysWithSuccess:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
SSKCreatePreKeysOperation *operation = [SSKCreatePreKeysOperation new];
[self.operationQueue addOperations:@[ operation ] waitUntilFinished:YES];
NSError *_Nullable error = operation.failingError;
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
failureHandler(error);
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
successHandler();
});
}
});
}
+ (void)rotateSignedPreKeyWithSuccess:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
SSKRotateSignedPreKeyOperation *operation = [SSKRotateSignedPreKeyOperation new];
[self.operationQueue addOperations:@[ operation ] waitUntilFinished:YES];
NSError *_Nullable error = operation.failingError;
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
failureHandler(error);
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
successHandler();
});
}
});
}
+ (void)checkPreKeys
{
if (!CurrentAppContext().isMainApp) {
return;
}
SSKRefreshPreKeysOperation *operation = [SSKRefreshPreKeysOperation new];
[self.operationQueue addOperation:operation];
}
+ (void)clearSignedPreKeyRecords {
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
NSNumber *_Nullable currentSignedPrekeyId = [primaryStorage currentSignedPrekeyId];
[self clearSignedPreKeyRecordsWithKeyId:currentSignedPrekeyId];
}
+ (void)clearSignedPreKeyRecordsWithKeyId:(NSNumber *_Nullable)keyId
{
if (!keyId) {
// currentSignedPreKeyId should only be nil before we've completed registration.
// We have this guard here for robustness, but we should never get here.
OWSFailDebug(@"Ignoring request to clear signed preKeys since no keyId was specified");
return;
}
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
SignedPreKeyRecord *currentRecord = [primaryStorage loadSignedPrekeyOrNil:keyId.intValue];
if (!currentRecord) {
OWSFailDebug(@"Couldn't find signed prekey for id: %@", keyId);
}
NSArray *allSignedPrekeys = [primaryStorage loadSignedPreKeys];
NSArray *oldSignedPrekeys
= (currentRecord != nil ? [self removeCurrentRecord:currentRecord fromRecords:allSignedPrekeys]
: allSignedPrekeys);
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateStyle = NSDateFormatterMediumStyle;
dateFormatter.timeStyle = NSDateFormatterMediumStyle;
dateFormatter.locale = [NSLocale systemLocale];
// Sort the signed prekeys in ascending order of generation time.
oldSignedPrekeys = [oldSignedPrekeys sortedArrayUsingComparator:^NSComparisonResult(
SignedPreKeyRecord *_Nonnull left, SignedPreKeyRecord *_Nonnull right) {
return [left.generatedAt compare:right.generatedAt];
}];
NSUInteger oldSignedPreKeyCount = oldSignedPrekeys.count;
int oldAcceptedSignedPreKeyCount = 0;
for (SignedPreKeyRecord *signedPrekey in oldSignedPrekeys) {
if (signedPrekey.wasAcceptedByService) {
oldAcceptedSignedPreKeyCount++;
}
}
// Iterate the signed prekeys in ascending order so that we try to delete older keys first.
for (SignedPreKeyRecord *signedPrekey in oldSignedPrekeys) {
// Always keep at least 3 keys, accepted or otherwise.
if (oldSignedPreKeyCount <= 3) {
continue;
}
// Never delete signed prekeys until they are N days old.
if (fabs([signedPrekey.generatedAt timeIntervalSinceNow]) < kSignedPreKeysDeletionTime) {
continue;
}
// We try to keep a minimum of 3 "old, accepted" signed prekeys.
if (signedPrekey.wasAcceptedByService) {
if (oldAcceptedSignedPreKeyCount <= 3) {
continue;
} else {
oldAcceptedSignedPreKeyCount--;
}
}
if (signedPrekey.wasAcceptedByService) {
OWSProdInfo([OWSAnalyticsEvents prekeysDeletedOldAcceptedSignedPrekey]);
} else {
OWSProdInfo([OWSAnalyticsEvents prekeysDeletedOldUnacceptedSignedPrekey]);
}
oldSignedPreKeyCount--;
[primaryStorage removeSignedPreKey:signedPrekey.Id];
}
}
+ (NSArray *)removeCurrentRecord:(SignedPreKeyRecord *)currentRecord fromRecords:(NSArray *)allRecords {
NSMutableArray *oldRecords = [NSMutableArray array];
for (SignedPreKeyRecord *record in allRecords) {
if (currentRecord.Id != record.Id) {
[oldRecords addObject:record];
}
}
return oldRecords;
}
@end