diff --git a/Signal/Images.xcassets/safety-numbers/Contents.json b/Signal/Images.xcassets/safety-numbers/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Signal/Images.xcassets/safety-numbers/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/safety-number-change.imageset/Contents.json b/Signal/Images.xcassets/safety-numbers/safety-number-change.imageset/Contents.json similarity index 100% rename from Signal/Images.xcassets/safety-number-change.imageset/Contents.json rename to Signal/Images.xcassets/safety-numbers/safety-number-change.imageset/Contents.json diff --git a/Signal/Images.xcassets/safety-number-change.imageset/safety-number-change.pdf b/Signal/Images.xcassets/safety-numbers/safety-number-change.imageset/safety-number-change.pdf similarity index 100% rename from Signal/Images.xcassets/safety-number-change.imageset/safety-number-change.pdf rename to Signal/Images.xcassets/safety-numbers/safety-number-change.imageset/safety-number-change.pdf diff --git a/Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/Contents.json b/Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/Contents.json new file mode 100644 index 0000000000..0dd833d47e --- /dev/null +++ b/Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "safety-number-verification.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/safety-number-verification.pdf b/Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/safety-number-verification.pdf new file mode 100644 index 0000000000..ac65e9d349 Binary files /dev/null and b/Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/safety-number-verification.pdf differ diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIPrompts.swift b/Signal/src/ViewControllers/DebugUI/DebugUIPrompts.swift index f38103696c..c120da9772 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIPrompts.swift +++ b/Signal/src/ViewControllers/DebugUI/DebugUIPrompts.swift @@ -19,6 +19,11 @@ class DebugUIPrompts: DebugUIPage { var items = [OWSTableItem]() items += [ + OWSTableItem(title: "Reenable KT first-time education", actionBlock: { + db.write { tx in + KeyTransparencyManager.setHasShownFirstTimeEducation(false, tx: tx) + } + }), OWSTableItem(title: "Reenable disabled inactive linked device reminder megaphones", actionBlock: { db.write { tx in diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index e37c52ee64..c05c2695bd 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -7609,6 +7609,12 @@ /* Title for a button while automatic key verification is ongoing. */ "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_BUTTON_VERIFYING" = "Verifying Encryption…"; +/* Body for a sheet introducing Key Transparency. */ +"SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_EDUCATION_SHEET_BODY" = "For contacts you’re connected to by phone number, Signal can automatically confirm whether the connection is secure using a process called key transparency. For added security, you can still verify connections manually using a QR code or number."; + +/* Title for a sheet introducing Key Transparency. */ +"SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_EDUCATION_SHEET_TITLE" = "Signal can now automatically verify encryption"; + /* Body for a sheet explaining that encryption auto-verification did not succeed. Embeds {{ 1: the contact's name }}. */ "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_FAILURE_SHEET_BODY_FORMAT" = "Signal can no longer automatically verify the encryption for this chat. This is likely because %1$@ changed their phone number. Verify end-to-end encryption manually by comparing the numbers on the previous screen or scanning the code on their device."; diff --git a/SignalServiceKit/KeyTransparency/KeyTransparencyManager.swift b/SignalServiceKit/KeyTransparency/KeyTransparencyManager.swift index 2330da6aa2..d487f52639 100644 --- a/SignalServiceKit/KeyTransparency/KeyTransparencyManager.swift +++ b/SignalServiceKit/KeyTransparency/KeyTransparencyManager.swift @@ -10,6 +10,25 @@ 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 @@ -253,20 +272,17 @@ public final class KeyTransparencyManager { case failedRepeatedlyAndWarned = 4 } - private static let selfCheckKVStore = NewKeyValueStore(collection: "KT.SelfCheck") - private static let selfCheckKVStoreKey = "state" - private static func selfCheckState(tx: DBReadTransaction) -> SelfCheckState? { - return selfCheckKVStore.fetchValue( + return kvStore.fetchValue( Int64.self, - forKey: selfCheckKVStoreKey, + forKey: KVStoreKeys.selfCheckState, tx: tx, ) .map { SelfCheckState(rawValue: $0)! } } private static func setSelfCheckState(_ state: SelfCheckState, tx: DBWriteTransaction) { - selfCheckKVStore.writeValue(state.rawValue, forKey: selfCheckKVStoreKey, tx: tx) + kvStore.writeValue(state.rawValue, forKey: KVStoreKeys.selfCheckState, tx: tx) } public static func shouldWarnSelfCheckFailed(tx: DBReadTransaction) -> Bool { @@ -504,43 +520,47 @@ public final class KeyTransparencyManager { // MARK: - Opt-out - private static let isEnabledKVStore = NewKeyValueStore(collection: "KT.IsEnabled") - private static let isEnabledKVStoreKey = "isEnabled" - public static func isEnabled(tx: DBReadTransaction) -> Bool { guard BuildFlags.KeyTransparency.enabled else { return false } - return isEnabledKVStore.fetchValue(Bool.self, forKey: isEnabledKVStoreKey, tx: tx) ?? true + return kvStore.fetchValue(Bool.self, forKey: KVStoreKeys.isEnabled, tx: tx) ?? true } public static func setIsEnabled(_ isEnabled: Bool, tx: DBWriteTransaction) { logger.info("\(isEnabled)") - isEnabledKVStore.writeValue(isEnabled, forKey: isEnabledKVStoreKey, tx: tx) + kvStore.writeValue(isEnabled, forKey: KVStoreKeys.isEnabled, tx: tx) if !isEnabled { - selfCheckKVStore.removeAll(tx: tx) + kvStore.removeValue(forKey: KVStoreKeys.distinguishedTreeHead, tx: tx) + kvStore.removeValue(forKey: KVStoreKeys.selfCheckState, tx: tx) selfCheckCronStore.setMostRecentDate(.distantPast, jitter: 0, tx: tx) - distinguishedTreeKVStore.removeAll(tx: tx) failIfThrows { try KeyTransparencyRecord.deleteAll(tx.database) } } } + // MARK: - First-time education + + public static func shouldShowFirstTimeEducation(tx: DBReadTransaction) -> Bool { + return kvStore.fetchValue(Bool.self, forKey: KVStoreKeys.shouldShowFirstTimeEducation, tx: tx) ?? true + } + + public static func setHasShownFirstTimeEducation(_ value: Bool, tx: DBWriteTransaction) { + kvStore.writeValue(value, forKey: KVStoreKeys.shouldShowFirstTimeEducation, tx: tx) + } + // MARK: - - private static let distinguishedTreeKVStore = NewKeyValueStore(collection: "KT.DistinguishedTree") - private static let distinguishedTreeKVStoreKey = "head" - fileprivate static func getLastDistinguishedTreeHead(tx: DBReadTransaction) -> Data? { - return distinguishedTreeKVStore.fetchValue(Data.self, forKey: distinguishedTreeKVStoreKey, tx: tx) + return kvStore.fetchValue(Data.self, forKey: KVStoreKeys.distinguishedTreeHead, tx: tx) } fileprivate static func setLastDistinguishedTreeHead(_ blob: Data, tx: DBWriteTransaction) { - distinguishedTreeKVStore.writeValue(blob, forKey: distinguishedTreeKVStoreKey, tx: tx) + kvStore.writeValue(blob, forKey: KVStoreKeys.distinguishedTreeHead, tx: tx) } fileprivate static func getKeyTransparencyRecord( diff --git a/SignalUI/SafetyNumbers/FingerprintViewController.swift b/SignalUI/SafetyNumbers/FingerprintViewController.swift index 6a2bc7107c..39eecec8ed 100644 --- a/SignalUI/SafetyNumbers/FingerprintViewController.swift +++ b/SignalUI/SafetyNumbers/FingerprintViewController.swift @@ -32,12 +32,14 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon let fingerprintResult: FingerprintResult? let keyTransparencyState: KeyTransparencyState? + let keyTransparencyShouldShowEducation: Bool ( fingerprintResult, keyTransparencyState, + keyTransparencyShouldShowEducation, ) = db.read { tx in guard let theirAci else { - return (nil, nil) + return (nil, nil, false) } let theirAddress = SignalServiceAddress(theirAci) @@ -50,7 +52,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx), let myAciIdentityKey = identityManager.identityKeyPair(for: .aci, tx: tx)?.keyPair.identityKey else { - return (nil, nil) + return (nil, nil, false) } let keyTransparencyIsEnabled = KeyTransparencyManager.isEnabled(tx: tx) @@ -59,6 +61,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon localIdentifiers: localIdentifiers, tx: tx, ) + let keyTransparencyShouldShowEducation = KeyTransparencyManager.shouldShowFirstTimeEducation(tx: tx) return ( FingerprintResult( @@ -78,6 +81,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon checkParams: keyTransparencyCheckParams, viewInitialState: keyTransparencyCheckParams == nil ? .unableToVerify : .readyToVerify, ), + keyTransparencyShouldShowEducation, ) } @@ -112,7 +116,19 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon ), ) let navigationController = OWSNavigationController(rootViewController: fingerprintViewController) - viewController.present(navigationController, animated: true) + + if keyTransparencyShouldShowEducation { + let educationSheet = KeyTransparencyFirstTimeEducationHeroSheet { + db.write { tx in + KeyTransparencyManager.setHasShownFirstTimeEducation(true, tx: tx) + } + + viewController.present(navigationController, animated: true) + } + viewController.present(educationSheet, animated: true) + } else { + viewController.present(navigationController, animated: true) + } } // MARK: - @@ -846,6 +862,32 @@ private final class KeyTransparencyFailureHeroSheet: HeroSheetViewController { // MARK: - +private final class KeyTransparencyFirstTimeEducationHeroSheet: HeroSheetViewController { + init(onContinue: @MainActor @escaping () -> Void) { + super.init( + hero: .image(UIImage(named: "safety-number-verification")!), + title: OWSLocalizedString( + "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_EDUCATION_SHEET_TITLE", + comment: "Title for a sheet introducing Key Transparency.", + ), + body: OWSLocalizedString( + "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_EDUCATION_SHEET_BODY", + comment: "Body for a sheet introducing Key Transparency.", + ), + primaryButton: HeroSheetViewController.Button( + title: CommonStrings.continueButton, + action: { sheet in + sheet.dismiss(animated: true) { + onContinue() + } + }, + ), + ) + } +} + +// MARK: - + #if DEBUG private extension IdentityKey {