Signal-iOS/SignalServiceKit/Util/OWS2FAManager.swift
Max Radermacher e7a0d760ca
Simplify SVR2
2026-05-22 14:12:57 -05:00

352 lines
12 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public let kMin2FAv2PinLength: UInt = 4
private let kOWS2FAManager_IsRegistrationLockV2Enabled = "isRegistrationLockV2Enabled"
private let kOWS2FAManager_AreRemindersEnabled = "kOWS2FAManager_AreRemindersEnabled"
private let kOWS2FAManager_LastSuccessfulReminderDateKey = "kOWS2FAManager_LastSuccessfulReminderDateKey"
private let kOWS2FAManager_PinCode = "kOWS2FAManager_PinCode"
private let kOWS2FAManager_RepetitionInterval = "kOWS2FAManager_RepetitionInterval"
public class OWS2FAManager {
private var accountAttributesUpdater: AccountAttributesUpdater { DependenciesBridge.shared.accountAttributesUpdater }
private var accountKeyStore: AccountKeyStore { DependenciesBridge.shared.accountKeyStore }
private var db: DB { DependenciesBridge.shared.db }
private var keyValueStore: KeyValueStore { KeyValueStore(collection: "kOWS2FAManager_Collection") }
private var networkManager: NetworkManagerProtocol { SSKEnvironment.shared.networkManagerRef }
private var svr: SecureValueRecovery { DependenciesBridge.shared.svr }
private var tsAccountManager: TSAccountManager { DependenciesBridge.shared.tsAccountManager }
init() {
// Does not take dependencies on init, because circular dependencies
// abound in this class and I did not have the motivation to break them.
SwiftSingletons.register(self)
}
// MARK: -
public var isRegistrationLockV2Enabled: Bool {
return db.read { isRegistrationLockV2Enabled(transaction: $0) }
}
public func isRegistrationLockV2Enabled(transaction: DBReadTransaction) -> Bool {
return keyValueStore.getBool(
kOWS2FAManager_IsRegistrationLockV2Enabled,
defaultValue: false,
transaction: transaction,
)
}
// MARK: -
public var isPinEnabledWithSneakyTransaction: Bool {
return db.read { isPinEnabled(tx: $0) }
}
public func isPinEnabled(tx: DBReadTransaction) -> Bool {
return pinCode(transaction: tx) != nil
}
public func pinCode(transaction: DBReadTransaction) -> String? {
return keyValueStore.getString(kOWS2FAManager_PinCode, transaction: transaction)
}
public enum PinType {
case numeric
case alphanumeric
public static func forPin(_ pin: String) -> Self {
let normalizedPin = SVRUtil.normalizePin(pin)
return normalizedPin.digitsOnly() == normalizedPin ? .numeric : .alphanumeric
}
}
// MARK: -
static var allRepetitionIntervals: [TimeInterval] = [1 * .day, 3 * .day, 7 * .day, 14 * .day, 28 * .day]
var defaultRepetitionInterval: TimeInterval {
return Self.allRepetitionIntervals.first!
}
public func setDefaultRepetitionInterval(transaction: DBWriteTransaction) {
keyValueStore.removeValue(forKey: kOWS2FAManager_RepetitionInterval, transaction: transaction)
}
public func setDefaultRepetitionIntervalForBackupRestore(transaction: DBWriteTransaction) {
keyValueStore.setDouble(7 * .day, key: kOWS2FAManager_RepetitionInterval, transaction: transaction)
// Reset the interval as part of the restore
setLastCompletedReminderDate(Date(), transaction: transaction)
}
public var repetitionInterval: TimeInterval {
return db.read { repetitionInterval(transaction: $0) }
}
func repetitionInterval(transaction: DBReadTransaction) -> TimeInterval {
return keyValueStore.getDouble(kOWS2FAManager_RepetitionInterval, defaultValue: defaultRepetitionInterval, transaction: transaction)
}
// MARK: -
public var areRemindersEnabled: Bool {
return db.read { areRemindersEnabled(transaction: $0) }
}
public func areRemindersEnabled(transaction: DBReadTransaction) -> Bool {
return keyValueStore.getBool(kOWS2FAManager_AreRemindersEnabled, defaultValue: true, transaction: transaction)
}
public func setAreRemindersEnabled(_ areRemindersEnabled: Bool, transaction: DBWriteTransaction) {
keyValueStore.setBool(areRemindersEnabled, key: kOWS2FAManager_AreRemindersEnabled, transaction: transaction)
}
// MARK: -
public func lastCompletedReminderDate(transaction: DBReadTransaction) -> Date? {
return keyValueStore.getDate(kOWS2FAManager_LastSuccessfulReminderDateKey, transaction: transaction)
}
public func setLastCompletedReminderDate(_ date: Date, transaction: DBWriteTransaction) {
keyValueStore.setDate(date, key: kOWS2FAManager_LastSuccessfulReminderDateKey, transaction: transaction)
}
public func nextReminderDate(transaction: DBReadTransaction) -> Date {
let lastCompletedReminderDate = lastCompletedReminderDate(transaction: transaction) ?? .distantPast
let repetitionInterval = repetitionInterval(transaction: transaction)
return lastCompletedReminderDate.addingTimeInterval(repetitionInterval)
}
public func isDueForV2Reminder(transaction: DBReadTransaction) -> Bool {
guard
tsAccountManager.registrationState(tx: transaction).isRegistered,
isPinEnabled(tx: transaction),
areRemindersEnabled(transaction: transaction)
else {
return false
}
return nextReminderDate(transaction: transaction) < Date()
}
public func reminderCompleted(incorrectAttempts: Bool) {
db.write { transaction in
setLastCompletedReminderDate(Date(), transaction: transaction)
let oldInterval = repetitionInterval(transaction: transaction)
let newInterval = adjustRepetitionInterval(oldInterval: oldInterval, incorrectAttempts: incorrectAttempts)
Logger.info("Updating repetition interval: \(oldInterval) -> \(newInterval). Had incorrect attempts: \(incorrectAttempts)")
keyValueStore.setDouble(newInterval, key: kOWS2FAManager_RepetitionInterval, transaction: transaction)
}
}
private func adjustRepetitionInterval(oldInterval: TimeInterval, incorrectAttempts: Bool) -> TimeInterval {
let allIntervals = Self.allRepetitionIntervals
guard let oldIndex = allIntervals.firstIndex(where: { oldInterval <= $0 }) else {
return allIntervals.first!
}
let newIndex: Int
if incorrectAttempts {
newIndex = oldIndex <= 0 ? 0 : oldIndex - 1
} else {
newIndex = oldIndex < allIntervals.count - 1 ? oldIndex + 1 : oldIndex
}
return allIntervals[newIndex]
}
// MARK: -
public func verifyPin(_ pin: String, tx: DBReadTransaction) -> Bool {
let pinToMatch = pinCode(transaction: tx)
owsAssertDebug(pinToMatch != nil, "can't verify pin when 2fa is disabled")
return SVRUtil.normalizePin(pin) == pinToMatch
}
// MARK: -
public func markDisabled(transaction tx: DBWriteTransaction) {
keyValueStore.removeValue(forKey: kOWS2FAManager_PinCode, transaction: tx)
keyValueStore.removeValue(forKey: kOWS2FAManager_IsRegistrationLockV2Enabled, transaction: tx)
tx.addSyncCompletion {
self.triggerAccountAttributesUpdate()
}
}
public func clearLocalPinCode(transaction: DBWriteTransaction) {
keyValueStore.removeValue(forKey: kOWS2FAManager_PinCode, transaction: transaction)
}
/// Marks the given PIN as enabled locally.
/// - SeeAlso ``enablePin(_:)``
public func markEnabled(
pin: String,
resetReminderInterval: Bool,
transaction: DBWriteTransaction,
) {
owsPrecondition(!pin.isEmpty)
setNormalizedPin(pin, tx: transaction)
if resetReminderInterval {
// Reset the reminder repetition interval for the new pin.
setDefaultRepetitionInterval(transaction: transaction)
// Schedule next reminder relative to now
setLastCompletedReminderDate(Date(), transaction: transaction)
}
transaction.addSyncCompletion {
self.triggerAccountAttributesUpdate()
}
}
public func restorePinFromBackup(_ pin: String, transaction: DBWriteTransaction) {
setNormalizedPin(pin, tx: transaction)
}
private func setNormalizedPin(_ pin: String, tx: DBWriteTransaction) {
owsPrecondition(!pin.isEmpty)
keyValueStore.setString(pin, key: kOWS2FAManager_PinCode, transaction: tx)
}
// MARK: -
/// "Enables" the PIN by using it to back up the master key, then setting
/// local state as appropriate.
///
/// - Important
/// This does not enable reglock. See ``enableRegistrationLockV2()``.
public func enablePin(_ pin: String) async throws {
owsAssertDebug(!pin.isEmpty)
// Enabling V2 2FA doesn't inherently enable registration lock,
// it's managed by a separate setting.
guard
let masterKey = db.read(block: {
accountKeyStore.getMasterKey(tx: $0)
})
else {
throw OWSAssertionError("Missing master key")
}
try await svr.backupMasterKey(
pin: pin,
masterKey: masterKey,
force: false,
authMethod: .implicit,
)
await db.awaitableWrite { tx in
markEnabled(pin: pin, resetReminderInterval: true, transaction: tx)
}
}
// MARK: -
public func enableRegistrationLockV2(logger: PrefixedLogger) async throws {
let token = db.read { tx in
let masterKey = accountKeyStore.getMasterKey(tx: tx)
return masterKey?.data(
for: .registrationLock,
).canonicalStringRepresentation
}
guard let token else {
throw OWSAssertionError("Cannot enable registration lock without an existing PIN")
}
let request = OWSRequestFactory.enableRegistrationLockV2Request(token: token, logger: logger)
_ = try await networkManager.asyncRequest(request)
await db.awaitableWrite { transaction in
keyValueStore.setBool(
true,
key: kOWS2FAManager_IsRegistrationLockV2Enabled,
transaction: transaction,
)
}
triggerAccountAttributesUpdate()
}
public func markRegistrationLockV2Enabled(transaction: DBWriteTransaction) {
guard !tsAccountManager.registrationState(tx: transaction).isRegistered else {
return owsFailDebug("Unexpectedly attempted to mark reglock as enabled after registration")
}
keyValueStore.setBool(
true,
key: kOWS2FAManager_IsRegistrationLockV2Enabled,
transaction: transaction,
)
}
public func disableRegistrationLockV2() async throws {
let request = OWSRequestFactory.disableRegistrationLockV2Request()
_ = try await networkManager.asyncRequest(request)
await db.awaitableWrite { transaction in
keyValueStore.removeValue(
forKey: kOWS2FAManager_IsRegistrationLockV2Enabled,
transaction: transaction,
)
}
triggerAccountAttributesUpdate()
}
// MARK: -
private func triggerAccountAttributesUpdate() {
Task {
do {
try await accountAttributesUpdater.updateAccountAttributes(authedAccount: .implicit())
} catch {
Logger.warn("\(error)")
}
}
}
// MARK: -
public static func isWeakPin(_ pin: String) -> Bool {
let normalizedPin = SVRUtil.normalizePin(pin)
guard pin.count >= kMin2FAv2PinLength else { return true }
// We only check numeric pins for weakness
guard normalizedPin.digitsOnly() == normalizedPin else { return false }
var allTheSame = true
var forwardSequential = true
var reverseSequential = true
var previousWholeNumberValue: Int?
for character in normalizedPin {
guard let current = character.wholeNumberValue else {
owsFailDebug("numeric pin unexpectedly contatined non-numeric characters")
break
}
defer { previousWholeNumberValue = current }
guard let previous = previousWholeNumberValue else { continue }
if previous != current { allTheSame = false }
if previous + 1 != current { forwardSequential = false }
if previous - 1 != current { reverseSequential = false }
if !allTheSame, !forwardSequential, !reverseSequential { break }
}
return allTheSame || forwardSequential || reverseSequential
}
}