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

import Foundation
import LibSignalClient
import SignalServiceKit

class ProvisioningCoordinatorImpl: ProvisioningCoordinator {

    private let chatConnectionManager: ChatConnectionManager
    private let db: any DB
    private let identityManager: OWSIdentityManager
    private let linkAndSyncManager: LinkAndSyncManager
    private let accountKeyStore: AccountKeyStore
    private let networkManager: any NetworkManagerProtocol
    private let preKeyManager: PreKeyManager
    private let profileManager: ProfileManager
    private let pushRegistrationManager: Shims.PushRegistrationManager
    private let receiptManager: Shims.ReceiptManager
    private let registrationStateChangeManager: RegistrationStateChangeManager
    private let registrationWebSocketManager: any RegistrationWebSocketManager
    private let signalProtocolStoreManager: SignalProtocolStoreManager
    private let signalService: OWSSignalServiceProtocol
    private let storageServiceManager: StorageServiceManager
    private let svr: SecureValueRecovery
    private let syncManager: SyncManagerProtocol
    private let threadStore: ThreadStore
    private let tsAccountManager: TSAccountManager
    private let udManager: OWSUDManager

    init(
        chatConnectionManager: ChatConnectionManager,
        db: any DB,
        identityManager: OWSIdentityManager,
        linkAndSyncManager: LinkAndSyncManager,
        accountKeyStore: AccountKeyStore,
        networkManager: any NetworkManagerProtocol,
        preKeyManager: PreKeyManager,
        profileManager: ProfileManager,
        pushRegistrationManager: Shims.PushRegistrationManager,
        receiptManager: Shims.ReceiptManager,
        registrationStateChangeManager: RegistrationStateChangeManager,
        registrationWebSocketManager: any RegistrationWebSocketManager,
        signalProtocolStoreManager: SignalProtocolStoreManager,
        signalService: OWSSignalServiceProtocol,
        storageServiceManager: StorageServiceManager,
        svr: SecureValueRecovery,
        syncManager: SyncManagerProtocol,
        threadStore: ThreadStore,
        tsAccountManager: TSAccountManager,
        udManager: OWSUDManager,
    ) {
        self.chatConnectionManager = chatConnectionManager
        self.db = db
        self.identityManager = identityManager
        self.linkAndSyncManager = linkAndSyncManager
        self.accountKeyStore = accountKeyStore
        self.networkManager = networkManager
        self.preKeyManager = preKeyManager
        self.profileManager = profileManager
        self.pushRegistrationManager = pushRegistrationManager
        self.receiptManager = receiptManager
        self.registrationStateChangeManager = registrationStateChangeManager
        self.registrationWebSocketManager = registrationWebSocketManager
        self.signalProtocolStoreManager = signalProtocolStoreManager
        self.signalService = signalService
        self.storageServiceManager = storageServiceManager
        self.svr = svr
        self.syncManager = syncManager
        self.threadStore = threadStore
        self.tsAccountManager = tsAccountManager
        self.udManager = udManager
    }

