From e4e9d7013f8ea3eeae895107938bd6a02dfa169c Mon Sep 17 00:00:00 2001 From: Sasha Weiss Date: Wed, 4 Feb 2026 14:17:13 -0800 Subject: [PATCH] Add `KeyTransparencyStore`, instead of static methods on `KeyTransparencyManager` --- ...dvancedPrivacySettingsViewController.swift | 6 +- .../DebugUI/DebugUIPrompts.swift | 3 +- .../ChatListFYISheetCoordinator.swift | 7 +- .../Chat List/ChatListViewController.swift | 1 + .../BackupArchiveAccountDataArchiver.swift | 11 +- ...ackupArchiveContactRecipientArchiver.swift | 7 +- SignalServiceKit/Environment/AppSetup.swift | 4 + .../KeyTransparencyManager.swift | 279 +++++++++++------- .../FingerprintViewController.swift | 7 +- 9 files changed, 197 insertions(+), 128 deletions(-) diff --git a/Signal/src/ViewControllers/AppSettings/Privacy/AdvancedPrivacySettingsViewController.swift b/Signal/src/ViewControllers/AppSettings/Privacy/AdvancedPrivacySettingsViewController.swift index 78cbcb0fbd..6bb1d4600c 100644 --- a/Signal/src/ViewControllers/AppSettings/Privacy/AdvancedPrivacySettingsViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Privacy/AdvancedPrivacySettingsViewController.swift @@ -164,14 +164,16 @@ class AdvancedPrivacySettingsViewController: OWSTableViewController2 { ), isOn: { let db = DependenciesBridge.shared.db + let keyTransparencyStore = KeyTransparencyStore() return db.read { tx in - KeyTransparencyManager.isEnabled(tx: tx) + keyTransparencyStore.isEnabled(tx: tx) } }, actionBlock: { uiSwitch in let db = DependenciesBridge.shared.db + let keyTransparencyStore = KeyTransparencyStore() db.write { tx in - KeyTransparencyManager.setIsEnabled(uiSwitch.isOn, tx: tx) + keyTransparencyStore.setIsEnabled(uiSwitch.isOn, tx: tx) } }, )) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIPrompts.swift b/Signal/src/ViewControllers/DebugUI/DebugUIPrompts.swift index c120da9772..943bdbc260 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIPrompts.swift +++ b/Signal/src/ViewControllers/DebugUI/DebugUIPrompts.swift @@ -14,6 +14,7 @@ class DebugUIPrompts: DebugUIPage { func section(thread: TSThread?) -> OWSTableSection? { let db = DependenciesBridge.shared.db let inactiveLinkedDeviceFinder = DependenciesBridge.shared.inactiveLinkedDeviceFinder + let keyTransparencyStore = KeyTransparencyStore() let usernameEducationManager = DependenciesBridge.shared.usernameEducationManager var items = [OWSTableItem]() @@ -21,7 +22,7 @@ class DebugUIPrompts: DebugUIPage { items += [ OWSTableItem(title: "Reenable KT first-time education", actionBlock: { db.write { tx in - KeyTransparencyManager.setHasShownFirstTimeEducation(false, tx: tx) + keyTransparencyStore.setHasShownFirstTimeEducation(false, tx: tx) } }), diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift index b9ae2bd67b..c765492422 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift @@ -53,6 +53,7 @@ class ChatListFYISheetCoordinator { private let donationReceiptCredentialResultStore: DonationReceiptCredentialResultStore private let donationSubscriptionManager: DonationSubscriptionManager.Type private let db: DB + private let keyTransparencyStore: KeyTransparencyStore private let networkManager: NetworkManager private let profileManager: ProfileManager @@ -62,6 +63,7 @@ class ChatListFYISheetCoordinator { donationReceiptCredentialResultStore: DonationReceiptCredentialResultStore, donationSubscriptionManager: DonationSubscriptionManager.Type, db: DB, + keyTransparencyStore: KeyTransparencyStore, networkManager: NetworkManager, profileManager: ProfileManager, ) { @@ -70,6 +72,7 @@ class ChatListFYISheetCoordinator { self.donationReceiptCredentialResultStore = donationReceiptCredentialResultStore self.donationSubscriptionManager = donationSubscriptionManager self.db = db + self.keyTransparencyStore = keyTransparencyStore self.networkManager = networkManager self.profileManager = profileManager } @@ -116,7 +119,7 @@ class ChatListFYISheetCoordinator { return .backupSubscriptionExpired(FYISheet.BackupSubscriptionExpired(subscriptionType: .testFlight)) } else if backupSubscriptionIssueStore.shouldWarnIAPSubscriptionFailedToRenew(tx: tx) { return .backupSubscriptionFailedToRenew(FYISheet.BackupSubscriptionFailedToRenew()) - } else if KeyTransparencyManager.shouldWarnSelfCheckFailed(tx: tx) { + } else if keyTransparencyStore.shouldWarnSelfCheckFailed(tx: tx) { return .keyTransparencySelfCheckFailed(FYISheet.KeyTransparencySelfCheckFailed()) } else { return nil @@ -453,7 +456,7 @@ class ChatListFYISheetCoordinator { chatListViewController.present(sheet, animated: true) { [self] in db.write { tx in - KeyTransparencyManager.setWarnedSelfCheckFailed(tx: tx) + keyTransparencyStore.setWarnedSelfCheckFailed(tx: tx) } } } diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift index 97b0b27a36..99dd25cc88 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift @@ -372,6 +372,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController { donationReceiptCredentialResultStore: DependenciesBridge.shared.donationReceiptCredentialResultStore, donationSubscriptionManager: DonationSubscriptionManager.self, db: DependenciesBridge.shared.db, + keyTransparencyStore: KeyTransparencyStore(), networkManager: SSKEnvironment.shared.networkManagerRef, profileManager: SSKEnvironment.shared.profileManagerRef, ) diff --git a/SignalServiceKit/Backups/Archiving/Archivers/AccountData/BackupArchiveAccountDataArchiver.swift b/SignalServiceKit/Backups/Archiving/Archivers/AccountData/BackupArchiveAccountDataArchiver.swift index 3bb9b3f5b7..d37d523f66 100644 --- a/SignalServiceKit/Backups/Archiving/Archivers/AccountData/BackupArchiveAccountDataArchiver.swift +++ b/SignalServiceKit/Backups/Archiving/Archivers/AccountData/BackupArchiveAccountDataArchiver.swift @@ -58,6 +58,7 @@ public class BackupArchiveAccountDataArchiver: BackupArchiveProtoStreamWriter { private let disappearingMessageConfigurationStore: DisappearingMessagesConfigurationStore private let donationSubscriptionManager: BackupArchive.Shims.DonationSubscriptionManager private let imageQuality: BackupArchive.Shims.ImageQuality + private let keyTransparencyStore: KeyTransparencyStore private let linkPreviewSettingStore: LinkPreviewSettingStore private let localUsernameManager: LocalUsernameManager private let logger: PrefixedLogger @@ -86,6 +87,7 @@ public class BackupArchiveAccountDataArchiver: BackupArchiveProtoStreamWriter { disappearingMessageConfigurationStore: DisappearingMessagesConfigurationStore, donationSubscriptionManager: BackupArchive.Shims.DonationSubscriptionManager, imageQuality: BackupArchive.Shims.ImageQuality, + keyTransparencyStore: KeyTransparencyStore, linkPreviewSettingStore: LinkPreviewSettingStore, localUsernameManager: LocalUsernameManager, mediaBandwidthPreferenceStore: MediaBandwidthPreferenceStore, @@ -112,6 +114,7 @@ public class BackupArchiveAccountDataArchiver: BackupArchiveProtoStreamWriter { self.disappearingMessageConfigurationStore = disappearingMessageConfigurationStore self.donationSubscriptionManager = donationSubscriptionManager self.imageQuality = imageQuality + self.keyTransparencyStore = keyTransparencyStore self.linkPreviewSettingStore = linkPreviewSettingStore self.localUsernameManager = localUsernameManager self.logger = PrefixedLogger(prefix: "[Backups]") @@ -195,7 +198,7 @@ public class BackupArchiveAccountDataArchiver: BackupArchiveProtoStreamWriter { } if - let keyTransparencyBlob = KeyTransparencyManager.getKeyTransparencyBlob( + let keyTransparencyBlob = keyTransparencyStore.getKeyTransparencyBlob( aci: context.localIdentifiers.aci, tx: context.tx, ) @@ -325,7 +328,7 @@ public class BackupArchiveAccountDataArchiver: BackupArchiveProtoStreamWriter { } accountSettings.allowSealedSenderFromAnyone = udManager.shouldAllowUnrestrictedAccessLocal(tx: context.tx) - accountSettings.allowAutomaticKeyVerification = KeyTransparencyManager.isEnabled(tx: context.tx) + accountSettings.allowAutomaticKeyVerification = keyTransparencyStore.isEnabled(tx: context.tx) accountSettings.defaultSentMediaQuality = imageQuality.fetchValue(tx: context.tx) == .high ? .high : .standard var downloadSettings = BackupProto_AccountData.AutoDownloadSettings() @@ -572,7 +575,7 @@ public class BackupArchiveAccountDataArchiver: BackupArchiveProtoStreamWriter { } udManager.setShouldAllowUnrestrictedAccessLocal(settings.allowSealedSenderFromAnyone, tx: context.tx) - KeyTransparencyManager.setIsEnabled(settings.allowAutomaticKeyVerification, tx: context.tx) + keyTransparencyStore.setIsEnabled(settings.allowAutomaticKeyVerification, tx: context.tx) switch settings.defaultSentMediaQuality { case .high: @@ -666,7 +669,7 @@ public class BackupArchiveAccountDataArchiver: BackupArchiveProtoStreamWriter { } if accountData.hasKeyTransparencyData { - KeyTransparencyManager.setKeyTransparencyBlob( + keyTransparencyStore.setKeyTransparencyBlob( accountData.keyTransparencyData, aci: context.localIdentifiers.aci, tx: context.tx, diff --git a/SignalServiceKit/Backups/Archiving/Archivers/Recipient/BackupArchiveContactRecipientArchiver.swift b/SignalServiceKit/Backups/Archiving/Archivers/Recipient/BackupArchiveContactRecipientArchiver.swift index 52e51d6265..f007e0b712 100644 --- a/SignalServiceKit/Backups/Archiving/Archivers/Recipient/BackupArchiveContactRecipientArchiver.swift +++ b/SignalServiceKit/Backups/Archiving/Archivers/Recipient/BackupArchiveContactRecipientArchiver.swift @@ -22,6 +22,7 @@ public class BackupArchiveContactRecipientArchiver: BackupArchiveProtoStreamWrit private let avatarFetcher: BackupArchiveAvatarFetcher private let blockingManager: BackupArchive.Shims.BlockingManager private let contactManager: BackupArchive.Shims.ContactManager + private let keyTransparencyStore: KeyTransparencyStore private let nicknameManager: NicknameManager private let profileManager: BackupArchive.Shims.ProfileManager private let recipientHidingManager: RecipientHidingManager @@ -38,6 +39,7 @@ public class BackupArchiveContactRecipientArchiver: BackupArchiveProtoStreamWrit avatarFetcher: BackupArchiveAvatarFetcher, blockingManager: BackupArchive.Shims.BlockingManager, contactManager: BackupArchive.Shims.ContactManager, + keyTransparencyStore: KeyTransparencyStore, nicknameManager: NicknameManager, profileManager: BackupArchive.Shims.ProfileManager, recipientHidingManager: RecipientHidingManager, @@ -53,6 +55,7 @@ public class BackupArchiveContactRecipientArchiver: BackupArchiveProtoStreamWrit self.avatarFetcher = avatarFetcher self.blockingManager = blockingManager self.contactManager = contactManager + self.keyTransparencyStore = keyTransparencyStore self.nicknameManager = nicknameManager self.profileManager = profileManager self.recipientHidingManager = recipientHidingManager @@ -264,7 +267,7 @@ public class BackupArchiveContactRecipientArchiver: BackupArchiveProtoStreamWrit tx: context.tx, ), keyTransparencyBlob: recipient.aci.flatMap { aci in - KeyTransparencyManager.getKeyTransparencyBlob(aci: aci, tx: context.tx) + self.keyTransparencyStore.getKeyTransparencyBlob(aci: aci, tx: context.tx) }, ) @@ -875,7 +878,7 @@ public class BackupArchiveContactRecipientArchiver: BackupArchiveProtoStreamWrit contactProto.hasKeyTransparencyData, let aci = backupContactAddress.aci { - KeyTransparencyManager.setKeyTransparencyBlob( + keyTransparencyStore.setKeyTransparencyBlob( contactProto.keyTransparencyData, aci: aci, tx: context.tx, diff --git a/SignalServiceKit/Environment/AppSetup.swift b/SignalServiceKit/Environment/AppSetup.swift index 70296fe7e3..b76c97c8d7 100644 --- a/SignalServiceKit/Environment/AppSetup.swift +++ b/SignalServiceKit/Environment/AppSetup.swift @@ -1233,11 +1233,13 @@ extension AppSetup.GlobalsContinuation { whoAmIManager: whoAmIManager, )) + let keyTransparencyStore = KeyTransparencyStore() let keyTransparencyManager = KeyTransparencyManager( chatConnectionManager: chatConnectionManager, dateProvider: dateProvider, db: db, identityManager: identityManager, + keyTransparencyStore: keyTransparencyStore, localUsernameManager: localUsernameManager, recipientDatabaseTable: recipientDatabaseTable, storageServiceManager: storageServiceManager, @@ -1379,6 +1381,7 @@ extension AppSetup.GlobalsContinuation { avatarFetcher: backupArchiveAvatarFetcher, blockingManager: BackupArchive.Wrappers.BlockingManager(blockingManager), contactManager: BackupArchive.Wrappers.ContactManager(contactManager), + keyTransparencyStore: keyTransparencyStore, nicknameManager: nicknameManager, profileManager: BackupArchive.Wrappers.ProfileManager(profileManager), recipientHidingManager: recipientHidingManager, @@ -1423,6 +1426,7 @@ extension AppSetup.GlobalsContinuation { disappearingMessageConfigurationStore: disappearingMessagesConfigurationStore, donationSubscriptionManager: BackupArchive.Wrappers.DonationSubscriptionManager(), imageQuality: BackupArchive.Wrappers.ImageQuality(), + keyTransparencyStore: keyTransparencyStore, linkPreviewSettingStore: linkPreviewSettingStore, localUsernameManager: localUsernameManager, mediaBandwidthPreferenceStore: mediaBandwidthPreferenceStore, diff --git a/SignalServiceKit/KeyTransparency/KeyTransparencyManager.swift b/SignalServiceKit/KeyTransparency/KeyTransparencyManager.swift index e167456014..954f108a99 100644 --- a/SignalServiceKit/KeyTransparency/KeyTransparencyManager.swift +++ b/SignalServiceKit/KeyTransparency/KeyTransparencyManager.swift @@ -10,29 +10,11 @@ public final class KeyTransparencyManager { private static let logger = PrefixedLogger(prefix: "[KT]") private var logger: PrefixedLogger { Self.logger } - private static let kvStore = NewKeyValueStore(collection: "KeyTransparencyManager") - private var kvStore: NewKeyValueStore { Self.kvStore } - - /// Keys for `kvStore`. - /// - Important - /// If you're adding a new key here, consider whether it should be wiped - /// when Key Transparency is disabled. See: `setIsEnabled`. - private enum KVStoreKeys { - /// Keys to a `Bool` representing whether or not KT is enabled. - static let isEnabled = "isEnabled" - /// Keys to a `SelfCheckState`'s raw value. - static let selfCheckState = "selfCheckState" - /// Keys to a `Bool` representing whether or not we should show - /// first-time education about KT. - static let shouldShowFirstTimeEducation = "shouldShowFirstTimeEducation" - /// Keys to an opaque LibSignalClient blob. - static let distinguishedTreeHead = "distinguishedTreeHead" - } - private let chatConnectionManager: ChatConnectionManager private let dateProvider: DateProvider private let db: DB private let identityManager: OWSIdentityManager + private let keyTransparencyStore: KeyTransparencyStore private let localUsernameManager: LocalUsernameManager private let recipientDatabaseTable: RecipientDatabaseTable private let storageServiceManager: StorageServiceManager @@ -46,6 +28,7 @@ public final class KeyTransparencyManager { dateProvider: @escaping DateProvider, db: DB, identityManager: OWSIdentityManager, + keyTransparencyStore: KeyTransparencyStore, localUsernameManager: LocalUsernameManager, recipientDatabaseTable: RecipientDatabaseTable, storageServiceManager: StorageServiceManager, @@ -56,6 +39,7 @@ public final class KeyTransparencyManager { self.dateProvider = dateProvider self.db = db self.identityManager = identityManager + self.keyTransparencyStore = keyTransparencyStore self.localUsernameManager = localUsernameManager self.recipientDatabaseTable = recipientDatabaseTable self.storageServiceManager = storageServiceManager @@ -98,7 +82,7 @@ public final class KeyTransparencyManager { let logger = logger.suffixed(with: "[\(aci)]") logger.info("") - guard Self.isEnabled(tx: tx) else { + guard keyTransparencyStore.isEnabled(tx: tx) else { logger.warn("Is opted out.") return nil } @@ -193,20 +177,23 @@ public final class KeyTransparencyManager { logger: PrefixedLogger, ) async throws { let ktClient = try await chatConnectionManager.keyTransparencyClient() - let libSignalStore = KeyTransparencyStoreForLibSignal(db: db) + let libSignalStore = KeyTransparencyStoreForLibSignal( + db: db, + keyTransparencyStore: keyTransparencyStore, + ) let existingKeyTransparencyBlob: Data? - let selfCheckState: SelfCheckState? + let selfCheckState: KeyTransparencyStore.SelfCheckState? ( existingKeyTransparencyBlob, selfCheckState, ) = db.read { tx in return ( - Self.getKeyTransparencyBlob( + keyTransparencyStore.getKeyTransparencyBlob( aci: params.aciInfo.aci, tx: tx, ), - Self.selfCheckState(tx: tx), + keyTransparencyStore.selfCheckState(tx: tx), ) } @@ -263,55 +250,8 @@ public final class KeyTransparencyManager { } } - // MARK: - Self-check state - - private enum SelfCheckState: Int64 { - case succeeded = 1 - case failedOnce = 2 - case failedRepeatedly = 3 - case failedRepeatedlyAndWarned = 4 - } - - private static func selfCheckState(tx: DBReadTransaction) -> SelfCheckState? { - return kvStore.fetchValue( - Int64.self, - forKey: KVStoreKeys.selfCheckState, - tx: tx, - ) - .map { SelfCheckState(rawValue: $0)! } - } - - private static func setSelfCheckState(_ state: SelfCheckState, tx: DBWriteTransaction) { - kvStore.writeValue(state.rawValue, forKey: KVStoreKeys.selfCheckState, tx: tx) - } - - public static func shouldWarnSelfCheckFailed(tx: DBReadTransaction) -> Bool { - switch selfCheckState(tx: tx) { - case .failedRepeatedly: - return true - case nil, .succeeded, .failedOnce, .failedRepeatedlyAndWarned: - return false - } - } - - public static func setWarnedSelfCheckFailed(tx: DBWriteTransaction) { - switch selfCheckState(tx: tx) { - case .failedRepeatedly: - Self.setSelfCheckState(.failedRepeatedlyAndWarned, tx: tx) - case nil, .succeeded, .failedOnce, .failedRepeatedlyAndWarned: - owsFailDebug("Unexpectedly setting warned, but shouldn't have warned?") - } - } - // MARK: - Self-check - private static let selfCheckCronStore = CronStore(uniqueKey: .keyTransparencySelfCheck) - private static let selfCheckCronInterval: TimeInterval = if BuildFlags.KeyTransparency.conservativeSelfCheck { - .day - } else { - .week - } - /// Use `Cron` to periodically perform a Key Transparency validation on the /// local user. public func registerSelfCheckForCron(cron: Cron) { @@ -323,25 +263,25 @@ public final class KeyTransparencyManager { return false }, operation: { [self] () async throws -> Void in - let mostRecentDate: Date - let localIdentifiers: LocalIdentifiers? let isEnabled: Bool + let localIdentifiers: LocalIdentifiers? + let isTimeForSelfCheck: Bool ( - mostRecentDate, - localIdentifiers, isEnabled, + localIdentifiers, + isTimeForSelfCheck, ) = db.read { tx in return ( - Self.selfCheckCronStore.mostRecentDate(tx: tx), + keyTransparencyStore.isEnabled(tx: tx), tsAccountManager.localIdentifiers(tx: tx), - Self.isEnabled(tx: tx), + keyTransparencyStore.getIsTimeForSelfCheckCronJob(now: dateProvider(), tx: tx), ) } guard isEnabled, let localIdentifiers, - dateProvider() > mostRecentDate.addingTimeInterval(Self.selfCheckCronInterval) + isTimeForSelfCheck else { return } @@ -366,7 +306,7 @@ public final class KeyTransparencyManager { public func debugUI_setSelfCheckFailed() { db.write { tx in - Self.setSelfCheckState(.failedRepeatedly, tx: tx) + keyTransparencyStore.setSelfCheckState(.failedRepeatedly, tx: tx) } } @@ -442,11 +382,10 @@ public final class KeyTransparencyManager { await db.awaitableWrite { tx in logger.info("Self-check success.") - Self.setSelfCheckState(.succeeded, tx: tx) - - Self.selfCheckCronStore.setMostRecentDate( - dateProvider(), - jitter: Self.selfCheckCronInterval / Cron.jitterFactor, + keyTransparencyStore.setSelfCheckState(.succeeded, tx: tx) + keyTransparencyStore.setSelfCheckCronJobCompletedAt( + now: dateProvider(), + specialIntervalTillNextCron: nil, tx: tx, ) } @@ -461,14 +400,14 @@ public final class KeyTransparencyManager { } private func recordSelfCheckFailure(tx: DBWriteTransaction) { - let intervalTillNextCron: TimeInterval - let newSelfCheckState: SelfCheckState? + let specialIntervalTillNextCron: TimeInterval? + let newSelfCheckState: KeyTransparencyStore.SelfCheckState? - switch Self.selfCheckState(tx: tx) { + switch keyTransparencyStore.selfCheckState(tx: tx) { case nil, .succeeded: logger.warn("Self-check first failure.") newSelfCheckState = .failedOnce - intervalTillNextCron = .day + specialIntervalTillNextCron = .day // A known failure mode is if a linked device changed something // KT-related (e.g., a username) and this device hasn't yet learned @@ -484,12 +423,12 @@ public final class KeyTransparencyManager { case .failedOnce: logger.warn("Self-check second failure.") newSelfCheckState = .failedRepeatedly - intervalTillNextCron = Self.selfCheckCronInterval + specialIntervalTillNextCron = nil case .failedRepeatedly: logger.warn("Self-check continued failure.") newSelfCheckState = nil - intervalTillNextCron = Self.selfCheckCronInterval + specialIntervalTillNextCron = nil case .failedRepeatedlyAndWarned: logger.warn("Self-check continued failure, already warned.") @@ -500,27 +439,54 @@ public final class KeyTransparencyManager { } else { nil } - intervalTillNextCron = Self.selfCheckCronInterval + specialIntervalTillNextCron = nil } if let newSelfCheckState { - Self.setSelfCheckState(newSelfCheckState, tx: tx) + keyTransparencyStore.setSelfCheckState(newSelfCheckState, tx: tx) } - // Tell Cron we last completed in the past such that we'll try again - // after the appropriate interval. - Self.selfCheckCronStore.setMostRecentDate( - dateProvider() - .addingTimeInterval(-Self.selfCheckCronInterval) - .addingTimeInterval(intervalTillNextCron), - jitter: intervalTillNextCron / Cron.jitterFactor, + keyTransparencyStore.setSelfCheckCronJobCompletedAt( + now: dateProvider(), + specialIntervalTillNextCron: specialIntervalTillNextCron, tx: tx, ) } +} + +// MARK: - KeyTransparencyStore + +public struct KeyTransparencyStore { + + /// Keys for `kvStore`. + /// - Important + /// If you're adding a new key here, consider whether it should be wiped + /// when Key Transparency is disabled. See: `setIsEnabled`. + private enum KVStoreKeys { + /// Keys to a `Bool` representing whether or not KT is enabled. + static let isEnabled = "isEnabled" + /// Keys to a `SelfCheckState`'s raw value. + static let selfCheckState = "selfCheckState" + /// Keys to a `Bool` representing whether or not we should show + /// first-time education about KT. + static let shouldShowFirstTimeEducation = "shouldShowFirstTimeEducation" + /// Keys to an opaque LibSignalClient blob. + static let distinguishedTreeHead = "distinguishedTreeHead" + } + + private let cronStore: CronStore + private let kvStore: NewKeyValueStore + private let logger: PrefixedLogger + + public init() { + self.cronStore = CronStore(uniqueKey: .keyTransparencySelfCheck) + self.kvStore = NewKeyValueStore(collection: "KeyTransparency") + self.logger = PrefixedLogger(prefix: "[KT]") + } // MARK: - Opt-out - public static func isEnabled(tx: DBReadTransaction) -> Bool { + public func isEnabled(tx: DBReadTransaction) -> Bool { guard BuildFlags.KeyTransparency.enabled else { return false } @@ -528,7 +494,7 @@ public final class KeyTransparencyManager { return kvStore.fetchValue(Bool.self, forKey: KVStoreKeys.isEnabled, tx: tx) ?? true } - public static func setIsEnabled(_ isEnabled: Bool, tx: DBWriteTransaction) { + public func setIsEnabled(_ isEnabled: Bool, tx: DBWriteTransaction) { logger.info("\(isEnabled)") kvStore.writeValue(isEnabled, forKey: KVStoreKeys.isEnabled, tx: tx) @@ -536,7 +502,7 @@ public final class KeyTransparencyManager { if !isEnabled { kvStore.removeValue(forKey: KVStoreKeys.distinguishedTreeHead, tx: tx) kvStore.removeValue(forKey: KVStoreKeys.selfCheckState, tx: tx) - selfCheckCronStore.setMostRecentDate(.distantPast, jitter: 0, tx: tx) + cronStore.setMostRecentDate(.distantPast, jitter: 0, tx: tx) failIfThrows { try KeyTransparencyRecord.deleteAll(tx.database) } @@ -545,25 +511,109 @@ public final class KeyTransparencyManager { // MARK: - First-time education - public static func shouldShowFirstTimeEducation(tx: DBReadTransaction) -> Bool { + public func shouldShowFirstTimeEducation(tx: DBReadTransaction) -> Bool { return kvStore.fetchValue(Bool.self, forKey: KVStoreKeys.shouldShowFirstTimeEducation, tx: tx) ?? true } - public static func setHasShownFirstTimeEducation(_ value: Bool, tx: DBWriteTransaction) { + public func setHasShownFirstTimeEducation(_ value: Bool, tx: DBWriteTransaction) { kvStore.writeValue(value, forKey: KVStoreKeys.shouldShowFirstTimeEducation, tx: tx) } - // MARK: - + // MARK: - SelfCheckState - fileprivate static func getLastDistinguishedTreeHead(tx: DBReadTransaction) -> Data? { + fileprivate enum SelfCheckState: Int64 { + case succeeded = 1 + case failedOnce = 2 + case failedRepeatedly = 3 + case failedRepeatedlyAndWarned = 4 + } + + fileprivate func selfCheckState(tx: DBReadTransaction) -> SelfCheckState? { + return kvStore.fetchValue( + Int64.self, + forKey: KVStoreKeys.selfCheckState, + tx: tx, + ) + .map { SelfCheckState(rawValue: $0)! } + } + + fileprivate func setSelfCheckState(_ state: SelfCheckState, tx: DBWriteTransaction) { + kvStore.writeValue(state.rawValue, forKey: KVStoreKeys.selfCheckState, tx: tx) + } + + public func shouldWarnSelfCheckFailed(tx: DBReadTransaction) -> Bool { + switch selfCheckState(tx: tx) { + case .failedRepeatedly: + return true + case nil, .succeeded, .failedOnce, .failedRepeatedlyAndWarned: + return false + } + } + + public func setWarnedSelfCheckFailed(tx: DBWriteTransaction) { + switch selfCheckState(tx: tx) { + case .failedRepeatedly: + setSelfCheckState(.failedRepeatedlyAndWarned, tx: tx) + case nil, .succeeded, .failedOnce, .failedRepeatedlyAndWarned: + owsFailDebug("Unexpectedly setting warned, but shouldn't have warned?") + } + } + + // MARK: - Self-check and Cron + + private let selfCheckCronInterval: TimeInterval = if BuildFlags.KeyTransparency.conservativeSelfCheck { + .day + } else { + .week + } + + fileprivate func getIsTimeForSelfCheckCronJob( + now: Date, + tx: DBReadTransaction, + ) -> Bool { + let mostRecentDate = cronStore.mostRecentDate(tx: tx) + return now > mostRecentDate.addingTimeInterval(selfCheckCronInterval) + } + + /// Set that the self-check `Cron` job just completed. + /// - Parameter specialIntervalTillNextCheck + /// If non-`nil`, indicates when the next `Cron` job should run. If `nil`, + /// the next `Cron` job will run at the default interval. + fileprivate func setSelfCheckCronJobCompletedAt( + now: Date, + specialIntervalTillNextCron: TimeInterval?, + tx: DBWriteTransaction, + ) { + var mostRecentDate = now + + // Cron tracks the most-recent date, not the next date. If we want to + // run at a specific future date, set the most-recent date in the past + // such that our next check will happen at that future interval. + if let specialIntervalTillNextCron { + mostRecentDate.addTimeInterval(-selfCheckCronInterval) + mostRecentDate.addTimeInterval(specialIntervalTillNextCron) + } + + cronStore.setMostRecentDate( + mostRecentDate, + jitter: (specialIntervalTillNextCron ?? selfCheckCronInterval) / Cron.jitterFactor, + tx: tx, + ) + } + + // MARK: - LastDistinguishedTreeHead + + fileprivate func getLastDistinguishedTreeHead(tx: DBReadTransaction) -> Data? { return kvStore.fetchValue(Data.self, forKey: KVStoreKeys.distinguishedTreeHead, tx: tx) } - fileprivate static func setLastDistinguishedTreeHead(_ blob: Data, tx: DBWriteTransaction) { + fileprivate func setLastDistinguishedTreeHead(_ blob: Data, tx: DBWriteTransaction) { kvStore.writeValue(blob, forKey: KVStoreKeys.distinguishedTreeHead, tx: tx) } - public static func getKeyTransparencyBlob( + // MARK: - LibSignal blobs + + public func getKeyTransparencyBlob( aci: Aci, tx: DBReadTransaction, ) -> Data? { @@ -572,7 +622,7 @@ public final class KeyTransparencyManager { } } - public static func setKeyTransparencyBlob( + public func setKeyTransparencyBlob( _ libsignalBlob: Data, aci: Aci, tx: DBWriteTransaction, @@ -594,33 +644,34 @@ public final class KeyTransparencyManager { /// exclusively when calling LibSignal's KT APIs. private struct KeyTransparencyStoreForLibSignal: KeyTransparency.Store { let db: DB + let keyTransparencyStore: KeyTransparencyStore func getLastDistinguishedTreeHead() async -> Data? { db.read { tx in - KeyTransparencyManager.getLastDistinguishedTreeHead(tx: tx) + keyTransparencyStore.getLastDistinguishedTreeHead(tx: tx) } } func setLastDistinguishedTreeHead(to blob: Data) async { await db.awaitableWrite { tx in - KeyTransparencyManager.setLastDistinguishedTreeHead(blob, tx: tx) + keyTransparencyStore.setLastDistinguishedTreeHead(blob, tx: tx) } } func getAccountData(for aci: Aci) async -> Data? { db.read { tx in - KeyTransparencyManager.getKeyTransparencyBlob(aci: aci, tx: tx) + keyTransparencyStore.getKeyTransparencyBlob(aci: aci, tx: tx) } } func setAccountData(_ data: Data, for aci: Aci) async { await db.awaitableWrite { tx in - KeyTransparencyManager.setKeyTransparencyBlob(data, aci: aci, tx: tx) + keyTransparencyStore.setKeyTransparencyBlob(data, aci: aci, tx: tx) } } } -// MARK: - +// MARK: - KeyTransparencyRecord private struct KeyTransparencyRecord: Codable, FetchableRecord, PersistableRecord { static let databaseTableName: String = "KeyTransparency" diff --git a/SignalUI/SafetyNumbers/FingerprintViewController.swift b/SignalUI/SafetyNumbers/FingerprintViewController.swift index 39eecec8ed..1c62ec3177 100644 --- a/SignalUI/SafetyNumbers/FingerprintViewController.swift +++ b/SignalUI/SafetyNumbers/FingerprintViewController.swift @@ -28,6 +28,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon let db = DependenciesBridge.shared.db let identityManager = DependenciesBridge.shared.identityManager let keyTransparencyManager = DependenciesBridge.shared.keyTransparencyManager + let keyTransparencyStore = KeyTransparencyStore() let tsAccountManager = DependenciesBridge.shared.tsAccountManager let fingerprintResult: FingerprintResult? @@ -55,13 +56,13 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon return (nil, nil, false) } - let keyTransparencyIsEnabled = KeyTransparencyManager.isEnabled(tx: tx) + let keyTransparencyIsEnabled = keyTransparencyStore.isEnabled(tx: tx) let keyTransparencyCheckParams = keyTransparencyManager.prepareCheck( aci: theirAci, localIdentifiers: localIdentifiers, tx: tx, ) - let keyTransparencyShouldShowEducation = KeyTransparencyManager.shouldShowFirstTimeEducation(tx: tx) + let keyTransparencyShouldShowEducation = keyTransparencyStore.shouldShowFirstTimeEducation(tx: tx) return ( FingerprintResult( @@ -120,7 +121,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon if keyTransparencyShouldShowEducation { let educationSheet = KeyTransparencyFirstTimeEducationHeroSheet { db.write { tx in - KeyTransparencyManager.setHasShownFirstTimeEducation(true, tx: tx) + keyTransparencyStore.setHasShownFirstTimeEducation(true, tx: tx) } viewController.present(navigationController, animated: true)