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

import Foundation

enum OWSFrontingHost {
    case fastly
    case googleEgypt
    case googleUae
    case googleOman
    case googlePakistan
    case googleQatar
    case googleUzbekistan
    case googleVenezuela
    case `default`

    /// When using censorship circumvention, we pin to the fronted domain host.
    /// Adding a new domain front entails adding a corresponding HttpSecurityPolicy
    /// and specifying which CAs it can use.
    ///
    /// If the security policy requires new certificates, include them in the SSK bundle
    fileprivate var securityPolicy: HttpSecurityPolicy {
        switch self {
        case .googleEgypt, .googleUae, .googleOman, .googlePakistan, .googleQatar, .googleUzbekistan, .googleVenezuela, .default:
            return PinningPolicy.google.securityPolicy
        case .fastly:
            return PinningPolicy.fastly.securityPolicy
        }
    }

    fileprivate var host: String {
        switch self {
        case .googleEgypt, .googleUae, .googleOman, .googlePakistan, .googleQatar, .googleUzbekistan, .googleVenezuela, .`default`:
            return TSConstants.censorshipGReflectorHost
        case .fastly:
            return TSConstants.censorshipFReflectorHost
        }
    }

    private var requiresPathPrefix: Bool {
        switch self {
        case .googleEgypt, .googleUae, .googleOman, .googlePakistan, .googleQatar, .googleUzbekistan, .googleVenezuela, .`default`:
            return true
        case .fastly:
            return false
        }
    }

    fileprivate func randomSniHeader() -> String {
        self.sniHeaders.randomElement()!
    }

    /// None of these can be empty arrays or a crash will occur in `randomSniHeader()` above.
    private var sniHeaders: [String] {
        switch self {
        case .fastly:
            Self.fastlySniHeaders
        case .googleEgypt:
            Self.googleEgyptSniHeaders
        case .googleUae:
            Self.googleOmanSniHeaders
        case .googleOman:
            Self.googleOmanSniHeaders
        case .googlePakistan:
            Self.googlePakistanSniHeaders
        case .googleQatar:
            Self.googleQatarSniHeaders
        case .googleUzbekistan:
            Self.googleUzbekistanSniHeaders
        case .googleVenezuela:
            Self.googleVenezuelaSniHeaders
        case .default:
            Self.googleCommonSniHeaders
        }
    }

    private static let fastlySniHeaders = ["github.githubassets.com", "pinterest.com", "www.redditstatic.com"]
    private static let googleCommonSniHeaders = [
        "www.google.com",
        "android.clients.google.com",
        "clients3.google.com",
        "clients4.google.com",
        "googlemail.com",
    ]
    private static let googleEgyptSniHeaders = googleCommonSniHeaders + ["www.google.com.eg"]
    private static let googleUaeSniHeaders = googleCommonSniHeaders + ["www.google.ae"]
    private static let googleOmanSniHeaders = googleCommonSniHeaders + ["www.google.com.om"]
    private static let googlePakistanSniHeaders = googleCommonSniHeaders + ["www.google.com.pk"]
    private static let googleQatarSniHeaders = googleCommonSniHeaders + ["www.google.com.qa"]
    private static let googleUzbekistanSniHeaders = googleCommonSniHeaders + ["www.google.co.uz"]
    private static let googleVenezuelaSniHeaders = googleCommonSniHeaders + ["www.google.co.ve"]
}

struct OWSCensorshipConfiguration {

    let domainFrontBaseUrl: URL
    let domainFrontSecurityPolicy: HttpSecurityPolicy
    let host: String

    /// Returns a service specific host header.
    ///
    /// Callers should use a default host header if there's not a service specific host header.
    func reflectorHost() -> String {
        return host
    }

    /// Returns `nil` if `e164` is not known to be censored.
    static func censorshipConfiguration(e164: String) -> OWSCensorshipConfiguration? {
        guard let countryCode = censoredCountryCode(e164: e164) else {
            return nil
        }

        return censorshipConfiguration(countryCode: countryCode)
    }

    /// Returns the best censorship configuration for `countryCode`. Will return a default if one
    /// hasn't been specifically configured.
    static func censorshipConfiguration(countryCode: String) -> OWSCensorshipConfiguration {
        let countryMetadata = OWSCountryMetadata.countryMetadata(countryCode: countryCode)
        guard let specifiedDomain = countryMetadata?.frontingDomain else {
            return defaultConfiguration
        }

        let sniHeader = specifiedDomain.randomSniHeader()
        guard let baseUrl = URL(string: "https://\(sniHeader)") else {
            owsFailDebug("baseUrl was unexpectedly nil with specifiedDomain: \(sniHeader)")
            return defaultConfiguration
        }

        return OWSCensorshipConfiguration(domainFrontBaseUrl: baseUrl, securityPolicy: specifiedDomain.securityPolicy, host: specifiedDomain.host)
    }

    static var defaultConfiguration: OWSCensorshipConfiguration {
        let baseUrl = URL(string: "https://\(OWSFrontingHost.default.randomSniHeader())")!
        return OWSCensorshipConfiguration(domainFrontBaseUrl: baseUrl, securityPolicy: OWSFrontingHost.default.securityPolicy, host: OWSFrontingHost.default.host)

    }

    static func isCensored(e164: String) -> Bool {
        censoredCountryCode(e164: e164) != nil
    }

    private init(domainFrontBaseUrl: URL, securityPolicy: HttpSecurityPolicy, host: String) {
        self.domainFrontBaseUrl = domainFrontBaseUrl
        self.domainFrontSecurityPolicy = securityPolicy
        self.host = host
    }

    /// The set of countries for which domain fronting should be automatically enabled.
    ///
    /// If you want to use a domain front other than the default, specify the domain front
    /// in OWSCountryMetadata, and ensure we have a Security Policy for that domain in
    /// `securityPolicyForDomain:`
    private static let censoredCountryCodes: [String: String] = [
        // Egypt
        "+20": "EG",
        // Oman
        "+968": "OM",
        // Qatar
        "+974": "QA",
        // UAE
        "+971": "AE",
        // Cuba
        "+53": "CU",
        // Venezuela
        "+58": "VE",
        // Uzbekistan,
        "+998": "UZ",
        // Pakistan
        "+92": "PK",
    ]

    /// Returns nil if the phone number is not known to be censored
    private static func censoredCountryCode(e164: String) -> String? {
        for (key: callingCode, value: countryCode) in censoredCountryCodes {
            if e164.hasPrefix(callingCode) {
                return countryCode
            }
        }

        return nil
    }
}

private enum PinningPolicy {
    case fastly
    case google

    var securityPolicy: HttpSecurityPolicy {
        switch self {
        case .fastly:
            return Self.fastlySecurityPolicy
        case .google:
            return Self.googleSecurityPolicy
        }
    }

    private static func securityPolicy(certNames: [String]) -> HttpSecurityPolicy {
        HttpSecurityPolicy(pinnedCertificates: certNames.map { Certificates.load($0, extension: "crt") })
    }

    private static let fastlySecurityPolicy = HttpSecurityPolicy.systemDefault

    // GIAG2 cert plus root certs from pki.goog
    private static let googleSecurityPolicy = securityPolicy(certNames: ["GIAG2", "GSR2", "GSR4", "GTSR1", "GTSR2", "GTSR3", "GTSR4"])
}