180 lines
5.9 KiB
Swift
180 lines
5.9 KiB
Swift
//
|
|
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
public protocol AccountEntropyPoolManager {
|
|
func generateIfMissing() async
|
|
|
|
func setAccountEntropyPool(
|
|
newAccountEntropyPool: AccountEntropyPool,
|
|
tx: DBWriteTransaction,
|
|
)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
class AccountEntropyPoolManagerImpl: AccountEntropyPoolManager {
|
|
private let accountAttributesUpdater: AccountAttributesUpdater
|
|
private let accountKeyStore: AccountKeyStore
|
|
private let appContext: AppContext
|
|
private let backupSettingsStore: BackupSettingsStore
|
|
private let db: DB
|
|
private let logger: PrefixedLogger
|
|
private let storageServiceManager: StorageServiceManager
|
|
private let svr: SecureValueRecovery
|
|
private let syncManager: SyncManagerProtocol
|
|
private let tsAccountManager: TSAccountManager
|
|
|
|
init(
|
|
accountAttributesUpdater: AccountAttributesUpdater,
|
|
accountKeyStore: AccountKeyStore,
|
|
appContext: AppContext,
|
|
backupSettingsStore: BackupSettingsStore,
|
|
db: DB,
|
|
storageServiceManager: StorageServiceManager,
|
|
svr: SecureValueRecovery,
|
|
syncManager: SyncManagerProtocol,
|
|
tsAccountManager: TSAccountManager,
|
|
) {
|
|
self.accountAttributesUpdater = accountAttributesUpdater
|
|
self.accountKeyStore = accountKeyStore
|
|
self.appContext = appContext
|
|
self.backupSettingsStore = backupSettingsStore
|
|
self.db = db
|
|
self.logger = PrefixedLogger(prefix: "[Backups]")
|
|
self.storageServiceManager = storageServiceManager
|
|
self.svr = svr
|
|
self.syncManager = syncManager
|
|
self.tsAccountManager = tsAccountManager
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
func generateIfMissing() async {
|
|
await db.awaitableWrite { tx in
|
|
_generateIfMissing(tx: tx)
|
|
}
|
|
}
|
|
|
|
func _generateIfMissing(tx: DBWriteTransaction) {
|
|
guard
|
|
appContext.isMainApp,
|
|
tsAccountManager.registrationState(tx: tx).isRegisteredPrimaryDevice
|
|
else {
|
|
owsFailDebug("Attempting to generate AEP, but not registered primary && main app!")
|
|
return
|
|
}
|
|
|
|
guard accountKeyStore.getAccountEntropyPool(tx: tx) == nil else {
|
|
return
|
|
}
|
|
|
|
logger.info("Generating new AEP for registered primary missing one.")
|
|
|
|
setAccountEntropyPool(
|
|
newAccountEntropyPool: AccountEntropyPool(),
|
|
tx: tx,
|
|
)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
func setAccountEntropyPool(
|
|
newAccountEntropyPool: AccountEntropyPool,
|
|
tx: DBWriteTransaction,
|
|
) {
|
|
logger.warn("Setting new AEP!")
|
|
|
|
// Eventually, we may support rotating the AEP without rotating related-
|
|
// but-non-derived keys such as the MRBK and the Storage Service
|
|
// recordIkm. For now, though, "rotating the AEP" should also rotate all
|
|
// our keys.
|
|
let rotateRelatedNonDerivedKeys = true
|
|
|
|
switch backupSettingsStore.backupPlan(tx: tx) {
|
|
case .disabled:
|
|
break
|
|
case .disabling, .free, .paid, .paidExpiringSoon, .paidAsTester:
|
|
owsFail("Attempting to set AEP while Backups are not disabled.")
|
|
}
|
|
|
|
let isRegisteredPrimaryDevice = tsAccountManager.registrationState(tx: tx).isRegisteredPrimaryDevice
|
|
|
|
if !isRegisteredPrimaryDevice {
|
|
logger.warn("Setting AEP, but not a registered primary device.")
|
|
}
|
|
|
|
if rotateRelatedNonDerivedKeys {
|
|
accountKeyStore.setMediaRootBackupKey(
|
|
MediaRootBackupKey(backupKey: .generateRandom()),
|
|
tx: tx,
|
|
)
|
|
}
|
|
|
|
accountKeyStore.setAccountEntropyPool(newAccountEntropyPool, tx: tx)
|
|
|
|
// Skip the steps below if we're not yet registered. This check matters
|
|
// because one of our big callers is registration itself.
|
|
guard isRegisteredPrimaryDevice else {
|
|
return
|
|
}
|
|
|
|
tx.addSyncCompletion { [svr] in
|
|
Task {
|
|
do {
|
|
try await svr.refreshBackupIfNecessary()
|
|
} catch {
|
|
Logger.warn("couldn't refresh svr after rotating aep: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Schedule an account attributes update, since we need to update the
|
|
// reglock and reg recovery password downstream of the master key
|
|
// changing.
|
|
accountAttributesUpdater.scheduleAccountAttributesUpdate(
|
|
authedAccount: .implicit(),
|
|
tx: tx,
|
|
)
|
|
|
|
// Proactively rotate our Storage Service manifest, since the master key
|
|
// has changed and the Storage Service manifest key is derived from the
|
|
// master key.
|
|
//
|
|
// It's okay if this doesn't succeed; we'll get decryption errors the
|
|
// next time we do a Storage Service operation, from which we'll recover
|
|
// by creating a new manifest anyway.
|
|
Task {
|
|
try? await storageServiceManager.rotateManifest(
|
|
mode: rotateRelatedNonDerivedKeys ? .alsoRotatingRecords : .preservingRecordsIfPossible,
|
|
authedDevice: .implicit,
|
|
)
|
|
|
|
// Sync our new keys with linked devices, but wait until the storage
|
|
// service restore is done. Otherwise, linked devices might get the
|
|
// new keys and try and restore Storage Service before we've updated
|
|
// it, in which case they'd ask us for the keys again.
|
|
//
|
|
// Regardless, things should eventually recover regardless of what
|
|
// succeeds and in what order.
|
|
syncManager.sendKeysSyncMessage()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
#if TESTABLE_BUILD
|
|
|
|
class MockAccountEntropyPoolManager: AccountEntropyPoolManager {
|
|
func generateIfMissing() async {}
|
|
|
|
var setAccountEntropyPoolMock: (() -> Void)?
|
|
func setAccountEntropyPool(newAccountEntropyPool: AccountEntropyPool, tx: DBWriteTransaction) {
|
|
setAccountEntropyPoolMock?()
|
|
}
|
|
}
|
|
|
|
#endif
|