// // 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 pinning to its CA. /// /// 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 } } fileprivate 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"]) }