    func completeProvisioning(
        provisionMessage: LinkingProvisioningMessage,
        deviceName: String,
        progressViewModel: LinkAndSyncSecondaryProgressViewModel,
    ) async throws(CompleteProvisioningError) {
        // * Primary devices that are re-registering can provision instead as long as either
        // the phone number or aci matches.
        // * Secondary devices _cannot_ be re-linked to primaries with a different aci.
        switch self.tsAccountManager.registrationStateWithMaybeSneakyTransaction {
        case .reregistering(let reregistrationPhoneNumber, let reregistrationAci):
            let acisMatch = reregistrationAci != nil && reregistrationAci == provisionMessage.aci
            let phoneNumbersMatch = reregistrationPhoneNumber == provisionMessage.phoneNumber
            guard acisMatch || phoneNumbersMatch else {
                Logger.warn("Cannot re-link primary a different aci and phone number")
                throw .previouslyLinkedWithDifferentAccount
            }
        case .relinking(_, let relinkingAci):
            if let oldAci = relinkingAci, oldAci != provisionMessage.aci {
                Logger.warn("Cannot re-link with a different aci")
                throw .previouslyLinkedWithDifferentAccount
            }
        default:
            break
        }

        guard let phoneNumber = E164(provisionMessage.phoneNumber) else {
            throw .genericError(OWSAssertionError("Primary E164 isn't valid"))
        }

        let result = try await completeProvisioning_updateCensorshipCircumvention(
            provisionMessage: provisionMessage,
            deviceName: deviceName,
            aci: provisionMessage.aci,
            pni: provisionMessage.pni,
            phoneNumber: phoneNumber,
        )

        try await continueFromLinkNSync(
            authedDevice: result.authedDevice,
            ephemeralBackupKey: provisionMessage.ephemeralBackupKey,
            progressViewModel: progressViewModel,
            undoAllPreviousSteps: result.undoBlock,
        )
    }

    // MARK: Link'n'Sync

    class LinkAndSyncError {
        let error: any Error
        let ephemeralBackupKey: MessageRootBackupKey
        let authedDevice: AuthedDevice.Explicit
        let progressViewModel: LinkAndSyncSecondaryProgressViewModel
        let undoAllPreviousSteps: () async throws -> Void
        weak var provisioningCoordinator: ProvisioningCoordinatorImpl?

        init(
            error: any Error,
            ephemeralBackupKey: MessageRootBackupKey,
            authedDevice: AuthedDevice.Explicit,
            progressViewModel: LinkAndSyncSecondaryProgressViewModel,
            undoAllPreviousSteps: @escaping () async throws -> Void,
            provisioningCoordinator: ProvisioningCoordinatorImpl,
        ) {
            self.error = error
            self.ephemeralBackupKey = ephemeralBackupKey
            self.authedDevice = authedDevice
            self.progressViewModel = progressViewModel
            self.undoAllPreviousSteps = undoAllPreviousSteps
            self.provisioningCoordinator = provisioningCoordinator
        }

        func retryLinkAndSync() async throws(CompleteProvisioningError) {
            guard let provisioningCoordinator else {
                throw .genericError(OWSAssertionError("ProvisioningCoordinator deallocated!"))
            }
            try await provisioningCoordinator.continueFromLinkNSync(
                authedDevice: authedDevice,
                ephemeralBackupKey: ephemeralBackupKey,
                progressViewModel: progressViewModel,
                undoAllPreviousSteps: undoAllPreviousSteps,
            )
        }

        func continueWithoutSyncing() async throws(CompleteProvisioningError) {
            guard let provisioningCoordinator else {
                throw .genericError(OWSAssertionError("ProvisioningCoordinator deallocated!"))
            }
            try await provisioningCoordinator.completeProvisioning_nonReversibleSteps(
                authedDevice: authedDevice,
                didLinkNSync: false,
            )
        }

        func restartProvisioning() async throws {
            try await undoAllPreviousSteps()
        }
    }

    private func continueFromLinkNSync(
        authedDevice: AuthedDevice.Explicit,
        ephemeralBackupKey: MessageRootBackupKey?,
        progressViewModel: LinkAndSyncSecondaryProgressViewModel,
        undoAllPreviousSteps: @escaping () async throws -> Void,
    ) async throws(CompleteProvisioningError) {
        var didLinkNSync = false
        if let ephemeralBackupKey {
            try await completeProvisioning_linkAndSync(
                ephemeralBackupKey: ephemeralBackupKey,
                authedDevice: authedDevice,
                progressViewModel: progressViewModel,
                undoAllPreviousSteps: undoAllPreviousSteps,
            )
            didLinkNSync = true
        }

        try await completeProvisioning_nonReversibleSteps(
            authedDevice: authedDevice,
            didLinkNSync: didLinkNSync,
        )
    }

    // MARK: - Steps

