Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

236 lines
9.1 KiB
Swift

import CommonCrypto
import Foundation
public struct WebProxyConfiguration: Equatable, Hashable {
public static let port: UInt16 = 443
public static let maximumPathLength = 128
public let host: String
public let path: String
public let secret: Data
public init?(host: String, path: String = "", secret: Data) {
guard let host = Self.canonicalHost(host), let path = Self.canonicalPath(path), Self.isValidSecret(secret) else {
return nil
}
self.host = host
self.path = path
self.secret = secret
}
/// The marker BASE_PATH.md §3 puts in front of a base-path link's secret.
///
/// Deliberately not `0xDD`: a client without base-path support reads a 17-byte secret
/// beginning with `0xDD` as an ordinary padded secret and accepts the link, having
/// normalized `host/path` down to an empty host. With `0x70` it instead sees a secret
/// it cannot classify and reports an unsupported proxy type.
public static let secretMarker: UInt8 = 0x70
public static func isValidSecret(_ secret: Data) -> Bool {
return secret.count == 16 || (secret.count == 17 && secret.first == 0xdd)
}
/// Decodes a secret as written in a link or typed into the editor, reporting whether
/// it carried the `0x70` marker.
///
/// BASE_PATH.md §3's inverse rule - strip a leading `0x70` from a value of at least 17
/// bytes - is unambiguous because a canonical secret is 16 bytes, 17 beginning with
/// `0xDD`, or 21+ beginning with `0xEE`.
public static func parseMarkedSecret(_ value: String) -> (secret: Data, isMarked: Bool)? {
let value = value.trimmingCharacters(in: .whitespacesAndNewlines)
// Hex first: a plain 32-character secret is also well-formed base64url, and hex is
// the canonical root form. A hex value that is not a secret still falls through, so
// a marked secret whose encoding happens to be all hex digits is not lost.
if let hex = self.hexBytes(value), let result = self.unmarkedSecret(hex) {
return result
}
guard let bytes = self.base64UrlBytes(value) else {
return nil
}
return self.unmarkedSecret(bytes)
}
public static func parseSecret(_ value: String) -> Data? {
return self.parseMarkedSecret(value)?.secret
}
/// The link form of `secret`: plain hex at the root, `0x70`-marked base64url under a
/// base path. A path-bearing link must use the marked form (BASE_PATH.md §3).
public static func linkSecretString(_ secret: Data, path: String) -> String {
if path.isEmpty {
return secret.map { String(format: "%02x", $0) }.joined()
}
return (Data([self.secretMarker]) + secret).webProxyBase64Url
}
private static func unmarkedSecret(_ bytes: Data) -> (secret: Data, isMarked: Bool)? {
let isMarked = bytes.count >= 17 && bytes.first == self.secretMarker
let secret = isMarked ? Data(bytes.dropFirst()) : bytes
guard self.isValidSecret(secret) else {
return nil
}
return (secret, isMarked)
}
private static func hexBytes(_ value: String) -> Data? {
guard !value.isEmpty, value.count % 2 == 0, value.allSatisfy({ $0.isHexDigit }) else {
return nil
}
var result = Data()
result.reserveCapacity(value.count / 2)
var index = value.startIndex
while index < value.endIndex {
let next = value.index(index, offsetBy: 2)
guard let byte = UInt8(value[index ..< next], radix: 16) else {
return nil
}
result.append(byte)
index = next
}
return result
}
private static func base64UrlBytes(_ value: String) -> Data? {
guard !value.isEmpty else {
return nil
}
var base64 = value.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
let remainder = base64.count % 4
if remainder != 0 {
base64.append(String(repeating: "=", count: 4 - remainder))
}
return Data(base64Encoded: base64)
}
public static func canonicalHost(_ value: String) -> String? {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty,
!trimmed.contains("/"),
!trimmed.contains("@"),
!trimmed.contains(":"),
let components = URLComponents(string: "https://\(trimmed)"),
components.user == nil,
components.password == nil,
components.port == nil,
let host = components.url?.host?.lowercased(),
!host.isEmpty,
host.utf8.count <= 253 else {
return nil
}
let labels = host.split(separator: ".", omittingEmptySubsequences: false)
guard labels.count >= 2,
labels.allSatisfy({ label in
!label.isEmpty && label.utf8.count <= 63 && label.first != "-" && label.last != "-" && label.allSatisfy { character in
character.isASCII && (character.isLetter || character.isNumber || character == "-")
}
}), !lastLabelIsNumeric(labels) else {
return nil
}
return host
}
public static func canonicalPath(_ value: String) -> String? {
var path = value.trimmingCharacters(in: .whitespacesAndNewlines)
if path.isEmpty {
return ""
}
guard !path.hasPrefix("/") else {
return nil
}
if path.hasSuffix("/") {
path.removeLast()
}
guard !path.isEmpty, path.count <= maximumPathLength else {
return nil
}
let segments = path.split(separator: "/", omittingEmptySubsequences: false)
guard segments.allSatisfy({ segment in
guard let first = segment.first, first.isASCII, first.isLetter || first.isNumber else {
return false
}
return segment.allSatisfy { character in
character.isASCII && (character.isLetter || character.isNumber || character == "-" || character == "_")
}
}) else {
return nil
}
return path
}
public static func canonicalAddress(_ value: String) -> (host: String, path: String)? {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard let separator = trimmed.firstIndex(of: "/") else {
guard let host = canonicalHost(trimmed) else {
return nil
}
return (host, "")
}
guard let host = canonicalHost(String(trimmed[trimmed.startIndex ..< separator])),
let path = canonicalPath(String(trimmed[trimmed.index(after: separator)...])) else {
return nil
}
return (host, path)
}
private static func lastLabelIsNumeric(_ labels: [Substring]) -> Bool {
guard let label = labels.last, !label.isEmpty else {
return false
}
let hexadecimal = label.count >= 2 && label.first == "0" && (label[label.index(after: label.startIndex)] == "x" || label[label.index(after: label.startIndex)] == "X")
let digits = hexadecimal ? label.dropFirst(2) : label[...]
return digits.allSatisfy { character in
character.isASCII && (character.isNumber || (hexadecimal && character.isHexDigit))
}
}
public var address: String {
return self.path.isEmpty ? self.host : "\(self.host)/\(self.path)"
}
public var base: String {
return self.path.isEmpty ? "/" : "/\(self.path)/"
}
public var secretHex: String {
return self.secret.map { String(format: "%02x", $0) }.joined()
}
public func bridgeCapability() -> String {
let context: Data
if self.path.isEmpty {
context = Data("tdesktop-web-proxy-bridge-v1\n\(self.host)".utf8)
} else {
context = Data("tdesktop-web-proxy-bridge-v2\n\(self.host)\n\(self.path)".utf8)
}
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
self.secret.withUnsafeBytes { keyBytes in
context.withUnsafeBytes { contextBytes in
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyBytes.baseAddress, self.secret.count, contextBytes.baseAddress, context.count, &digest)
}
}
return Data(digest).webProxyBase64Url
}
public func bridgeURL(nonce: String) -> URL? {
guard nonce.utf8.count == 43 else {
return nil
}
var components = URLComponents()
components.scheme = "https"
components.host = self.host
components.path = self.base
components.queryItems = [URLQueryItem(name: "bridge", value: self.bridgeCapability())]
components.fragment = "android=\(nonce)"
return components.url
}
}
extension Data {
var webProxyBase64Url: String {
return self.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}