// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation /// Broadly speaking, this class does not perform PreKey operations. It just manages scheduling /// them (they must occur in serial), including deciding which need to happen in the first place. /// Actual execution is handed off to ``PreKeyTaskManager``. public class PreKeyManagerImpl: PreKeyManager { private let logger = PrefixedLogger(prefix: "[PreKey]") public enum Constants { // How often we check prekey state on app activation. static let oneTimePreKeyCheckFrequencySeconds: TimeInterval = 12 * .hour // Maximum amount of time that can elapse without rotating signed prekeys // before the message sending is disabled. static let SignedPreKeyMaxRotationDuration: TimeInterval = ( BuildFlags.shouldUseTestIntervals ? (4 * .day) : (14 * .day), ) /// Maximum amount of time a pre key can be used before a new one will be /// fetched. This should be equivalent to the largest /// `MAX_UNACKNOWLEDGED_SESSION_AGE` (from LibSignalClient) value currently /// in use by any client. static let maxUnacknowledgedSessionAge: TimeInterval = 30 * .day } /// 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 they must run in serial. private let taskQueue = ConcurrentTaskQueue(concurrentLimit: 1) private let db: any DB private let identityManager: OWSIdentityManager private let keyValueStore: KeyValueStore private let protocolStoreManager: SignalProtocolStoreManager private let chatConnectionManager: any ChatConnectionManager private let tsAccountManager: any TSAccountManager private let taskManager: PreKeyTaskManager init( dateProvider: @escaping DateProvider, db: any DB, identityKeyMismatchManager: IdentityKeyMismatchManager, identityManager: OWSIdentityManager, messageProcessor: MessageProcessor, preKeyTaskAPIClient: PreKeyTaskAPIClient, protocolStoreManager: SignalProtocolStoreManager, remoteConfigProvider: any RemoteConfigProvider, chatConnectionManager: any ChatConnectionManager, tsAccountManager: TSAccountManager, ) { self.db = db self.identityManager = identityManager self.keyValueStore = KeyValueStore(collection: "PreKeyManager") self.protocolStoreManager = protocolStoreManager self.chatConnectionManager = chatConnectionManager self.tsAccountManager = tsAccountManager self.taskManager = PreKeyTaskManager( apiClient: preKeyTaskAPIClient, dateProvider: dateProvider, db: db, identityKeyMismatchManager: identityKeyMismatchManager, identityManager: identityManager, messageProcessor: messageProcessor, protocolStoreManager: protocolStoreManager, remoteConfigProvider: remoteConfigProvider, tsAccountManager: tsAccountManager, ) } @Atomic private var lastOneTimePreKeyCheckTimestamp: Date? private func needsSignedPreKeyRotation(identity: OWSIdentity, tx: DBReadTransaction) -> Bool { let store = protocolStoreManager.signalProtocolStore(for: identity).signedPreKeyStore guard let lastSuccessDate = store.getLastSuccessfulRotationDate(tx: tx) else { return true } return lastSuccessDate.addingTimeInterval(Constants.SignedPreKeyMaxRotationDuration) < Date() } private func needsLastResortPreKeyRotation(identity: OWSIdentity, tx: DBReadTransaction) -> Bool { let store = protocolStoreManager.signalProtocolStore(for: identity).kyberPreKeyStore guard let lastSuccessDate = store.getLastSuccessfulRotationDate(tx: tx) else { return true } return lastSuccessDate.addingTimeInterval(Constants.SignedPreKeyMaxRotationDuration) < Date() } public func isAppLockedDueToPreKeyUpdateFailures(tx: DBReadTransaction) -> Bool { return needsSignedPreKeyRotation(identity: .aci, tx: tx) || needsSignedPreKeyRotation(identity: .pni, tx: tx) || needsLastResortPreKeyRotation(identity: .aci, tx: tx) || needsLastResortPreKeyRotation(identity: .pni, tx: tx) } private func refreshOneTimePreKeysCheckDidSucceed() { lastOneTimePreKeyCheckTimestamp = Date() } public func checkPreKeysIfNecessary() async throws { try await checkPreKeys(shouldThrottle: true) } fileprivate func checkPreKeys(shouldThrottle: Bool) async throws { guard CurrentAppContext().isMainAppAndActive else { throw OWSGenericError("must be the main app") } let shouldCheckOneTimePreKeys = { if shouldThrottle, let lastOneTimePreKeyCheckTimestamp, fabs(lastOneTimePreKeyCheckTimestamp.timeIntervalSinceNow) < Constants.oneTimePreKeyCheckFrequencySeconds { return false } return true }() // If we can throttle this check, and if we're changing our number, assume // that the change number will refresh our pre keys. (This check is // optional, so it's fine to skip it.) let shouldSkipPniPreKeyCheck = shouldThrottle && changeNumberState.update(block: { $0.isChangingNumber }) if shouldSkipPniPreKeyCheck { logger.warn("Skipping PNI pre key check due to change number.") } try await self._checkPreKeys( shouldCheckOneTimePreKeys: shouldCheckOneTimePreKeys, shouldCheckPniPreKeys: !shouldSkipPniPreKeyCheck, ) } private func _checkPreKeys( shouldCheckOneTimePreKeys: Bool, shouldCheckPniPreKeys: Bool, ) async throws { var targets: PreKeyTargets = [.signedPreKey, .lastResortPqPreKey] if shouldCheckOneTimePreKeys { targets.insert(target: .oneTimePreKey) targets.insert(target: .oneTimePqPreKey) } try await taskQueue.run { try await chatConnectionManager.waitForIdentifiedConnectionToOpen() try Task.checkCancellation() try await taskManager.refresh(identity: .aci, targets: targets, auth: .implicit()) if shouldCheckPniPreKeys { try Task.checkCancellation() try await self.waitUntilNotChangingNumberIfNeeded(targets: targets) try await taskManager.refresh(identity: .pni, targets: targets, auth: .implicit()) } if shouldCheckOneTimePreKeys, shouldCheckPniPreKeys { self.refreshOneTimePreKeysCheckDidSucceed() } } } public func createPreKeysForRegistration() async -> RegistrationPreKeyUploadBundles { logger.info("Create registration prekeys") return await taskManager.createForRegistration() } public func createPreKeysForProvisioning( aciIdentityKeyPair: ECKeyPair, pniIdentityKeyPair: ECKeyPair, ) async -> RegistrationPreKeyUploadBundles { logger.info("Create provisioning prekeys") return await taskManager.createForProvisioning( aciIdentityKeyPair: aciIdentityKeyPair, pniIdentityKeyPair: pniIdentityKeyPair, ) } public func finalizeRegistrationPreKeys( _ bundles: RegistrationPreKeyUploadBundles, uploadDidSucceed: Bool, ) async { logger.info("Finalize registration prekeys") await taskManager.persistAfterRegistration( bundles: bundles, uploadDidSucceed: uploadDidSucceed, ) } public func rotateOneTimePreKeysForRegistration(auth: ChatServiceAuth) async throws { logger.info("Rotate one-time prekeys for registration") return try await taskQueue.run { try Task.checkCancellation() try await taskManager.createOneTimePreKeys(identity: .aci, auth: auth) try Task.checkCancellation() try await taskManager.createOneTimePreKeys(identity: .pni, auth: auth) self.refreshOneTimePreKeysCheckDidSucceed() } } public func rotateSignedPreKeysIfNeeded() async throws { logger.info("Rotating signed prekeys if needed") try await _checkPreKeys(shouldCheckOneTimePreKeys: false, shouldCheckPniPreKeys: true) } /// Refresh one-time pre-keys for the given identity, and optionally refresh /// the signed pre-key. public func refreshOneTimePreKeys( forIdentity identity: OWSIdentity, alsoRefreshSignedPreKey shouldRefreshSignedPreKey: Bool, ) async throws { logger.info("[\(identity)] Force refresh onetime prekeys (also refresh signed pre key? \(shouldRefreshSignedPreKey))") /// Note that we do not report a `refreshOneTimePreKeysCheckDidSucceed` /// because this operation does not generate BOTH types of one time prekeys, /// so we shouldn't mark the routine refresh as having been "checked". var targets: PreKeyTargets = [.oneTimePreKey, .oneTimePqPreKey] if shouldRefreshSignedPreKey { targets.insert(.signedPreKey) targets.insert(.lastResortPqPreKey) } try await waitUntilNotChangingNumberIfNeeded(targets: targets) try await taskQueue.run { try Task.checkCancellation() try await taskManager.refresh( identity: identity, targets: targets, force: true, auth: .implicit(), ) } } /// If we don't have a PNI identity key, we should not run PNI operations. /// If we try, they will fail, and we will count the joint pni+aci operation as failed. private func hasPniIdentityKey(tx: DBReadTransaction) -> Bool { return self.identityManager.identityKeyPair(for: .pni, tx: tx) != nil } // MARK: - Change Number private struct ChangeNumberState { var isChangingNumber = false var onNotChangingNumber = [NSObject: Monitor.Continuation]() } private let changeNumberState = AtomicValue(ChangeNumberState(), lock: .init()) private let notChangingNumberCondition = Monitor.Condition( isSatisfied: { !$0.isChangingNumber }, waiters: \.onNotChangingNumber, ) /// Waits until the current "Change Number" operation is resolved. /// /// If we're changing our number, the currently-active PNI identity key is /// ambiguous (it's either the old one or the new one, but we don't know /// which). We should therefore defer periodic pre key refreshes until after /// we've finished changing our number. private func waitUntilNotChangingNumberIfNeeded(targets: PreKeyTargets) async throws(CancellationError) { guard targets.intersects([.signedPreKey, .lastResortPqPreKey]) else { return } try await Monitor.waitForCondition(notChangingNumberCondition, in: changeNumberState) } public func setIsChangingNumber(_ isChangingNumber: Bool) { Monitor.updateAndNotify( in: changeNumberState, block: { $0.isChangingNumber = isChangingNumber }, conditions: notChangingNumberCondition, ) } } // MARK: - Debug UI #if TESTABLE_BUILD public extension PreKeyManagerImpl { func checkPreKeysImmediately() async throws { try await checkPreKeys(shouldThrottle: false) } } #endif