Add first-time education sheet for Key Transparency

This commit is contained in:
Sasha Weiss 2026-02-04 14:13:37 -08:00 committed by GitHub
parent 3cfa87b143
commit 2419e4d753
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 112 additions and 21 deletions

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "safety-number-verification.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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

View File

@ -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 youre 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.";

View File

@ -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(

View File

@ -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 {