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

import LibSignalClient

public protocol RegistrationIdMismatchManager {
    func validateRegistrationIds() async
}

public class RegistrationIdMismatchManagerImpl: RegistrationIdMismatchManager {

    private enum Constants {
        static let collection = "RegistrationIdMismatchManagerImpl"
        static let hasRecordedSuspectedIssue = "hasRecordedSuspectedIssue"
        static let haveRegistrationIdsBeenChecked = "haveRegistrationIdsBeenChecked"
    }

    private let db: DB
    private let kvStore: KeyValueStore = KeyValueStore(collection: Constants.collection)
    private let tsAccountManager: TSAccountManager
    private let udManager: OWSUDManager
    public init(db: DB, tsAccountManager: TSAccountManager, udManager: OWSUDManager) {
        self.db = db
        self.tsAccountManager = tsAccountManager
        self.udManager = udManager
    }

    public func validateRegistrationIds() async {
        guard
            !db.read(block: {
                kvStore.getBool(Constants.haveRegistrationIdsBeenChecked, defaultValue: false, transaction: $0)
            })
        else {
            return
        }

        guard
            let registeredState = db.read(block: { tx in
                return try? tsAccountManager.registeredState(tx: tx)
            })
        else {
            Logger.warn("Attempting to check registrationId while unregistered.")
            return
        }

        do {
            // Check ACI
            try await _checkRegistrationIdMatches(identity: .aci, serviceId: registeredState.localIdentifiers.aci)

            // Check PNI
            if let pni = registeredState.localIdentifiers.pni {
                try await _checkRegistrationIdMatches(identity: .pni, serviceId: pni)
            } else {
                owsFailDebug("Missing PNI during registrationId check")
            }

            await db.awaitableWrite {
                kvStore.setBool(true, key: Constants.haveRegistrationIdsBeenChecked, transaction: $0)
            }
        } catch {
            owsFailDebug("Failed to validate registration IDs: \(error)")
            return
        }
    }

    private func _checkRegistrationIdMatches(identity: OWSIdentity, serviceId: ServiceId) async throws {

        let (udAccess, deviceId) = db.read { tx in (
            (serviceId as? Aci).flatMap { udManager.udAccess(for: $0, tx: tx) },
            tsAccountManager.storedDeviceId(tx: tx),
        ) }

        // Fetch a key bundle for yourself.
        let requestMaker = RequestMaker(
            label: "RegistrationId Prekey Fetch",
            serviceId: serviceId,
            canUseStoryAuth: false,
            accessKey: udAccess,
            endorsement: nil,
            authedAccount: .implicit(),
            options: [.allowIdentifiedFallback],
        )

        let result = try await requestMaker.makeRequest {
            return OWSRequestFactory.recipientPreKeyRequest(
                serviceId: serviceId,
                deviceId: deviceId.description,
                auth: $0,
            )
        }

        guard let responseData = result.response.responseBodyData else {
            throw OWSAssertionError("Prekey fetch missing response object.")
        }
        guard let bundle = try? JSONDecoder().decode(SignalServiceKit.PreKeyBundle.self, from: responseData) else {
            throw OWSAssertionError("Prekey fetch returned an invalid bundle.")
        }
        guard let registrationId = bundle.devices.first?.registrationId else {
            throw OWSAssertionError("Prekey fetch missing registration Id")
        }

        if let localRegistrationId = db.read(block: { tsAccountManager.getRegistrationId(for: identity, tx: $0) }) {
            // Fetch local registration Id
            // Check if it's out of sync.
            if localRegistrationId == registrationId {
                // Everything matches, return
                Logger.info("\(identity) registrationId matches the server's understanding.")
                return
            }
            Logger.warn("\(identity) registrationId out of sync")
        } else {
            Logger.warn("\(identity) missing registrationId.")
        }

        await db.awaitableWrite {
            // update local state to match remote
            Logger.warn("Updating local \(identity) registrationId to match remote.")
            self.tsAccountManager.setRegistrationId(registrationId, for: identity, tx: $0)
            self.kvStore.setBool(true, key: Constants.hasRecordedSuspectedIssue, transaction: $0)
        }
    }
}