298 lines
12 KiB
Swift
298 lines
12 KiB
Swift
//
|
|
// 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<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
|