    struct CompleteProvisioningStepResult {
        let authedDevice: AuthedDevice.Explicit
        var undoBlock: () async throws -> Void

        func withUndoOnFailureStep(_ nextUndoBlock: @escaping () async throws -> Void) -> Self {
            let undoBlock = self.undoBlock
            return CompleteProvisioningStepResult(authedDevice: authedDevice, undoBlock: {
                try await undoBlock()
                try await nextUndoBlock()
            })
        }
    }

    private func completeProvisioning_updateCensorshipCircumvention(
        provisionMessage: LinkingProvisioningMessage,
        deviceName: String,
        aci: Aci,
        pni: Pni,
        phoneNumber: E164,
    ) async throws(CompleteProvisioningError) -> CompleteProvisioningStepResult {
        // Update censorship circumvention state as e164 could be changing.
        signalService.updateHasCensoredPhoneNumberDuringProvisioning(phoneNumber)

        return try await completeProvisioning_createPrekeys(
            provisionMessage: provisionMessage,
            deviceName: deviceName,
            aci: aci,
            pni: pni,
            phoneNumber: phoneNumber,
        ).withUndoOnFailureStep {
            self.signalService.resetHasCensoredPhoneNumberFromProvisioning()
        }
    }

    private func completeProvisioning_createPrekeys(
        provisionMessage: LinkingProvisioningMessage,
        deviceName: String,
        aci: Aci,
        pni: Pni,
        phoneNumber: E164,
    ) async throws(CompleteProvisioningError) -> CompleteProvisioningStepResult {
        let prekeyBundles = await self.preKeyManager.createPreKeysForProvisioning(
            aciIdentityKeyPair: provisionMessage.aciIdentityKeyPair.asECKeyPair,
            pniIdentityKeyPair: provisionMessage.pniIdentityKeyPair.asECKeyPair,
        )

        return try await completeProvisioning_createRegistrationIds(
            provisionMessage: provisionMessage,
            deviceName: deviceName,
            aci: aci,
            pni: pni,
            phoneNumber: phoneNumber,
            prekeyBundles: prekeyBundles,
        ).withUndoOnFailureStep {
            await self.preKeyManager.finalizeRegistrationPreKeys(
                prekeyBundles,
                uploadDidSucceed: false,
            )
        }
    }

    private func completeProvisioning_createRegistrationIds(
        provisionMessage: LinkingProvisioningMessage,
        deviceName: String,
        aci: Aci,
        pni: Pni,
        phoneNumber: E164,
        prekeyBundles: RegistrationPreKeyUploadBundles,
    ) async throws(CompleteProvisioningError) -> CompleteProvisioningStepResult {
        return try await completeProvisioning_verifyAndLinkOnServer(
            provisionMessage: provisionMessage,
            deviceName: deviceName,
            aci: aci,
            pni: pni,
            phoneNumber: phoneNumber,
            prekeyBundles: prekeyBundles,
            aciRegistrationId: RegistrationIdGenerator.generate(),
            pniRegistrationId: RegistrationIdGenerator.generate(),
        ).withUndoOnFailureStep {
            await self.db.awaitableWrite { tx in
                self.tsAccountManager.clearRegistrationIds(tx: tx)
            }
        }
    }

