TM-SGNL-iOS/SignalServiceKit/Account/TSAccountManager/TSAccountManagerImpl.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

675 lines
26 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import LibSignalClient
public class TSAccountManagerImpl: TSAccountManager {
private let appReadiness: AppReadiness
private let dateProvider: DateProvider
private let db: any DB
private let schedulers: Schedulers
private let kvStore: KeyValueStore
private let accountStateLock = UnfairLock()
private var cachedAccountState: AccountState?
public init(
appReadiness: AppReadiness,
dateProvider: @escaping DateProvider,
databaseChangeObserver: DatabaseChangeObserver,
db: any DB,
schedulers: Schedulers
) {
self.appReadiness = appReadiness
self.dateProvider = dateProvider
self.db = db
self.schedulers = schedulers
let kvStore = KeyValueStore(
collection: "TSStorageUserAccountCollection"
)
self.kvStore = kvStore
appReadiness.runNowOrWhenMainAppDidBecomeReadyAsync {
databaseChangeObserver.appendDatabaseChangeDelegate(self)
}
}
fileprivate static let regStateLogger = PrefixedLogger(prefix: "TSRegistrationState")
public func warmCaches() {
// Load account state into the cache and log.
db.read { tx in
reloadAccountState(logger: Self.regStateLogger, tx: tx).log(Self.regStateLogger)
}
}
// MARK: - Local Identifiers
public var localIdentifiersWithMaybeSneakyTransaction: LocalIdentifiers? {
return getOrLoadAccountStateWithMaybeTransaction().localIdentifiers
}
public func localIdentifiers(tx: DBReadTransaction) -> LocalIdentifiers? {
return getOrLoadAccountState(tx: tx).localIdentifiers
}
// MARK: - Registration State
public var registrationStateWithMaybeSneakyTransaction: TSRegistrationState {
return getOrLoadAccountStateWithMaybeTransaction().registrationState
}
public func registrationState(tx: DBReadTransaction) -> TSRegistrationState {
return getOrLoadAccountState(tx: tx).registrationState
}
public func registrationDate(tx: DBReadTransaction) -> Date? {
return getOrLoadAccountState(tx: tx).registrationDate
}
public var storedServerUsernameWithMaybeTransaction: String? {
return getOrLoadAccountStateWithMaybeTransaction().serverUsername
}
public func storedServerUsername(tx: DBReadTransaction) -> String? {
return getOrLoadAccountState(tx: tx).serverUsername
}
public var storedServerAuthTokenWithMaybeTransaction: String? {
return getOrLoadAccountStateWithMaybeTransaction().serverAuthToken
}
public func storedServerAuthToken(tx: DBReadTransaction) -> String? {
return getOrLoadAccountState(tx: tx).serverAuthToken
}
public var storedDeviceIdWithMaybeTransaction: UInt32 {
return getOrLoadAccountStateWithMaybeTransaction().deviceId
}
public func storedDeviceId(tx: DBReadTransaction) -> UInt32 {
return getOrLoadAccountState(tx: tx).deviceId
}
// MARK: - Registration IDs
private static var aciRegistrationIdKey: String = "TSStorageLocalRegistrationId"
private static var pniRegistrationIdKey: String = "TSStorageLocalPniRegistrationId"
public func getOrGenerateAciRegistrationId(tx: DBWriteTransaction) -> UInt32 {
getOrGenerateRegistrationId(
forStorageKey: Self.aciRegistrationIdKey,
nounForLogging: "ACI registration ID",
tx: tx
)
}
public func getOrGeneratePniRegistrationId(tx: DBWriteTransaction) -> UInt32 {
getOrGenerateRegistrationId(
forStorageKey: Self.pniRegistrationIdKey,
nounForLogging: "PNI registration ID",
tx: tx
)
}
public func setPniRegistrationId(_ newRegistrationId: UInt32, tx: DBWriteTransaction) {
kvStore.setUInt32(newRegistrationId, key: Self.pniRegistrationIdKey, transaction: tx)
}
private func getOrGenerateRegistrationId(
forStorageKey key: String,
nounForLogging: String,
tx: DBWriteTransaction
) -> UInt32 {
guard
let storedId = kvStore.getUInt32(key, transaction: tx),
storedId != 0
else {
let result = RegistrationIdGenerator.generate()
Logger.info("Generated a new \(nounForLogging): \(result)")
kvStore.setUInt32(result, key: key, transaction: tx)
return result
}
return storedId
}
// MARK: - Manual Message Fetch
public func isManualMessageFetchEnabled(tx: DBReadTransaction) -> Bool {
return getOrLoadAccountState(tx: tx).isManualMessageFetchEnabled
}
public func setIsManualMessageFetchEnabled(_ isEnabled: Bool, tx: DBWriteTransaction) {
mutateWithLock(tx: tx) {
kvStore.setBool(isEnabled, key: Keys.isManualMessageFetchEnabled, transaction: tx)
}
}
// MARK: - Phone Number Discoverability
public func phoneNumberDiscoverability(tx: DBReadTransaction) -> PhoneNumberDiscoverability? {
return getOrLoadAccountState(tx: tx).phoneNumberDiscoverability
}
public func lastSetIsDiscoverableByPhoneNumber(tx: DBReadTransaction) -> Date {
return getOrLoadAccountState(tx: tx).lastSetIsDiscoverableByPhoneNumberAt
}
}
extension TSAccountManagerImpl: PhoneNumberDiscoverabilitySetter {
public func setPhoneNumberDiscoverability(_ phoneNumberDiscoverability: PhoneNumberDiscoverability, tx: DBWriteTransaction) {
mutateWithLock(tx: tx) {
kvStore.setBool(
phoneNumberDiscoverability == .everybody,
key: Keys.isDiscoverableByPhoneNumber,
transaction: tx
)
kvStore.setDate(
dateProvider(),
key: Keys.lastSetIsDiscoverableByPhoneNumber,
transaction: tx
)
}
}
}
extension TSAccountManagerImpl: LocalIdentifiersSetter {
public func initializeLocalIdentifiers(
e164: E164,
aci: Aci,
pni: Pni,
deviceId: UInt32,
serverAuthToken: String,
tx: DBWriteTransaction
) {
mutateWithLock(tx: tx) {
let oldNumber = kvStore.getString(Keys.localPhoneNumber, transaction: tx)
Self.regStateLogger.info("local number \(oldNumber ?? "nil") -> \(e164)")
kvStore.setString(e164.stringValue, key: Keys.localPhoneNumber, transaction: tx)
let oldAci = Aci.parseFrom(aciString: kvStore.getString(Keys.localAci, transaction: tx))
Self.regStateLogger.info("local aci \(oldAci?.logString ?? "nil") -> \(aci)")
kvStore.setString(aci.serviceIdUppercaseString, key: Keys.localAci, transaction: tx)
let oldPni = Pni.parseFrom(pniString: kvStore.getString(Keys.localPni, transaction: tx))
Self.regStateLogger.info("local pni \(oldPni?.logString ?? "nil") -> \(pni)")
// Encoded without the "PNI:" prefix for backwards compatibility.
kvStore.setString(pni.rawUUID.uuidString, key: Keys.localPni, transaction: tx)
Self.regStateLogger.info("device id is primary? \(deviceId == OWSDevice.primaryDeviceId)")
kvStore.setUInt32(deviceId, key: Keys.deviceId, transaction: tx)
kvStore.setString(serverAuthToken, key: Keys.serverAuthToken, transaction: tx)
kvStore.setDate(dateProvider(), key: Keys.registrationDate, transaction: tx)
kvStore.removeValues(
forKeys: [
Keys.isDeregisteredOrDelinked,
Keys.reregistrationPhoneNumber,
Keys.reregistrationAci
],
transaction: tx
)
}
}
public func changeLocalNumber(
newE164: E164,
aci: Aci,
pni: Pni,
tx: DBWriteTransaction
) {
mutateWithLock(tx: tx) {
let oldNumber = kvStore.getString(Keys.localPhoneNumber, transaction: tx)
Self.regStateLogger.info("local number \(oldNumber ?? "nil") -> \(newE164.stringValue)")
kvStore.setString(newE164.stringValue, key: Keys.localPhoneNumber, transaction: tx)
let oldAci = kvStore.getString(Keys.localAci, transaction: tx)
Self.regStateLogger.info("local aci \(oldAci ?? "nil") -> \(aci)")
kvStore.setString(aci.serviceIdUppercaseString, key: Keys.localAci, transaction: tx)
let oldPni = kvStore.getString(Keys.localPni, transaction: tx)
Self.regStateLogger.info("local pni \(oldPni ?? "nil") -> \(pni)")
// Encoded without the "PNI:" prefix for backwards compatibility.
kvStore.setString(pni.rawUUID.uuidString, key: Keys.localPni, transaction: tx)
}
}
public func setIsDeregisteredOrDelinked(_ isDeregisteredOrDelinked: Bool, tx: DBWriteTransaction) -> Bool {
return mutateWithLock(tx: tx) {
let oldValue = kvStore.getBool(Keys.isDeregisteredOrDelinked, defaultValue: false, transaction: tx)
guard oldValue != isDeregisteredOrDelinked else {
return false
}
if isDeregisteredOrDelinked {
Self.regStateLogger.warn("Deregistered!")
} else {
Self.regStateLogger.info("Resetting isDeregistered/Delinked")
}
kvStore.setBool(isDeregisteredOrDelinked, key: Keys.isDeregisteredOrDelinked, transaction: tx)
return true
}
}
public func resetForReregistration(
localNumber: E164,
localAci: Aci,
wasPrimaryDevice: Bool,
tx: DBWriteTransaction
) {
mutateWithLock(tx: tx) {
Self.regStateLogger.info("Resetting for reregistration, was primary? \(wasPrimaryDevice)")
kvStore.removeAll(transaction: tx)
kvStore.setString(localNumber.stringValue, key: Keys.reregistrationPhoneNumber, transaction: tx)
kvStore.setString(localAci.serviceIdUppercaseString, key: Keys.reregistrationAci, transaction: tx)
kvStore.setBool(wasPrimaryDevice, key: Keys.reregistrationWasPrimaryDevice, transaction: tx)
}
}
public func setIsTransferInProgress(_ isTransferInProgress: Bool, tx: DBWriteTransaction) -> Bool {
let oldValue = kvStore.getBool(Keys.isTransferInProgress, transaction: tx)
guard oldValue != isTransferInProgress else {
return false
}
if isTransferInProgress {
Self.regStateLogger.warn("Transfer in progress!")
} else {
Self.regStateLogger.info("Resetting isTransferInProgress")
}
mutateWithLock(tx: tx) {
kvStore.setBool(isTransferInProgress, key: Keys.isTransferInProgress, transaction: tx)
}
return true
}
public func setWasTransferred(_ wasTransferred: Bool, tx: DBWriteTransaction) -> Bool {
let oldValue = kvStore.getBool(Keys.wasTransferred, transaction: tx)
guard oldValue != wasTransferred else {
return false
}
if wasTransferred {
Self.regStateLogger.warn("Marking wasTransferred!")
} else {
Self.regStateLogger.info("Resetting wasTransferred")
}
mutateWithLock(tx: tx) {
kvStore.setBool(wasTransferred, key: Keys.wasTransferred, transaction: tx)
}
return true
}
public func cleanUpTransferStateOnAppLaunchIfNeeded() {
guard getOrLoadAccountStateWithMaybeTransaction().isTransferInProgress else {
// No need for cleanup if transfer wasn't already in progress.
return
}
db.write { tx in
mutateWithLock(tx: tx) {
guard kvStore.getBool(Keys.isTransferInProgress, defaultValue: false, transaction: tx) else {
return
}
Self.regStateLogger.info("Transfer was in progress but app relaunched; resetting")
kvStore.setBool(false, key: Keys.isTransferInProgress, transaction: tx)
}
}
}
}
extension TSAccountManagerImpl: DatabaseChangeDelegate {
public func databaseChangesDidUpdate(databaseChanges: any DatabaseChanges) {}
public func databaseChangesDidUpdateExternally() {
self.db.read { tx in
_ = reloadAccountState(logger: nil, tx: tx)
}
}
public func databaseChangesDidReset() {}
}
extension TSAccountManagerImpl {
private typealias Keys = AccountState.Keys
// MARK: - External methods (acquire the lock)
private func getOrLoadAccountStateWithMaybeTransaction() -> AccountState {
return accountStateLock.withLock {
if let accountState = self.cachedAccountState {
return accountState
}
return db.read { tx in
return self.loadAccountState(tx: tx)
}
}
}
private func getOrLoadAccountState(tx: DBReadTransaction) -> AccountState {
return accountStateLock.withLock {
if let accountState = self.cachedAccountState {
return accountState
}
return loadAccountState(tx: tx)
}
}
@discardableResult
private func reloadAccountState(
logger: PrefixedLogger?,
tx: DBReadTransaction
) -> AccountState {
return accountStateLock.withLock {
return loadAccountState(
logger: logger,
tx: tx
)
}
}
// MARK: Mutations
private func mutateWithLock<T>(tx: DBWriteTransaction, _ block: () -> T) -> T {
return accountStateLock.withLock {
let returnValue = block()
// Reload to repopulate the cache; the mutations will
// write to disk and not actually modify the cached value.
loadAccountState(tx: tx)
return returnValue
}
}
// MARK: - Internal methods (must have lock)
/// Must be called within the lock
@discardableResult
private func loadAccountState(
logger: PrefixedLogger? = nil,
tx: DBReadTransaction
) -> AccountState {
let accountState = AccountState.init(
kvStore: kvStore,
logger: logger,
tx: tx
)
self.cachedAccountState = accountState
return accountState
}
/// A cache of frequently-accessed database state.
///
/// * Instances of AccountState are immutable.
/// * None of this state should change often.
/// * Whenever any of this state changes, we reload all of it.
///
/// This cache changes all of its properties in lockstep, which
/// helps ensure consistency.
private struct AccountState {
let localIdentifiers: LocalIdentifiers?
let deviceId: UInt32
let serverAuthToken: String?
let registrationState: TSRegistrationState
let registrationDate: Date?
fileprivate let isTransferInProgress: Bool
let phoneNumberDiscoverability: PhoneNumberDiscoverability?
let lastSetIsDiscoverableByPhoneNumberAt: Date
let isManualMessageFetchEnabled: Bool
var serverUsername: String? {
guard let aciString = self.localIdentifiers?.aci.serviceIdString else {
return nil
}
return registrationState.isRegisteredPrimaryDevice ? aciString : "\(aciString).\(deviceId)"
}
/// Logger is optional so we don't log every time we load state (which is a lot),
/// and can just do so once per app launch.
init(
kvStore: KeyValueStore,
logger: PrefixedLogger?,
tx: DBReadTransaction
) {
// WARNING: AccountState is loaded before data migrations have run (as well as after).
// Do not use data migrations to update AccountState data; do it through schema migrations
// or through normal write transactions. TSAccountManager should be the only code accessing this state anyway.
let localIdentifiers = Self.loadLocalIdentifiers(
kvStore: kvStore,
logger: logger,
tx: tx
)
self.localIdentifiers = localIdentifiers
let persistedDeviceId = kvStore.getUInt32(
Keys.deviceId,
transaction: tx
)
// Assume primary, for backwards compatibility.
self.deviceId = persistedDeviceId ?? OWSDevice.primaryDeviceId
self.serverAuthToken = kvStore.getString(Keys.serverAuthToken, transaction: tx)
logger?.info("Has server auth token: \(self.serverAuthToken != nil)")
let isPrimaryDevice: Bool?
if let persistedDeviceId {
isPrimaryDevice = persistedDeviceId == OWSDevice.primaryDeviceId
logger?.info("Device id loaded, is primary: \(isPrimaryDevice!)")
} else {
isPrimaryDevice = nil
logger?.info("Using default primary device id")
}
let isTransferInProgress = kvStore.getBool(Keys.isTransferInProgress, defaultValue: false, transaction: tx)
self.isTransferInProgress = isTransferInProgress
self.registrationState = Self.loadRegistrationState(
localIdentifiers: localIdentifiers,
isPrimaryDevice: isPrimaryDevice,
isTransferInProgress: isTransferInProgress,
kvStore: kvStore,
logger: logger,
tx: tx
)
self.registrationDate = kvStore.getDate(Keys.registrationDate, transaction: tx)
self.phoneNumberDiscoverability = kvStore.getBool(Keys.isDiscoverableByPhoneNumber, transaction: tx).map {
return $0 ? .everybody : .nobody
}
self.lastSetIsDiscoverableByPhoneNumberAt = kvStore.getDate(
Keys.lastSetIsDiscoverableByPhoneNumber,
transaction: tx
) ?? .distantPast
self.isManualMessageFetchEnabled = kvStore.getBool(
Keys.isManualMessageFetchEnabled,
defaultValue: false,
transaction: tx
)
}
private static func loadLocalIdentifiers(
kvStore: KeyValueStore,
logger: PrefixedLogger?,
tx: DBReadTransaction
) -> LocalIdentifiers? {
guard
let localNumber = kvStore.getString(Keys.localPhoneNumber, transaction: tx)
else {
logger?.info("No local phone number!")
return nil
}
guard let localAci = Aci.parseFrom(aciString: kvStore.getString(Keys.localAci, transaction: tx)) else {
logger?.info("No local aci!")
return nil
}
let localPni = Pni.parseFrom(pniString: kvStore.getString(Keys.localPni, transaction: tx))
logger?.info("Has local pni? \(localPni != nil)")
return LocalIdentifiers(aci: localAci, pni: localPni, phoneNumber: localNumber)
}
private static func loadRegistrationState(
localIdentifiers: LocalIdentifiers?,
isPrimaryDevice: Bool?,
isTransferInProgress: Bool,
kvStore: KeyValueStore,
logger: PrefixedLogger?,
tx: DBReadTransaction
) -> TSRegistrationState {
let reregistrationPhoneNumber = kvStore.getString(
Keys.reregistrationPhoneNumber,
transaction: tx
)
// TODO: Eventually require reregistrationAci during re-registration.
let reregistrationAci = Aci.parseFrom(aciString: kvStore.getString(
Keys.reregistrationAci,
transaction: tx
))
let isDeregisteredOrDelinked = kvStore.getBool(
Keys.isDeregisteredOrDelinked,
transaction: tx
)
let wasTransferred = kvStore.getBool(
Keys.wasTransferred,
defaultValue: false,
transaction: tx
)
// Go in semi-reverse order; with higher priority stuff going first.
if wasTransferred {
logger?.info("WasTransferred=true; marking as transferred")
// If we transferred, we are transferred regardless of what else
// may be going on. Other state might be a mess; doesn't matter.
return .transferred
} else if isTransferInProgress {
// Ditto for a transfer in progress; regardless of whatever
// else is going on (except being transferred) this takes precedence.
switch isPrimaryDevice {
case true:
logger?.info("isTransferInProgress=true on primary")
return .transferringPrimaryOutgoing
case false:
logger?.info("isTransferInProgress=true on secondary")
return .transferringLinkedOutgoing
default:
logger?.info("isTransferInProgress=true on unknown primary state; transfer incoming")
// If we never knew primary device state, it must be an
// incoming transfer, where we started from a blank state.
return .transferringIncoming
}
} else if let reregistrationPhoneNumber {
// If a "reregistrationPhoneNumber" is present, we are reregistering.
// reregistrationAci is optional (for now, see above TODO).
// isDeregistered is probably also true; this takes precedence.
let shouldDefaultToPrimaryDevice = UIDevice.current.userInterfaceIdiom == .phone
if kvStore.getBool(
Keys.reregistrationWasPrimaryDevice,
defaultValue: shouldDefaultToPrimaryDevice,
transaction: tx
) {
logger?.info("rereg phone number set, and wasPrimaryDevice true; reregistering")
return .reregistering(
phoneNumber: reregistrationPhoneNumber,
aci: reregistrationAci
)
} else {
logger?.info("rereg phone number set, and wasPrimaryDevice false; relinking")
return .relinking(
phoneNumber: reregistrationPhoneNumber,
aci: reregistrationAci
)
}
} else if isDeregisteredOrDelinked == true {
// if isDeregistered is true, we may have been registered
// or not. But its being true means we should be deregistered
// (or delinked, based on whether this is a primary).
// isPrimaryDevice should have some value; if we've explicitly
// set isDeregistered that means we _were_ registered before.
switch isPrimaryDevice {
case true:
logger?.info("Deregistered")
return .deregistered
case false:
logger?.info("Delinked")
return .delinked
default:
let logString = "Deregistered, but primary device state unknown!"
if let logger {
logger.warn(logString)
} else {
owsAssertDebug(false, logString)
}
return .delinked
}
} else if localIdentifiers == nil {
// Setting localIdentifiers is what marks us as registered
// in primary registration. (As long as above conditions don't
// override that state)
// For provisioning, we set them before finishing, but the fact
// that we set them means we linked (but didn't finish yet).
logger?.info("Not deregistered but local identifiers unavailable; not registered yet")
return .unregistered
} else {
// We have local identifiers, so we are registered/provisioned.
switch isPrimaryDevice {
case true:
logger?.info("Registered")
return .registered
case false:
logger?.info("Provisioned")
return .provisioned
default:
logger?.warn("Registered but primary state unknown; marking provisioned")
return .provisioned
}
}
}
func log(_ logger: PrefixedLogger) {
logger.info("RegistrationState: \(registrationState.logString)")
}
fileprivate enum Keys {
static let deviceId = "TSAccountManager_DeviceId"
static let serverAuthToken = "TSStorageServerAuthToken"
static let localPhoneNumber = "TSStorageRegisteredNumberKey"
static let localAci = "TSStorageRegisteredUUIDKey"
static let localPni = "TSAccountManager_RegisteredPNIKey"
static let registrationDate = "TSAccountManager_RegistrationDateKey"
static let isDeregisteredOrDelinked = "TSAccountManager_IsDeregisteredKey"
static let reregistrationPhoneNumber = "TSAccountManager_ReregisteringPhoneNumberKey"
static let reregistrationAci = "TSAccountManager_ReregisteringUUIDKey"
static let reregistrationWasPrimaryDevice = "TSAccountManager_ReregisteringWasPrimaryDeviceKey"
static let isTransferInProgress = "TSAccountManager_IsTransferInProgressKey"
static let wasTransferred = "TSAccountManager_WasTransferredKey"
static let isDiscoverableByPhoneNumber = "TSAccountManager_IsDiscoverableByPhoneNumber"
static let lastSetIsDiscoverableByPhoneNumber = "TSAccountManager_LastSetIsDiscoverableByPhoneNumberKey"
static let isManualMessageFetchEnabled = "TSAccountManager_ManualMessageFetchKey"
}
}
}