Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/SecureValueRecovery/AccountEntropyPool/AccountEntropyPoolManager.swift
1 views
//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

public protocol AccountEntropyPoolManager {
    func generateIfMissing() async

    func setAccountEntropyPool(
        newAccountEntropyPool: AccountEntropyPool,
        disablePIN: Bool,
        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(),
            disablePIN: false,
            tx: tx,
        )
    }

    // MARK: -

    func setAccountEntropyPool(
        newAccountEntropyPool: AccountEntropyPool,
        disablePIN: Bool,
        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)

        svr.handleMasterKeyUpdated(
            newMasterKey: newAccountEntropyPool.getMasterKey(),
            disablePIN: disablePIN,
            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
        }

        // 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, disablePIN: Bool, tx: DBWriteTransaction) {
        setAccountEntropyPoolMock?()
    }
}

#endif