    private func completeProvisioning_verifyAndLinkOnServer(
        provisionMessage: LinkingProvisioningMessage,
        deviceName: String,
        aci: Aci,
        pni: Pni,
        phoneNumber: E164,
        prekeyBundles: RegistrationPreKeyUploadBundles,
        aciRegistrationId: UInt32,
        pniRegistrationId: UInt32,
    ) async throws(CompleteProvisioningError) -> CompleteProvisioningStepResult {
        let apnRegistrationId: RegistrationRequestFactory.ApnRegistrationId?
        let encryptedDeviceName: Data
        do {
            apnRegistrationId = try await getApnRegistrationId()
            encryptedDeviceName = try OWSDeviceNames.encryptDeviceName(
                plaintext: deviceName,
                identityKeyPair: provisionMessage.aciIdentityKeyPair,
            )
        } catch {
            throw .genericError(error)
        }

        let authedDevice = try await self.verifyAndLinkOnServer(
            provisionMessage: provisionMessage,
            aci: aci,
            pni: pni,
            phoneNumber: phoneNumber,
            aciRegistrationId: aciRegistrationId,
            pniRegistrationId: pniRegistrationId,
            encryptedDeviceName: encryptedDeviceName,
            apnRegistrationId: apnRegistrationId,
            prekeyBundles: prekeyBundles,
        )

        await registrationWebSocketManager.acquireRestrictedWebSocket(
            chatServiceAuth: authedDevice.authedAccount.chatServiceAuth,
        )

        return try await completeProvisioning_setLocalKeys(
            provisionMessage: provisionMessage,
            prekeyBundles: prekeyBundles,
            authedDevice: authedDevice,
            aciRegistrationId: aciRegistrationId,
            pniRegistrationId: pniRegistrationId,
        ).withUndoOnFailureStep {
            try await self.undoVerifyAndLinkOnServer(authedDevice: authedDevice)
        }
    }

    private func completeProvisioning_setLocalKeys(
        provisionMessage: LinkingProvisioningMessage,
        prekeyBundles: RegistrationPreKeyUploadBundles,
        authedDevice: AuthedDevice.Explicit,
        aciRegistrationId: UInt32,
        pniRegistrationId: UInt32,
    ) async throws(CompleteProvisioningError) -> CompleteProvisioningStepResult {
        let error: CompleteProvisioningError? = await self.db.awaitableWrite { tx in
            self.identityManager.setIdentityKeyPair(
                provisionMessage.aciIdentityKeyPair.asECKeyPair,
                for: .aci,
                tx: tx,
            )
            self.identityManager.setIdentityKeyPair(
                provisionMessage.pniIdentityKeyPair.asECKeyPair,
                for: .pni,
                tx: tx,
            )

            self.profileManager.setLocalProfileKey(
                provisionMessage.profileKey,
                userProfileWriter: .linking,
                transaction: tx,
            )

            self.tsAccountManager.setRegistrationId(aciRegistrationId, for: .aci, tx: tx)
            self.tsAccountManager.setRegistrationId(pniRegistrationId, for: .pni, tx: tx)

            do {
                try svr.storeKeys(
                    fromProvisioningMessage: provisionMessage,
                    authedDevice: .explicit(authedDevice),
                    tx: tx,
                )
            } catch {
                switch error {
                case SVR.KeysError.missingMasterKey:
                    owsFailDebug("Failed to store master key from provisioning message")
                    return .obsoleteLinkedDeviceError
                case SVR.KeysError.missingOrInvalidMRBK:
                    return .obsoleteLinkedDeviceError
                default:
                    owsFailDebug("Unexpected Error")
                }
            }

            self.receiptManager.setAreReadReceiptsEnabled(
                provisionMessage.areReadReceiptsEnabled,
                tx: tx,
            )

            return nil
        }
        if let error {
            throw error
        }

        return try await completeProvisioning_finalizePrekeys(
            provisionMessage: provisionMessage,
            prekeyBundles: prekeyBundles,
            authedDevice: authedDevice,
        ).withUndoOnFailureStep {
            await self.db.awaitableWrite { tx in
                self.identityManager.wipeIdentityKeysFromFailedProvisioning(tx: tx)

                // Set to a random value (we never set it to nil)
                self.profileManager.setLocalProfileKey(
                    Aes256Key.generateRandom(),
                    userProfileWriter: .linking,
                    transaction: tx,
                )
                self.svr.clearKeys(transaction: tx)

                // reset to default (false)
                self.receiptManager.setAreReadReceiptsEnabled(
                    false,
                    tx: tx,
                )

                self.accountKeyStore.wipeMediaRootBackupKeyFromFailedProvisioning(tx: tx)
            }
        }
    }

