Files
2026-07-13 12:22:33 +08:00

511 lines
20 KiB
Swift

import Foundation
#if os(macOS)
import CommonCrypto
import Security
import SQLite3
import SweetCookieKit
private let alibabaCookieImportOrder: BrowserCookieImportOrder =
ProviderDefaults.metadata[.alibaba]?.browserCookieOrder ?? Browser.defaultImportOrder
public enum AlibabaCodingPlanCookieImporter {
private static let cookieClient = BrowserCookieClient()
private static let cookieDomains = [
"bailian-singapore-cs.alibabacloud.com",
"bailian-cs.console.aliyun.com",
"bailian-beijing-cs.aliyuncs.com",
"modelstudio.console.alibabacloud.com",
"bailian.console.aliyun.com",
"free.aliyun.com",
"account.aliyun.com",
"signin.aliyun.com",
"passport.alibabacloud.com",
"console.alibabacloud.com",
"console.aliyun.com",
"alibabacloud.com",
"aliyun.com",
]
public struct SessionInfo: Sendable {
public let cookies: [HTTPCookie]
public let sourceLabel: String
public init(cookies: [HTTPCookie], sourceLabel: String) {
self.cookies = cookies
self.sourceLabel = sourceLabel
}
public var cookieHeader: String {
var byName: [String: HTTPCookie] = [:]
byName.reserveCapacity(self.cookies.count)
for cookie in self.cookies {
if let expiry = cookie.expiresDate, expiry < Date() {
continue
}
guard !cookie.value.isEmpty else { continue }
if let existing = byName[cookie.name] {
let existingExpiry = existing.expiresDate ?? .distantPast
let candidateExpiry = cookie.expiresDate ?? .distantPast
if candidateExpiry >= existingExpiry {
byName[cookie.name] = cookie
}
} else {
byName[cookie.name] = cookie
}
}
return byName.keys.sorted().compactMap { name in
guard let cookie = byName[name] else { return nil }
return "\(cookie.name)=\(cookie.value)"
}.joined(separator: "; ")
}
}
nonisolated(unsafe) static var importSessionOverrideForTesting:
((BrowserDetection, ((String) -> Void)?) throws -> SessionInfo)?
public static func importSession(
browserDetection: BrowserDetection,
logger: ((String) -> Void)? = nil) throws -> SessionInfo
{
if let override = self.importSessionOverrideForTesting {
return try override(browserDetection, logger)
}
let log: (String) -> Void = { msg in logger?("[alibaba-cookie] \(msg)") }
var accessDeniedHints: [String] = []
var failureDetails: [String] = []
let installedBrowsers = self.cookieImportCandidates(browserDetection: browserDetection)
log("Cookie import candidates: \(installedBrowsers.map(\.displayName).joined(separator: ", "))")
for browserSource in installedBrowsers {
do {
log("Checking \(browserSource.displayName)")
let query = BrowserCookieQuery(domains: self.cookieDomains)
let sources = try Self.cookieClient.codexBarRecords(
matching: query,
in: browserSource,
logger: log)
if sources.isEmpty {
log("No matching cookie records in \(browserSource.displayName)")
if let fallbackSession = try Self.importChromiumFallbackSession(
browser: browserSource,
logger: log)
{
return fallbackSession
}
}
for source in sources where !source.records.isEmpty {
let httpCookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)
if self.isAuthenticatedSession(cookies: httpCookies) {
log("Found \(httpCookies.count) Alibaba cookies in \(source.label)")
return SessionInfo(cookies: httpCookies, sourceLabel: source.label)
}
let cookieNames = Set(httpCookies.map(\.name))
let hasTicket = cookieNames.contains("login_aliyunid_ticket")
let hasAccount =
cookieNames.contains("login_aliyunid_pk") ||
cookieNames.contains("login_current_pk") ||
cookieNames.contains("login_aliyunid")
log("Skipping \(source.label): missing auth cookies (ticket=\(hasTicket), account=\(hasAccount))")
}
if let fallbackSession = try Self.importChromiumFallbackSession(browser: browserSource, logger: log) {
return fallbackSession
}
} catch let error as BrowserCookieError {
BrowserCookieAccessGate.recordIfNeeded(error)
if let hint = error.accessDeniedHint {
accessDeniedHints.append(hint)
}
failureDetails.append("\(browserSource.displayName): \(error.localizedDescription)")
log("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)")
} catch {
failureDetails.append("\(browserSource.displayName): \(error.localizedDescription)")
log("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)")
}
}
let details = (Array(Set(accessDeniedHints)).sorted() + Array(Set(failureDetails)).sorted())
.joined(separator: " ")
throw AlibabaCodingPlanSettingsError.missingCookie(details: details.isEmpty ? nil : details)
}
private static func isAuthenticatedSession(cookies: [HTTPCookie]) -> Bool {
guard !cookies.isEmpty else { return false }
let names = Set(cookies.map(\.name))
let hasTicket = names.contains("login_aliyunid_ticket")
let hasAccount =
names.contains("login_aliyunid_pk") ||
names.contains("login_current_pk") ||
names.contains("login_aliyunid")
return hasTicket && hasAccount
}
public static func hasSession(
browserDetection: BrowserDetection,
logger: ((String) -> Void)? = nil) -> Bool
{
do {
_ = try self.importSession(browserDetection: browserDetection, logger: logger)
return true
} catch {
return false
}
}
private static func importChromiumFallbackSession(
browser: Browser,
logger: ((String) -> Void)? = nil) throws -> SessionInfo?
{
guard browser.usesChromiumProfileStore else { return nil }
return try AlibabaChromiumCookieFallbackImporter.importSession(
browser: browser,
domains: self.cookieDomains,
logger: logger)
}
static func cookieImportCandidates(
browserDetection: BrowserDetection,
importOrder: BrowserCookieImportOrder = alibabaCookieImportOrder) -> [Browser]
{
importOrder.cookieImportCandidates(using: browserDetection)
}
static func matchesCookieDomain(_ domain: String, patterns: [String] = Self.cookieDomains) -> Bool {
let normalized = self.normalizeCookieDomain(domain)
return patterns.contains { pattern in
let normalizedPattern = self.normalizeCookieDomain(pattern)
return normalized == normalizedPattern || normalized.hasSuffix(".\(normalizedPattern)")
}
}
static func normalizeCookieDomain(_ domain: String) -> String {
let trimmed = domain.trimmingCharacters(in: .whitespacesAndNewlines)
let normalized = trimmed.hasPrefix(".") ? String(trimmed.dropFirst()) : trimmed
return normalized.lowercased()
}
}
enum AlibabaChromiumCookieFallbackImporter {
private struct ChromiumCookieRecord {
let domain: String
let name: String
let path: String
let value: String
let expires: Date?
let isSecure: Bool
}
enum ImportError: LocalizedError {
case keyUnavailable(browser: Browser)
case keychainDenied(browser: Browser)
case sqliteFailed(label: String, details: String)
var errorDescription: String? {
switch self {
case let .keyUnavailable(browser):
"\(browser.displayName) Safe Storage key not found."
case let .keychainDenied(browser):
"macOS Keychain denied access to \(browser.displayName) Safe Storage."
case let .sqliteFailed(label, details):
"\(label) cookie fallback failed: \(details)"
}
}
}
static func importSession(
browser: Browser,
domains: [String],
cookieClient: BrowserCookieClient = BrowserCookieClient(),
logger: ((String) -> Void)? = nil) throws -> AlibabaCodingPlanCookieImporter.SessionInfo?
{
let stores = try cookieClient.codexBarStores(for: browser).filter { $0.databaseURL != nil }
guard !stores.isEmpty else { return nil }
logger?("[alibaba-cookie] Trying \(browser.displayName) Chromium fallback")
let keys = try self.derivedKeys(for: browser)
for store in stores {
let cookies = try self.loadCookies(from: store, domains: domains, keys: keys)
guard !cookies.isEmpty else { continue }
if self.isAuthenticatedSession(cookies) {
logger?("[alibaba-cookie] Found \(cookies.count) Alibaba cookies via \(store.label) fallback")
return AlibabaCodingPlanCookieImporter.SessionInfo(cookies: cookies, sourceLabel: store.label)
}
}
return nil
}
private static func isAuthenticatedSession(_ cookies: [HTTPCookie]) -> Bool {
let names = Set(cookies.map(\.name))
let hasTicket = names.contains("login_aliyunid_ticket")
let hasAccount =
names.contains("login_aliyunid_pk") ||
names.contains("login_current_pk") ||
names.contains("login_aliyunid")
return hasTicket && hasAccount
}
private static func loadCookies(
from store: BrowserCookieStore,
domains: [String],
keys: [Data]) throws -> [HTTPCookie]
{
guard let sourceDB = store.databaseURL else { return [] }
let records = try self.readCookiesFromLockedDB(
sourceDB: sourceDB,
domains: domains,
keys: keys,
label: store.label)
return records.compactMap(self.makeCookie)
}
private static func readCookiesFromLockedDB(
sourceDB: URL,
domains: [String],
keys: [Data],
label: String) throws -> [ChromiumCookieRecord]
{
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("alibaba-chromium-cookies-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
let copiedDB = tempDir.appendingPathComponent("Cookies")
try FileManager.default.copyItem(at: sourceDB, to: copiedDB)
for suffix in ["-wal", "-shm"] {
let src = URL(fileURLWithPath: sourceDB.path + suffix)
if FileManager.default.fileExists(atPath: src.path) {
let dst = URL(fileURLWithPath: copiedDB.path + suffix)
try? FileManager.default.copyItem(at: src, to: dst)
}
}
defer { try? FileManager.default.removeItem(at: tempDir) }
return try self.readCookies(fromDB: copiedDB.path, domains: domains, keys: keys, label: label)
}
private static func readCookies(
fromDB path: String,
domains: [String],
keys: [Data],
label: String) throws -> [ChromiumCookieRecord]
{
var db: OpaquePointer?
guard sqlite3_open_v2(path, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK else {
throw ImportError.sqliteFailed(label: label, details: String(cString: sqlite3_errmsg(db)))
}
defer { sqlite3_close(db) }
let sql = "SELECT host_key, name, path, expires_utc, is_secure, value, encrypted_value FROM cookies"
var stmt: OpaquePointer?
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
throw ImportError.sqliteFailed(label: label, details: String(cString: sqlite3_errmsg(db)))
}
defer { sqlite3_finalize(stmt) }
var records: [ChromiumCookieRecord] = []
while sqlite3_step(stmt) == SQLITE_ROW {
guard let hostKey = self.readText(stmt, index: 0), self.matches(domain: hostKey, patterns: domains) else {
continue
}
guard let name = self.readText(stmt, index: 1), let path = self.readText(stmt, index: 2) else {
continue
}
let value: String? = if let plain = self.readText(stmt, index: 5), !plain.isEmpty {
plain
} else if let encrypted = self.readBlob(stmt, index: 6) {
self.decrypt(encrypted, usingAnyOf: keys)
} else {
nil
}
guard let value, !value.isEmpty else { continue }
records.append(ChromiumCookieRecord(
domain: AlibabaCodingPlanCookieImporter.normalizeCookieDomain(hostKey),
name: name,
path: path,
value: value,
expires: self.chromiumExpiry(sqlite3_column_int64(stmt, 3)),
isSecure: sqlite3_column_int(stmt, 4) != 0))
}
return records.filter { record in
guard let expires = record.expires else { return true }
return expires >= Date()
}
}
private static func derivedKeys(for browser: Browser) throws -> [Data] {
var keys: [Data] = []
var sawDenied = false
for label in browser.safeStorageLabels {
switch KeychainAccessPreflight.checkGenericPassword(service: label.service, account: label.account) {
case .interactionRequired:
sawDenied = true
continue
case .allowed, .notFound, .failure:
break
}
if let password = self.safeStoragePassword(service: label.service, account: label.account) {
keys.append(self.deriveKey(from: password))
}
}
if !keys.isEmpty {
return keys
}
if sawDenied {
throw ImportError.keychainDenied(browser: browser)
}
throw ImportError.keyUnavailable(browser: browser)
}
private static func safeStoragePassword(service: String, account: String) -> String? {
// The preflight classifies prompt-requiring items as .interactionRequired, but its
// .notFound (gate disabled) and .failure outcomes still reach this read. Honor the
// access gate and keep the read strictly non-interactive so it can never prompt.
guard !KeychainAccessGate.isDisabled else { return nil }
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
KeychainNoUIQuery.apply(to: &query)
var result: AnyObject?
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
private static func deriveKey(from password: String) -> Data {
let salt = Data("saltysalt".utf8)
var key = Data(count: kCCKeySizeAES128)
let keyLength = key.count
_ = key.withUnsafeMutableBytes { keyBytes in
password.utf8CString.withUnsafeBytes { passBytes in
salt.withUnsafeBytes { saltBytes in
CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
passBytes.bindMemory(to: Int8.self).baseAddress,
passBytes.count - 1,
saltBytes.bindMemory(to: UInt8.self).baseAddress,
salt.count,
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA1),
1003,
keyBytes.bindMemory(to: UInt8.self).baseAddress,
keyLength)
}
}
}
return key
}
private static func decrypt(_ encryptedValue: Data, usingAnyOf keys: [Data]) -> String? {
for key in keys {
if let value = self.decrypt(encryptedValue, key: key) {
return value
}
}
return nil
}
private static func decrypt(_ encryptedValue: Data, key: Data) -> String? {
guard encryptedValue.count > 3 else { return nil }
let prefix = String(data: encryptedValue.prefix(3), encoding: .utf8)
guard prefix == "v10" else { return nil }
let payload = Data(encryptedValue.dropFirst(3))
let iv = Data(repeating: 0x20, count: kCCBlockSizeAES128)
var outLength = 0
var out = Data(count: payload.count + kCCBlockSizeAES128)
let outCapacity = out.count
let status = out.withUnsafeMutableBytes { outBytes in
payload.withUnsafeBytes { payloadBytes in
key.withUnsafeBytes { keyBytes in
iv.withUnsafeBytes { ivBytes in
CCCrypt(
CCOperation(kCCDecrypt),
CCAlgorithm(kCCAlgorithmAES),
CCOptions(kCCOptionPKCS7Padding),
keyBytes.baseAddress,
key.count,
ivBytes.baseAddress,
payloadBytes.baseAddress,
payload.count,
outBytes.baseAddress,
outCapacity,
&outLength)
}
}
}
}
guard status == kCCSuccess else { return nil }
out.count = outLength
if let value = String(data: out, encoding: .utf8), !value.isEmpty {
return value
}
if out.count > 32 {
let trimmed = out.dropFirst(32)
if let value = String(data: trimmed, encoding: .utf8), !value.isEmpty {
return value
}
}
return nil
}
private static func makeCookie(from record: ChromiumCookieRecord) -> HTTPCookie? {
var properties: [HTTPCookiePropertyKey: Any] = [
.domain: record.domain,
.path: record.path,
.name: record.name,
.value: record.value,
]
if record.isSecure {
properties[.secure] = true
}
if let expires = record.expires {
properties[.expires] = expires
}
return HTTPCookie(properties: properties)
}
private static func readText(_ stmt: OpaquePointer?, index: Int32) -> String? {
guard sqlite3_column_type(stmt, index) != SQLITE_NULL,
let value = sqlite3_column_text(stmt, index)
else {
return nil
}
return String(cString: value)
}
private static func readBlob(_ stmt: OpaquePointer?, index: Int32) -> Data? {
guard sqlite3_column_type(stmt, index) != SQLITE_NULL,
let bytes = sqlite3_column_blob(stmt, index)
else {
return nil
}
return Data(bytes: bytes, count: Int(sqlite3_column_bytes(stmt, index)))
}
private static func matches(domain: String, patterns: [String]) -> Bool {
AlibabaCodingPlanCookieImporter.matchesCookieDomain(domain, patterns: patterns)
}
private static func chromiumExpiry(_ expiresUTC: Int64) -> Date? {
guard expiresUTC > 0 else { return nil }
let seconds = (Double(expiresUTC) / 1_000_000.0) - 11_644_473_600.0
guard seconds > 0 else { return nil }
return Date(timeIntervalSince1970: seconds)
}
}
#endif