// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import LibSignalClient /// Implementation of `SecureValueRecovery` that talks to the SVR2 server. public class SecureValueRecovery2Impl: SecureValueRecovery { private let accountAttributesUpdater: AccountAttributesUpdater private let appReadiness: SVR2.Shims.AppReadiness private let appVersion: AppVersion private let clientWrapper: SVR2ClientWrapper private let connectionFactory: SgxWebsocketConnectionFactory private let credentialStorage: SVRAuthCredentialStorage private let db: DB private let keyValueStoreFactory: KeyValueStoreFactory private let localStorage: SVRLocalStorageInternal private let schedulers: Schedulers private let storageServiceManager: StorageServiceManager private let syncManager: SyncManagerProtocolSwift private let tsAccountManager: TSAccountManager private let tsConstants: TSConstantsProtocol private let twoFAManager: SVR2.Shims.OWS2FAManager public convenience init( accountAttributesUpdater: AccountAttributesUpdater, appReadiness: SVR2.Shims.AppReadiness, appVersion: AppVersion, connectionFactory: SgxWebsocketConnectionFactory, credentialStorage: SVRAuthCredentialStorage, db: DB, keyValueStoreFactory: KeyValueStoreFactory, schedulers: Schedulers, storageServiceManager: StorageServiceManager, svrLocalStorage: SVRLocalStorageInternal, syncManager: SyncManagerProtocolSwift, tsAccountManager: TSAccountManager, tsConstants: TSConstantsProtocol, twoFAManager: SVR2.Shims.OWS2FAManager ) { self.init( accountAttributesUpdater: accountAttributesUpdater, appReadiness: appReadiness, appVersion: appVersion, clientWrapper: SVR2ClientWrapperImpl(), connectionFactory: connectionFactory, credentialStorage: credentialStorage, db: db, keyValueStoreFactory: keyValueStoreFactory, schedulers: schedulers, storageServiceManager: storageServiceManager, svrLocalStorage: svrLocalStorage, syncManager: syncManager, tsAccountManager: tsAccountManager, tsConstants: tsConstants, twoFAManager: twoFAManager ) } private let scheduler: Scheduler internal init( accountAttributesUpdater: AccountAttributesUpdater, appReadiness: SVR2.Shims.AppReadiness, appVersion: AppVersion, clientWrapper: SVR2ClientWrapper, connectionFactory: SgxWebsocketConnectionFactory, credentialStorage: SVRAuthCredentialStorage, db: DB, keyValueStoreFactory: KeyValueStoreFactory, schedulers: Schedulers, storageServiceManager: StorageServiceManager, svrLocalStorage: SVRLocalStorageInternal, syncManager: SyncManagerProtocolSwift, tsAccountManager: TSAccountManager, tsConstants: TSConstantsProtocol, twoFAManager: SVR2.Shims.OWS2FAManager ) { self.accountAttributesUpdater = accountAttributesUpdater self.appReadiness = appReadiness self.appVersion = appVersion self.clientWrapper = clientWrapper self.connectionFactory = connectionFactory self.credentialStorage = credentialStorage self.db = db self.keyValueStoreFactory = keyValueStoreFactory self.schedulers = schedulers self.storageServiceManager = storageServiceManager self.syncManager = syncManager self.localStorage = svrLocalStorage self.tsAccountManager = tsAccountManager self.tsConstants = tsConstants self.twoFAManager = twoFAManager self.scheduler = schedulers.queue(label: "org.signal.svr2", qos: .userInitiated) } public func warmCaches() { setLocalMasterKeyIfMissing() // Require migrations to succeed before we check for old stuff // to wipe, because migrations add old stuff to be wiped. // If a migration isn't needed, this returns a success immediately. migrateEnclavesIfNecessary()? .done(on: scheduler) { [weak self] in self?.wipeOldEnclavesIfNeeded(auth: .implicit) self?.periodicRefreshCredentialIfNecessary() } .cauterize() } // MARK: - Periodic Backups private static let periodicCredentialRefreshAppVersionKey = "periodicCredentialRefreshAppVersion" private func getNeedsCredentialRefreshBasedOnVersion(tx: DBReadTransaction) -> Bool { guard let lastAppVersion = self.kvStore.getString( Self.periodicCredentialRefreshAppVersionKey, transaction: tx ) else { return true } return lastAppVersion != appVersion.currentAppVersion } private func didRefreshCredentialInCurrentVersion(tx: DBWriteTransaction) { self.kvStore.setString( appVersion.currentAppVersion, key: Self.periodicCredentialRefreshAppVersionKey, transaction: tx ) } private func periodicRefreshCredentialIfNecessary() { appReadiness.runNowOrWhenMainAppDidBecomeReadyAsync { [weak self] in guard let self else { return } let needsRefresh = self.db.read { tx -> Bool in guard self.tsAccountManager.registrationState(tx: tx).isRegistered else { // Only refresh if registered. return false } guard self.hasBackedUpMasterKey(transaction: tx) else { // If we've never backed up, don't refresh periodically. return false } return self.getNeedsCredentialRefreshBasedOnVersion(tx: tx) } guard needsRefresh else { return } // Force refresh a credential, even if we have one cached, to ensure we // have a fresh credential to back up. Logger.info("Refreshing auth credential for periodic backup") RemoteAttestation.authForSVR2(chatServiceAuth: .implicit()) .observe(on: self.scheduler) { [weak self] result in switch result { case .success(let credential): Logger.info("Storing refreshed credential") self?.db.write { tx in self?.credentialStorage.storeAuthCredentialForCurrentUsername( SVR2AuthCredential(credential: credential), tx ) self?.didRefreshCredentialInCurrentVersion(tx: tx) } case .failure: Logger.warn("Unable to fetch auth credential") } } } } // MARK: - Key Existence public func hasMasterKey(transaction: DBReadTransaction) -> Bool { return localStorage.getMasterKey(transaction) != nil } public func hasBackedUpMasterKey(transaction: DBReadTransaction) -> Bool { return localStorage.getIsMasterKeyBackedUp(transaction) } public func useDeviceLocalMasterKey(authedAccount: AuthedAccount, transaction: DBWriteTransaction) { Logger.info("") setLocalDataAndSyncStorageServiceIfNeeded( masterKey: Cryptography.generateRandomBytes(SVR.masterKeyLengthBytes), isMasterKeyBackedUp: false, pinType: .alphanumeric, encodedPINVerificationString: nil, mrEnclaveStringValue: nil, mode: .syncStorageService(authedAccount), transaction: transaction ) // Disable the PIN locally. twoFAManager.markDisabled(transaction: transaction) // Wipe credentials; they're now useless. credentialStorage.removeSVR2CredentialsForCurrentUser(transaction) // This will prevent us continuing any in progress backups/exposes. // If either are in flight, they will no-op when they get a response // and see no in progress backup state. clearInProgressBackup(transaction) // We should update account attributes so we wipe the reglock and // reg recovery password. accountAttributesUpdater.scheduleAccountAttributesUpdate(authedAccount: authedAccount, tx: transaction) } // MARK: - PIN Management public func currentPinType(transaction: DBReadTransaction) -> SVR.PinType? { return localStorage.getPinType(transaction) } public func verifyPin(_ pin: String, resultHandler: @escaping (Bool) -> Void) { Logger.info("") // Kick off to a background thread to do expensive cryptography operations. schedulers.global().async { [schedulers, localStorage, db] in var isValid = false defer { schedulers.main.async { resultHandler(isValid) } } guard let encodedVerificationString = db.read(block: { tx in localStorage.getEncodedPINVerificationString(tx) }) else { owsFailDebug("Attempted to verify pin locally when we don't have a verification string") return } isValid = SVRUtil.verifyPIN(pin: pin, againstEncodedPINVerificationString: encodedVerificationString) } } // MARK: - Key Management public func acquireRegistrationLockForNewNumber(with pin: String, and auth: SVRAuthCredential) -> Promise { Logger.info("") return doRestore(pin: pin, authMethod: .svrAuth(auth, backup: nil)).then(on: scheduler) { restoreResult -> Promise in switch restoreResult { case .success(let masterKey, _): // Ignore whether we restored from an old enclave; we aren't backing up to the new enclave // on this code path so its not safe to wipe the old one anyway. guard let reglockToken = Self.deriveReglockKey(masterKey: masterKey)?.canonicalStringRepresentation else { return .init(error: SVR.SVRError.assertion) } return .value(reglockToken) case .backupMissing: return .init(error: SVR.SVRError.backupMissing) case .invalidPin(let remainingAttempts): return .init(error: SVR.SVRError.invalidPin(remainingAttempts: remainingAttempts)) case .decryptionError, .serverError, .unretainedError: return .init(error: SVR.SVRError.assertion) case .networkError(let error): return .init(error: error) case .genericError(let error): return .init(error: error) } } } public func generateAndBackupKeys(pin: String, authMethod: SVR.AuthMethod) -> Promise { let promise: Promise = self.generateAndBackupKeys(pin: pin, authMethod: authMethod) return promise.asVoid(on: schedulers.sync) } internal func generateAndBackupKeys(pin: String, authMethod: SVR.AuthMethod) -> Promise { Logger.info("backing up") return firstly(on: scheduler) { [weak self] () -> Promise in guard let self else { return .init(error: SVR.SVRError.assertion) } let masterKey: Data = { // We never change the master key once stored (on the primary). if let masterKey = self.db.read(block: { tx in self.localStorage.getMasterKey(tx) }) { return masterKey } return self.generateMasterKey() }() return self.doBackupAndExpose(pin: pin, masterKey: masterKey, authMethod: authMethod) } } public func restoreKeys(pin: String, authMethod: SVR.AuthMethod) -> Guarantee { Logger.info("") // When we restore, we remember which enclave it was from. On some future app startup, we check // this enclave, and migrate to a new one if available. This code path relies on that happening // asynchronously. return doRestore(pin: pin, authMethod: authMethod).map(on: schedulers.sync, \.asSVRResult) } public func restoreKeysAndBackup(pin: String, authMethod: SVR.AuthMethod) -> Guarantee { Logger.info("") return doRestore(pin: pin, authMethod: authMethod) .then(on: scheduler) { [weak self] restoreResult in switch restoreResult { case .backupMissing, .invalidPin, .genericError, .networkError, .decryptionError, .unretainedError, .serverError: return .value(restoreResult.asSVRResult) case .success(let masterKey, let enclaveWeRestoredFrom): guard let self else { return .value(.genericError(SVR.SVRError.assertion)) } // Backup our keys again, even though we just fetched them. // This resets the number of remaining attempts. We always // backup to the current enclave, even if we restored from // a previous enclave. return self .doBackupAndExpose( pin: pin, masterKey: masterKey, authMethod: authMethod ) .map(on: self.schedulers.sync) { [weak self] _ in // If the backup succeeds, and the restore was from some old enclave, // delete from that older enclave. if enclaveWeRestoredFrom.stringValue != self?.tsConstants.svr2Enclave.stringValue { // Strictly speaking, this happens in a separate transaction from when we mark the // backup/expose complete. But no matter what this is best effort; the client // can be uninstalled before it gets a chance to delete the old backup, for example. Logger.info("Restored from old enclave and backed up to new enclave; marking old one for deletion.") self?.db.write { tx in self?.addOldEnclaveToDeleteFrom(enclaveWeRestoredFrom, tx) } self?.wipeOldEnclavesIfNeeded(auth: authMethod) } return .success } .recover(on: self.schedulers.sync) { error in if error.isNetworkFailureOrTimeout { return .value(.networkError(error)) } return .value(.genericError(error)) } } } } public func deleteKeys() -> Promise { Logger.info("") return doDelete( mrEnclave: tsConstants.svr2Enclave, authMethod: .implicit ).then(on: scheduler) { [weak self] (result: DeleteResult) -> Promise in // Historically, this has cleared our local keys regardless of whether // the remote request succeeded. // This is because (a) other state in OWS2FAManager is wiped regardless // of outcome, and (b) callsites ignore what the result is. // This can probably be revisited at some point. self?.db.write { tx in self?.clearKeys(transaction: tx) } switch result { case .success: return .value(()) case .unretainedError, .serverError: return .init(error: SVR.SVRError.assertion) case .genericError(let error), .networkError(let error): return .init(error: error) } } } public func clearKeys(transaction: DBWriteTransaction) { Logger.info("") // This will prevent us continuing any in progress backups/exposes. // If either are in flight, they will no-op when they get a response // and see no in progress backup state. clearInProgressBackup(transaction) localStorage.clearKeys(transaction) } // MARK: - Master Key Encryption public func encrypt( keyType: SVR.DerivedKey, data: Data, transaction: DBReadTransaction ) -> SVR.ApplyDerivedKeyResult { guard let keyData = self.data(for: keyType, transaction: transaction) else { return .masterKeyMissing } do { return .success(try Aes256GcmEncryptedData.encrypt(data, key: keyData.rawData).concatenate()) } catch let error { return .cryptographyError(error) } } public func decrypt( keyType: SVR.DerivedKey, encryptedData: Data, transaction: DBReadTransaction ) -> SVR.ApplyDerivedKeyResult { guard let keyData = self.data(for: keyType, transaction: transaction) else { return .masterKeyMissing } do { return .success(try Aes256GcmEncryptedData(concatenated: encryptedData).decrypt(key: keyData.rawData)) } catch let error { return .cryptographyError(error) } } // TODO: By 03/2024, we can remove this method. Starting in 10/2023, we started sending // master keys in syncs. 90 days later, all active primaries will be sending the master key. // 30 days after that all message queues will have been flushed, at which point sync messages // without a master key will be impossible. public func storeSyncedStorageServiceKey(data: Data?, authedAccount: AuthedAccount, transaction: DBWriteTransaction) { Logger.info("") guard tsAccountManager.registrationState(tx: transaction).isPrimaryDevice == false else { owsFailDebug("Should not be storing synced keys on primary!") return } guard let storageServiceKey = data else { localStorage.setSyncedStorageServiceKey(nil, transaction) return } if let masterKey = localStorage.getMasterKey(transaction), Self.deriveStorageServiceKey(masterKey: masterKey)?.rawData == storageServiceKey { // We already have a master key, it already produces this storage service key. // Nothing needs to change. return } // Otherwise we are either missing a master key or it doesn't match; // in either case we want to nil out our master key and store the storage // service key. localStorage.setSyncedStorageServiceKey(data, transaction) localStorage.setMasterKey(nil, transaction) // Trigger a re-fetch of the storage manifest, our keys have changed storageServiceManager.restoreOrCreateManifestIfNecessary(authedDevice: authedAccount.authedDevice(isPrimaryDevice: false)) } public func storeSyncedMasterKey( data: Data, authedDevice: AuthedDevice, updateStorageService: Bool, transaction: DBWriteTransaction ) { Logger.info("") let oldMasterKey = localStorage.getMasterKey(transaction) localStorage.setMasterKey(data, transaction) // Wipe the storage service key, we don't need it anymore. localStorage.setSyncedStorageServiceKey(nil, transaction) // Trigger a re-fetch of the storage manifest if our keys have changed if oldMasterKey != data, updateStorageService { storageServiceManager.restoreOrCreateManifestIfNecessary(authedDevice: authedDevice) } } public func masterKeyDataForKeysSyncMessage(tx: DBReadTransaction) -> Data? { return localStorage.getMasterKey(tx) } public func clearSyncedStorageServiceKey(transaction: DBWriteTransaction) { Logger.info("") localStorage.setSyncedStorageServiceKey(nil, transaction) localStorage.setMasterKey(nil, transaction) } // MARK: - Value Derivation public func data(for key: SVR.DerivedKey, transaction: DBReadTransaction) -> SVR.DerivedKeyData? { func withMasterKey(_ handler: (Data) -> SVR.DerivedKeyData?) -> SVR.DerivedKeyData? { guard let masterKey = self.localStorage.getMasterKey(transaction) else { return nil } return handler(masterKey) } func withStorageServiceKey(_ handler: (SVR.DerivedKeyData) -> SVR.DerivedKeyData?) -> SVR.DerivedKeyData? { // NEW: linked devices might have the master key now, synced from the primary. // This was not the case before, and in fact we may still not have the master // key if it hasn't been synced yet. // So we should _first_ check if we have the master key, and if we don't // fall back to the storage service key which has historically been synced. if let masterKey = self.localStorage.getMasterKey(transaction) { guard let storageServiceKey = Self.deriveStorageServiceKey(masterKey: masterKey) else { return nil } return handler(storageServiceKey) } else if // TODO: By 10/2024, we can remove this check. Starting in 10/2023, we started sending // master keys in syncs. A year later, any primary that has not yet delivered a master // key must not have launched and is therefore deregistered; we are ok to ignore the // storage service key and take the master key or bust. let storageServiceKeyData = self.localStorage.getSyncedStorageServiceKey(transaction), let storageServiceKey = SVR.DerivedKeyData(storageServiceKeyData, .storageService) { return handler(storageServiceKey) } else { // We have no keys at all. return nil } } switch key { case .registrationLock: return withMasterKey(Self.deriveReglockKey(masterKey:)) case .registrationRecoveryPassword: return withMasterKey(Self.deriveRegRecoveryPwKey(masterKey:)) case .storageService: return withStorageServiceKey { $0 } case .storageServiceManifest(let version): return withStorageServiceKey { Self.deriveStorageServiceManifestKey(version: version, storageServiceKey: $0) } case .storageServiceRecord(let identifier): return withStorageServiceKey { Self.deriveStorageServiceRecordKey(identifier: identifier, storageServiceKey: $0) } case .backupKey: return withMasterKey(Self.deriveBackupKey(masterKey:)) } } public func isKeyAvailable(_ key: SVR.DerivedKey, transaction: DBReadTransaction) -> Bool { return data(for: key, transaction: transaction) != nil } private static func deriveReglockKey(masterKey: Data) -> SVR.DerivedKeyData? { return SVR.DerivedKeyData(SVR.DerivedKey.registrationLock.derivedData(from: masterKey), .registrationLock) } private static func deriveRegRecoveryPwKey(masterKey: Data) -> SVR.DerivedKeyData? { return SVR.DerivedKeyData(SVR.DerivedKey.registrationRecoveryPassword.derivedData(from: masterKey), .registrationRecoveryPassword) } private static func deriveBackupKey(masterKey: Data) -> SVR.DerivedKeyData? { return SVR.DerivedKeyData(SVR.DerivedKey.backupKey.derivedData(from: masterKey), .backupKey) } private static func deriveStorageServiceKey(masterKey: Data) -> SVR.DerivedKeyData? { return SVR.DerivedKeyData(SVR.DerivedKey.storageService.derivedData(from: masterKey), .storageService) } // StorageService manifest and record keys are derived from // the root storageService key, which itself is derived from // the svr master key. // Linked devices have the storage service key but not the master key, // so try that first before doing the double derivation from the master key. private static func deriveStorageServiceManifestKey(version: UInt64, storageServiceKey: SVR.DerivedKeyData) -> SVR.DerivedKeyData? { let keyType = SVR.DerivedKey.storageServiceManifest(version: version) return SVR.DerivedKeyData(keyType.derivedData(from: storageServiceKey.rawData), keyType) } private static func deriveStorageServiceRecordKey(identifier: StorageService.StorageIdentifier, storageServiceKey: SVR.DerivedKeyData) -> SVR.DerivedKeyData? { let keyType = SVR.DerivedKey.storageServiceRecord(identifier: identifier) return SVR.DerivedKeyData(keyType.derivedData(from: storageServiceKey.rawData), keyType) } // MARK: - Backup/Expose Request private lazy var kvStore = keyValueStoreFactory.keyValueStore(collection: "SecureValueRecovery2Impl") /// We must be careful to never repeat a backup request when an expose request fails, or /// even if an expose request was made. Once we get a success response from a backup /// request, we create and persist one of these to track that, and only ever make expose /// requests from then on until either: /// 1. The expose requests succeeeds (we are done backing up and can wipe this) /// 2. The user chooses a different PIN (we will make a new backup request) /// 3. The user wipes SVR2 backups private struct InProgressBackup: Codable, Equatable { let masterKey: Data let encryptedMasterKey: Data let rawPinType: Int let encodedPINVerificationString: String // If we make a backup to one mrenclave, then update the mrenclave, // we are safe to drop it and start again with a backup to the new // mrenclave. let mrEnclaveStringValue: String func matches( pin: String, mrEnclave: MrEnclave ) -> Bool { if !SVRUtil.verifyPIN(pin: pin, againstEncodedPINVerificationString: self.encodedPINVerificationString) { return false } if mrEnclave.stringValue != self.mrEnclaveStringValue { return false } return true } func matches(_ other: InProgressBackup) -> Bool { return self == other } var pinType: SVR.PinType { return SVR.PinType(rawValue: rawPinType) ?? .alphanumeric } } private static let inProgressBackupKey = "InProgressBackup" private func getInProgressBackup(_ tx: DBReadTransaction) throws -> InProgressBackup? { return try kvStore.getCodableValue(forKey: Self.inProgressBackupKey, transaction: tx) } private func setInProgressBackup(_ value: InProgressBackup, _ tx: DBWriteTransaction) throws { try kvStore.setCodable(optional: value, key: Self.inProgressBackupKey, transaction: tx) } private func clearInProgressBackup(_ tx: DBWriteTransaction) { kvStore.removeValue(forKey: Self.inProgressBackupKey, transaction: tx) } private func doBackupAndExpose( pin: String, masterKey: Data, authMethod: SVR2.AuthMethod ) -> Promise { let config = SVR2WebsocketConfigurator(mrenclave: tsConstants.svr2Enclave, authMethod: authMethod) return makeHandshakeAndOpenConnection(config) .then(on: scheduler) { [weak self] connection -> Promise in guard let self else { return .init(error: SVR.SVRError.assertion) } Logger.info("Connection open; beginning backup/expose") let weakSelf = Weak(value: self) let weakConnection = Weak(value: connection) func continueWithExpose(backup: InProgressBackup) -> Promise { guard let self = weakSelf.value, let connection = weakConnection.value else { return .init(error: SVR.SVRError.assertion) } return self .performExposeRequest( backup: backup, authedAccount: authMethod.authedAccount, connection: connection ) .then(on: self.schedulers.sync) { result -> Promise in switch result { case .success: return .value(backup.masterKey) case .serverError, .networkError, .unretainedError, .localPersistenceError: return .init(error: SVR.SVRError.assertion) } } } func startFreshBackupExpose() -> Promise { return self .performBackupRequest( pin: pin, masterKey: masterKey, mrEnclave: config.mrenclave, connection: connection ) .then(on: self.scheduler) { (backupResult: BackupResult) -> Promise in switch backupResult { case .serverError, .networkError, .localPersistenceError, .localEncryptionError, .unretainedError: return .init(error: SVR.SVRError.assertion) case .success(let inProgressBackup): return continueWithExpose(backup: inProgressBackup) } } } // Check if we had an in flight backup. let inProgressBackup: InProgressBackup? do { inProgressBackup = try self.db.read(block: self.getInProgressBackup) } catch { // If we fail to decode, something has gone wrong locally. But we can // treat this like if we never had a backup; after all the user may uninstall, // reinstall, and do a backup again with the same PIN. This, like that, is // a local-only trigger. Logger.error("Failed to decode in progress backup state") inProgressBackup = nil } if let inProgressBackup { if inProgressBackup.matches(pin: pin, mrEnclave: config.mrenclave) { // Continue the backup from where we left off. Logger.info("Continuing existing backup with expose") return continueWithExpose(backup: inProgressBackup) } else { // We had an in flight backup, but for a different PIN. // If its expose hasn't started, it should stop itself. Logger.info("Cancelling in progress backup because master key changed") return startFreshBackupExpose() } } else { Logger.info("Starting fresh backup + expose") return startFreshBackupExpose() } } } private enum BackupResult { case success(InProgressBackup) case localEncryptionError case localPersistenceError case networkError case serverError case unretainedError } private func performBackupRequest( pin: String, masterKey: Data, mrEnclave: MrEnclave, connection: WebsocketConnection ) -> Guarantee { guard let encodedPINVerificationString = try? SVRUtil.deriveEncodedPINVerificationString(pin: pin) else { return .value(.localEncryptionError) } let pinHash: SVR2PinHash let encryptedMasterKey: Data do { pinHash = try connection.hashPin( pin: pin, wrapper: clientWrapper ) encryptedMasterKey = try pinHash.encryptMasterKey(masterKey) } catch { return .value(.localEncryptionError) } var backupRequest = SVR2Proto_BackupRequest() backupRequest.maxTries = SVR.maximumKeyAttempts backupRequest.pin = pinHash.accessKey backupRequest.data = encryptedMasterKey var request = SVR2Proto_Request() request.backup = backupRequest return connection.sendRequestAndReadResponse(request, unretainedError: .unretainedError) { [weak self] makeRequest in guard let self else { return .value(.unretainedError) } return makeRequest().map(on: self.scheduler) { [weak self] (response: SVR2Proto_Response) -> BackupResult in guard let self else { return .unretainedError } guard response.hasBackup else { Logger.error("Backup response missing from server") return .serverError } switch response.backup.status { case .ok: Logger.info("Backup success!") let inProgressBackup = InProgressBackup( masterKey: masterKey, encryptedMasterKey: encryptedMasterKey, rawPinType: SVR.PinType(forPin: pin).rawValue, encodedPINVerificationString: encodedPINVerificationString, mrEnclaveStringValue: mrEnclave.stringValue ) do { // Write the in progress state to disk; we want to continue // from here and not redo the backup request. try self.db.write { tx in try self.setInProgressBackup(inProgressBackup, tx) } } catch { Logger.error("Failed to serialize in progress backup") return .localPersistenceError } return .success(inProgressBackup) case .UNRECOGNIZED, .unset: Logger.error("Unknown backup status response") return .serverError } } .recover(on: self.schedulers.sync) { error in Logger.error("Backup failed with closed connection") if error.isNetworkFailureOrTimeout { return .value(.networkError) } else { return .value(.serverError) } } } } private enum ExposeResult { case success case localPersistenceError case networkError case serverError case unretainedError } private func performExposeRequest( backup: InProgressBackup, authedAccount: AuthedAccount, connection: WebsocketConnection ) -> Guarantee { var exposeRequest = SVR2Proto_ExposeRequest() exposeRequest.data = backup.encryptedMasterKey var request = SVR2Proto_Request() request.expose = exposeRequest Logger.info("Issuing expose request") return connection.sendRequestAndReadResponse(request, unretainedError: .unretainedError) { [weak self] makeRequest in guard let self else { return .value(.unretainedError) } // Check that the backup is still the latest before we actually // issue the request. let currentBackup: InProgressBackup? do { currentBackup = try self.db.read { return try self.getInProgressBackup($0) } } catch { Logger.error("Unable to read in progress backup to continue expose") return .value(.localPersistenceError) } if let currentBackup, backup.matches(currentBackup).negated { // This expose is out of date. But its fine to let the caller // think it was a success; the backup that took its place // is now in charge and this one is done and shouldn't be repeated. return .value(.success) } return makeRequest().map(on: self.scheduler) { [weak self] (response) -> ExposeResult in guard let self else { return .unretainedError } guard response.hasExpose else { Logger.error("Expose response missing from server") return .serverError } switch response.expose.status { case .ok: Logger.info("Expose success!") do { try self.db.write { tx in guard let persistedBackup = try self.getInProgressBackup(tx), persistedBackup.matches(backup) else { Logger.info("Backup state changed while expose ongoing; throwing away results") return } self.clearInProgressBackup(tx) self.setLocalDataAndSyncStorageServiceIfNeeded( masterKey: backup.masterKey, isMasterKeyBackedUp: true, pinType: backup.pinType, encodedPINVerificationString: backup.encodedPINVerificationString, mrEnclaveStringValue: backup.mrEnclaveStringValue, mode: .syncStorageService(authedAccount), transaction: tx ) } } catch { Logger.error("Unable to read in progress backup to finalize expose") return .localPersistenceError } return .success case .error: // Every expose is a pair with a backup request. For it to fail, // one of three things happened: // 1. The local client sent a second backup, invalidating the one // this expose is paired with. // 2. A second client has sent its own backup, invalidating the // backup this expose is paired with. // 3. The server is misbehaving and reporting an error. // // 1 should be impossible; this class enforces serial execution to // prevent this. It is developer error if it does. // // 2 is impossible; only a primary device does backups, and if there // were another primary this one would be deregistered and its // auth credentials invalidated. // // 3 could be a legitimate server error or a compromised server; in either // case we do NOT want to make another backup; report a failure but keep // any InProgressBackup state around so that retries just retry the expose. // This prevents any possibility of repeated PIN guessing by a compromised server. Logger.error("Got error response when exposing on SVR2 server; something has gone horribly wrong.") return .serverError case .UNRECOGNIZED, .unset: Logger.error("Unknown expose status response") return .serverError } } .recover(on: self.schedulers.sync) { error in Logger.error("Expose failed with closed connection") if error.isNetworkFailureOrTimeout { return .value(.networkError) } else { return .value(.serverError) } } } } // MARK: - Restore Request private enum RestoreResult { case success(masterKey: Data, mrEnclave: MrEnclave) case backupMissing case invalidPin(remainingAttempts: UInt32) case decryptionError case serverError case networkError(Error) case genericError(Error) case unretainedError var asSVRResult: SVR.RestoreKeysResult { switch self { case .success: return .success case .backupMissing: return .backupMissing case .invalidPin(let remainingAttempts): return .invalidPin(remainingAttempts: remainingAttempts) case .networkError(let error): return .networkError(error) case .genericError(let error): return .genericError(error) case .decryptionError, .serverError, .unretainedError: return .genericError(SVR.SVRError.assertion) } } } private func doRestore( pin: String, authMethod: SVR2.AuthMethod ) -> Guarantee { var enclavesToTry = [tsConstants.svr2Enclave] + tsConstants.svr2PreviousEnclaves let weakSelf = Weak(value: self) func tryNextEnclave() -> Guarantee { guard enclavesToTry.isEmpty.negated else { // If we reach the end, there's no backup. return .value(.backupMissing) } guard let self = weakSelf.value else { return .value(.unretainedError) } let enclave = enclavesToTry.removeFirst() return self .doRestoreForSpecificEnclave( pin: pin, mrEnclave: enclave, authMethod: authMethod ) .then(on: self.scheduler) { enclaveResult in switch enclaveResult { case .backupMissing: // Only if we get an explicit backup missing result // from the server, try prior enclaves. // This works because we always wipe old enclaves when // we know about newer ones, so the only reason we'd have // anything in an old enclave is that we haven't migrated yet. // Once we migrate, we wipe the old one. return tryNextEnclave() case .success, .invalidPin, .decryptionError, .serverError, .networkError, .genericError, .unretainedError: return .value(enclaveResult) } } } return tryNextEnclave() } private func doRestoreForSpecificEnclave( pin: String, mrEnclave: MrEnclave, authMethod: SVR2.AuthMethod ) -> Guarantee { let config = SVR2WebsocketConfigurator(mrenclave: mrEnclave, authMethod: authMethod) return makeHandshakeAndOpenConnection(config) .then(on: scheduler) { [weak self] connection -> Guarantee in guard let self else { return .value(.unretainedError) } Logger.info("Connection open; making restore request") return self.performRestoreRequest( mrEnclave: mrEnclave, pin: pin, connection: connection, authedAccount: authMethod.authedAccount ) } .recover(on: SyncScheduler()) { error in if error.isNetworkFailureOrTimeout { return .value(.networkError(error)) } return .value(.genericError(error)) } } private func performRestoreRequest( mrEnclave: MrEnclave, pin: String, connection: WebsocketConnection, authedAccount: AuthedAccount ) -> Guarantee { let pinHash: SVR2PinHash let encodedPINVerificationString: String do { pinHash = try connection.hashPin(pin: pin, wrapper: clientWrapper) encodedPINVerificationString = try SVRUtil.deriveEncodedPINVerificationString(pin: pin) } catch { return .value(.decryptionError) } var restoreRequest = SVR2Proto_RestoreRequest() restoreRequest.pin = pinHash.accessKey var request = SVR2Proto_Request() request.restore = restoreRequest return connection.sendRequestAndReadResponse(request, unretainedError: .genericError(SVR.SVRError.assertion)) { [weak self] makeRequest in guard let self else { return .value(.unretainedError) } return makeRequest().map(on: self.scheduler) { [weak self] (response) -> RestoreResult in guard let self else { return .unretainedError } guard response.hasRestore else { Logger.error("Restore missing in server response") return .serverError } switch response.restore.status { case .unset, .UNRECOGNIZED: Logger.error("Unknown restore status response") return .serverError case .missing: Logger.info("Restore response: backup missing") return .backupMissing case .pinMismatch: Logger.info("Restore response: invalid pin") return .invalidPin(remainingAttempts: response.restore.tries) case .ok: Logger.info("Restore success!") let encryptedMasterKey = response.restore.data do { let masterKey = try pinHash.decryptMasterKey(encryptedMasterKey) self.db.write { tx in self.setLocalDataAndSyncStorageServiceIfNeeded( masterKey: masterKey, isMasterKeyBackedUp: true, pinType: .init(forPin: pin), encodedPINVerificationString: encodedPINVerificationString, mrEnclaveStringValue: mrEnclave.stringValue, mode: .syncStorageService(authedAccount), transaction: tx ) } return .success(masterKey: masterKey, mrEnclave: mrEnclave) } catch { Logger.info("Failed to decrypt master key from restore") return .decryptionError } } } .recover(on: self.schedulers.sync) { error -> Guarantee in Logger.error("Restore failed with closed connection") if error.isNetworkFailureOrTimeout { return .value(.networkError(error)) } else { return .value(.genericError(error)) } } } } // MARK: - Delete Request private enum DeleteResult { case success case serverError case networkError(Error) case genericError(Error) case unretainedError } private func doDelete( mrEnclave: MrEnclave, authMethod: SVR2.AuthMethod ) -> Guarantee { let config = SVR2WebsocketConfigurator(mrenclave: mrEnclave, authMethod: authMethod) return makeHandshakeAndOpenConnection(config) .then(on: scheduler) { [weak self] connection -> Guarantee in guard let self else { return .value(.unretainedError) } return self.performDeleteRequest( mrEnclave: mrEnclave, connection: connection, authedAccount: authMethod.authedAccount ) } .recover(on: SyncScheduler()) { error in if error.isNetworkFailureOrTimeout { return .value(.networkError(error)) } return .value(.genericError(error)) } } private func performDeleteRequest( mrEnclave: MrEnclave, connection: WebsocketConnection, authedAccount: AuthedAccount ) -> Guarantee { var request = SVR2Proto_Request() request.delete = SVR2Proto_DeleteRequest() return connection.sendRequestAndReadResponse(request, unretainedError: .genericError(SVR.SVRError.assertion)) { [weak self] makeRequest in guard let self else { return .value(.unretainedError) } return makeRequest().map(on: self.scheduler) { (response) -> DeleteResult in guard response.hasDelete else { Logger.error("Delete missing in server response") return .serverError } Logger.info("Delete success!") return .success } .recover(on: self.schedulers.sync) { error -> Guarantee in Logger.error("Delete failed with closed connection") if error.isNetworkFailureOrTimeout { return .value(.networkError(error)) } else { return .value(.genericError(error)) } } } } // MARK: Durable deletes private static let oldEnclavesToDeleteFromKey = "OldEnclavesToDeleteFrom" private func getOldEnclavesToDeleteFrom(_ tx: DBReadTransaction) -> [MrEnclave] { // This is decoding a Set. It won't actually ever fail, so just eat up errors. let enclaveStrings: Set? = try? kvStore.getCodableValue( forKey: Self.oldEnclavesToDeleteFromKey, transaction: tx ) guard var enclaveStrings else { return [] } var enclavesToDeleteFrom = [MrEnclave]() for enclave in tsConstants.svr2PreviousEnclaves { if enclaveStrings.remove(enclave.stringValue) != nil { enclavesToDeleteFrom.append(enclave) } } return enclavesToDeleteFrom } private func addOldEnclaveToDeleteFrom(_ enclave: MrEnclave, _ tx: DBWriteTransaction) { // This is (en/de)coding a Set. It won't actually ever fail, so just eat up errors. var enclaveStrings: Set = (try? kvStore.getCodableValue( forKey: Self.oldEnclavesToDeleteFromKey, transaction: tx )) ?? Set() enclaveStrings.insert(enclave.stringValue) cleanUpForgottenEnclaves(in: &enclaveStrings) try? kvStore.setCodable(enclaveStrings, key: Self.oldEnclavesToDeleteFromKey, transaction: tx) } private func markOldEnclaveDeleted(_ enclave: MrEnclave, _ tx: DBWriteTransaction) { // This is (en/de)coding a Set. It won't actually ever fail, so just eat up errors. var enclaveStrings: Set = (try? kvStore.getCodableValue( forKey: Self.oldEnclavesToDeleteFromKey, transaction: tx )) ?? Set() enclaveStrings.remove(enclave.stringValue) cleanUpForgottenEnclaves(in: &enclaveStrings) try? kvStore.setCodable(enclaveStrings, key: Self.oldEnclavesToDeleteFromKey, transaction: tx) } private func cleanUpForgottenEnclaves(in enclaveStrings: inout Set) { let knownEnclaves = Set(tsConstants.svr2PreviousEnclaves.map(\.stringValue)) enclaveStrings.formIntersection(knownEnclaves) } private func wipeOldEnclavesIfNeeded(auth: SVR2.AuthMethod) { var (isRegistered, enclavesToDeleteFrom) = db.read { tx in return ( self.tsAccountManager.registrationState(tx: tx).isRegistered, self.getOldEnclavesToDeleteFrom(tx) ) } guard isRegistered else { return } let weakSelf = Weak(value: self) func doNextDelete() -> Guarantee { guard let self = weakSelf.value, enclavesToDeleteFrom.isEmpty.negated else { return .value(.unretainedError) } let enclave = enclavesToDeleteFrom.removeFirst() Logger.info("Wiping old enclave: \(enclave.stringValue)") return self.doDelete(mrEnclave: enclave, authMethod: auth).then(on: self.scheduler) { result in switch result { case .success: weakSelf.value?.db.write { tx in weakSelf.value?.markOldEnclaveDeleted(enclave, tx) } case .serverError, .networkError, .genericError, .unretainedError: Logger.error("Failed to wipe old enclave; will retry eventually.") } return doNextDelete() } } _ = doNextDelete() } // MARK: - Migrations /// There was a bug with registration that would allow the user to register without having set a master key, /// if they skipped the PIN code entry. What we actually wanted was to not _sync_ that master key with /// SVR server, but we still want one locally. /// Clean up this state by setting a local-only master key if we are a registered primary without one. private func setLocalMasterKeyIfMissing() { let ( hasMasterKey, pinCode, isRegisteredPrimary ) = db.read { tx in return ( self.hasMasterKey(transaction: tx), self.twoFAManager.pinCode(transaction: tx), self.tsAccountManager.registrationState(tx: tx).isRegisteredPrimaryDevice ) } if !hasMasterKey, isRegisteredPrimary { db.write { tx in if pinCode != nil { // We have a pin code but no master key? We know this has happened // in the wild but have no idea how. Logger.error("Have PIN but no master key") } self.useDeviceLocalMasterKey(authedAccount: .implicit(), transaction: tx) } } } /// If there is a newer enclave than the one we most recently backed up to, backs up known /// master key data to it instead, marking the old enclave for deletion. /// If there is no migration needed, returns a success promise immediately. private func migrateEnclavesIfNecessary() -> Promise? { // Never migrate in the NSE or extensions. guard self.appReadiness.isMainApp else { return nil } return firstly(on: scheduler) { [weak self] () -> (String?, String, Data)? in return self?.db.read { tx -> (String?, String, Data)? in guard let self, self.tsAccountManager.registrationState(tx: tx).isRegisteredPrimaryDevice, let masterKey = self.localStorage.getMasterKey(tx), let pin = self.twoFAManager.pinCode(transaction: tx) else { // Need to be registered with a master key and PIN to migrate. Logger.info("Not migrating; local state not ready") return nil } let currentEnclaveString = self.tsConstants.svr2Enclave.stringValue let oldSVR2EnclaveString = self.localStorage.getSVR2MrEnclaveStringValue(tx) let isBackedUpToOlderEnclaveNeedingMigration: Bool = { guard self.localStorage.getIsMasterKeyBackedUp(tx) else { // "isMasterKeyBackedUp" is shared between svr2 and kbs; if its // false that means we had no backups to begin with and therefore // should not back up to any new enclave. Logger.info("Not migrating; no previous backups.") return false } if let oldSVR2EnclaveString, oldSVR2EnclaveString != currentEnclaveString { // We are backed up to an svr2 enclave that isn't the current one. Logger.info("Migrating from old svr2 enclave") return true } if oldSVR2EnclaveString == nil, self.localStorage.hadSVR1Enclave(tx) { // We have not ever backed up to svr2, but we had backed up // to svr1 (kbs), so we should migrate immediately. Logger.info("Migrating from kbs") return true } return false }() guard isBackedUpToOlderEnclaveNeedingMigration else { // We only migrate if we actually have a backup on some enclave that isn't // the current one. return nil } return (oldSVR2EnclaveString, pin, masterKey) } }.then(on: scheduler) { [weak self] values -> Promise in guard let self, let (oldSVR2EnclaveString, pin, masterKey) = values else { // No migration needed. return .value(()) } Logger.info("Migrating SVR2 Enclaves") return self .doBackupAndExpose(pin: pin, masterKey: masterKey, authMethod: .implicit) .done(on: self.scheduler) { [weak self] _ in Logger.info("Successfully migrated SVR2 enclave") guard let self else { return } // The oldSVR2EnclaveString might be nil if we are migrating from KBS. // In these cases we shouldn't wipe anything from kbs; we will mirror // to kbs for a while and eventually kbs will be torn down for us. if let oldSVR2EnclaveString, let backedUpEnclave = self.tsConstants.svr2PreviousEnclaves.first(where: { $0.stringValue == oldSVR2EnclaveString }) { Logger.info("Adding old enclave to be deleted") // Strictly speaking, this happens in a separate transaction from when we mark the // backup/expose complete. But no matter what this is best effort; the client // can be uninstalled before it gets a chance to delete the old backup, for example. self.db.write { tx in self.addOldEnclaveToDeleteFrom(backedUpEnclave, tx) } // We start wiping any old enclaves right after doing this migration, // no need to kick it off here. } } .catch(on: self.schedulers.sync) { _ in owsFailDebug("Failed to migrate SVR2 enclave") } } } // MARK: - Opening websocket /// A connection that manages its own lifecycle and executes all requests in serial. /// /// In general, we open a connection, make a few requests, and close it. /// 1) We ensure all requests happen in serial; if a second request comes in before the first /// has goten a response, we make it wait for the response (and all response handling) /// before continuing. /// 2) Instead of managing the open/close in our request creation code, we do that here: /// we keep track of how many requests are going out, decrement when they finish, /// and close the connection when there are none left. private class WebsocketConnection { private let connection: SgxWebsocketConnection private let scheduler: Scheduler private let onDisconnect: () -> Void init( connection: SgxWebsocketConnection, scheduler: Scheduler, onDisconnect: @escaping () -> Void ) { self.connection = connection self.scheduler = scheduler self.onDisconnect = onDisconnect } private var requestQueue = [(Error?) -> Promise]() private var isMakingRequest = false private func startNextRequestIfPossible() { guard !isMakingRequest else { return } if requestQueue.isEmpty { self.scheduleDisconnectIfNeeded() return } let errorToReport: Error? switch connectionState { case .connected: errorToReport = nil case .disconnected(let error): errorToReport = error } let nextRequest = requestQueue.removeFirst() self.isMakingRequest = true nextRequest(errorToReport).ensure(on: scheduler) { [weak self] in self?.isMakingRequest = false self?.startNextRequestIfPossible() }.cauterize() } /// Should only be called from the scheduler's queue. /// - Parameter handler: Called once all previously enqueued requests have gotten a response /// AND executed the contents of their handlers. Anything you want done before another request /// starts should happen before the resolution of the promise returned by this handler. func sendRequestAndReadResponse( _ request: SVR2Proto_Request, unretainedError: T, handler: @escaping (() -> Promise) -> Guarantee ) -> Guarantee { let (returnedGuarantee, returnedFuture) = Guarantee.pending() let scheduler = self.scheduler requestQueue.append({ [weak self] (initialError: Error?) -> Promise in guard let self else { let guarantee = handler({ return .init(error: SVR.SVRError.assertion) }) returnedFuture.resolve(on: scheduler, with: guarantee) return .init(error: SVR.SVRError.assertion) } if let initialError { let guarantee = handler({ return .init(error: initialError) }) returnedFuture.resolve(on: self.scheduler, with: guarantee) return .init(error: SVR.SVRError.assertion) } let guarantee = handler({ return Promise.race(on: self.scheduler, [ self.connection.sendRequestAndReadResponse(request), self.deinitFuture.0 ]) .recover(on: self.scheduler) { [weak self] error in // Treat all errors as terminating the connection. self?.disconnect(error) return Promise.init(error: error) } }) returnedFuture.resolve(on: self.scheduler, with: guarantee) return guarantee.asVoid(on: SyncScheduler()) }) startNextRequestIfPossible() return returnedGuarantee } private func scheduleDisconnectIfNeeded() { // Give a little leeway to start another request, after // which if nothing is happening we can close the connection. self.scheduler.asyncAfter(deadline: .now() + 0.1) { [weak self] in if self?.requestQueue.isEmpty == true, self?.isMakingRequest != true { self?.disconnect(nil) } } } private enum ConnectionState { case connected case disconnected(Error?) var isDisconnected: Bool { switch self { case .connected: return false case .disconnected: return true } } } private var connectionState = ConnectionState.connected private func disconnect(_ error: Error?) { guard !connectionState.isDisconnected else { return } connectionState = .disconnected(error) connection.disconnect(code: error == nil ? .normalClosure : nil) onDisconnect() } private let deinitFuture = Promise.pending() deinit { disconnect(SVR.SVRError.assertion) let error = SVR.SVRError.assertion deinitFuture.1.reject(error) // In normal disconnects, the chain of requests continues // until all of the handlers are called with the failure. // For the deinit case, that stops because the weak self // reference dies. // To ensure we fail all pending requests, make a copy // and fail them in sequence without any self reference. let scheduler = self.scheduler var requestQueue = self.requestQueue func failNextRequestInQueue() { guard requestQueue.isEmpty.negated else { return } requestQueue.removeFirst()(error).ensure(on: scheduler) { failNextRequestInQueue() }.cauterize() } failNextRequestInQueue() } func hashPin( pin: String, wrapper: SVR2ClientWrapper ) throws -> SVR2PinHash { guard let utf8NormalizedPin = SVRUtil.normalizePin(pin).data(using: .utf8) else { throw SVR.SVRError.assertion } return try wrapper.hashPin( connection: connection, utf8NormalizedPin: utf8NormalizedPin, username: connection.auth.username ) } } /// Maps from mrenclave string to open connection. /// A single connection makes all requests in serial; the next isn't made until we get a response. private var openConnectionByMrEnclaveString: [String: WebsocketConnection] = [:] private lazy var openConnectionChainedPromise = ChainedPromise(initialValue: nil, scheduler: scheduler) private func makeHandshakeAndOpenConnection(_ config: SVR2WebsocketConfigurator) -> Promise { // Update the auth method with cached credentials if we have them. switch config.authMethod { case .svrAuth, .chatServerAuth: // If we explicitly want to use some credential, use that. break case .implicit: // If implicit, use any cached values. if let cachedCredential: SVR2AuthCredential = db.read(block: credentialStorage.getAuthCredentialForCurrentUser) { config.authMethod = .svrAuth(cachedCredential, backup: .implicit) } } let weakSelf = Weak(value: self) var innerError: Error = SVR.SVRError.assertion func innerConnectAndPerformHandshake() -> Promise { guard let self = weakSelf.value else { return .init(error: SVR.SVRError.assertion) } if let openConnection = self.openConnectionByMrEnclaveString[config.mrenclave.stringValue] { Logger.info("Reusing already open websocket connection") return .value(openConnection) } Logger.info("Opening new connection") return self.connectionFactory .connectAndPerformHandshake( configurator: config, on: self.scheduler ) .then(on: self.scheduler) { [weak self] connection -> Promise in guard let self else { return .init(error: SVR.SVRError.assertion) } let knownGoodAuthCredential = connection.auth let connection = WebsocketConnection( connection: connection, scheduler: self.scheduler, onDisconnect: { [weak self] in self?.openConnectionByMrEnclaveString[config.mrenclave.stringValue] = nil } ) self.openConnectionByMrEnclaveString[config.mrenclave.stringValue] = connection // If we were able to open a connection, that means the auth used is valid // and we should cache it. self.db.write { tx in self.credentialStorage.storeAuthCredentialForCurrentUsername( SVR2AuthCredential(credential: knownGoodAuthCredential), tx ) } return .value(connection) } .recover(on: self.schedulers.sync) { [weak self] (error: Error) -> Promise in Logger.error("Failed to open websocket connection and complete handshake") innerError = error guard let self else { return .init(error: error) } // if we fail to connect for any reason, assume the credential we tried to use was bad. // clear it out, and if we have a backup, try again with that. switch config.authMethod { case .svrAuth(let attemptedCredential, let backup): self.db.write { tx in self.credentialStorage.deleteInvalidCredentials([attemptedCredential].compacted(), tx) } if let backup { config.authMethod = backup return innerConnectAndPerformHandshake() } case .chatServerAuth, .implicit: break } return .init(error: error) } } return openConnectionChainedPromise.enqueue(recoverValue: nil) { (_: WebsocketConnection?) -> Promise in innerConnectAndPerformHandshake() }.then(on: schedulers.sync) { connection -> Promise in if let connection { return .value(connection) } else { return .init(error: innerError) } } } // MARK: - Master key generation func generateMasterKey() -> Data { return Cryptography.generateRandomBytes(SVR.masterKeyLengthBytes) } // MARK: - Local key storage helpers private enum LocalDataUpdateMode { case dontSyncStorageService case syncStorageService(AuthedAccount) } private func setLocalDataAndSyncStorageServiceIfNeeded( masterKey: Data, isMasterKeyBackedUp: Bool, pinType: SVR.PinType, encodedPINVerificationString: String?, mrEnclaveStringValue: String?, mode: LocalDataUpdateMode, transaction: DBWriteTransaction ) { let masterKeyChanged = masterKey != localStorage.getMasterKey(transaction) if masterKeyChanged { localStorage.setMasterKey(masterKey, transaction) } if isMasterKeyBackedUp != localStorage.getIsMasterKeyBackedUp(transaction) { localStorage.setIsMasterKeyBackedUp(isMasterKeyBackedUp, transaction) } if pinType != localStorage.getPinType(transaction) { localStorage.setPinType(pinType, transaction) } if encodedPINVerificationString != localStorage.getEncodedPINVerificationString(transaction) { localStorage.setEncodedPINVerificationString(encodedPINVerificationString, transaction) } if mrEnclaveStringValue != localStorage.getSVR2MrEnclaveStringValue(transaction) { localStorage.setSVR2MrEnclaveStringValue(mrEnclaveStringValue, transaction) } // Only continue if we didn't previously have a master key or our master key has changed // and we are on the primary device. guard masterKeyChanged, tsAccountManager.registrationState(tx: transaction).isRegisteredPrimaryDevice else { return } // Trigger a re-creation of the storage manifest, our keys have changed storageServiceManager.resetLocalData(transaction: transaction) // If the app is ready start that restoration. guard AppReadiness.isAppReady else { return } switch mode { case .syncStorageService(let authedAccount): storageServiceManager.restoreOrCreateManifestIfNecessary(authedDevice: authedAccount.authedDevice(isPrimaryDevice: true)) let syncManager = self.syncManager storageServiceManager.waitForPendingRestores().observe { _ in // Sync our new keys with linked devices, but wait until the storage // service restore is done. That way we avoid the linked device getting // the new keys first, failing to decrypt old storage service data, // and asking for new keys even though thats not the problem. // We don't wanna miss sending one of these, though, so go ahead and send it // even if it fails. In any scenario it should eventually recover once // both storage service and the linked device have the latest stuff. syncManager.sendKeysSyncMessage() } case .dontSyncStorageService: break } } } fileprivate extension SVR2.AuthMethod { var authedAccount: AuthedAccount { switch self { case .svrAuth(_, let backup): return backup?.authedAccount ?? .implicit() case .chatServerAuth(let authedAccount): return authedAccount case .implicit: return .implicit() } } }