    private func completeProvisioning_finalizePrekeys(
        provisionMessage: LinkingProvisioningMessage,
        prekeyBundles: RegistrationPreKeyUploadBundles,
        authedDevice: AuthedDevice.Explicit,
    ) async throws(CompleteProvisioningError) -> CompleteProvisioningStepResult {
        do {
            await self.preKeyManager
                .finalizeRegistrationPreKeys(prekeyBundles, uploadDidSucceed: true)
            try await self.preKeyManager
                .rotateOneTimePreKeysForRegistration(auth: authedDevice.authedAccount.chatServiceAuth)
        } catch {
            throw .genericError(error)
        }

        return CompleteProvisioningStepResult(
            authedDevice: authedDevice,
            undoBlock: {
                await self.db.awaitableWrite { tx in
                    self.signalProtocolStoreManager.removeAllKeys(tx: tx)
                }
            },
        )
    }

    // MARK: -

    private func completeProvisioning_linkAndSync(
        ephemeralBackupKey: MessageRootBackupKey,
        authedDevice: AuthedDevice.Explicit,
        progressViewModel: LinkAndSyncSecondaryProgressViewModel,
        undoAllPreviousSteps: @escaping () async throws -> Void,
    ) async throws(CompleteProvisioningError) {
        let linkNSyncProgress = await OWSSequentialProgress<SecondaryLinkNSyncProgressPhase>.createSink { progress in
            await MainActor.run {
                progressViewModel.updateProgress(progress)
            }
        }

        do {
            try await self.linkAndSyncManager.waitForBackupAndRestore(
                localIdentifiers: authedDevice.localIdentifiers,
                auth: authedDevice.authedAccount.chatServiceAuth,
                ephemeralBackupKey: ephemeralBackupKey,
                progress: linkNSyncProgress,
            )
        } catch {
            Logger.warn("Failed link'n'sync \(error)")
            throw .linkAndSyncError(LinkAndSyncError(
                error: error,
                ephemeralBackupKey: ephemeralBackupKey,
                authedDevice: authedDevice,
                progressViewModel: progressViewModel,
                undoAllPreviousSteps: undoAllPreviousSteps,
                provisioningCoordinator: self,
            ))
        }
    }

    // MARK: -

    private func completeProvisioning_nonReversibleSteps(
        authedDevice: AuthedDevice.Explicit,
        didLinkNSync: Bool,
    ) async throws(CompleteProvisioningError) {
        let hasBackedUpMasterKey = self.db.read { tx in
            self.svr.hasBackedUpMasterKey(transaction: tx)
        }
        let capabilities = AccountAttributes.Capabilities(hasSVRBackups: hasBackedUpMasterKey)
        do {
            try await Service.makeUpdateSecondaryDeviceCapabilitiesRequest(
                capabilities: capabilities,
                auth: authedDevice.authedAccount.chatServiceAuth,
                networkManager: self.networkManager,
                tsAccountManager: self.tsAccountManager,
            )
        } catch {
            throw .genericError(error)
        }

        await self.db.awaitableWrite { tx in
            self.registrationStateChangeManager.didProvisionSecondary(
                e164: authedDevice.phoneNumber,
                aci: authedDevice.aci,
                pni: authedDevice.pni,
                authToken: authedDevice.authPassword,
                deviceId: authedDevice.deviceId,
                tx: tx,
            )
        }

        await registrationWebSocketManager.releaseRestrictedWebSocket(isRegistered: true)

        return try await performNecessarySyncsAndRestores(
            authedDevice: authedDevice,
            didLinkNSync: didLinkNSync,
        )
    }

