Path: blob/main/SignalServiceKit/Account/PreKeys/PrekeyManagerImpl.swift
1 views
//
// 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
fileprivate static let preKeyRotationVersion = 1
fileprivate static let aciPreKeyRotationVersionKey = "ACIPreKeyRotationVersion"
fileprivate static let pniPreKeyRotationVersionKey = "PNIPreKeyRotationVersion"
}
/// 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
}
public func rotatePreKeysOnUpgradeIfNecessary(for identity: OWSIdentity) async throws {
let keyValueStoreKey: String = {
switch identity {
case .aci:
return Constants.aciPreKeyRotationVersionKey
case .pni:
return Constants.pniPreKeyRotationVersionKey
}
}()
let preKeyRotationVersion = db.read { tx in
return keyValueStore.getInt(keyValueStoreKey, defaultValue: 0, transaction: tx)
}
guard preKeyRotationVersion < Constants.preKeyRotationVersion else {
return
}
try await Retry.performWithBackoff(maxAttempts: .max, isRetryable: { _ in true }) {
guard db.read(block: tsAccountManager.registrationState(tx:)).isRegistered else {
// If we're not registered, we don't need to do this. Our pre keys will be
// rotated when we re-register.
return
}
do {
try await chatConnectionManager.waitForIdentifiedConnectionToOpen()
try await refreshOneTimePreKeys(forIdentity: identity, alsoRefreshSignedPreKey: true)
} catch {
logger.warn("Couldn't rotate pre keys: \(error)")
throw error
}
}
await db.awaitableWrite { [keyValueStore] tx in
keyValueStore.setInt(
Constants.preKeyRotationVersion,
key: keyValueStoreKey,
transaction: tx,
)
}
}
// 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<ChangeNumberState>(
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