Merge branch 'mkirk/debounce-prekey-checks2'

pull/1/head
Michael Kirk 7 years ago
commit e9f0c31d49

@ -1 +1 @@
Subproject commit e9fb539a0061d7e9f87ba48a724f99092232b41b Subproject commit c20b80e3952ccdb215831c4063c79d479d1cd702

@ -104,7 +104,7 @@ class AccountManagerTest: SignalBaseTest {
func testSuccessfulRegistration() { func testSuccessfulRegistration() {
Environment.clearSharedForTests() Environment.clearSharedForTests()
Environment.setCurrent(Release.releaseEnvironment()) Environment.shared = Release.releaseEnvironment()
let tsAccountManager = TokenObtainingTSAccountManager(networkManager: TSNetworkManager.shared(), primaryStorage: OWSPrimaryStorage.shared()) let tsAccountManager = TokenObtainingTSAccountManager(networkManager: TSNetworkManager.shared(), primaryStorage: OWSPrimaryStorage.shared())

@ -1,3 +1,7 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "FunctionalUtilTest.h" #import "FunctionalUtilTest.h"
#import "FunctionalUtil.h" #import "FunctionalUtil.h"
#import "TestUtil.h" #import "TestUtil.h"
@ -24,19 +28,6 @@
test([[(@[@1,@2]) filter:^(NSNumber* x) { return x.intValue == 1; }] isEqualToArray:(@[@1])]); test([[(@[@1,@2]) filter:^(NSNumber* x) { return x.intValue == 1; }] isEqualToArray:(@[@1])]);
test([[(@[@1,@2]) filter:^(NSNumber* x) { return x.intValue == 2; }] isEqualToArray:(@[@2])]); test([[(@[@1,@2]) filter:^(NSNumber* x) { return x.intValue == 2; }] isEqualToArray:(@[@2])]);
} }
-(void) testSum {
test([(@[]) sumDouble] == 0);
test([(@[]) sumNSUInteger] == 0);
test([(@[]) sumNSInteger] == 0);
test([(@[@1]) sumDouble] == 1);
test([(@[@2]) sumNSUInteger] == 2);
test([(@[@3]) sumNSInteger] == 3);
test([(@[@1.5, @2.75]) sumDouble] == 4.25);
test([(@[@1, @3]) sumNSUInteger] == 4);
test([(@[@-1, @4]) sumNSInteger] == 3);
}
-(void) testKeyedBy { -(void) testKeyedBy {
test([[@[] keyedBy:^id(id value) { return @true; }] isEqual:@{}]); test([[@[] keyedBy:^id(id value) { return @true; }] isEqual:@{}]);
test([[@[@1] keyedBy:^id(id value) { return @true; }] isEqual:@{@true : @1}]); test([[@[@1] keyedBy:^id(id value) { return @true; }] isEqual:@{@true : @1}]);

@ -2,15 +2,15 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
#import "OWSDevice.h"
#import "OWSOrphanDataCleaner.h" #import "OWSOrphanDataCleaner.h"
#import "OWSDevice.h"
#import "OWSPrimaryStorage.h" #import "OWSPrimaryStorage.h"
#import "SSKBaseTest.h"
#import "TSAttachmentStream.h" #import "TSAttachmentStream.h"
#import "TSContactThread.h" #import "TSContactThread.h"
#import "TSIncomingMessage.h" #import "TSIncomingMessage.h"
#import <XCTest/XCTest.h>
@interface OWSOrphanDataCleanerTest : SSKBaseTest @interface OWSOrphanDataCleanerTest : XCTestCase
@end @end
@ -43,7 +43,9 @@
- (NSUInteger)numberOfItemsInAttachmentsFolder - (NSUInteger)numberOfItemsInAttachmentsFolder
{ {
return [OWSOrphanDataCleaner filePathsInAttachmentsFolder].count; XCTFail(@"Test broken to fix compilation");
// return [OWSOrphanDataCleaner filePathsInAttachmentsFolder].count;
return 0;
} }
- (TSIncomingMessage *)createIncomingMessageWithThread:(TSThread *)thread - (TSIncomingMessage *)createIncomingMessageWithThread:(TSThread *)thread
@ -91,9 +93,11 @@
XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]); XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]);
XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"]; XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"];
[OWSOrphanDataCleaner auditAndCleanupAsync:^{
[expectation fulfill]; XCTFail(@"Test broken to fix compilation");
}]; // [OWSOrphanDataCleaner auditAndCleanupAsync:^{
// [expectation fulfill];
// }];
[self waitForExpectationsWithTimeout:5.0 [self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) { handler:^(NSError *error) {
if (error) { if (error) {
@ -114,9 +118,10 @@
XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]); XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]);
XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"]; XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"];
[OWSOrphanDataCleaner auditAndCleanupAsync:^{ XCTFail(@"Test broken to fix compilation");
[expectation fulfill]; // [OWSOrphanDataCleaner auditAndCleanupAsync:^{
}]; // [expectation fulfill];
// }];
[self waitForExpectationsWithTimeout:5.0 [self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) { handler:^(NSError *error) {
if (error) { if (error) {
@ -134,7 +139,7 @@
TSAttachmentStream *attachmentStream = [self createAttachmentStream]; TSAttachmentStream *attachmentStream = [self createAttachmentStream];
NSString *orphanedFilePath = [attachmentStream filePath]; NSString *orphanedFilePath = [attachmentStream originalFilePath];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath]; BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
XCTAssert(fileExists); XCTAssert(fileExists);
XCTAssertEqual(1, [self numberOfItemsInAttachmentsFolder]); XCTAssertEqual(1, [self numberOfItemsInAttachmentsFolder]);
@ -142,9 +147,10 @@
// Do multiple cleanup passes. // Do multiple cleanup passes.
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"]; XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"];
[OWSOrphanDataCleaner auditAndCleanupAsync:^{ XCTFail(@"Test broken to fix compilation");
[expectation fulfill]; // [OWSOrphanDataCleaner auditAndCleanupAsync:^{
}]; // [expectation fulfill];
// }];
[self waitForExpectationsWithTimeout:5.0 [self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) { handler:^(NSError *error) {
if (error) { if (error) {
@ -168,15 +174,16 @@
__unused TSIncomingMessage *incomingMessage = __unused TSIncomingMessage *incomingMessage =
[self createIncomingMessageWithThread:savedThread attachmentIds:@[ attachmentStream.uniqueId ]]; [self createIncomingMessageWithThread:savedThread attachmentIds:@[ attachmentStream.uniqueId ]];
NSString *attachmentFilePath = [attachmentStream filePath]; NSString *attachmentFilePath = [attachmentStream originalFilePath];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:attachmentFilePath]; BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:attachmentFilePath];
XCTAssert(fileExists); XCTAssert(fileExists);
XCTAssertEqual(1, [self numberOfItemsInAttachmentsFolder]); XCTAssertEqual(1, [self numberOfItemsInAttachmentsFolder]);
XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"]; XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"];
[OWSOrphanDataCleaner auditAndCleanupAsync:^{ XCTFail(@"Test broken to fix compilation");
[expectation fulfill]; // [OWSOrphanDataCleaner auditAndCleanupAsync:^{
}]; // [expectation fulfill];
// }];
[self waitForExpectationsWithTimeout:5.0 [self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) { handler:^(NSError *error) {
if (error) { if (error) {
@ -197,15 +204,16 @@
[attachmentStream writeData:[NSData new] error:&error]; [attachmentStream writeData:[NSData new] error:&error];
// Intentionally not saved, because we want a lingering file. // Intentionally not saved, because we want a lingering file.
NSString *orphanedFilePath = [attachmentStream filePath]; NSString *orphanedFilePath = [attachmentStream originalFilePath];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath]; BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
XCTAssert(fileExists); XCTAssert(fileExists);
XCTAssertEqual(1, [self numberOfItemsInAttachmentsFolder]); XCTAssertEqual(1, [self numberOfItemsInAttachmentsFolder]);
XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"]; XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"];
[OWSOrphanDataCleaner auditAndCleanupAsync:^{ XCTFail(@"Test broken to fix compilation");
[expectation fulfill]; // [OWSOrphanDataCleaner auditAndCleanupAsync:^{
}]; // [expectation fulfill];
// }];
[self waitForExpectationsWithTimeout:5.0 [self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) { handler:^(NSError *error) {
if (error) { if (error) {

@ -106,7 +106,7 @@ class ConversationSearcherTest: SignalBaseTest {
override func tearDown() { override func tearDown() {
super.tearDown() super.tearDown()
SSKEnvironment.setShared(originalEnvironment!) SSKEnvironment.shared = originalEnvironment!
} }
override func setUp() { override func setUp() {
@ -123,7 +123,7 @@ class ConversationSearcherTest: SignalBaseTest {
let testEnvironment: StubbableEnvironment = StubbableEnvironment(proxy: originalEnvironment!) let testEnvironment: StubbableEnvironment = StubbableEnvironment(proxy: originalEnvironment!)
testEnvironment.stubbedContactsManager = FakeContactsManager() testEnvironment.stubbedContactsManager = FakeContactsManager()
SSKEnvironment.setShared(testEnvironment) SSKEnvironment.shared = testEnvironment
self.dbConnection.readWrite { transaction in self.dbConnection.readWrite { transaction in
let bookModel = TSGroupModel(title: "Book Club", memberIds: [aliceRecipientId, bobRecipientId], image: nil, groupId: Randomness.generateRandomBytes(16)) let bookModel = TSGroupModel(title: "Book Club", memberIds: [aliceRecipientId, bobRecipientId], image: nil, groupId: Randomness.generateRandomBytes(16))

@ -101,13 +101,12 @@ public class OWS106EnsureProfileComplete: OWSDatabaseMigration {
case SignalServiceProfile.ValidationError.invalidIdentityKey(let description): case SignalServiceProfile.ValidationError.invalidIdentityKey(let description):
Logger.warn("detected incomplete profile for \(localRecipientId) error: \(description)") Logger.warn("detected incomplete profile for \(localRecipientId) error: \(description)")
// This is the error condition we're looking for. Update prekeys to properly set the identity key, completing registration. // This is the error condition we're looking for. Update prekeys to properly set the identity key, completing registration.
TSPreKeyManager.registerPreKeys(with: .signedAndOneTime, TSPreKeyManager.createPreKeys(success: {
success: { Logger.info("successfully uploaded pre-keys. Profile should be fixed.")
Logger.info("successfully uploaded pre-keys. Profile should be fixed.") fulfill(())
fulfill(())
}, },
failure: { _ in failure: { _ in
reject(OWSErrorWithCodeDescription(.signalServiceFailure, "\(self.logTag) Unknown error")) reject(OWSErrorWithCodeDescription(.signalServiceFailure, "\(self.logTag) Unknown error"))
}) })
default: default:
reject(error) reject(error)

@ -0,0 +1,34 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
// TODO define actual type, and validate length
public typealias IdentityKey = Data
/// based on libsignal-service-java's AccountManager class
@objc(SSKAccountServiceClient)
public class AccountServiceClient: NSObject {
static var shared = AccountServiceClient()
private let serviceClient: SignalServiceClient
override init() {
self.serviceClient = SignalServiceRestClient()
}
public func getPreKeysCount() -> Promise<Int> {
return serviceClient.getAvailablePreKeys()
}
public func setPreKeys(identityKey: IdentityKey, signedPreKeyRecord: SignedPreKeyRecord, preKeyRecords: [PreKeyRecord]) -> Promise<Void> {
return serviceClient.registerPreKeys(identityKey: identityKey, signedPreKeyRecord: signedPreKeyRecord, preKeyRecords: preKeyRecords)
}
public func setSignedPreKey(_ signedPreKey: SignedPreKeyRecord) -> Promise<Void> {
return serviceClient.setCurrentSignedPreKey(signedPreKey)
}
}

@ -0,0 +1,49 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
@objc(SSKCreatePreKeysOperation)
public class CreatePreKeysOperation: OWSOperation {
private var accountServiceClient: AccountServiceClient {
return AccountServiceClient.shared
}
private var primaryStorage: OWSPrimaryStorage {
return OWSPrimaryStorage.shared()
}
private var identityKeyManager: OWSIdentityManager {
return OWSIdentityManager.shared()
}
public override func run() {
Logger.debug("")
if self.identityKeyManager.identityKeyPair() == nil {
self.identityKeyManager.generateNewIdentityKey()
}
let identityKey: Data = self.identityKeyManager.identityKeyPair()!.publicKey
let signedPreKeyRecord: SignedPreKeyRecord = self.primaryStorage.generateRandomSignedRecord()
let preKeyRecords: [PreKeyRecord] = self.primaryStorage.generatePreKeyRecords()
firstly {
self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
self.primaryStorage.storePreKeyRecords(preKeyRecords)
return self.accountServiceClient.setPreKeys(identityKey: identityKey, signedPreKeyRecord: signedPreKeyRecord, preKeyRecords: preKeyRecords)
}.then { () -> Void in
signedPreKeyRecord.markAsAcceptedByService()
self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
self.primaryStorage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
}.then { () -> Void in
Logger.debug("done")
self.reportSuccess()
}.catch { error in
self.reportError(error)
}.retainUntilComplete()
}
}

@ -0,0 +1,93 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
// We generate 100 one-time prekeys at a time. We should replenish
// whenever ~2/3 of them have been consumed.
let kEphemeralPreKeysMinimumCount: UInt = 35
@objc(SSKRefreshPreKeysOperation)
public class RefreshPreKeysOperation: OWSOperation {
private var tsAccountManager: TSAccountManager {
return TSAccountManager.sharedInstance()
}
private var accountServiceClient: AccountServiceClient {
return AccountServiceClient.shared
}
private var primaryStorage: OWSPrimaryStorage {
return OWSPrimaryStorage.shared()
}
private var identityKeyManager: OWSIdentityManager {
return OWSIdentityManager.shared()
}
public override func run() {
Logger.debug("")
guard tsAccountManager.isRegistered() else {
Logger.debug("skipping - not registered")
return
}
firstly {
self.accountServiceClient.getPreKeysCount()
}.then(on: DispatchQueue.global()) { preKeysCount -> Promise<Void> in
Logger.debug("preKeysCount: \(preKeysCount)")
guard preKeysCount < kEphemeralPreKeysMinimumCount || self.primaryStorage.currentSignedPrekeyId() == nil else {
Logger.debug("Available keys sufficient: \(preKeysCount)")
return Promise(value: ())
}
let identityKey: Data = self.identityKeyManager.identityKeyPair()!.publicKey
let signedPreKeyRecord: SignedPreKeyRecord = self.primaryStorage.generateRandomSignedRecord()
let preKeyRecords: [PreKeyRecord] = self.primaryStorage.generatePreKeyRecords()
self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
self.primaryStorage.storePreKeyRecords(preKeyRecords)
return self.accountServiceClient.setPreKeys(identityKey: identityKey, signedPreKeyRecord: signedPreKeyRecord, preKeyRecords: preKeyRecords).then { () -> Void in
signedPreKeyRecord.markAsAcceptedByService()
self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
self.primaryStorage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
TSPreKeyManager.clearPreKeyUpdateFailureCount()
TSPreKeyManager.clearSignedPreKeyRecords()
}
}.then { () -> Void in
Logger.debug("done")
self.reportSuccess()
}.catch { error in
self.reportError(error)
}.retainUntilComplete()
}
public override func didSucceed() {
TSPreKeyManager.refreshPreKeysDidSucceed()
}
override public func didFail(error: Error) {
switch error {
case let networkManagerError as NetworkManagerError:
guard !networkManagerError.isNetworkError else {
Logger.debug("don't report SPK rotation failure w/ network error")
return
}
guard networkManagerError.statusCode >= 400 && networkManagerError.statusCode <= 599 else {
Logger.debug("don't report SPK rotation failure w/ non application error")
return
}
TSPreKeyManager.incrementPreKeyUpdateFailureCount()
default:
Logger.debug("don't report SPK rotation failure w/ non NetworkManager error: \(error)")
}
}
}

@ -0,0 +1,69 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
@objc(SSKRotateSignedPreKeyOperation)
public class RotateSignedPreKeyOperation: OWSOperation {
private var tsAccountManager: TSAccountManager {
return TSAccountManager.sharedInstance()
}
private var accountServiceClient: AccountServiceClient {
return AccountServiceClient.shared
}
private var primaryStorage: OWSPrimaryStorage {
return OWSPrimaryStorage.shared()
}
public override func run() {
Logger.debug("")
guard tsAccountManager.isRegistered() else {
Logger.debug("skipping - not registered")
return
}
let signedPreKeyRecord: SignedPreKeyRecord = self.primaryStorage.generateRandomSignedRecord()
firstly {
self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
return self.accountServiceClient.setSignedPreKey(signedPreKeyRecord)
}.then(on: DispatchQueue.global()) { () -> Void in
Logger.info("Successfully uploaded signed PreKey")
signedPreKeyRecord.markAsAcceptedByService()
self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
self.primaryStorage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
TSPreKeyManager.clearPreKeyUpdateFailureCount()
TSPreKeyManager.clearSignedPreKeyRecords()
}.then { () -> Void in
Logger.debug("done")
self.reportSuccess()
}.catch { error in
self.reportError(error)
}.retainUntilComplete()
}
override public func didFail(error: Error) {
switch error {
case let networkManagerError as NetworkManagerError:
guard !networkManagerError.isNetworkError else {
Logger.debug("don't report SPK rotation failure w/ network error")
return
}
guard networkManagerError.statusCode >= 400 && networkManagerError.statusCode <= 599 else {
Logger.debug("don't report SPK rotation failure w/ non application error")
return
}
TSPreKeyManager.incrementPreKeyUpdateFailureCount()
default:
Logger.debug("don't report SPK rotation failure w/ non NetworkManager error: \(error)")
}
}
}

@ -371,9 +371,7 @@ NSString *const TSAccountManager_ServerSignalingKey = @"TSStorageServerSignaling
case 204: { case 204: {
OWSLogInfo(@"Verification code accepted."); OWSLogInfo(@"Verification code accepted.");
[self storeServerAuthToken:authToken signalingKey:signalingKey]; [self storeServerAuthToken:authToken signalingKey:signalingKey];
[TSPreKeyManager registerPreKeysWithMode:RefreshPreKeysMode_SignedAndOneTime [TSPreKeyManager createPreKeysWithSuccess:successBlock failure:failureBlock];
success:successBlock
failure:failureBlock];
break; break;
} }
default: { default: {

@ -1,29 +1,29 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
#import "TSAccountManager.h" #import "TSAccountManager.h"
typedef NS_ENUM(NSInteger, RefreshPreKeysMode) {
// Refresh the signed prekey AND the one-time prekeys.
RefreshPreKeysMode_SignedAndOneTime,
// Only refresh the signed prekey, which should happen around every 48 hours.
//
// 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.
RefreshPreKeysMode_SignedOnly,
};
@interface TSPreKeyManager : NSObject @interface TSPreKeyManager : NSObject
#pragma mark - State Tracking
+ (BOOL)isAppLockedDueToPreKeyUpdateFailures; + (BOOL)isAppLockedDueToPreKeyUpdateFailures;
+ (void)registerPreKeysWithMode:(RefreshPreKeysMode)mode + (void)incrementPreKeyUpdateFailureCount;
success:(void (^)(void))successHandler
failure:(void (^)(NSError *error))failureHandler; + (void)clearPreKeyUpdateFailureCount;
+ (void)clearSignedPreKeyRecords;
// This should only be called from the TSPreKeyManager.operationQueue
+ (void)refreshPreKeysDidSucceed;
#pragma mark - Check/Request Initiation
+ (void)rotateSignedPreKeyWithSuccess:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler;
+ (void)createPreKeysWithSuccess:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler;
+ (void)checkPreKeys; + (void)checkPreKeys;

@ -8,9 +8,9 @@
#import "NSURLSessionDataTask+StatusCode.h" #import "NSURLSessionDataTask+StatusCode.h"
#import "OWSIdentityManager.h" #import "OWSIdentityManager.h"
#import "OWSPrimaryStorage+SignedPreKeyStore.h" #import "OWSPrimaryStorage+SignedPreKeyStore.h"
#import "OWSRequestFactory.h"
#import "TSNetworkManager.h" #import "TSNetworkManager.h"
#import "TSStorageHeaders.h" #import "TSStorageHeaders.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
// Time before deletion of signed prekeys (measured in seconds) // Time before deletion of signed prekeys (measured in seconds)
#define kSignedPreKeysDeletionTime (7 * kDayInterval) #define kSignedPreKeysDeletionTime (7 * kDayInterval)
@ -21,10 +21,6 @@
// How often we check prekey state on app activation. // How often we check prekey state on app activation.
#define kPreKeyCheckFrequencySeconds (12 * kHourInterval) #define kPreKeyCheckFrequencySeconds (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. // This global should only be accessed on prekeyQueue.
static NSDate *lastPreKeyCheckTimestamp = nil; static NSDate *lastPreKeyCheckTimestamp = nil;
@ -40,6 +36,8 @@ static const NSUInteger kMaxPrekeyUpdateFailureCount = 5;
@implementation TSPreKeyManager @implementation TSPreKeyManager
#pragma mark - State Tracking
+ (BOOL)isAppLockedDueToPreKeyUpdateFailures + (BOOL)isAppLockedDueToPreKeyUpdateFailures
{ {
// Only disable message sending if we have failed more than N times // Only disable message sending if we have failed more than N times
@ -56,6 +54,8 @@ static const NSUInteger kMaxPrekeyUpdateFailureCount = 5;
// Record a prekey update failure. // Record a prekey update failure.
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
int failureCount = [primaryStorage incrementPrekeyUpdateFailureCount]; int failureCount = [primaryStorage incrementPrekeyUpdateFailureCount];
OWSLogInfo(@"new failureCount: %d", failureCount);
if (failureCount == 1 || ![primaryStorage firstPrekeyUpdateFailureDate]) { if (failureCount == 1 || ![primaryStorage firstPrekeyUpdateFailureDate]) {
// If this is the "first" failure, record the timestamp of that // If this is the "first" failure, record the timestamp of that
// failure. // failure.
@ -70,15 +70,29 @@ static const NSUInteger kMaxPrekeyUpdateFailureCount = 5;
[primaryStorage clearPrekeyUpdateFailureCount]; [primaryStorage clearPrekeyUpdateFailureCount];
} }
// We should never dispatch sync to this queue. + (void)refreshPreKeysDidSucceed
+ (dispatch_queue_t)prekeyQueue {
lastPreKeyCheckTimestamp = [NSDate new];
}
#pragma mark - Check/Request Initiation
+ (NSOperationQueue *)operationQueue
{ {
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
static dispatch_queue_t queue; 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, ^{ dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("org.whispersystems.signal.prekeyQueue", NULL); operationQueue = [NSOperationQueue new];
operationQueue.name = @"TSPreKeyManager";
operationQueue.maxConcurrentOperationCount = 1;
}); });
return queue; return operationQueue;
} }
+ (void)checkPreKeysIfNecessary + (void)checkPreKeysIfNecessary
@ -88,119 +102,83 @@ static const NSUInteger kMaxPrekeyUpdateFailureCount = 5;
} }
OWSAssertDebug(CurrentAppContext().isMainAppAndActive); OWSAssertDebug(CurrentAppContext().isMainAppAndActive);
// Update the prekey check timestamp. if (!TSAccountManager.isRegistered) {
dispatch_async(TSPreKeyManager.prekeyQueue, ^{ return;
}
SSKRefreshPreKeysOperation *refreshOperation = [SSKRefreshPreKeysOperation new];
__weak SSKRefreshPreKeysOperation *weakRefreshOperation = refreshOperation;
NSBlockOperation *checkIfRefreshNecessaryOperation = [NSBlockOperation blockOperationWithBlock:^{
BOOL shouldCheck = (lastPreKeyCheckTimestamp == nil BOOL shouldCheck = (lastPreKeyCheckTimestamp == nil
|| fabs([lastPreKeyCheckTimestamp timeIntervalSinceNow]) >= kPreKeyCheckFrequencySeconds); || fabs([lastPreKeyCheckTimestamp timeIntervalSinceNow]) >= kPreKeyCheckFrequencySeconds);
if (shouldCheck) { if (!shouldCheck) {
// Optimistically mark the prekeys as checked. This [weakRefreshOperation cancel];
// 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 [refreshOperation addDependency:checkIfRefreshNecessaryOperation];
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; SSKRotateSignedPreKeyOperation *rotationOperation = [SSKRotateSignedPreKeyOperation new];
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
ECKeyPair *identityKeyPair = [[OWSIdentityManager sharedManager] identityKeyPair];
if (!identityKeyPair) { __weak SSKRotateSignedPreKeyOperation *weakRotationOperation = rotationOperation;
[[OWSIdentityManager sharedManager] generateNewIdentityKey]; NSBlockOperation *checkIfRotationNecessaryOperation = [NSBlockOperation blockOperationWithBlock:^{
identityKeyPair = [[OWSIdentityManager sharedManager] identityKeyPair]; OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
SignedPreKeyRecord *_Nullable signedPreKey = [primaryStorage currentSignedPreKey];
// Switch modes if necessary. BOOL shouldCheck
modeCopy = RefreshPreKeysMode_SignedAndOneTime; = !signedPreKey || fabs(signedPreKey.generatedAt.timeIntervalSinceNow) >= kSignedPreKeyRotationTime;
if (!shouldCheck) {
[weakRotationOperation cancel];
} }
}];
SignedPreKeyRecord *signedPreKey = [primaryStorage generateRandomSignedRecord]; [rotationOperation addDependency:checkIfRotationNecessaryOperation];
// 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 // Order matters here - if we rotated *before* refreshing, we'd risk uploading
success:^(NSURLSessionDataTask *task, id responseObject) { // two SPK's in a row since RefreshPreKeysOperation can also upload a new SPK.
OWSLogInfo(@"Successfully registered %@.", description); [checkIfRotationNecessaryOperation addDependency:refreshOperation];
// Mark signed prekey as accepted by service. NSArray<NSOperation *> *operations =
[signedPreKey markAsAcceptedByService]; @[ checkIfRefreshNecessaryOperation, refreshOperation, checkIfRotationNecessaryOperation, rotationOperation ];
[primaryStorage storeSignedPreKey:signedPreKey.Id signedPreKeyRecord:signedPreKey]; [self.operationQueue addOperations:operations waitUntilFinished:NO];
}
// On success, update the "current" signed prekey state. + (void)createPreKeysWithSuccess:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler
[primaryStorage setCurrentSignedPrekeyId:signedPreKey.Id]; {
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(); successHandler();
});
}
});
}
[TSPreKeyManager clearPreKeyUpdateFailureCount]; + (void)rotateSignedPreKeyWithSuccess:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler
} {
failure:^(NSURLSessionDataTask *task, NSError *error) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (!IsNSErrorNetworkFailure(error)) { SSKRotateSignedPreKeyOperation *operation = [SSKRotateSignedPreKeyOperation new];
if (modeCopy == RefreshPreKeysMode_SignedAndOneTime) { [self.operationQueue addOperations:@[ operation ] waitUntilFinished:YES];
OWSProdError([OWSAnalyticsEvents errorPrekeysUpdateFailedSignedAndOnetime]);
} else {
OWSProdError([OWSAnalyticsEvents errorPrekeysUpdateFailedJustSigned]);
}
}
// Mark the prekeys as _NOT_ checked on failure.
[self markPreKeysAsNotChecked];
NSError *_Nullable error = operation.failingError;
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
failureHandler(error); failureHandler(error);
});
NSInteger statusCode = 0; } else {
if ([task.response isKindOfClass:[NSHTTPURLResponse class]]) { dispatch_async(dispatch_get_main_queue(), ^{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response; successHandler();
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];
}
}];
}); });
} }
@ -210,207 +188,85 @@ static const NSUInteger kMaxPrekeyUpdateFailureCount = 5;
return; return;
} }
// Optimistically mark the prekeys as checked. This SSKRefreshPreKeysOperation *operation = [SSKRefreshPreKeysOperation new];
// de-bounces prekey checks. [self.operationQueue addOperation:operation];
//
// 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:^{
OWSLogInfo(@"New prekeys registered with server.");
[self clearSignedPreKeyRecords];
}
failure:^(NSError *error) {
OWSLogWarn(@"Failed to update prekeys with the server: %@", error);
}];
};
BOOL shouldUpdateOneTimePreKeys = count.integerValue <= kEphemeralPreKeysMinimumCount;
if (shouldUpdateOneTimePreKeys) {
OWSLogInfo(@"Updating one-time and signed prekeys due to shortage of one-time prekeys.");
updatePreKeys(RefreshPreKeysMode_SignedAndOneTime);
didUpdatePreKeys = YES;
} else {
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
NSNumber *currentSignedPrekeyId = [primaryStorage currentSignedPrekeyId];
BOOL shouldUpdateSignedPrekey = NO;
if (!currentSignedPrekeyId) {
OWSLogError(@"Couldn't find current signed prekey id");
shouldUpdateSignedPrekey = YES;
} else {
SignedPreKeyRecord *currentRecord =
[primaryStorage loadSignedPrekeyOrNil:currentSignedPrekeyId.intValue];
if (!currentRecord) {
OWSFailDebug(@"Couldn't find signed prekey for id: %@", currentSignedPrekeyId);
shouldUpdateSignedPrekey = YES;
} else {
shouldUpdateSignedPrekey
= fabs([currentRecord.generatedAt timeIntervalSinceNow]) >= kSignedPreKeyRotationTime;
}
}
if (shouldUpdateSignedPrekey) {
OWSLogInfo(@"Updating signed prekey due to rotation period.");
updatePreKeys(RefreshPreKeysMode_SignedOnly);
didUpdatePreKeys = YES;
} else {
OWSLogDebug(@"Not updating prekeys.");
}
}
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];
OWSAssertDebug(keyId);
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
NSNumber *currentSignedPrekeyId = [primaryStorage currentSignedPrekeyId];
if (!keyId || !currentSignedPrekeyId || ![currentSignedPrekeyId isEqualToNumber:keyId]) {
OWSLogError(@"Local and service 'current signed prekey ids' did not match. %@ == %@ == %d.",
keyId,
currentSignedPrekeyId,
[currentSignedPrekeyId isEqualToNumber:keyId]);
}
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
if (!IsNSErrorNetworkFailure(error)) {
OWSProdError([OWSAnalyticsEvents errorPrekeysCurrentSignedPrekeyRequestFailed]);
}
OWSLogWarn(@"Could not retrieve current signed key from the service.");
// Mark the prekeys as _NOT_ checked on failure.
[self markPreKeysAsNotChecked];
}];
}
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
if (!IsNSErrorNetworkFailure(error)) {
OWSProdError([OWSAnalyticsEvents errorPrekeysAvailablePrekeysRequestFailed]);
}
OWSLogError(@"Failed to retrieve the number of available prekeys.");
// Mark the prekeys as _NOT_ checked on failure.
[self markPreKeysAsNotChecked];
}];
}
+ (void)markPreKeysAsNotChecked
{
dispatch_async(TSPreKeyManager.prekeyQueue, ^{
lastPreKeyCheckTimestamp = nil;
});
} }
+ (void)clearSignedPreKeyRecords { + (void)clearSignedPreKeyRecords {
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
NSNumber *currentSignedPrekeyId = [primaryStorage currentSignedPrekeyId]; NSNumber *_Nullable currentSignedPrekeyId = [primaryStorage currentSignedPrekeyId];
[self clearSignedPreKeyRecordsWithKeyId:currentSignedPrekeyId success:nil]; [self clearSignedPreKeyRecordsWithKeyId:currentSignedPrekeyId];
} }
+ (void)clearSignedPreKeyRecordsWithKeyId:(NSNumber *)keyId success:(void (^_Nullable)(void))successHandler + (void)clearSignedPreKeyRecordsWithKeyId:(NSNumber *_Nullable)keyId
{ {
if (!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"); OWSFailDebug(@"Ignoring request to clear signed preKeys since no keyId was specified");
return; return;
} }
// We use prekeyQueue to serialize this logic and ensure that only OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
// one thread is "registering" or "clearing" prekeys at a time. SignedPreKeyRecord *currentRecord = [primaryStorage loadSignedPrekeyOrNil:keyId.intValue];
dispatch_async(TSPreKeyManager.prekeyQueue, ^{ if (!currentRecord) {
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; OWSFailDebug(@"Couldn't find signed prekey for id: %@", keyId);
SignedPreKeyRecord *currentRecord = [primaryStorage loadSignedPrekeyOrNil:keyId.intValue]; }
if (!currentRecord) { NSArray *allSignedPrekeys = [primaryStorage loadSignedPreKeys];
OWSFailDebug(@"Couldn't find signed prekey for id: %@", keyId); 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++;
} }
NSArray *allSignedPrekeys = [primaryStorage loadSignedPreKeys]; }
NSArray *oldSignedPrekeys
= (currentRecord != nil ? [self removeCurrentRecord:currentRecord fromRecords:allSignedPrekeys] // Iterate the signed prekeys in ascending order so that we try to delete older keys first.
: allSignedPrekeys); for (SignedPreKeyRecord *signedPrekey in oldSignedPrekeys) {
// Always keep at least 3 keys, accepted or otherwise.
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; if (oldSignedPreKeyCount <= 3) {
dateFormatter.dateStyle = NSDateFormatterMediumStyle; continue;
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. // Never delete signed prekeys until they are N days old.
for (SignedPreKeyRecord *signedPrekey in oldSignedPrekeys) { if (fabs([signedPrekey.generatedAt timeIntervalSinceNow]) < kSignedPreKeysDeletionTime) {
// Always keep at least 3 keys, accepted or otherwise. continue;
if (oldSignedPreKeyCount <= 3) { }
continue;
}
// Never delete signed prekeys until they are N days old. // We try to keep a minimum of 3 "old, accepted" signed prekeys.
if (fabs([signedPrekey.generatedAt timeIntervalSinceNow]) < kSignedPreKeysDeletionTime) { if (signedPrekey.wasAcceptedByService) {
if (oldAcceptedSignedPreKeyCount <= 3) {
continue; 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 { } else {
OWSProdInfo([OWSAnalyticsEvents prekeysDeletedOldUnacceptedSignedPrekey]); oldAcceptedSignedPreKeyCount--;
} }
oldSignedPreKeyCount--;
[primaryStorage removeSignedPreKey:signedPrekey.Id];
} }
if (successHandler) { if (signedPrekey.wasAcceptedByService) {
successHandler(); OWSProdInfo([OWSAnalyticsEvents prekeysDeletedOldAcceptedSignedPrekey]);
} else {
OWSProdInfo([OWSAnalyticsEvents prekeysDeletedOldUnacceptedSignedPrekey]);
} }
});
oldSignedPreKeyCount--;
[primaryStorage removeSignedPreKey:signedPrekey.Id];
}
} }
+ (NSArray *)removeCurrentRecord:(SignedPreKeyRecord *)currentRecord fromRecords:(NSArray *)allRecords { + (NSArray *)removeCurrentRecord:(SignedPreKeyRecord *)currentRecord fromRecords:(NSArray *)allRecords {

@ -390,12 +390,10 @@ class CDSBatchOperation: OWSOperation {
func parseAndDecrypt(response: Any?, remoteAttestation: RemoteAttestation) throws -> Data { func parseAndDecrypt(response: Any?, remoteAttestation: RemoteAttestation) throws -> Data {
guard let responseDict = response as? [String: AnyObject] else { guard let params = ParamParser(responseObject: response) else {
throw ContactDiscoveryError.parseError(description: "missing response dict") throw ContactDiscoveryError.parseError(description: "missing response dict")
} }
let params = ParamParser(dictionary: responseDict)
let cipherText = try params.requiredBase64EncodedData(key: "data") let cipherText = try params.requiredBase64EncodedData(key: "data")
let initializationVector = try params.requiredBase64EncodedData(key: "iv") let initializationVector = try params.requiredBase64EncodedData(key: "iv")
let authTag = try params.requiredBase64EncodedData(key: "mac") let authTag = try params.requiredBase64EncodedData(key: "mac")

@ -732,17 +732,17 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// //
// Only try to update the signed prekey; updating it is sufficient to // Only try to update the signed prekey; updating it is sufficient to
// re-enable message sending. // re-enable message sending.
[TSPreKeyManager registerPreKeysWithMode:RefreshPreKeysMode_SignedOnly [TSPreKeyManager
success:^{ rotateSignedPreKeyWithSuccess:^{
OWSLogInfo(@"New prekeys registered with server."); OWSLogInfo(@"New prekeys registered with server.");
NSError *error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError();
[error setIsRetryable:YES];
return failureHandler(error);
} }
failure:^(NSError *error) { failure:^(NSError *error) {
OWSLogWarn(@"Failed to update prekeys with the server: %@", error); OWSLogWarn(@"Failed to update prekeys with the server: %@", error);
return failureHandler(error);
}]; }];
NSError *error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError();
[error setIsRetryable:YES];
return failureHandler(error);
} }
if (remainingAttemptsParam <= 0) { if (remainingAttemptsParam <= 0) {

@ -0,0 +1,46 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
enum NetworkManagerError: Error {
/// Wraps TSNetworkManager failure callback params in a single throwable error
case taskError(task: URLSessionDataTask, underlyingError: Error)
}
extension NetworkManagerError {
var isNetworkError: Bool {
switch self {
case .taskError(_, let underlyingError):
return IsNSErrorNetworkFailure(underlyingError)
}
}
var statusCode: Int {
switch self {
case .taskError(let task, _):
return task.statusCode()
}
}
}
extension TSNetworkManager {
func makePromise(request: TSRequest) -> Promise<(task: URLSessionDataTask, responseObject: Any?)> {
let (promise, fulfill, reject) = Promise<(task: URLSessionDataTask, responseObject: Any?)>.pending()
self.makeRequest(request,
success: { task, responseObject in
fulfill((task: task, responseObject: responseObject))
},
failure: { task, error in
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
let nsError: NSError = nmError as NSError
nsError.isRetryable = (error as NSError).isRetryable
reject(nsError)
})
return promise
}
}

@ -67,8 +67,7 @@ typedef NS_ENUM(NSUInteger, TSVerificationTransport) { TSVerificationTransportVo
+ (TSRequest *)registerPrekeysRequestWithPrekeyArray:(NSArray *)prekeys + (TSRequest *)registerPrekeysRequestWithPrekeyArray:(NSArray *)prekeys
identityKey:(NSData *)identityKeyPublic identityKey:(NSData *)identityKeyPublic
signedPreKey:(SignedPreKeyRecord *)signedPreKey signedPreKey:(SignedPreKeyRecord *)signedPreKey;
preKeyLastResort:(PreKeyRecord *)preKeyLastResort;
+ (TSRequest *)remoteAttestationRequest:(ECKeyPair *)keyPair + (TSRequest *)remoteAttestationRequest:(ECKeyPair *)keyPair
enclaveId:(NSString *)enclaveId enclaveId:(NSString *)enclaveId

@ -235,12 +235,10 @@ NS_ASSUME_NONNULL_BEGIN
+ (TSRequest *)registerPrekeysRequestWithPrekeyArray:(NSArray *)prekeys + (TSRequest *)registerPrekeysRequestWithPrekeyArray:(NSArray *)prekeys
identityKey:(NSData *)identityKeyPublic identityKey:(NSData *)identityKeyPublic
signedPreKey:(SignedPreKeyRecord *)signedPreKey signedPreKey:(SignedPreKeyRecord *)signedPreKey
preKeyLastResort:(PreKeyRecord *)preKeyLastResort
{ {
OWSAssertDebug(prekeys.count > 0); OWSAssertDebug(prekeys.count > 0);
OWSAssertDebug(identityKeyPublic.length > 0); OWSAssertDebug(identityKeyPublic.length > 0);
OWSAssertDebug(signedPreKey); OWSAssertDebug(signedPreKey);
OWSAssertDebug(preKeyLastResort);
NSString *path = textSecureKeysAPI; NSString *path = textSecureKeysAPI;
NSString *publicIdentityKey = [[identityKeyPublic prependKeyType] base64EncodedStringWithOptions:0]; NSString *publicIdentityKey = [[identityKeyPublic prependKeyType] base64EncodedStringWithOptions:0];
@ -252,7 +250,6 @@ NS_ASSUME_NONNULL_BEGIN
method:@"PUT" method:@"PUT"
parameters:@{ parameters:@{
@"preKeys" : serializedPrekeyList, @"preKeys" : serializedPrekeyList,
@"lastResortKey" : [self dictionaryFromPreKey:preKeyLastResort],
@"signedPreKey" : [self dictionaryFromSignedPreKey:signedPreKey], @"signedPreKey" : [self dictionaryFromSignedPreKey:signedPreKey],
@"identityKey" : publicIdentityKey @"identityKey" : publicIdentityKey
}]; }];

@ -28,7 +28,7 @@ typedef void (^TSNetworkManagerFailure)(NSURLSessionDataTask *task, NSError *err
- (void)makeRequest:(TSRequest *)request - (void)makeRequest:(TSRequest *)request
completionQueue:(dispatch_queue_t)completionQueue completionQueue:(dispatch_queue_t)completionQueue
success:(TSNetworkManagerSuccess)success success:(TSNetworkManagerSuccess)success
failure:(TSNetworkManagerFailure)failure NS_SWIFT_NAME(makeRequest(_:shouldCompleteOnMainQueue:success:failure:)); failure:(TSNetworkManagerFailure)failure NS_SWIFT_NAME(makeRequest(_:completionQueue:success:failure:));
@end @end

@ -0,0 +1,54 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
protocol SignalServiceClient {
func getAvailablePreKeys() -> Promise<Int>
func registerPreKeys(identityKey: IdentityKey, signedPreKeyRecord: SignedPreKeyRecord, preKeyRecords: [PreKeyRecord]) -> Promise<Void>
func setCurrentSignedPreKey(_ signedPreKey: SignedPreKeyRecord) -> Promise<Void>
}
/// Based on libsignal-service-java's PushServiceSocket class
class SignalServiceRestClient: SignalServiceClient {
var networkManager: TSNetworkManager {
return TSNetworkManager.shared()
}
func unexpectedServerResponseError() -> Error {
return OWSErrorMakeUnableToProcessServerResponseError()
}
func getAvailablePreKeys() -> Promise<Int> {
Logger.debug("")
let request = OWSRequestFactory.availablePreKeysCountRequest()
return networkManager.makePromise(request: request).then { (_, responseObject) -> Int in
Logger.debug("got response")
guard let params = ParamParser(responseObject: responseObject) else {
throw self.unexpectedServerResponseError()
}
let count: Int = try! params.required(key: "count")
return count
}
}
func registerPreKeys(identityKey: IdentityKey, signedPreKeyRecord: SignedPreKeyRecord, preKeyRecords: [PreKeyRecord]) -> Promise<Void> {
Logger.debug("")
let request = OWSRequestFactory.registerPrekeysRequest(withPrekeyArray: preKeyRecords, identityKey: identityKey, signedPreKey: signedPreKeyRecord)
return networkManager.makePromise(request: request).asVoid()
}
public func setCurrentSignedPreKey(_ signedPreKey: SignedPreKeyRecord) -> Promise<Void> {
Logger.debug("")
let request = OWSRequestFactory.registerSignedPrekeyRequest(with: signedPreKey)
return networkManager.makePromise(request: request).asVoid()
}
}

@ -7,8 +7,7 @@
@interface OWSPrimaryStorage (PreKeyStore) <PreKeyStore> @interface OWSPrimaryStorage (PreKeyStore) <PreKeyStore>
- (NSArray *)generatePreKeyRecords; - (NSArray<PreKeyRecord *> *)generatePreKeyRecords;
- (PreKeyRecord *)getOrGenerateLastResortKey; - (void)storePreKeyRecords:(NSArray<PreKeyRecord *> *)preKeyRecords NS_SWIFT_NAME(storePreKeyRecords(_:));
- (void)storePreKeyRecords:(NSArray *)preKeyRecords;
@end @end

@ -16,19 +16,7 @@
@implementation OWSPrimaryStorage (PreKeyStore) @implementation OWSPrimaryStorage (PreKeyStore)
- (PreKeyRecord *)getOrGenerateLastResortKey - (NSArray<PreKeyRecord *> *)generatePreKeyRecords;
{
if ([self containsPreKey:kPreKeyOfLastResortId]) {
return [self loadPreKey:kPreKeyOfLastResortId];
} else {
PreKeyRecord *lastResort =
[[PreKeyRecord alloc] initWithId:kPreKeyOfLastResortId keyPair:[Curve25519 generateKeyPair]];
[self storePreKey:kPreKeyOfLastResortId preKeyRecord:lastResort];
return lastResort;
}
}
- (NSArray *)generatePreKeyRecords
{ {
NSMutableArray *preKeyRecords = [NSMutableArray array]; NSMutableArray *preKeyRecords = [NSMutableArray array];
@ -52,7 +40,7 @@
return preKeyRecords; return preKeyRecords;
} }
- (void)storePreKeyRecords:(NSArray *)preKeyRecords - (void)storePreKeyRecords:(NSArray<PreKeyRecord *> *)preKeyRecords
{ {
for (PreKeyRecord *record in preKeyRecords) { for (PreKeyRecord *record in preKeyRecords) {
[self.dbReadWriteConnection setObject:record [self.dbReadWriteConnection setObject:record

@ -19,6 +19,7 @@ extern NSString *const OWSPrimaryStorageSignedPreKeyStoreCollection;
// Returns nil if no current signed prekey id is found. // Returns nil if no current signed prekey id is found.
- (nullable NSNumber *)currentSignedPrekeyId; - (nullable NSNumber *)currentSignedPrekeyId;
- (void)setCurrentSignedPrekeyId:(int)value; - (void)setCurrentSignedPrekeyId:(int)value;
- (nullable SignedPreKeyRecord *)currentSignedPreKey;
#pragma mark - Prekey update failures #pragma mark - Prekey update failures

@ -105,12 +105,31 @@ NSString *const OWSPrimaryStorageKeyPrekeyCurrentSignedPrekeyId = @"currentSigne
inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection]; inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection];
} }
- (nullable SignedPreKeyRecord *)currentSignedPreKey
{
__block SignedPreKeyRecord *_Nullable currentRecord;
[self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
NSNumber *_Nullable preKeyId = [transaction objectForKey:OWSPrimaryStorageKeyPrekeyCurrentSignedPrekeyId
inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection];
if (preKeyId == nil) {
return;
}
currentRecord =
[transaction objectForKey:preKeyId.stringValue inCollection:OWSPrimaryStorageSignedPreKeyStoreCollection];
}];
return currentRecord;
}
#pragma mark - Prekey update failures #pragma mark - Prekey update failures
- (int)prekeyUpdateFailureCount - (int)prekeyUpdateFailureCount
{ {
NSNumber *value = [self.dbReadConnection objectForKey:OWSPrimaryStorageKeyPrekeyUpdateFailureCount NSNumber *_Nullable value = [self.dbReadConnection objectForKey:OWSPrimaryStorageKeyPrekeyUpdateFailureCount
inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection]; inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection];
// Will default to zero. // Will default to zero.
return [value intValue]; return [value intValue];
} }

@ -48,12 +48,15 @@ NSString *const OWSOperationKeyIsFinished = @"isFinished";
// Called one time only // Called one time only
- (nullable NSError *)checkForPreconditionError - (nullable NSError *)checkForPreconditionError
{ {
// OWSOperation have a notion of failure, which is inferred by the presence of a `failingError`.
//
// By default, any failing dependency cascades that failure to it's dependent.
// If you'd like different behavior, override this method (`checkForPreconditionError`) without calling `super`.
for (NSOperation *dependency in self.dependencies) { for (NSOperation *dependency in self.dependencies) {
if (![dependency isKindOfClass:[OWSOperation class]]) { if (![dependency isKindOfClass:[OWSOperation class]]) {
NSString *errorDescription = // Native operations, like NSOperation and NSBlockOperation have no notion of "failure".
[NSString stringWithFormat:@"%@ unknown dependency: %@", self.logTag, dependency.class]; // So there's no `failingError` to cascade.
continue;
return OWSErrorMakeAssertionError(errorDescription);
} }
OWSOperation *dependentOperation = (OWSOperation *)dependency; OWSOperation *dependentOperation = (OWSOperation *)dependency;
@ -107,6 +110,11 @@ NSString *const OWSOperationKeyIsFinished = @"isFinished";
return; return;
} }
if (self.isCancelled) {
[self reportCancelled];
return;
}
[self run]; [self run];
} }

@ -34,6 +34,14 @@ public class ParamParser {
self.dictionary = dictionary self.dictionary = dictionary
} }
public convenience init?(responseObject: Any?) {
guard let responseDict = responseObject as? [String: AnyObject] else {
return nil
}
self.init(dictionary: responseDict)
}
// MARK: Errors // MARK: Errors
public enum ParseError: Error { public enum ParseError: Error {

Loading…
Cancel
Save