    private func performNecessarySyncsAndRestores(
        authedDevice: AuthedDevice.Explicit,
        didLinkNSync: Bool,
    ) async throws(CompleteProvisioningError) {
        func doSyncsAndRestores() async throws(CompleteProvisioningError) {
            try await performInitialStorageServiceRestore(authedDevice: .explicit(authedDevice))
            try await performInitialContactSync(didLinkNSync: didLinkNSync)
        }

        if didLinkNSync {
            // Because link'n'sync gives us basic contact info, we don't
            // block on a contact sync after doing one. We still do the
            // contact sync in the background to get contact avatars.
            Task {
                try await doSyncsAndRestores()
            }
        } else {
            try await doSyncsAndRestores()
        }
    }

    private func performInitialStorageServiceRestore(
        authedDevice: AuthedDevice,
    ) async throws(CompleteProvisioningError) {
        do {
            try await self.storageServiceManager
                .restoreOrCreateManifestIfNecessary(authedDevice: authedDevice, masterKeySource: .implicit)
                .timeout(seconds: 60, substituteValue: ())
                .awaitable()
        } catch {
            throw .genericError(error)
        }
    }

    private func performInitialContactSync(didLinkNSync: Bool) async throws(CompleteProvisioningError) {
        // we wait a bit for the initial syncs to come in before proceeding to the inbox
        // because we want to present the inbox already populated with groups and contacts,
        // rather than have the trickle in moments later.
        // NOTE: in practice...groups do trickle in later, as of the time of this comment.
        // TODO: Eventually, we can rely entirely on the storage service and will no longer
        // need to do any initial sync. For now, we try and do both operations in parallel.

        let orderedThreadIds: [String]
        do {
            orderedThreadIds = try await syncManager
                .sendInitialSyncRequestsAwaitingCreatedThreadOrdering(timeoutSeconds: 60).awaitable()
        } catch {
            throw .genericError(error)
        }

        if !didLinkNSync {
            // Maintain the remote sort ordering of threads by inserting `syncedThread` messages
            // in that thread order. Don't do this if we link'n'synced.
            await self.db.awaitableWrite { tx in
                for threadId in orderedThreadIds.reversed() {
                    guard let thread = self.threadStore.fetchThread(uniqueId: threadId, tx: tx) else {
                        owsFailDebug("thread was unexpectedly nil")
                        continue
                    }
                    let infoMessage = TSInfoMessage(thread: thread, messageType: .syncedThread)
                    infoMessage.anyInsert(transaction: tx)
                }
            }
        }
    }

    // MARK: Network steps

    private func verifyAndLinkOnServer(
        provisionMessage: LinkingProvisioningMessage,
        aci: Aci,
        pni: Pni,
        phoneNumber: E164,
        aciRegistrationId: UInt32,
        pniRegistrationId: UInt32,
        encryptedDeviceName: Data,
        apnRegistrationId: RegistrationRequestFactory.ApnRegistrationId?,
        prekeyBundles: RegistrationPreKeyUploadBundles,
    ) async throws(CompleteProvisioningError) -> AuthedDevice.Explicit {
        let serverAuthToken = generateServerAuthToken()

        let accountAttributes = self.db.read { tx in
            return self.makeAccountAttributes(
                encryptedDeviceName: encryptedDeviceName,
                isManualMessageFetchEnabled: apnRegistrationId == nil,
                profileKey: provisionMessage.profileKey,
                aciRegistrationId: aciRegistrationId,
                pniRegistrationId: pniRegistrationId,
                tx: tx,
            )
        }

        let rawVerifyDeviceResponse = await Self.Service.makeVerifySecondaryDeviceRequest(
            verificationCode: provisionMessage.provisioningCode,
            phoneNumber: provisionMessage.phoneNumber,
            authPassword: serverAuthToken,
            accountAttributes: accountAttributes,
            apnRegistrationId: apnRegistrationId,
            prekeyBundles: prekeyBundles,
            signalService: self.signalService,
        )

        let verifyDeviceResponse: ProvisioningServiceResponses.VerifySecondaryDeviceResponse
        switch rawVerifyDeviceResponse {
        case .genericError(let error):
            throw .genericError(error)
        case .obsoleteLinkedDevice:
            throw .obsoleteLinkedDeviceError
        case .deviceLimitExceeded(let error):
            throw .deviceLimitExceededError(error)
        case .success(let response):
            verifyDeviceResponse = response
        }
        if pni != verifyDeviceResponse.pni {
            throw .genericError(OWSAssertionError("PNI from primary is out of sync with the server!"))
        }
        if verifyDeviceResponse.deviceId.isPrimary {
            throw .genericError(OWSAssertionError("Server is trying to link device as primary!"))
        }

        let authedDevice = AuthedDevice.Explicit(
            aci: aci,
            phoneNumber: phoneNumber,
            pni: pni,
            deviceId: verifyDeviceResponse.deviceId,
            authPassword: serverAuthToken,
        )
        return authedDevice
    }

