// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "TSAccountManager.h" #import "AppContext.h" #import "AppReadiness.h" #import "NSNotificationCenter+OWS.h" #import "ProfileManagerProtocol.h" #import "SSKEnvironment.h" #import "YapDatabaseConnection+OWS.h" #import "YapDatabaseTransaction+OWS.h" #import #import #import #import #import #import NS_ASSUME_NONNULL_BEGIN NSString *const TSRegistrationErrorDomain = @"TSRegistrationErrorDomain"; NSString *const TSRegistrationErrorUserInfoHTTPStatus = @"TSHTTPStatus"; NSString *const RegistrationStateDidChangeNotification = @"RegistrationStateDidChangeNotification"; NSString *const kNSNotificationName_LocalNumberDidChange = @"kNSNotificationName_LocalNumberDidChange"; NSString *const TSAccountManager_RegisteredNumberKey = @"TSStorageRegisteredNumberKey"; NSString *const TSAccountManager_IsDeregisteredKey = @"TSAccountManager_IsDeregisteredKey"; NSString *const TSAccountManager_ReregisteringPhoneNumberKey = @"TSAccountManager_ReregisteringPhoneNumberKey"; NSString *const TSAccountManager_LocalRegistrationIdKey = @"TSStorageLocalRegistrationId"; NSString *const TSAccountManager_HasPendingRestoreDecisionKey = @"TSAccountManager_HasPendingRestoreDecisionKey"; NSString *const TSAccountManager_UserAccountCollection = @"TSStorageUserAccountCollection"; NSString *const TSAccountManager_ServerAuthToken = @"TSStorageServerAuthToken"; NSString *const TSAccountManager_ServerSignalingKey = @"TSStorageServerSignalingKey"; NSString *const TSAccountManager_ManualMessageFetchKey = @"TSAccountManager_ManualMessageFetchKey"; NSString *const TSAccountManager_NeedsAccountAttributesUpdateKey = @"TSAccountManager_NeedsAccountAttributesUpdateKey"; @interface TSAccountManager () @property (atomic, readonly) BOOL isRegistered; @property (nonatomic, nullable) NSString *cachedLocalNumber; @property (nonatomic, readonly) YapDatabaseConnection *dbConnection; @property (nonatomic, nullable) NSNumber *cachedIsDeregistered; @property (nonatomic) Reachability *reachability; @end #pragma mark - @implementation TSAccountManager @synthesize isRegistered = _isRegistered; - (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage { self = [super init]; if (!self) { return self; } _dbConnection = [primaryStorage newDatabaseConnection]; self.reachability = [Reachability reachabilityForInternetConnection]; if (!CurrentAppContext().isMainApp) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(yapDatabaseModifiedExternally:) name:YapDatabaseModifiedExternallyNotification object:nil]; } [AppReadiness runNowOrWhenAppDidBecomeReady:^{ [[self updateAccountAttributesIfNecessary] retainUntilComplete]; }]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reachabilityChanged) name:kReachabilityChangedNotification object:nil]; return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } + (instancetype)sharedInstance { return SSKEnvironment.shared.tsAccountManager; } #pragma mark - Dependencies - (id)profileManager { return SSKEnvironment.shared.profileManager; } #pragma mark - - (void)setPhoneNumberAwaitingVerification:(NSString *_Nullable)phoneNumberAwaitingVerification { _phoneNumberAwaitingVerification = phoneNumberAwaitingVerification; [[NSNotificationCenter defaultCenter] postNotificationNameAsync:kNSNotificationName_LocalNumberDidChange object:nil userInfo:nil]; } - (OWSRegistrationState)registrationState { if (!self.isRegistered) { return OWSRegistrationState_Unregistered; } else if (self.isDeregistered) { if (self.isReregistering) { return OWSRegistrationState_Reregistering; } else { return OWSRegistrationState_Deregistered; } } else if (self.isDeregistered) { return OWSRegistrationState_PendingBackupRestore; } else { return OWSRegistrationState_Registered; } } - (BOOL)isRegistered { @synchronized (self) { if (_isRegistered) { return YES; } else { // Cache this once it's true since it's called alot, involves a dbLookup, and once set - it doesn't change. _isRegistered = [self storedLocalNumber] != nil; } return _isRegistered; } } - (BOOL)isRegisteredAndReady { return self.registrationState == OWSRegistrationState_Registered; } - (void)didRegister { NSString *phoneNumber = self.phoneNumberAwaitingVerification; [self storeLocalNumber:phoneNumber]; // Warm these cached values. [self isRegistered]; [self localNumber]; [self isDeregistered]; [self postRegistrationStateDidChangeNotification]; } + (nullable NSString *)localNumber { return [[self sharedInstance] localNumber]; } - (nullable NSString *)localNumber { NSString *awaitingVerif = self.phoneNumberAwaitingVerification; if (awaitingVerif) { return awaitingVerif; } // Cache this since we access this a lot, and once set it will not change. @synchronized(self) { if (self.cachedLocalNumber == nil) { self.cachedLocalNumber = self.storedLocalNumber; } } return self.cachedLocalNumber; } - (nullable NSString *)storedLocalNumber { @synchronized (self) { return [self.dbConnection stringForKey:TSAccountManager_RegisteredNumberKey inCollection:TSAccountManager_UserAccountCollection]; } } - (nullable NSString *)storedOrCachedLocalNumber:(YapDatabaseReadTransaction *)transaction { @synchronized(self) { if (self.cachedLocalNumber) { return self.cachedLocalNumber; } } return [transaction stringForKey:TSAccountManager_RegisteredNumberKey inCollection:TSAccountManager_UserAccountCollection]; } - (void)storeLocalNumber:(NSString *)localNumber { @synchronized (self) { [self.dbConnection setObject:localNumber forKey:TSAccountManager_RegisteredNumberKey inCollection:TSAccountManager_UserAccountCollection]; [self.dbConnection removeObjectForKey:TSAccountManager_ReregisteringPhoneNumberKey inCollection:TSAccountManager_UserAccountCollection]; self.phoneNumberAwaitingVerification = nil; self.cachedLocalNumber = localNumber; } } + (uint32_t)getOrGenerateRegistrationId:(YapDatabaseReadWriteTransaction *)transaction { return [[self sharedInstance] getOrGenerateRegistrationId:transaction]; } - (uint32_t)getOrGenerateRegistrationId { __block uint32_t result; [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { result = [self getOrGenerateRegistrationId:transaction]; }]; return result; } - (uint32_t)getOrGenerateRegistrationId:(YapDatabaseReadWriteTransaction *)transaction { // Unlike other methods in this class, there's no need for a `@synchronized` block // here, since we're already in a write transaction, and all writes occur on a serial queue. // // Since other code in this class which uses @synchronized(self) also needs to open write // transaction, using @synchronized(self) here, inside of a WriteTransaction risks deadlock. uint32_t registrationID = [[transaction objectForKey:TSAccountManager_LocalRegistrationIdKey inCollection:TSAccountManager_UserAccountCollection] unsignedIntValue]; if (registrationID == 0) { registrationID = (uint32_t)arc4random_uniform(16380) + 1; [transaction setObject:[NSNumber numberWithUnsignedInteger:registrationID] forKey:TSAccountManager_LocalRegistrationIdKey inCollection:TSAccountManager_UserAccountCollection]; } return registrationID; } - (void)registerForPushNotificationsWithPushToken:(NSString *)pushToken voipToken:(NSString *)voipToken isForcedUpdate:(BOOL)isForcedUpdate success:(void (^)(void))successHandler failure:(void (^)(NSError *))failureHandler { [self registerForPushNotificationsWithPushToken:pushToken voipToken:voipToken isForcedUpdate:isForcedUpdate success:successHandler failure:failureHandler remainingRetries:3]; } - (void)registerForPushNotificationsWithPushToken:(NSString *)pushToken voipToken:(NSString *)voipToken isForcedUpdate:(BOOL)isForcedUpdate success:(void (^)(void))successHandler failure:(void (^)(NSError *))failureHandler remainingRetries:(int)remainingRetries { BOOL isUsingFullAPNs = [NSUserDefaults.standardUserDefaults boolForKey:@"isUsingFullAPNs"]; NSData *pushTokenAsData = [NSData dataFromHexString:pushToken]; AnyPromise *promise = isUsingFullAPNs ? [LKPushNotificationAPI registerWithToken:pushTokenAsData hexEncodedPublicKey:self.localNumber isForcedUpdate:isForcedUpdate] : [LKPushNotificationAPI unregisterToken:pushTokenAsData]; promise .then(^() { successHandler(); }) .catch(^(NSError *error) { if (remainingRetries > 0) { [self registerForPushNotificationsWithPushToken:pushToken voipToken:voipToken isForcedUpdate:isForcedUpdate success:successHandler failure:failureHandler remainingRetries:remainingRetries - 1]; } else { failureHandler(error); } }); } - (void)rerequestSMSWithCaptchaToken:(nullable NSString *)captchaToken success:(void (^)(void))successBlock failure:(void (^)(NSError *error))failureBlock { // TODO: Can we remove phoneNumberAwaitingVerification? NSString *number = self.phoneNumberAwaitingVerification; [self registerWithPhoneNumber:number captchaToken:captchaToken success:successBlock failure:failureBlock smsVerification:YES]; } - (void)rerequestVoiceWithCaptchaToken:(nullable NSString *)captchaToken success:(void (^)(void))successBlock failure:(void (^)(NSError *error))failureBlock { NSString *number = self.phoneNumberAwaitingVerification; [self registerWithPhoneNumber:number captchaToken:captchaToken success:successBlock failure:failureBlock smsVerification:NO]; } #pragma mark Server keying material + (NSString *)generateNewAccountAuthenticationToken { NSData *authToken = [Randomness generateRandomBytes:16]; NSString *authTokenPrint = [[NSData dataWithData:authToken] hexadecimalString]; return authTokenPrint; } + (nullable NSString *)signalingKey { return [[self sharedInstance] signalingKey]; } - (nullable NSString *)signalingKey { return [self.dbConnection stringForKey:TSAccountManager_ServerSignalingKey inCollection:TSAccountManager_UserAccountCollection]; } + (nullable NSString *)serverAuthToken { return [[self sharedInstance] serverAuthToken]; } - (nullable NSString *)serverAuthToken { return [self.dbConnection stringForKey:TSAccountManager_ServerAuthToken inCollection:TSAccountManager_UserAccountCollection]; } - (void)storeServerAuthToken:(NSString *)authToken { [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [transaction setObject:authToken forKey:TSAccountManager_ServerAuthToken inCollection:TSAccountManager_UserAccountCollection]; }]; } - (void)yapDatabaseModifiedExternally:(NSNotification *)notification { // Any database write by the main app might reflect a deregistration, // so clear the cached "is registered" state. This will significantly // erode the value of this cache in the SAE. @synchronized(self) { _isRegistered = NO; } } #pragma mark - De-Registration - (BOOL)isDeregistered { // Cache this since we access this a lot, and once set it will not change. @synchronized(self) { if (self.cachedIsDeregistered == nil) { self.cachedIsDeregistered = @([self.dbConnection boolForKey:TSAccountManager_IsDeregisteredKey inCollection:TSAccountManager_UserAccountCollection defaultValue:NO]); } return self.cachedIsDeregistered.boolValue; } } - (void)setIsDeregistered:(BOOL)isDeregistered { @synchronized(self) { if (self.cachedIsDeregistered && self.cachedIsDeregistered.boolValue == isDeregistered) { return; } self.cachedIsDeregistered = @(isDeregistered); } [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [transaction setObject:@(isDeregistered) forKey:TSAccountManager_IsDeregisteredKey inCollection:TSAccountManager_UserAccountCollection]; }]; [self postRegistrationStateDidChangeNotification]; } - (nullable NSString *)reregisterationPhoneNumber { NSString *_Nullable result = [self.dbConnection stringForKey:TSAccountManager_ReregisteringPhoneNumberKey inCollection:TSAccountManager_UserAccountCollection]; return result; } - (BOOL)isReregistering { return nil != [self.dbConnection stringForKey:TSAccountManager_ReregisteringPhoneNumberKey inCollection:TSAccountManager_UserAccountCollection]; } - (BOOL)hasPendingBackupRestoreDecision { return [self.dbConnection boolForKey:TSAccountManager_HasPendingRestoreDecisionKey inCollection:TSAccountManager_UserAccountCollection defaultValue:NO]; } - (void)setHasPendingBackupRestoreDecision:(BOOL)value { [self.dbConnection setBool:value forKey:TSAccountManager_HasPendingRestoreDecisionKey inCollection:TSAccountManager_UserAccountCollection]; [self postRegistrationStateDidChangeNotification]; } - (BOOL)isManualMessageFetchEnabled { return [self.dbConnection boolForKey:TSAccountManager_ManualMessageFetchKey inCollection:TSAccountManager_UserAccountCollection defaultValue:NO]; } - (AnyPromise *)setIsManualMessageFetchEnabled:(BOOL)value { [self.dbConnection setBool:value forKey:TSAccountManager_ManualMessageFetchKey inCollection:TSAccountManager_UserAccountCollection]; // Try to update the account attributes to reflect this change. return [self updateAccountAttributes]; } - (void)registerForTestsWithLocalNumber:(NSString *)localNumber { [self storeLocalNumber:localNumber]; } #pragma mark - Account Attributes - (AnyPromise *)updateAccountAttributes { // Enqueue a "account attribute update", recording the "request time". [self.dbConnection setObject:[NSDate new] forKey:TSAccountManager_NeedsAccountAttributesUpdateKey inCollection:TSAccountManager_UserAccountCollection]; return [self updateAccountAttributesIfNecessary]; } - (AnyPromise *)updateAccountAttributesIfNecessary { if (!self.isRegistered) { return [AnyPromise promiseWithValue:@(1)]; } return [AnyPromise promiseWithValue:@(1)]; NSDate *_Nullable updateRequestDate = [self.dbConnection objectForKey:TSAccountManager_NeedsAccountAttributesUpdateKey inCollection:TSAccountManager_UserAccountCollection]; if (!updateRequestDate) { return [AnyPromise promiseWithValue:@(1)]; } AnyPromise *promise = [self performUpdateAccountAttributes]; promise = promise.then(^(id value) { [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { // Clear the update request unless a new update has been requested // while this update was in flight. NSDate *_Nullable latestUpdateRequestDate = [transaction objectForKey:TSAccountManager_NeedsAccountAttributesUpdateKey inCollection:TSAccountManager_UserAccountCollection]; if (latestUpdateRequestDate && [latestUpdateRequestDate isEqual:updateRequestDate]) { [transaction removeObjectForKey:TSAccountManager_NeedsAccountAttributesUpdateKey inCollection:TSAccountManager_UserAccountCollection]; } }]; }); return promise; } - (void)reachabilityChanged { [AppReadiness runNowOrWhenAppDidBecomeReady:^{ [[self updateAccountAttributesIfNecessary] retainUntilComplete]; }]; } #pragma mark - Notifications - (void)postRegistrationStateDidChangeNotification { [[NSNotificationCenter defaultCenter] postNotificationNameAsync:RegistrationStateDidChangeNotification object:nil userInfo:nil]; } @end NS_ASSUME_NONNULL_END