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

import Foundation
public import LibSignalClient

public struct RegistrationProvisioningEnvelope {

    public let body: Data
    public let publicKey: Data

    public init(serializedData: Data) throws {
        let proto = try RegistrationProtos_RegistrationProvisionEnvelope(serializedBytes: serializedData)
        self.body = proto.body
        self.publicKey = proto.publicKey
    }
}

public struct RegistrationProvisioningMessage {

    public enum Platform {
        case ios
        case android
    }

    public enum BackupTier: Equatable {
        case free
        case paid
    }

    public let accountEntropyPool: AccountEntropyPool
    public let aci: Aci
    public let aciIdentityKeyPair: IdentityKeyPair
    public let pniIdentityKeyPair: IdentityKeyPair
    public let phoneNumber: E164
    public let pin: String?
    public let platform: Platform
    public let tier: BackupTier?
    public let backupVersion: UInt64?
    public let backupTimestamp: UInt64?
    public let backupSizeBytes: UInt64?
    public let restoreMethodToken: String?
    public let lastBackupForwardSecrecyToken: LibSignalClient.BackupForwardSecrecyToken?
    public let nextBackupSecretData: BackupNonce.NextSecretMetadata?

    public init(
        accountEntropyPool: AccountEntropyPool,
        aci: Aci,
        aciIdentityKeyPair: IdentityKeyPair,
        pniIdentityKeyPair: IdentityKeyPair,
        phoneNumber: E164,
        pin: String?,
        tier: BackupTier?,
        backupVersion: UInt64?,
        backupTimestamp: UInt64?,
        backupSizeBytes: UInt64?,
        restoreMethodToken: String?,
        lastBackupForwardSecrecyToken: LibSignalClient.BackupForwardSecrecyToken?,
        nextBackupSecretData: BackupNonce.NextSecretMetadata?,
    ) {
        self.platform = .ios
        self.accountEntropyPool = accountEntropyPool
        self.aci = aci
        self.aciIdentityKeyPair = aciIdentityKeyPair
        self.pniIdentityKeyPair = pniIdentityKeyPair
        self.phoneNumber = phoneNumber
        self.pin = pin
        self.tier = tier
        self.backupVersion = backupVersion
        self.backupTimestamp = backupTimestamp
        self.backupSizeBytes = backupSizeBytes
        self.restoreMethodToken = restoreMethodToken
        self.lastBackupForwardSecrecyToken = lastBackupForwardSecrecyToken
        self.nextBackupSecretData = nextBackupSecretData
    }

    public init(plaintext: Data) throws {
        let proto = try RegistrationProtos_RegistrationProvisionMessage(serializedBytes: plaintext)

        self.aciIdentityKeyPair = try IdentityKeyPair(
            publicKey: PublicKey(proto.aciIdentityKeyPublic),
            privateKey: PrivateKey(proto.aciIdentityKeyPrivate),
        )

        self.pniIdentityKeyPair = try IdentityKeyPair(
            publicKey: PublicKey(proto.pniIdentityKeyPublic),
            privateKey: PrivateKey(proto.pniIdentityKeyPrivate),
        )

        guard
            let accountEntropyPool = proto.accountEntropyPool.nilIfEmpty,
            let aep = try? AccountEntropyPool(key: accountEntropyPool)
        else {
            throw ProvisioningError.invalidProvisionMessage("missing master key from provisioning message")
        }
        self.accountEntropyPool = aep

        self.aci = try Aci.parseFrom(serviceIdBinary: proto.aci)

        guard let e164 = E164(proto.e164) else {
            throw ProvisioningError.invalidProvisionMessage("missing number from provisioning message")
        }
        self.phoneNumber = e164

        self.pin = proto.pin

        self.platform = proto.platform == .android ? .android : .ios

        self.tier = proto.tier == .paid ? .paid : .free
        self.backupVersion = proto.backupVersion
        self.backupTimestamp = proto.backupTimestampMs
        self.backupSizeBytes = proto.backupSizeBytes

        self.restoreMethodToken = proto.restoreMethodToken

        if let data = proto.lastBackupForwardSecrecyToken.nilIfEmpty {
            self.lastBackupForwardSecrecyToken = try LibSignalClient.BackupForwardSecrecyToken(contents: data)
        } else {
            self.lastBackupForwardSecrecyToken = nil
        }

        if let data = proto.nextBackupSecretData.nilIfEmpty {
            self.nextBackupSecretData = BackupNonce.NextSecretMetadata(data: data)
        } else {
            self.nextBackupSecretData = nil
        }
    }

    public func buildEncryptedMessageBody(theirPublicKey: PublicKey) throws -> Data {
        var messageBuilder = RegistrationProtos_RegistrationProvisionMessage()

        messageBuilder.accountEntropyPool = accountEntropyPool.rawString
        messageBuilder.aci = aci.serviceIdBinary
        messageBuilder.e164 = phoneNumber.stringValue
        if let pin {
            messageBuilder.pin = pin
        }

        messageBuilder.aciIdentityKeyPublic = aciIdentityKeyPair.publicKey.serialize()
        messageBuilder.aciIdentityKeyPrivate = aciIdentityKeyPair.privateKey.serialize()

        messageBuilder.pniIdentityKeyPublic = pniIdentityKeyPair.publicKey.serialize()
        messageBuilder.pniIdentityKeyPrivate = pniIdentityKeyPair.privateKey.serialize()

        messageBuilder.platform = .ios

        // TODO: [Backups] Check backups are enabled before populating this
        if let tier {
            let protoTier: RegistrationProtos_RegistrationProvisionMessage.Tier
            switch tier {
            case .free: protoTier = .free
            case .paid: protoTier = .paid
            }
            messageBuilder.tier = protoTier
        }

        if let backupVersion {
            messageBuilder.backupVersion = backupVersion
        }

        if let backupTimestamp {
            messageBuilder.backupTimestampMs = backupTimestamp
        }
        if let backupSizeBytes {
            messageBuilder.backupSizeBytes = backupSizeBytes
        }

        if let restoreMethodToken {
            messageBuilder.restoreMethodToken = restoreMethodToken
        }

        if let lastBackupForwardSecrecyToken {
            messageBuilder.lastBackupForwardSecrecyToken = lastBackupForwardSecrecyToken.serialize()
        }
        if let nextBackupSecretData {
            messageBuilder.nextBackupSecretData = nextBackupSecretData.data
        }

        let plainTextMessage = try messageBuilder.serializedData()

        let ourKeyPair = IdentityKeyPair.generate()
        let cipher = ProvisioningCipher(ourKeyPair: ourKeyPair)
        let encryptedMessage: Data
        do {
            encryptedMessage = try cipher.encrypt(plainTextMessage, theirPublicKey: theirPublicKey)
        } catch {
            throw OWSAssertionError("Failed to encrypt registration provisioning message")
        }

        var envelopeBuilder = RegistrationProtos_RegistrationProvisionEnvelope()
        envelopeBuilder.publicKey = ourKeyPair.publicKey.serialize()
        envelopeBuilder.body = encryptedMessage

        return try envelopeBuilder.serializedData()
    }
}