    private func undoVerifyAndLinkOnServer(authedDevice: AuthedDevice.Explicit) async throws(CompleteProvisioningError) {
        do {
            try await registrationStateChangeManager.unlinkLocalDevice(
                localDeviceId: .valid(authedDevice.deviceId),
                auth: authedDevice.authedAccount.chatServiceAuth,
            )
        } catch {
            throw .genericError(error)
        }
    }

    // MARK: - Helpers

    private func getApnRegistrationId() async throws -> RegistrationRequestFactory.ApnRegistrationId? {
        do {
            return try await pushRegistrationManager
                .requestPushTokens(forceRotation: false)
        } catch let error {
            switch error {
            case PushRegistrationError.pushNotSupported(let description):
                // This can happen with:
                // - simulators, none of which support receiving push notifications
                // - on iOS11 devices which have disabled "Allow Notifications" and disabled "Enable Background Refresh" in the system settings.
                Logger.info("Recovered push registration error. Leaving as manual message fetcher because push not supported: \(description)")

                // no-op since secondary devices already start as manual message fetchers.
                // Use a nil apn reg id.
                return nil
            default:
                throw error
            }
        }
    }

    private typealias VerifySecondaryDeviceResponse = Service.VerifySecondaryDeviceResponse

    private func makeAccountAttributes(
        encryptedDeviceName encryptedDeviceNameRaw: Data,
        isManualMessageFetchEnabled: Bool,
        profileKey: Aes256Key,
        aciRegistrationId: UInt32,
        pniRegistrationId: UInt32,
        tx: DBReadTransaction,
    ) -> AccountAttributes {
        let udAccessKey = SMKUDAccessKey(profileKey: profileKey).keyData.base64EncodedString()
        let allowUnrestrictedUD = udManager.shouldAllowUnrestrictedAccessLocal(transaction: tx)

        // Linked-device provisioning uses the same AccountAttributes object as
        // primary-device registration; however, the reglock token is ignored by
        // the server.
        let reglockToken: String? = nil

        let registrationRecoveryPassword = accountKeyStore.getMasterKey(tx: tx)?.data(
            for: .registrationRecoveryPassword,
        ).canonicalStringRepresentation

        let encryptedDeviceName = encryptedDeviceNameRaw.base64EncodedString()

        let phoneNumberDiscoverability = tsAccountManager.phoneNumberDiscoverability(tx: tx)

        let hasSVRBackups = svr.hasBackedUpMasterKey(transaction: tx)

        return AccountAttributes(
            isManualMessageFetchEnabled: isManualMessageFetchEnabled,
            registrationId: aciRegistrationId,
            pniRegistrationId: pniRegistrationId,
            unidentifiedAccessKey: udAccessKey,
            unrestrictedUnidentifiedAccess: allowUnrestrictedUD,
            reglockToken: reglockToken,
            registrationRecoveryPassword: registrationRecoveryPassword,
            encryptedDeviceName: encryptedDeviceName,
            discoverableByPhoneNumber: phoneNumberDiscoverability,
            capabilities: AccountAttributes.Capabilities(hasSVRBackups: hasSVRBackups),
        )
    }

    private func generateServerAuthToken() -> String {
        return Randomness.generateRandomBytes(16).hexadecimalString
    }
}