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

442 lines
19 KiB
Objective-C

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "TSPreKeyManager.h"
#import "AppContext.h"
#import "NSDate+OWS.h"
#import "NSURLSessionDataTask+StatusCode.h"
#import "OWSIdentityManager.h"
#import "OWSPrimaryStorage+SignedPreKeyStore.h"
#import "OWSRequestFactory.h"
#import "TSNetworkManager.h"
#import "TSStorageHeaders.h"
// Time before deletion of signed prekeys (measured in seconds)
//
// Currently we retain signed prekeys for at least 7 days.
#define kSignedPreKeysDeletionTime ((NSTimeInterval)7 * kDayInterval)
// Time before rotation of signed prekeys (measured in seconds)
//
// Currently we rotate signed prekeys every 2 days (48 hours).
#define kSignedPreKeyRotationTime ((NSTimeInterval)7 * kDayInterval)
// How often we check prekey state on app activation.
//
// Currently we check prekey state every 12 hours.
#define kPreKeyCheckFrequencySeconds ((NSTimeInterval)12 * kHourInterval)
// We generate 100 one-time prekeys at a time. We should replenish
// whenever ~2/3 of them have been consumed.
static const NSUInteger kEphemeralPreKeysMinimumCount = 35;
// 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.
//
// Current value is 10 days (240 hours).
#define kSignedPreKeyUpdateFailureMaxFailureDuration ((NSTimeInterval)10 * kDayInterval)
#pragma mark -
@implementation TSPreKeyManager
+ (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];
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];
}
// We should never dispatch sync to this queue.
+ (dispatch_queue_t)prekeyQueue
{
static dispatch_once_t onceToken;
static dispatch_queue_t queue;
dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("org.whispersystems.signal.prekeyQueue", NULL);
});
return queue;
}
+ (void)checkPreKeysIfNecessary
{
if (!CurrentAppContext().isMainApp) {
return;
}
OWSAssert(CurrentAppContext().isMainAppAndActive);
// Update the prekey check timestamp.
dispatch_async(TSPreKeyManager.prekeyQueue, ^{
BOOL shouldCheck = (lastPreKeyCheckTimestamp == nil
|| fabs([lastPreKeyCheckTimestamp timeIntervalSinceNow]) >= kPreKeyCheckFrequencySeconds);
if (shouldCheck) {
// Optimistically mark the prekeys as checked. This
// de-bounces prekey checks.
//
// If the check or key registration fails, the prekeys
// will be marked as _NOT_ checked.
//
// Note: [TSPreKeyManager checkPreKeys] will also
// optimistically mark them as checked. This
// redundancy is fine and precludes a race
// condition.
lastPreKeyCheckTimestamp = [NSDate date];
if ([TSAccountManager isRegistered]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[TSPreKeyManager checkPreKeys];
});
}
}
});
}
+ (void)registerPreKeysWithMode:(RefreshPreKeysMode)mode
success:(void (^)(void))successHandler
failure:(void (^)(NSError *error))failureHandler
{
// We use prekeyQueue to serialize this logic and ensure that only
// one thread is "registering" or "clearing" prekeys at a time.
dispatch_async(TSPreKeyManager.prekeyQueue, ^{
// Mark the prekeys as checked every time we try to register prekeys.
lastPreKeyCheckTimestamp = [NSDate date];
RefreshPreKeysMode modeCopy = mode;
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
ECKeyPair *identityKeyPair = [[OWSIdentityManager sharedManager] identityKeyPair];
if (!identityKeyPair) {
[[OWSIdentityManager sharedManager] generateNewIdentityKey];
identityKeyPair = [[OWSIdentityManager sharedManager] identityKeyPair];
// Switch modes if necessary.
modeCopy = RefreshPreKeysMode_SignedAndOneTime;
}
SignedPreKeyRecord *signedPreKey = [primaryStorage generateRandomSignedRecord];
// Store the new signed key immediately, before it is sent to the
// service to prevent race conditions and other edge cases.
[primaryStorage storeSignedPreKey:signedPreKey.Id signedPreKeyRecord:signedPreKey];
NSArray *preKeys = nil;
TSRequest *request;
NSString *description;
if (modeCopy == RefreshPreKeysMode_SignedAndOneTime) {
description = @"signed and one-time prekeys";
PreKeyRecord *lastResortPreKey = [primaryStorage getOrGenerateLastResortKey];
preKeys = [primaryStorage generatePreKeyRecords];
// Store the new one-time keys immediately, before they are sent to the
// service to prevent race conditions and other edge cases.
[primaryStorage storePreKeyRecords:preKeys];
request = [OWSRequestFactory registerPrekeysRequestWithPrekeyArray:preKeys
identityKey:identityKeyPair.publicKey
signedPreKey:signedPreKey
preKeyLastResort:lastResortPreKey];
} else {
description = @"just signed prekey";
request = [OWSRequestFactory registerSignedPrekeyRequestWithSignedPreKeyRecord:signedPreKey];
}
[[TSNetworkManager sharedManager] makeRequest:request
success:^(NSURLSessionDataTask *task, id responseObject) {
DDLogInfo(@"%@ Successfully registered %@.", self.logTag, description);
// Mark signed prekey as accepted by service.
[signedPreKey markAsAcceptedByService];
[primaryStorage storeSignedPreKey:signedPreKey.Id signedPreKeyRecord:signedPreKey];
// On success, update the "current" signed prekey state.
[primaryStorage setCurrentSignedPrekeyId:signedPreKey.Id];
successHandler();
[TSPreKeyManager clearPreKeyUpdateFailureCount];
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
if (!IsNSErrorNetworkFailure(error)) {
if (modeCopy == RefreshPreKeysMode_SignedAndOneTime) {
OWSProdError([OWSAnalyticsEvents errorPrekeysUpdateFailedSignedAndOnetime]);
} else {
OWSProdError([OWSAnalyticsEvents errorPrekeysUpdateFailedJustSigned]);
}
}
// Mark the prekeys as _NOT_ checked on failure.
[self markPreKeysAsNotChecked];
failureHandler(error);
NSInteger statusCode = 0;
if ([task.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response;
statusCode = httpResponse.statusCode;
}
if (statusCode >= 400 && statusCode <= 599) {
// Only treat 4xx and 5xx errors from the service as failures.
// Ignore network failures, for example.
[TSPreKeyManager incrementPreKeyUpdateFailureCount];
}
}];
});
}
+ (void)checkPreKeys
{
if (!CurrentAppContext().isMainApp) {
return;
}
// Optimistically mark the prekeys as checked. This
// de-bounces prekey checks.
//
// If the check or key registration fails, the prekeys
// will be marked as _NOT_ checked.
dispatch_async(TSPreKeyManager.prekeyQueue, ^{
lastPreKeyCheckTimestamp = [NSDate date];
});
// We want to update prekeys if either the one-time or signed prekeys need an update, so
// we check the status of both.
//
// Most users will refresh their signed prekeys much more often than their
// one-time PreKeys, so we use a "signed only" mode to avoid updating the
// one-time keys in this case.
//
// We do not need a "one-time only" mode.
TSRequest *preKeyCountRequest = [OWSRequestFactory availablePreKeysCountRequest];
[[TSNetworkManager sharedManager] makeRequest:preKeyCountRequest
success:^(NSURLSessionDataTask *task, NSDictionary *responseObject) {
NSString *preKeyCountKey = @"count";
NSNumber *count = [responseObject objectForKey:preKeyCountKey];
BOOL didUpdatePreKeys = NO;
void (^updatePreKeys)(RefreshPreKeysMode) = ^(RefreshPreKeysMode mode) {
[self registerPreKeysWithMode:mode
success:^{
DDLogInfo(@"%@ New prekeys registered with server.", self.logTag);
[self clearSignedPreKeyRecords];
}
failure:^(NSError *error) {
DDLogWarn(@"%@ Failed to update prekeys with the server: %@", self.logTag, error);
}];
};
BOOL shouldUpdateOneTimePreKeys = count.integerValue <= kEphemeralPreKeysMinimumCount;
if (shouldUpdateOneTimePreKeys) {
DDLogInfo(@"%@ Updating one-time and signed prekeys due to shortage of one-time prekeys.", self.logTag);
updatePreKeys(RefreshPreKeysMode_SignedAndOneTime);
didUpdatePreKeys = YES;
} else {
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
NSNumber *currentSignedPrekeyId = [primaryStorage currentSignedPrekeyId];
BOOL shouldUpdateSignedPrekey = NO;
if (!currentSignedPrekeyId) {
DDLogError(@"%@ %s Couldn't find current signed prekey id", self.logTag, __PRETTY_FUNCTION__);
shouldUpdateSignedPrekey = YES;
} else {
SignedPreKeyRecord *currentRecord =
[primaryStorage loadSignedPrekeyOrNil:currentSignedPrekeyId.intValue];
if (!currentRecord) {
OWSFail(@"%@ %s Couldn't find signed prekey for id: %@",
self.logTag,
__PRETTY_FUNCTION__,
currentSignedPrekeyId);
shouldUpdateSignedPrekey = YES;
} else {
shouldUpdateSignedPrekey
= fabs([currentRecord.generatedAt timeIntervalSinceNow]) >= kSignedPreKeyRotationTime;
}
}
if (shouldUpdateSignedPrekey) {
DDLogInfo(@"%@ Updating signed prekey due to rotation period.", self.logTag);
updatePreKeys(RefreshPreKeysMode_SignedOnly);
didUpdatePreKeys = YES;
} else {
DDLogDebug(@"%@ Not updating prekeys.", self.logTag);
}
}
if (!didUpdatePreKeys) {
// If we didn't update the prekeys, our local "current signed key" state should
// agree with the service's "current signed key" state. Let's verify that,
// since it's closely related to the issues we saw with the 2.7.0.10 release.
TSRequest *currentSignedPreKey = [OWSRequestFactory currentSignedPreKeyRequest];
[[TSNetworkManager sharedManager] makeRequest:currentSignedPreKey
success:^(NSURLSessionDataTask *task, NSDictionary *responseObject) {
NSString *keyIdDictKey = @"keyId";
NSNumber *keyId = [responseObject objectForKey:keyIdDictKey];
OWSAssert(keyId);
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
NSNumber *currentSignedPrekeyId = [primaryStorage currentSignedPrekeyId];
if (!keyId || !currentSignedPrekeyId || ![currentSignedPrekeyId isEqualToNumber:keyId]) {
DDLogError(
@"%@ Local and service 'current signed prekey ids' did not match. %@ == %@ == %d.",
self.logTag,
keyId,
currentSignedPrekeyId,
[currentSignedPrekeyId isEqualToNumber:keyId]);
}
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
if (!IsNSErrorNetworkFailure(error)) {
OWSProdError([OWSAnalyticsEvents errorPrekeysCurrentSignedPrekeyRequestFailed]);
}
DDLogWarn(@"%@ Could not retrieve current signed key from the service.", self.logTag);
// Mark the prekeys as _NOT_ checked on failure.
[self markPreKeysAsNotChecked];
}];
}
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
if (!IsNSErrorNetworkFailure(error)) {
OWSProdError([OWSAnalyticsEvents errorPrekeysAvailablePrekeysRequestFailed]);
}
DDLogError(@"%@ Failed to retrieve the number of available prekeys.", self.logTag);
// Mark the prekeys as _NOT_ checked on failure.
[self markPreKeysAsNotChecked];
}];
}
+ (void)markPreKeysAsNotChecked
{
dispatch_async(TSPreKeyManager.prekeyQueue, ^{
lastPreKeyCheckTimestamp = nil;
});
}
+ (void)clearSignedPreKeyRecords {
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
NSNumber *currentSignedPrekeyId = [primaryStorage currentSignedPrekeyId];
[self clearSignedPreKeyRecordsWithKeyId:currentSignedPrekeyId success:nil];
}
+ (void)clearSignedPreKeyRecordsWithKeyId:(NSNumber *)keyId success:(void (^_Nullable)(void))successHandler
{
if (!keyId) {
OWSFail(@"%@ Ignoring request to clear signed preKeys since no keyId was specified", self.logTag);
return;
}
// We use prekeyQueue to serialize this logic and ensure that only
// one thread is "registering" or "clearing" prekeys at a time.
dispatch_async(TSPreKeyManager.prekeyQueue, ^{
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
SignedPreKeyRecord *currentRecord = [primaryStorage loadSignedPrekeyOrNil:keyId.intValue];
if (!currentRecord) {
OWSFail(@"%@ %s Couldn't find signed prekey for id: %@", self.logTag, __PRETTY_FUNCTION__, 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];
}
if (successHandler) {
successHandler();
}
});
}
+ (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