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

public import LibSignalClient
import SignalServiceKit

public class DeviceProvisioningURL {

    /// Capabilities communicated in a provisioning QR code.
    /// NOT to be confused with Account Capabilities; this is a distinct set
    /// scoped specifically to provisioning to communicate between the primary
    /// and secondary device.
    public enum Capability: String {
        case linknsync = "backup5"
    }

    public let linkType: LinkType

    let ephemeralDeviceId: String

    let publicKey: PublicKey

    public let capabilities: [Capability]

    public enum Constants {
        static let sgnlPrefix = "sgnl"
        static let uuidParamName = "uuid"
        static let publicKeyParamName = "pub_key"
        static let capabilitiesParamName = "capabilities"
    }

    public enum LinkType: String {
        case linkDevice = "linkdevice"
        case quickRestore = "rereg"
    }

    public init(
        type: LinkType,
        ephemeralDeviceId: String,
        publicKey: PublicKey,
        capabilities: [Capability] = [],
    ) {
        self.linkType = type
        self.ephemeralDeviceId = ephemeralDeviceId
        self.publicKey = publicKey
        self.capabilities = capabilities
    }

    // We don't use URLComponents to generate this URL as it encodes '+' and '/'
    // in the base64 pub_key in a way the Android doesn't tolerate.
    public func buildUrl() throws -> URL {
        var urlString = Constants.sgnlPrefix
        urlString.append("://")
        urlString.append(linkType.rawValue)
        urlString.append("?\(Constants.uuidParamName)=\(ephemeralDeviceId)")
        urlString.append("&\(Constants.publicKeyParamName)=\(try Self.encodePublicKey(publicKey))")
        urlString.append("&\(Constants.capabilitiesParamName)=\(capabilities.map(\.rawValue).joined(separator: ","))")
        guard let url = URL(string: urlString) else {
            throw OWSAssertionError("invalid url: \(urlString)")
        }
        return url
    }

    public init?(urlString: String) {
        guard
            let urlComponents = URLComponents(string: urlString),
            urlComponents.scheme == Constants.sgnlPrefix,
            let host = urlComponents.host,
            let type = LinkType(rawValue: host),
            let queryItems = urlComponents.queryItems
        else {
            return nil
        }

        var ephemeralDeviceId: String?
        var publicKey: PublicKey?
        var capabilities: [Capability] = []
        for queryItem in queryItems {
            switch queryItem.name {
            case Constants.uuidParamName:
                ephemeralDeviceId = queryItem.value
            case Constants.publicKeyParamName:
                publicKey = Self.decodePublicKey(queryItem.value)
            case Constants.capabilitiesParamName:
                capabilities = queryItem.value?
                    .split(separator: ",")
                    .compactMap({
                        guard let capability = Capability(rawValue: String($0)) else {
                            Logger.warn("unknown capability in provisioning string \($0)")
                            return nil
                        }
                        return capability
                    })
                    ?? []
            default:
                Logger.warn("unknown query item in provisioning string: \(queryItem.name)")
            }
        }

        guard let ephemeralDeviceId, let publicKey else {
            return nil
        }

        self.linkType = type
        self.ephemeralDeviceId = ephemeralDeviceId
        self.publicKey = publicKey
        self.capabilities = capabilities
    }

    private static func encodePublicKey(_ publicKey: PublicKey) throws -> String {
        let base64PubKey: String = publicKey.serialize().base64EncodedString()
        guard let encodedPubKey = base64PubKey.encodeURIComponent else {
            throw OWSAssertionError("Failed to url encode query params")
        }
        return encodedPubKey
    }

    private static func decodePublicKey(_ encodedPublicKey: String?) -> PublicKey? {
        guard let encodedPublicKey else {
            return nil
        }
        guard let annotatedPublicKey = Data(base64Encoded: encodedPublicKey, options: [.ignoreUnknownCharacters]) else {
            return nil
        }
        let publicKey: PublicKey
        do {
            publicKey = try PublicKey(annotatedPublicKey)
        } catch {
            owsFailDebug("failed to parse key: \(error)")
            return nil
        }
        return publicKey
    }
}