Use swift-deps, fix things

This commit is contained in:
Kabir Oberai
2024-12-24 02:04:03 +05:30
parent 7e0bd6310b
commit 65257ae43f
34 changed files with 534 additions and 426 deletions

View File

@@ -1,5 +1,5 @@
{
"originHash" : "5b3298fb2e08290a3e47a886cedfefce16ee07f46d1f0ccdc86fcdd488f8c170",
"originHash" : "19c490b9f4eaa20eede93f53a3b352116957402a682ed84a47bfaf23c195de03",
"pins" : [
{
"identity" : "aexml",
@@ -28,6 +28,15 @@
"version" : "5.5.0"
}
},
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/combine-schedulers",
"state" : {
"revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13",
"version" : "1.0.2"
}
},
{
"identity" : "jsonutilities",
"kind" : "remoteSourceControl",
@@ -127,6 +136,15 @@
"version" : "1.6.1"
}
},
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
"revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53",
"version" : "1.0.5"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
@@ -154,6 +172,15 @@
"version" : "3.9.1"
}
},
{
"identity" : "swift-dependencies",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies",
"state" : {
"revision" : "5526c8a27675dc7b18d6fa643abfb64bcb200b77",
"version" : "1.6.2"
}
},
{
"identity" : "swift-http-types",
"kind" : "remoteSourceControl",
@@ -262,6 +289,15 @@
"version" : "1.0.2"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax",
"state" : {
"revision" : "0687f71944021d616d34d922343dcef086855920",
"version" : "600.0.1"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
@@ -334,6 +370,15 @@
"version" : "8.16.0"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1",
"version" : "1.4.3"
}
},
{
"identity" : "yams",
"kind" : "remoteSourceControl",

View File

@@ -46,6 +46,7 @@ let package = Package(
.package(url: "https://github.com/vapor/websocket-kit.git", from: "2.15.0"),
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.2"),
.package(url: "https://github.com/attaswift/BigInt", from: "5.5.0"),
.package(url: "https://github.com/mxcl/Version", from: "2.1.0"),
@@ -73,6 +74,7 @@ let package = Package(
"CSupersign",
.byName(name: "CSupersette", condition: .when(platforms: [.linux])),
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "SwiftyMobileDevice", package: "SwiftyMobileDevice"),
.product(name: "Zupersign", package: "zsign"),
.product(name: "SignerSupport", package: "SuperchargeCore"),

View File

@@ -221,7 +221,8 @@ public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation {
let mobileprovision = try await DeveloperServicesFetchProfileOperation(
context: self.context,
bundleID: newBundleID
bundleID: newBundleID,
signingInfo: signingInfo
).perform()
return ProvisioningInfo(

View File

@@ -8,6 +8,7 @@
import Foundation
import DeveloperAPI
import Dependencies
public typealias DeveloperServicesCertificate = Components.Schemas.Certificate
@@ -31,6 +32,8 @@ public struct DeveloperServicesFetchCertificateOperation: DeveloperServicesOpera
}
}
@Dependency(\.signingInfoManager) var signingInfoManager
public let context: SigningContext
public let confirmRevocation: @Sendable ([DeveloperServicesCertificate]) async -> Bool
public init(
@@ -84,14 +87,14 @@ public struct DeveloperServicesFetchCertificateOperation: DeveloperServicesOpera
try await group.waitForAll()
}
let signingInfo = try await createCertificate()
self.context.signingInfoManager[self.context.auth.identityID] = signingInfo
signingInfoManager[self.context.auth.identityID] = signingInfo
return signingInfo
}
public func perform() async throws -> SigningInfo {
let certificates = try await context.developerAPIClient.certificatesGetCollection().ok.body.json.data
guard let signingInfo = self.context.signingInfoManager[self.context.auth.identityID] else {
guard let signingInfo = signingInfoManager[self.context.auth.identityID] else {
return try await self.replaceCertificates(certificates, requireConfirmation: true)
}

View File

@@ -3,18 +3,19 @@ import DeveloperAPI
import HTTPTypes
import OpenAPIRuntime
import OpenAPIURLSession
import Dependencies
extension DeveloperAPIClient {
public init(
auth: DeveloperAPIAuthData,
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
auth: DeveloperAPIAuthData
) {
@Dependency(\.httpClient) var httpClient
self.init(
serverURL: try! Servers.Server1.url(),
configuration: .init(
dateTranscoder: .iso8601WithFractionalSeconds
),
transport: httpFactory.makeClient().asOpenAPITransport,
transport: httpClient.asOpenAPITransport,
middlewares: [
auth.middleware
]
@@ -48,24 +49,21 @@ public enum DeveloperAPIAuthData: Sendable {
public struct XcodeAuthData: Sendable {
public var loginToken: DeveloperServicesLoginToken
public var deviceInfo: DeviceInfo
public var teamID: DeveloperServicesTeam.ID
public var anisetteDataProvider: AnisetteDataProvider
public init(
loginToken: DeveloperServicesLoginToken,
deviceInfo: DeviceInfo,
teamID: DeveloperServicesTeam.ID,
anisetteDataProvider: AnisetteDataProvider
teamID: DeveloperServicesTeam.ID
) {
self.loginToken = loginToken
self.deviceInfo = deviceInfo
self.teamID = teamID
self.anisetteDataProvider = anisetteDataProvider
}
}
public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware {
@Dependency(\.deviceInfoProvider) private var deviceInfoProvider
@Dependency(\.anisetteDataProvider) private var anisetteDataProvider
public var authData: XcodeAuthData
public init(authData: XcodeAuthData) {
@@ -88,6 +86,8 @@ public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware {
) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
var request = request
let deviceInfo = try deviceInfoProvider.fetch()
// General
request.headerFields[.acceptLanguage] = Locale.preferredLanguages.joined(separator: ", ")
request.headerFields[.accept] = "application/vnd.api+json"
@@ -102,7 +102,7 @@ public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware {
request.headerFields[.init(DeviceInfo.clientInfoKey)!] = """
<VirtualMac2,1> <macOS;15.1.1;24B91> <com.apple.AuthKit/1 (com.apple.dt.Xcode/23505)>
""" // deviceInfo.clientInfo.clientString
request.headerFields[.init(DeviceInfo.deviceIDKey)!] = authData.deviceInfo.deviceID
request.headerFields[.init(DeviceInfo.deviceIDKey)!] = deviceInfo.deviceID
// GrandSlam authentication
request.headerFields[.init("X-Apple-App-Info")!] = AppTokenKey.xcode.rawValue
@@ -110,7 +110,7 @@ public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware {
request.headerFields[.init("X-Apple-GS-Token")!] = authData.loginToken.token
// Anisette
let anisetteData = try await authData.anisetteDataProvider.fetchAnisetteData()
let anisetteData = try await anisetteDataProvider.fetchAnisetteData()
for (key, value) in anisetteData.dictionary {
request.headerFields[.init(key)!] = value
}

View File

@@ -7,6 +7,7 @@
//
import Foundation
import Dependencies
public struct DeveloperServicesClient: Sendable {
@@ -53,28 +54,18 @@ public struct DeveloperServicesClient: Sendable {
}
}
public let loginToken: DeveloperServicesLoginToken
public let deviceInfo: DeviceInfo
public let anisetteDataProvider: AnisetteDataProvider
private let httpClient: HTTPClientProtocol
@Dependency(\.deviceInfoProvider) var deviceInfoProvider
@Dependency(\.anisetteDataProvider) var anisetteDataProvider
@Dependency(\.httpClient) var httpClient
public init(
loginToken: DeveloperServicesLoginToken,
deviceInfo: DeviceInfo,
anisetteProvider: AnisetteDataProvider,
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
) {
public let loginToken: DeveloperServicesLoginToken
public init(loginToken: DeveloperServicesLoginToken) {
self.loginToken = loginToken
self.deviceInfo = deviceInfo
self.anisetteDataProvider = anisetteProvider
self.httpClient = httpFactory.makeClient()
}
public init(authData: XcodeAuthData) {
self.loginToken = authData.loginToken
self.deviceInfo = authData.deviceInfo
self.anisetteDataProvider = authData.anisetteDataProvider
self.httpClient = defaultHTTPClientFactory.makeClient()
}
private func send<R: DeveloperServicesRequest>(
@@ -85,6 +76,8 @@ public struct DeveloperServicesClient: Sendable {
throw Error.malformedRequest
}
let deviceInfo = try deviceInfoProvider.fetch()
var httpRequest = HTTPRequest(url: url, method: "POST")
let acceptedLanguages = Locale.preferredLanguages.joined(separator: ", ")

View File

@@ -7,6 +7,7 @@
//
import Foundation
import Dependencies
public struct DeveloperServicesLoginToken: Codable, Sendable {
public let adsid: String
@@ -26,23 +27,9 @@ public struct DeveloperServicesLoginManager: Sendable {
case missingLoginToken
}
public let deviceInfo: DeviceInfo
public let anisetteProvider: AnisetteDataProvider
private let client: GrandSlamClient
private let client = GrandSlamClient()
public init(
deviceInfo: DeviceInfo,
anisetteProvider: AnisetteDataProvider,
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
) throws {
self.deviceInfo = deviceInfo
self.anisetteProvider = anisetteProvider
self.client = GrandSlamClient(
deviceInfo: deviceInfo,
anisetteProvider: anisetteProvider,
httpFactory: httpFactory
)
}
public init() {}
private func logIn(
withLoginData loginData: GrandSlamLoginData

View File

@@ -20,9 +20,11 @@ public struct DeveloperServicesFetchProfileOperation: DeveloperServicesOperation
public let context: SigningContext
public let bundleID: String
public init(context: SigningContext, bundleID: String) {
public let signingInfo: SigningInfo
public init(context: SigningContext, bundleID: String, signingInfo: SigningInfo) {
self.context = context
self.bundleID = bundleID
self.signingInfo = signingInfo
}
public func perform() async throws -> Mobileprovision {
@@ -47,18 +49,7 @@ public struct DeveloperServicesFetchProfileOperation: DeveloperServicesOperation
throw Errors.tooManyMatchingBundleIDs
}
// note: free developer accounts don't seem to persist profiles at all
// so this will often be empty.
let profiles = bundleIDs
.included?
.compactMap { included -> Components.Schemas.Profile? in
if case .Profile(let profile) = included, profile.relationships?.bundleId?.data?.id == bundleID.id {
profile
} else {
nil
}
} ?? []
let profiles = bundleID.relationships?.profiles?.data ?? []
switch profiles.count {
case 0:
// we're good
@@ -70,23 +61,36 @@ public struct DeveloperServicesFetchProfileOperation: DeveloperServicesOperation
break
}
let serialNumber = signingInfo.certificate.serialNumber()
let certs = try await context.developerAPIClient.certificatesGetCollection(
query: .init(
filter_lbrack_serialNumber_rbrack_: [serialNumber]
)
)
.ok.body.json.data
let allDevices = try await context.developerAPIClient.devicesGetCollection()
.ok.body.json.data
let response = try await context.developerAPIClient.profilesCreateInstance(
body: .json(
.init(
data: .init(
_type: .profiles,
attributes: .init(
name: "SC profile \(bundleID)",
name: "SC profile \(bundleID.id)",
profileType: .iosAppDevelopment
),
relationships: .init(
bundleId: .init(
data: .init(
_type: .bundleIds,
id: bundleID.id
)
data: .init(_type: .bundleIds, id: bundleID.id)
),
certificates: .init(data: [])
devices: .init(data: allDevices.map {
.init(_type: .devices, id: $0.id)
}),
certificates: .init(data: certs.map {
.init(_type: .certificates, id: $0.id)
})
)
)
)

View File

@@ -1,6 +1,7 @@
import Foundation
import Crypto
import ConcurrencyExtras
import Dependencies
public protocol RawADIProvider: Sendable {
func clientInfo() async throws -> String
@@ -26,6 +27,43 @@ extension RawADIProvider {
}
}
private struct UnimplementedRawADIProvider: RawADIProvider {
func startProvisioning(
spim: Data,
userID: UUID
) async throws -> (any RawADIProvisioningSession, Data) {
let closure: (Data, UUID) async throws -> (any RawADIProvisioningSession, Data) = unimplemented()
return try await closure(spim, userID)
}
func requestOTP(
userID: UUID,
routingInfo: inout UInt64,
provisioningInfo: Data
) async throws -> (machineID: Data, otp: Data) {
let closure: (UUID, inout UInt64, Data) async throws -> (Data, Data) = unimplemented()
return try await closure(userID, &routingInfo, provisioningInfo)
}
}
public enum RawADIProviderDependencyKey: DependencyKey {
public static let testValue: RawADIProvider = UnimplementedRawADIProvider()
public static let liveValue: RawADIProvider = {
#if os(Linux)
return SupersetteADIProvider()
#else
return OmnisetteADIProvider()
#endif
}()
}
extension DependencyValues {
public var rawADIProvider: RawADIProvider {
get { self[RawADIProviderDependencyKey.self] }
set { self[RawADIProviderDependencyKey.self] = newValue }
}
}
public protocol RawADIProvisioningSession: Sendable {
func endProvisioning(
routingInfo: UInt64,
@@ -35,7 +73,7 @@ public protocol RawADIProvisioningSession: Sendable {
}
// uses CoreADI APIs
public final class ADIDataProvider: AnisetteDataProvider {
public struct ADIDataProvider: AnisetteDataProvider {
public enum ADIError: Error {
case hashingFailed
@@ -44,41 +82,28 @@ public final class ADIDataProvider: AnisetteDataProvider {
case badEndResponse
}
public let rawProvider: RawADIProvider
public let deviceInfo: DeviceInfo
public let storage: KeyValueStorage
@Dependency(\.keyValueStorage) var storage
@Dependency(\.rawADIProvider) var rawProvider
@Dependency(\.httpClient) var httpClient
private let httpClient: HTTPClientProtocol
private let lookupManager: GrandSlamLookupManager
private let lookupManager = GrandSlamLookupManager()
private let localUserUID: UUID
private let localUserID: String
private let _clientInfo = LockIsolated<String?>(nil)
public init(
rawProvider: RawADIProvider,
deviceInfo: DeviceInfo,
storage: KeyValueStorage, // ideally secure, eg keychain
provisioningData: ProvisioningData? = nil,
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
) throws {
self.rawProvider = rawProvider
self.deviceInfo = deviceInfo
self.storage = storage
self.httpClient = httpFactory.makeClient()
self.lookupManager = .init(deviceInfo: deviceInfo, httpFactory: httpFactory)
public init(provisioningData: ProvisioningData? = nil) {
@Dependency(\.keyValueStorage) var storage
if let provisioningData {
self.localUserUID = provisioningData.localUserUID
try? storage.setData(provisioningData.adiPb, forKey: Self.provisioningKey)
try? storage.setString("\(provisioningData.routingInfo)", forKey: Self.routingInfoKey)
} else if let localUserUIDString = try storage.string(forKey: Self.localUserUIDKey),
} else if let localUserUIDString = try? storage.string(forKey: Self.localUserUIDKey),
let localUserUID = UUID(uuidString: localUserUIDString) {
self.localUserUID = localUserUID
} else {
let localUserUID = UUID()
try storage.setString(localUserUID.uuidString, forKey: Self.localUserUIDKey)
try? storage.setString(localUserUID.uuidString, forKey: Self.localUserUIDKey)
self.localUserUID = localUserUID
}
// localUserID = SHA256(local user UID)

View File

@@ -7,6 +7,7 @@
//
import Foundation
import Dependencies
public protocol AnisetteDataProvider: Sendable {
// This is a suggestion and not a requirement.
@@ -24,5 +25,24 @@ public struct ProvisioningData: Hashable, Codable, Sendable {
extension AnisetteDataProvider {
public func provisioningData() -> ProvisioningData? { nil }
public func setProvisioningData(_ data: ProvisioningData) {}
public func resetProvisioning() async {}
}
extension DependencyValues {
public var anisetteDataProvider: AnisetteDataProvider {
get { self[AnisetteDataProviderDependencyKey.self] }
set { self[AnisetteDataProviderDependencyKey.self] = newValue }
}
}
public struct AnisetteDataProviderDependencyKey: DependencyKey {
public static let testValue: AnisetteDataProvider = UnimplementedAnisetteDataProvider()
public static let liveValue: AnisetteDataProvider = ADIDataProvider()
}
private struct UnimplementedAnisetteDataProvider: AnisetteDataProvider {
func fetchAnisetteData() async throws -> AnisetteData {
let closure: () async throws -> AnisetteData = unimplemented()
return try await closure()
}
}

View File

@@ -7,6 +7,7 @@
//
import Foundation
import Dependencies
public struct DeviceInfo: Codable, Sendable {
@@ -90,3 +91,20 @@ public struct DeviceInfo: Codable, Sendable {
}
}
public struct DeviceInfoProvider: TestDependencyKey, Sendable {
public var fetch: @Sendable () throws -> DeviceInfo
public init(fetch: @escaping @Sendable () throws -> DeviceInfo) {
self.fetch = fetch
}
public static let testValue = DeviceInfoProvider(fetch: unimplemented())
}
extension DependencyValues {
public var deviceInfoProvider: DeviceInfoProvider {
get { self[DeviceInfoProvider.self] }
set { self[DeviceInfoProvider.self] = newValue }
}
}

View File

@@ -1,17 +1,17 @@
import Foundation
import Dependencies
struct OmnisetteADIProvider: RawADIProvider {
@Dependency(\.httpClient) private var client
// should implement v3 of https://github.com/SideStore/omnisette-server
// list: https://servers.sidestore.io/servers.json
// e.g. https://ani.sidestore.io
private let url: URL
private let client: HTTPClientProtocol
init(
url: URL = URL(string: "https://ani.sidestore.io")!, // URL(string: "http://localhost:6969")!,
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
url: URL = URL(string: "https://ani.sidestore.io")! // URL(string: "http://localhost:6969")!
) {
self.url = url
self.client = httpFactory.makeClient()
}
static let decoder: JSONDecoder = {
@@ -182,27 +182,3 @@ extension UUID {
withUnsafeBytes(of: uuid) { Data($0) }
}
}
extension ADIDataProvider {
public static func adiProvider(
deviceInfo: DeviceInfo,
storage: KeyValueStorage,
provisioningData: ProvisioningData? = nil,
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
) throws -> ADIDataProvider {
#if os(Linux)
let provider = SupersetteADIProvider(
configDirectory: URL.homeDirectory.appending(path: ".config/Supercharge/Anisette")
)
#else
let provider = OmnisetteADIProvider()
#endif
return try ADIDataProvider(
rawProvider: provider,
deviceInfo: deviceInfo,
storage: storage,
provisioningData: provisioningData,
httpFactory: httpFactory
)
}
}

View File

@@ -2,19 +2,15 @@
import Foundation
import CSupersette
import Dependencies
public actor SupersetteADIProvider: RawADIProvider {
@MainActor private static var loadTask: Task<Void, Error>?
@Dependency(\.httpClient) private var httpClient
public let directory: URL
public let httpClient: HTTPClientProtocol
public init(
configDirectory: URL,
httpClientFactory: HTTPClientFactory = defaultHTTPClientFactory
) {
self.directory = configDirectory
self.httpClient = httpClientFactory.makeClient()
public init() {
self.directory = URL.homeDirectory.appending(path: ".config/Supercharge/Anisette")
}
private func _loadADI(id: UUID) async throws {

View File

@@ -7,28 +7,23 @@
//
import Foundation
import Dependencies
final class GrandSlamClient: Sendable {
struct GrandSlamClient: Sendable {
private let encoder = PropertyListEncoder()
private let lookupManager: GrandSlamLookupManager
private let lookupManager = GrandSlamLookupManager()
let deviceInfo: DeviceInfo
let anisetteDataProvider: AnisetteDataProvider
private let httpClient: HTTPClientProtocol
init(
deviceInfo: DeviceInfo,
anisetteProvider: AnisetteDataProvider,
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
) {
self.deviceInfo = deviceInfo
self.anisetteDataProvider = anisetteProvider
self.lookupManager = .init(deviceInfo: deviceInfo, httpFactory: httpFactory)
self.httpClient = httpFactory.makeClient()
}
@Dependency(\.deviceInfoProvider) var deviceInfoProvider
@Dependency(\.anisetteDataProvider) var anisetteDataProvider
@Dependency(\.httpClient) var httpClient
init() {}
func send<R: GrandSlamRequest>(_ request: R) async throws -> R.Decoder.Value {
let deviceInfo = try deviceInfoProvider.fetch()
let anisetteData = try await anisetteDataProvider.fetchAnisetteData()
let url = try await lookupManager.fetchURL(forEndpoint: R.endpoint)

View File

@@ -7,6 +7,7 @@
//
import Foundation
import Dependencies
actor GrandSlamLookupManager {
@@ -19,14 +20,14 @@ actor GrandSlamLookupManager {
private let decoder = PropertyListDecoder()
private var endpoints: GrandSlamEndpoints?
let httpClient: HTTPClientProtocol
let deviceInfo: DeviceInfo
init(deviceInfo: DeviceInfo, httpFactory: HTTPClientFactory = defaultHTTPClientFactory) {
self.deviceInfo = deviceInfo
self.httpClient = httpFactory.makeClient()
}
@Dependency(\.deviceInfoProvider) var deviceInfoProvider
@Dependency(\.httpClient) var httpClient
init() {}
private func performLookup() async throws -> GrandSlamEndpoints {
let deviceInfo = try deviceInfoProvider.fetch()
/* {
"X-Apple-I-Locale" = "en_IN";
"X-Apple-I-TimeZone" = "Asia/Kolkata";

View File

@@ -15,6 +15,22 @@ import NIOFoundationCompat
import WebSocketKit
import OpenAPIRuntime
import OpenAPIAsyncHTTPClient
import Dependencies
extension HTTPClientDependencyKey: DependencyKey {
public static let liveValue: HTTPClientProtocol = {
// if ssl cert parsing fails we're screwed so we might as well force try
// swiftlint:disable:next force_try
let appleRootCA = try! NIOSSLCertificate(bytes: Array(appleRootPEM.utf8), format: .pem)
var tlsConfiguration: TLSConfiguration = .makeClientConfiguration()
tlsConfiguration.additionalTrustRoots = [.certificates([appleRootCA])]
let config = HTTPClient.Configuration(
tlsConfiguration: tlsConfiguration,
decompression: .enabled(limit: .none)
)
return HTTPClient(configuration: config)
}()
}
extension HTTPClient: HTTPClientProtocol {
@discardableResult
@@ -118,53 +134,34 @@ private final class WebSocketSessionWrapper: WebSocketSession {
}
}
final class AsyncHTTPClientFactory: HTTPClientFactory {
private static let appleRootPEM = """
-----BEGIN CERTIFICATE-----
MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET
MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv
biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0
MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw
bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx
FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+
+FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1
XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w
tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW
q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM
aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E
BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3
R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE
ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93
d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl
IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0
YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj
b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp
Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc
NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP
y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7
R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg
xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP
IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX
UKqK1drk/NAJBzewdXUh
-----END CERTIFICATE-----
"""
private let client: HTTPClient
private init() {
// if ssl cert parsing fails we're screwed so we might as well force try
// swiftlint:disable:next force_try
let appleRootCA = try! NIOSSLCertificate(bytes: Array(Self.appleRootPEM.utf8), format: .pem)
var tlsConfiguration: TLSConfiguration = .makeClientConfiguration()
tlsConfiguration.additionalTrustRoots = [.certificates([appleRootCA])]
let config = HTTPClient.Configuration(
tlsConfiguration: tlsConfiguration,
decompression: .enabled(limit: .none)
)
client = HTTPClient(configuration: config)
}
static let shared = AsyncHTTPClientFactory()
func makeClient() -> HTTPClientProtocol { client }
}
private let appleRootPEM = """
-----BEGIN CERTIFICATE-----
MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET
MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv
biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0
MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw
bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx
FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+
+FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1
XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w
tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW
q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM
aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E
BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3
R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE
ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93
d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl
IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0
YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj
b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp
Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc
NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP
y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7
R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg
xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP
IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX
UKqK1drk/NAJBzewdXUh
-----END CERTIFICATE-----
"""
#endif

View File

@@ -7,6 +7,8 @@
import Foundation
import OpenAPIRuntime
import Dependencies
import HTTPTypes
public struct HTTPRequest: Sendable {
public enum Body: Sendable {
@@ -57,6 +59,7 @@ public protocol HTTPClientProtocol: Sendable {
_ request: HTTPRequest,
onProgress: sending @isolated(any) (Double?) -> Void
) async throws -> HTTPResponse
func makeWebSocket(url: URL) async throws -> WebSocketSession
}
@@ -66,6 +69,44 @@ extension HTTPClientProtocol {
}
}
private struct UnimplementedHTTPClient: HTTPClientProtocol, ClientTransport {
public var asOpenAPITransport: ClientTransport { self }
func send(
_ request: HTTPTypes.HTTPRequest,
body: OpenAPIRuntime.HTTPBody?,
baseURL: URL,
operationID: String
) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
let closure: (HTTPTypes.HTTPRequest, OpenAPIRuntime.HTTPBody?, URL, String) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) = unimplemented()
return try await closure(request, body, baseURL, operationID)
}
func makeRequest(
_ request: HTTPRequest,
onProgress: sending @isolated(any) (Double?) -> Void
) async throws -> HTTPResponse {
let closure: () throws -> HTTPResponse = unimplemented()
return try closure()
}
public func makeWebSocket(url: URL) async throws -> any WebSocketSession {
let closure: (URL) async throws -> any WebSocketSession = unimplemented()
return try await closure(url)
}
}
public enum HTTPClientDependencyKey: TestDependencyKey {
public static let testValue: HTTPClientProtocol = UnimplementedHTTPClient()
}
extension DependencyValues {
public var httpClient: HTTPClientProtocol {
get { self[HTTPClientDependencyKey.self] }
set { self[HTTPClientDependencyKey.self] = newValue }
}
}
public protocol WebSocketSession: Sendable {
func receive() async throws -> WebSocketMessage
func send(_ message: WebSocketMessage) async throws
@@ -76,15 +117,3 @@ public enum WebSocketMessage: Sendable {
case text(String)
case data(Data)
}
public protocol HTTPClientFactory: Sendable {
func makeClient() -> HTTPClientProtocol
}
public let defaultHTTPClientFactory: HTTPClientFactory = {
#if os(Linux)
return AsyncHTTPClientFactory.shared
#else
return URLHTTPClientFactory.shared
#endif
}()

View File

@@ -10,15 +10,12 @@ import Foundation
import ConcurrencyExtras
import OpenAPIRuntime
import OpenAPIURLSession
import Dependencies
public struct UnknownHTTPError: Error {}
final class URLHTTPClientFactory: HTTPClientFactory {
static let shared = URLHTTPClientFactory()
private let client = Client()
func makeClient() -> HTTPClientProtocol { client }
extension HTTPClientDependencyKey: DependencyKey {
public static let liveValue: HTTPClientProtocol = Client()
}
private final class Client: HTTPClientProtocol {

View File

@@ -9,6 +9,7 @@
import Foundation
import SwiftyMobileDevice
import ConcurrencyExtras
import Dependencies
extension LockdownClient {
static let installerLabel = "supersign"
@@ -17,39 +18,11 @@ extension LockdownClient {
#if !os(iOS)
public protocol IntegratedInstallerDelegate: AnyObject, Sendable {
func setPresentedMessage(_ message: IntegratedInstaller.Message?)
func installerDidUpdate(toStage stage: String, progress: Double?)
// defaults to always returning true
func confirmRevocation(of certificates: [DeveloperServicesCertificate]) async -> Bool
/// Decompress the zipped ipa file
///
/// - Parameter ipa: The `ipa` file to decompress.
/// - Parameter directory: The directory into which `ipa` should be decompressed.
/// - Parameter progress: A closure to which the callee can provide progress updates.
/// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate.
func decompress(
ipa: URL,
in directory: URL,
progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
) async throws
// `compress` is required because the only way to upload symlinks via afc is by
// putting them in a zip archive (afc_make_symlink was disabled due to security or
// something)
/// Compress the app before installation.
///
/// - Parameter payloadDir: The `Payload` directory which is to be compressed.
/// - Parameter progress: A closure to which the callee can provide progress updates.
/// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate.
func compress(
payloadDir: URL,
progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
) async throws -> URL
}
extension IntegratedInstallerDelegate {
@@ -83,9 +56,10 @@ public actor IntegratedInstaller {
let lookupMode: LookupMode
let auth: DeveloperAPIAuthData
let configureDevice: Bool
let storage: KeyValueStorage
public weak var delegate: IntegratedInstallerDelegate?
@Dependency(\.zipCompressor) private var compressor
private var appInstaller: AppInstaller?
private let tempDir = FileManager.default.temporaryDirectoryShim
@@ -142,14 +116,12 @@ public actor IntegratedInstaller {
lookupMode: LookupMode,
auth: DeveloperAPIAuthData,
configureDevice: Bool,
storage: KeyValueStorage,
delegate: IntegratedInstallerDelegate
) {
self.udid = udid
self.lookupMode = lookupMode
self.auth = auth
self.configureDevice = configureDevice
self.storage = storage
self.delegate = delegate
}
@@ -251,8 +223,8 @@ public actor IntegratedInstaller {
switch app.pathExtension {
case "ipa":
try await delegate?.decompress(
ipa: app,
try await compressor.decompress(
file: app,
in: tempDir,
progress: { progress in
self.queueUpdateTask {
@@ -305,16 +277,10 @@ public actor IntegratedInstaller {
try await updateProgress(to: 1)
// guard let deviceInfo = DeviceInfo.current() else {
// throw Error.deviceInfoFetchFailed
// }
// let anisetteProvider = try ADIDataProvider.adiProvider(deviceInfo: deviceInfo, storage: storage)
let context = try SigningContext(
udid: udid,
deviceName: deviceName,
auth: auth,
signingInfoManager: KeyValueSigningInfoManager(storage: storage)
auth: auth
)
let signer = Signer(context: context) { certs in
@@ -335,26 +301,13 @@ public actor IntegratedInstaller {
didProvision: { @Sendable [self] in
// TODO: reintroduce Superconfig
_ = self
// if let pairingKeys = pairingKeys {
// let info = try context.signingInfoManager.info(forTeamID: context.teamID)
// try Superconfig(
// udid: udid,
// pairingKeys: pairingKeys,
// deviceInfo: deviceInfo,
// preferredTeamID: teamID.rawValue,
// preferredSigningInfo: info,
// appleID: appleID,
// provisioningData: anisetteProvider.provisioningData(),
// token: token
// ).save(inAppDir: appDir)
// }
}
)
try await self.updateStage(to: "Packaging", initialProgress: nil)
let ipa = try await delegate?.compress(
payloadDir: appDir.deletingLastPathComponent(),
let ipa = try await compressor.compress(
directory: appDir.deletingLastPathComponent(),
progress: { progress in
self.queueUpdateTask {
$0.updateProgressIgnoringCancellation(to: progress)
@@ -364,7 +317,7 @@ public actor IntegratedInstaller {
try await self.updateProgress(to: 1)
let appInstaller = AppInstaller(ipa: ipa!, udid: udid, connectionPreferences: .init(lookupMode: lookupMode))
let appInstaller = AppInstaller(ipa: ipa, udid: udid, connectionPreferences: .init(lookupMode: lookupMode))
self.appInstaller = appInstaller
try await appInstaller.install(
progress: { stage in

View File

@@ -13,10 +13,7 @@ public struct SigningContext: Sendable {
public let udid: String
public let deviceName: String
public let auth: DeveloperAPIAuthData
public let signingInfoManager: SigningInfoManager
public let signerImpl: SignerImpl
public var developerAPIClient: DeveloperAPIClient {
@@ -27,13 +24,11 @@ public struct SigningContext: Sendable {
udid: String,
deviceName: String,
auth: DeveloperAPIAuthData,
signingInfoManager: SigningInfoManager,
signerImpl: SignerImpl? = nil
) throws {
self.udid = udid
self.deviceName = deviceName
self.auth = auth
self.signingInfoManager = signingInfoManager
self.signerImpl = try signerImpl ?? .first()
}

View File

@@ -8,6 +8,7 @@
import Foundation
import ConcurrencyExtras
import Dependencies
public enum KeyValueStorageError: Error {
case stringConversionFailure
@@ -21,6 +22,17 @@ public protocol KeyValueStorage: Sendable {
func setString(_ string: String?, forKey key: String) throws
}
public enum KeyValueStorageDependencyKey: TestDependencyKey {
public static let testValue: KeyValueStorage = UnimplementedKeyValueStorage()
}
extension DependencyValues {
public var keyValueStorage: KeyValueStorage {
get { self[KeyValueStorageDependencyKey.self] }
set { self[KeyValueStorageDependencyKey.self] = newValue }
}
}
extension KeyValueStorage {
public func string(forKey key: String) throws -> String? {
try data(forKey: key).map {
@@ -44,6 +56,16 @@ extension KeyValueStorage {
}
}
private struct UnimplementedKeyValueStorage: KeyValueStorage {
func data(forKey key: String) throws -> Data? {
unimplemented(placeholder: nil)
}
func setData(_ data: Data?, forKey key: String) throws {
unimplemented()
}
}
public final class MemoryKeyValueStorage: KeyValueStorage {
private let dict = LockIsolated<[String: Data]>([:])

View File

@@ -8,6 +8,7 @@
import Foundation
import ConcurrencyExtras
import Dependencies
public struct SigningInfo: Codable, Sendable {
public let privateKey: PrivateKey
@@ -19,6 +20,18 @@ public protocol SigningInfoManager: Sendable {
func setInfo(_ info: SigningInfo?, forIdentityID identityID: String) throws
}
public enum SigningInfoManagerDependencyKey: DependencyKey {
public static let testValue: SigningInfoManager = UnimplementedSigningInfoManager()
public static let liveValue: SigningInfoManager = KeyValueSigningInfoManager()
}
extension DependencyValues {
public var signingInfoManager: SigningInfoManager {
get { self[SigningInfoManagerDependencyKey.self] }
set { self[SigningInfoManagerDependencyKey.self] = newValue }
}
}
extension SigningInfoManager {
subscript(identityID: String) -> SigningInfo? {
get {
@@ -30,6 +43,16 @@ extension SigningInfoManager {
}
}
private struct UnimplementedSigningInfoManager: SigningInfoManager {
func info(forIdentityID identityID: String) throws -> SigningInfo? {
unimplemented(placeholder: nil)
}
func setInfo(_ info: SigningInfo?, forIdentityID identityID: String) throws {
unimplemented()
}
}
public final class MemoryBackedSigningInfoManager: SigningInfoManager {
private let infos = LockIsolated<[String: SigningInfo]>([:])
@@ -50,10 +73,7 @@ public struct KeyValueSigningInfoManager: SigningInfoManager {
private let encoder = PropertyListEncoder()
private let decoder = PropertyListDecoder()
public let storage: KeyValueStorage
public init(storage: KeyValueStorage) {
self.storage = storage
}
@Dependency(\.keyValueStorage) var storage
public func info(forIdentityID identityID: String) throws -> SigningInfo? {
guard let data = try storage.data(forKey: identityID)

View File

@@ -0,0 +1,65 @@
import Foundation
import Dependencies
public struct ZIPCompressor: TestDependencyKey, Sendable {
public var compress: @Sendable (
_ directory: URL,
_ progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
) async throws -> URL
public var decompress: @Sendable (
_ file: URL,
_ directory: URL,
_ progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
) async throws -> Void
public init(
compress: @Sendable @escaping (URL, @Sendable @escaping (Double?) -> Void) async throws -> URL,
decompress: @Sendable @escaping (URL, URL, @Sendable @escaping (Double?) -> Void) async throws -> Void
) {
self.decompress = decompress
self.compress = compress
}
public static let testValue = ZIPCompressor(
compress: unimplemented(),
decompress: unimplemented()
)
/// Decompress the zipped ipa file
///
/// - Parameter ipa: The `ipa` file to decompress.
/// - Parameter directory: The directory into which `ipa` should be decompressed.
/// - Parameter progress: A closure to which the callee can provide progress updates.
/// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate.
public func decompress(
file: URL,
in directory: URL,
progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
) async throws {
try await decompress(file, directory, progress)
}
// `compress` is required because the only way to upload symlinks via afc is by
// putting them in a zip archive (afc_make_symlink was disabled due to security or
// something)
/// Compress the app before installation.
///
/// - Parameter payloadDir: The `Payload` directory which is to be compressed.
/// - Parameter progress: A closure to which the callee can provide progress updates.
/// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate.
public func compress(
directory: URL,
progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
) async throws -> URL {
try await compress(directory, progress)
}
}
extension DependencyValues {
public var zipCompressor: ZIPCompressor {
get { self[ZIPCompressor.self] }
set { self[ZIPCompressor.self] = newValue }
}
}

View File

@@ -0,0 +1,28 @@
import Foundation
import Supersign
import SupersignCLISupport
import Dependencies
@main enum SupersignCLIMain {
static func main() async throws {
try await SupersignCLI.run()
}
}
#warning("Improve persistence mechanism")
// for macOS, we can use KeychainStorage but we need to sign with entitlements.
// for Windows, we could use dpapi.h or wincred.h.
// for Linux, maybe use libsecret?
// see https://github.com/atom/node-keytar
extension KeyValueStorageDependencyKey: DependencyKey {
public static let liveValue: KeyValueStorage = {
// #if os(macOS)
// KeychainStorage(service: "com.kabiroberai.Supercharge-Keychain.credentials")
// #else
DirectoryStorage(
base: URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".config/Supercharge/data")
)
// #endif
}()
}

View File

@@ -1,25 +0,0 @@
import Foundation
import Supersign
import SupersignCLISupport
// let app = Bundle.module.url(forResource: "Supercharge", withExtension: "ipa")!
#warning("Improve persistence mechanism")
// for macOS, we can use KeychainStorage but we need to sign with entitlements.
// for Windows, we could use dpapi.h or wincred.h.
// for Linux, maybe use libsecret?
// see https://github.com/atom/node-keytar
let storage: KeyValueStorage
// #if os(macOS)
// storage = KeychainStorage(service: "com.kabiroberai.Supercharge-Keychain.credentials")
// #else
let directory = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".config/Supercharge/data")
storage = DirectoryStorage(base: directory)
// #endif
try await SupersignCLI.run(configuration: SupersignCLI.Configuration(
superchargeApp: nil,
storage: storage
))

View File

@@ -2,6 +2,7 @@ import Foundation
import Supersign
import ArgumentParser
import Crypto
import Dependencies
enum AuthMode: String, CaseIterable, CustomStringConvertible, ExpressibleByArgument {
case key
@@ -86,28 +87,15 @@ struct AuthOperation {
print("Logging in...")
let deviceInfo = try DeviceInfo.fetch()
let provider = try ADIDataProvider.adiProvider(
deviceInfo: deviceInfo,
storage: SupersignCLI.config.storage
)
let authDelegate = SupersignCLIAuthDelegate()
let manager = try DeveloperServicesLoginManager(
deviceInfo: deviceInfo,
anisetteProvider: provider
)
let manager = DeveloperServicesLoginManager()
let token = try await manager.logIn(
withUsername: username,
password: password,
twoFactorDelegate: authDelegate
)
let client = DeveloperServicesClient(
loginToken: token,
deviceInfo: deviceInfo,
anisetteProvider: provider
)
let client = DeveloperServicesClient(loginToken: token)
let teams = try await client.send(DeveloperServicesListTeamsRequest())
let team = try await Console.choose(
from: teams,
@@ -179,11 +167,8 @@ struct AuthLogoutCommand: AsyncParsableCommand {
}
if reset2FA {
try ADIDataProvider.adiProvider(
deviceInfo: .fetch(),
storage: SupersignCLI.config.storage
)
.resetProvisioning()
@Dependency(\.anisetteDataProvider) var anisetteProvider
await anisetteProvider.resetProvisioning()
print("Forgot device")
}
}
@@ -225,3 +210,8 @@ extension DeviceInfo {
return deviceInfo
}
}
extension DeviceInfoProvider: DependencyKey {
private static let current = Result { try DeviceInfo.fetch() }
public static let liveValue = DeviceInfoProvider { try current.get() }
}

View File

@@ -1,5 +1,6 @@
import Foundation
import Supersign
import Dependencies
enum AuthToken: Codable, CustomStringConvertible {
struct Xcode: Codable {
@@ -38,23 +39,28 @@ enum AuthToken: Codable, CustomStringConvertible {
extension AuthToken {
private static var storage: KeyValueStorage {
@Dependency(\.keyValueStorage) var storage
return storage
}
private static let encoder = JSONEncoder()
private static let decoder = JSONDecoder()
static func saved() throws -> Self {
guard let data = try SupersignCLI.config.storage.data(forKey: "SUPAuthToken") else {
guard let data = try storage.data(forKey: "SUPAuthToken") else {
throw Console.Error("Please log in with `supersign ds login` before running this command.")
}
return try decoder.decode(AuthToken.self, from: data)
}
static func clear() throws {
try SupersignCLI.config.storage.setData(nil, forKey: "SUPAuthToken")
try Self.storage.setData(nil, forKey: "SUPAuthToken")
}
func save() throws {
let data = try Self.encoder.encode(self)
try SupersignCLI.config.storage.setData(data, forKey: "SUPAuthToken")
try Self.storage.setData(data, forKey: "SUPAuthToken")
}
func authData() throws -> DeveloperAPIAuthData {
@@ -62,19 +68,13 @@ extension AuthToken {
case .appStoreConnect(let data):
return .appStoreConnect(.init(id: data.id, issuerID: data.issuerID, pem: data.pem))
case .xcode(let data):
let deviceInfo = try DeviceInfo.fetch()
return .xcode(.init(
loginToken: DeveloperServicesLoginToken(
adsid: data.adsid,
token: data.token,
expiry: data.expiry
),
deviceInfo: deviceInfo,
teamID: .init(rawValue: data.teamID),
anisetteDataProvider: try ADIDataProvider.adiProvider(
deviceInfo: deviceInfo,
storage: SupersignCLI.config.storage
)
teamID: .init(rawValue: data.teamID)
))
}
}

View File

@@ -2,6 +2,8 @@ import Foundation
import Supersign
import ArgumentParser
import DeveloperAPI
import OpenAPIRuntime
import Dependencies
struct DSTeamsListCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
@@ -62,13 +64,12 @@ struct DSAnisetteCommand: AsyncParsableCommand {
)
func run() async throws {
// swiftlint:disable:next force_try
let res = try! await ADIDataProvider(
rawProvider: Provider(),
deviceInfo: .current()!,
storage: SupersignCLI.config.storage
).fetchAnisetteData()
let provider = withDependencies {
$0.rawADIProvider = Provider()
} operation: {
ADIDataProvider()
}
let res = try await provider.fetchAnisetteData()
print(res)
}
}

View File

@@ -110,7 +110,6 @@ struct DevRunCommand: AsyncParsableCommand {
lookupMode: .only(client.connectionType),
auth: try token.authData(),
configureDevice: false,
storage: SupersignCLI.config.storage,
delegate: installDelegate
)

View File

@@ -2,9 +2,7 @@ import Foundation
import Supersign
import Version
import ArgumentParser
#if os(Linux)
import FoundationNetworking
#endif
import Dependencies
struct DevSDKCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
@@ -77,10 +75,9 @@ struct DarwinSDKVersions: Decodable {
var current: String
var metadata: [String: Metadata]
static func all(
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
) async throws -> DarwinSDKVersions {
let data = try await httpFactory.makeClient().makeRequest(HTTPRequest(url: url)).body ?? Data()
static func all() async throws -> DarwinSDKVersions {
@Dependency(\.httpClient) var httpClient
let data = try await httpClient.makeRequest(HTTPRequest(url: url)).body ?? Data()
return try decoder.decode(self, from: data)
}
}

View File

@@ -28,7 +28,6 @@ struct InstallCommand: AsyncParsableCommand {
lookupMode: .only(client.connectionType),
auth: try token.authData(),
configureDevice: false,
storage: SupersignCLI.config.storage,
delegate: installDelegate
)

View File

@@ -0,0 +1,50 @@
//
// File.swift
// Supersign
//
// Created by Kabir Oberai on 23/12/24.
//
import Foundation
import Supersign
import Dependencies
extension ZIPCompressor: DependencyKey {
// TODO: Use `powershell Compress-Archive` and `powershell Expand-Archive` on Windows
public static let liveValue = ZIPCompressor(
compress: { dir, progress in
progress(nil)
let dest = dir.deletingLastPathComponent().appendingPathComponent("app.ipa")
let zip = Process()
zip.executableURL = URL(fileURLWithPath: "/usr/bin/env")
zip.currentDirectoryURL = dir.deletingLastPathComponent()
zip.arguments = ["zip", "-yqru0", dest.path, "Payload"]
try zip.run()
await zip.waitForExit()
guard zip.terminationStatus == 0 else {
throw ZIPCompressorError.compressionFailed
}
return dest
},
decompress: { ipa, directory, progress in
progress(nil)
let unzip = Process()
unzip.executableURL = URL(fileURLWithPath: "/usr/bin/env")
unzip.arguments = ["unzip", "-q", ipa.path, "-d", directory.path]
try unzip.run()
await unzip.waitForExit()
guard unzip.terminationStatus == 0 else {
throw ZIPCompressorError.decompressionFailed
}
}
)
}
enum ZIPCompressorError: Error {
case compressionFailed
case decompressionFailed
}

View File

@@ -9,11 +9,6 @@ final class SupersignCLIAuthDelegate: TwoFactorAuthDelegate {
}
actor SupersignCLIDelegate: IntegratedInstallerDelegate {
public enum Error: Swift.Error {
case decompressionFailed
case compressionFailed
}
init() {}
private let updateTask = LockIsolated<Task<Void, Never>?>(nil)
@@ -96,57 +91,4 @@ actor SupersignCLIDelegate: IntegratedInstallerDelegate {
return false
}
}
// TODO: Use `powershell Compress-Archive` and `powershell Expand-Archive` on Windows
func decompress(
ipa: URL,
in directory: URL,
progress: @escaping (Double?) -> Void
) async throws {
progress(nil)
let unzip = Process()
unzip.executableURL = URL(fileURLWithPath: "/usr/bin/env")
unzip.arguments = ["unzip", "-q", ipa.path, "-d", directory.path]
try await unzip.launchAndWait()
guard unzip.terminationStatus == 0 else {
throw Error.decompressionFailed
}
}
func compress(
payloadDir: URL,
progress: @escaping (Double?) -> Void
) async throws -> URL {
progress(nil)
let dest = payloadDir.deletingLastPathComponent().appendingPathComponent("app.ipa")
let zip = Process()
zip.executableURL = URL(fileURLWithPath: "/usr/bin/env")
zip.currentDirectoryURL = payloadDir.deletingLastPathComponent()
zip.arguments = ["zip", "-yqru0", dest.path, "Payload"]
try await zip.launchAndWait()
guard zip.terminationStatus == 0 else { throw Error.compressionFailed }
return dest
}
}
extension Process {
fileprivate func launchAndWait() async throws {
try await withCheckedThrowingContinuation { continuation in
terminationHandler = { _ in
continuation.resume()
}
do {
try self.run()
} catch {
continuation.resume(throwing: error)
return
}
Task.detached { self.waitUntilExit() }
}
}
}

View File

@@ -3,24 +3,7 @@ import Supersign
import ArgumentParser
public enum SupersignCLI {
public struct Configuration: Sendable {
public let superchargeApp: URL?
public let storage: KeyValueStorage
public init(
superchargeApp: URL?,
storage: KeyValueStorage
) {
self.superchargeApp = superchargeApp
self.storage = storage
}
}
private static nonisolated(unsafe) var _config: Configuration!
static var config: Configuration { _config }
public static func run(configuration: Configuration, arguments: [String]? = nil) async throws {
_config = configuration
public static func run(arguments: [String]? = nil) async throws {
await SupersignCommand.cancellableMain(arguments)
}
}