// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalCoreKit public protocol LinkedDevicePniKeyManager { /// Records that we encountered an issue we suspect is due to a problem with /// our PNI identity key. func recordSuspectedIssueWithPniIdentityKey(tx: DBWriteTransaction) /// Validates this device's PNI identity key against the one on the server, /// if necessary. /// /// It is possible for some linked devices to have missing or outdated PNI /// identity keys. To address this we will run "PNI Hello World", an /// operation in which the primary sends a message to linked devices with /// the correct PNI identity key. /// /// We cannot, unfortunately, guarantee that a linked device in this state /// will successfully receive the PNI Hello World. For example, its primary /// may never come online to fire it, or the associated message may age out /// of the queue. While these are edge cases, they will result in a linked /// device that cannot perform actions related to our PNI identity key, such /// as decrypting messages to our PNI or prekey API calls. /// /// Newly-linked devices will receive the PNI identity key as part of /// provisioning. Consequently, if we find ourselves with a missing or /// incorrect PNI identity key we will correct that state by unlinking the /// linked device. On re-link, it'll get the correct state. /// /// This operation checks for suspected issues with our PNI identity key and /// subsequently compares our local PNI identity key with the service. If /// our local key does not match we unlink this device. /// /// We do not expect many devices to have ended up in a bad state, and so we /// hope that this unlinking will be a rare last resort. func validateLocalPniIdentityKeyIfNecessary(tx: DBReadTransaction) } class LinkedDevicePniKeyManagerImpl: LinkedDevicePniKeyManager { private enum Constants { static let collection = "LinkedDevicePniKeyManagerImpl" static let hasRecordedSuspectedIssueKey = "hasSuspectedIssue" } /// Scenarios that should interrupt regular processing. private enum Interrupts: Error { case noPniDecryptionError case missingLocalPni case localPniIdentityKeyDidntMatchRemote /// Whether this interrupt should result in unlinking. var shouldResultInUnlink: Bool { switch self { case .noPniDecryptionError: return false case .missingLocalPni, .localPniIdentityKeyDidntMatchRemote: return true } } } private let logger = PrefixedLogger(prefix: "LDPKM") private let db: DB private let kvStore: KeyValueStore private let messageProcessor: Shims.MessageProcessor private let pniIdentityKeyChecker: PniIdentityKeyChecker private let registrationStateChangeManager: RegistrationStateChangeManager private let schedulers: Schedulers private let tsAccountManager: TSAccountManager private let isValidating = AtomicBool(false, lock: .init()) init( db: DB, keyValueStoreFactory: KeyValueStoreFactory, messageProcessor: Shims.MessageProcessor, pniIdentityKeyChecker: PniIdentityKeyChecker, registrationStateChangeManager: RegistrationStateChangeManager, schedulers: Schedulers, tsAccountManager: TSAccountManager ) { self.db = db self.kvStore = keyValueStoreFactory.keyValueStore(collection: Constants.collection) self.messageProcessor = messageProcessor self.pniIdentityKeyChecker = pniIdentityKeyChecker self.registrationStateChangeManager = registrationStateChangeManager self.schedulers = schedulers self.tsAccountManager = tsAccountManager } func recordSuspectedIssueWithPniIdentityKey(tx: DBWriteTransaction) { guard tsAccountManager.registrationState(tx: tx).isPrimaryDevice == false else { logger.warn("Not recording suspected PNI identity key issue - not a linked device!") return } kvStore.setBool( true, key: Constants.hasRecordedSuspectedIssueKey, transaction: tx ) validateLocalPniIdentityKeyIfNecessary(tx: tx) } func validateLocalPniIdentityKeyIfNecessary(tx syncTx: DBReadTransaction) { let logger = logger guard isValidating.tryToSetFlag() else { logger.warn("Skipping validation - already in flight!") return } guard tsAccountManager.registrationState(tx: syncTx).isPrimaryDevice == false else { logger.info("Skipping validation - not a linked device!") return } firstly(on: schedulers.sync) { () -> Guarantee in return self.messageProcessor.waitForFetchingAndProcessing() } .then(on: schedulers.global()) { () throws -> Promise in return try self.db.read { tx throws in guard self.kvStore.getBool( Constants.hasRecordedSuspectedIssueKey, defaultValue: false, transaction: tx ) else { logger.info("Skipping validation - no reason to suspect an issue.") throw Interrupts.noPniDecryptionError } guard let localPni = self.tsAccountManager.localIdentifiers( tx: tx )?.pni else { logger.warn("Missing local PNI!") throw Interrupts.missingLocalPni } return self.pniIdentityKeyChecker.serverHasSameKeyAsLocal( localPni: localPni, tx: tx ) } } .done(on: schedulers.global()) { matched throws in guard matched else { logger.warn("Local PNI identity key didn't match remote!") throw Interrupts.localPniIdentityKeyDidntMatchRemote } self.db.write { tx in self.clearPniMessageDecryptionError(tx: tx) } logger.info("Local identity keys matched remote - no further action.") } .catch(on: schedulers.global()) { error in guard let interrupt = error as? Interrupts else { logger.error("Error during validation! \(error)") return } self.db.write { tx in if interrupt.shouldResultInUnlink { logger.warn("Marking as deregistered.") self.registrationStateChangeManager.setIsDeregisteredOrDelinked(true, tx: tx) } self.clearPniMessageDecryptionError(tx: tx) } } .ensure(on: schedulers.sync) { self.isValidating.set(false) } .cauterize() } private func clearPniMessageDecryptionError(tx: DBWriteTransaction) { kvStore.removeValue( forKey: Constants.hasRecordedSuspectedIssueKey, transaction: tx ) } } // MARK: - Mocks extension LinkedDevicePniKeyManagerImpl { enum Shims { typealias MessageProcessor = _LinkedDevicePniKeyManagerImpl_MessageProcessor_Shim } enum Wrappers { typealias MessageProcessor = _LinkedDevicePniKeyManagerImpl_MessageProcessor_Wrapper } } // MARK: MessageProcessor protocol _LinkedDevicePniKeyManagerImpl_MessageProcessor_Shim { func waitForFetchingAndProcessing() -> Guarantee } class _LinkedDevicePniKeyManagerImpl_MessageProcessor_Wrapper: _LinkedDevicePniKeyManagerImpl_MessageProcessor_Shim { private let messageProcessor: MessageProcessor public init(_ messageProcessor: MessageProcessor) { self.messageProcessor = messageProcessor } public func waitForFetchingAndProcessing() -> Guarantee { messageProcessor.waitForFetchingAndProcessing() } }