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

import MultipeerConnectivity
import SignalServiceKit

extension DeviceTransferService {
    private static let currentTransferVersion = 1

    private static let versionKey = "version"
    private static let peerIdKey = "peerId"
    private static let certificateHashKey = "certificateHash"
    private static let transferModeKey = "transferMode"

    private enum Constants {
        static let transferHost = "transfer"
    }

    func urlForTransfer(mode: TransferMode) throws -> URL {
        guard let identity else {
            throw OWSAssertionError("unexpectedly missing identity")
        }

        var components = URLComponents()
        components.scheme = UrlOpener.Constants.sgnlPrefix
        components.host = Constants.transferHost

        guard let base64CertificateHash = try identity.computeCertificateHash().base64EncodedString().encodeURIComponent else {
            throw OWSAssertionError("failed to get base64 certificate hash")
        }

        guard let base64PeerId = try NSKeyedArchiver.archivedData(withRootObject: peerId, requiringSecureCoding: true).base64EncodedString().encodeURIComponent else {
            throw OWSAssertionError("failed to get base64 peerId")
        }

        let queryItems = [
            DeviceTransferService.versionKey: String(DeviceTransferService.currentTransferVersion),
            DeviceTransferService.transferModeKey: mode.rawValue,
            DeviceTransferService.certificateHashKey: base64CertificateHash,
            DeviceTransferService.peerIdKey: base64PeerId,
        ]

        components.queryItems = queryItems.map { URLQueryItem(name: $0.key, value: $0.value) }

        return components.url!
    }

    func parseTransferURL(_ url: URL) throws -> (peerId: MCPeerID, certificateHash: Data) {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = components.queryItems else {
            throw OWSAssertionError("Invalid url")
        }

        let queryItemsDictionary = [String: String](uniqueKeysWithValues: queryItems.compactMap { item in
            guard let value = item.value else { return nil }
            return (item.name, value)
        })

        guard
            let version = queryItemsDictionary[DeviceTransferService.versionKey],
            Int(version) == DeviceTransferService.currentTransferVersion
        else {
            throw Error.unsupportedVersion
        }

        let currentMode: TransferMode = DependenciesBridge.shared.tsAccountManager
            .registrationStateWithMaybeSneakyTransaction.isPrimaryDevice == true ? .primary : .linked

        guard
            let rawMode = queryItemsDictionary[DeviceTransferService.transferModeKey],
            rawMode == currentMode.rawValue
        else {
            throw Error.modeMismatch
        }

        guard
            let base64CertificateHash = queryItemsDictionary[DeviceTransferService.certificateHashKey],
            let uriDecodedHash = base64CertificateHash.removingPercentEncoding,
            let certificateHash = Data(base64Encoded: uriDecodedHash)
        else {
            throw OWSAssertionError("failed to decode certificate hash")
        }

        guard
            let base64PeerId = queryItemsDictionary[DeviceTransferService.peerIdKey],
            let uriDecodedPeerId = base64PeerId.removingPercentEncoding,
            let peerIdData = Data(base64Encoded: uriDecodedPeerId),
            let peerId = try NSKeyedUnarchiver.unarchivedObject(ofClass: MCPeerID.self, from: peerIdData)
        else {
            throw OWSAssertionError("failed to decode MCPeerId")
        }

        return (peerId, certificateHash)
    }
}