Skip to content

Commit b0c9304

Browse files
authored
Merge branch 'XcodesOrg:main' into main
2 parents 073de69 + 93bce87 commit b0c9304

19 files changed

Lines changed: 738 additions & 58 deletions

File tree

.github/release-drafter.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ categories:
1111
- 'documentation'
1212
- 'dependencies'
1313
template: |
14-
If you installed xcodes with homebrew you can upgrade with `brew upgrade robotsandpencils/made/xcodes`.
14+
If you installed xcodes with homebrew you can upgrade with `brew upgrade xcodesorg/made/xcodes`.
1515
1616
## Changes
1717

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ name: CI
22
on: [push, pull_request]
33
jobs:
44
test:
5-
runs-on: macos-12
5+
runs-on: macos-15
66
steps:
77
- uses: actions/checkout@v4
88
- name: Run tests
99
env:
10-
DEVELOPER_DIR: /Applications/Xcode_13.4.1.app
10+
DEVELOPER_DIR: /Applications/Xcode_16.4.app
1111
run: swift test

.swiftpm/xcode/xcshareddata/xcschemes/xcodes.xcscheme

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,13 @@
128128
isEnabled = "NO">
129129
</CommandLineArgument>
130130
<CommandLineArgument
131-
argument = "runtimes install &quot;visionOS 1.0-beta1&quot;"
131+
argument = "runtimes install &quot;visionOS 26.0-beta3 arm64&quot;"
132132
isEnabled = "YES">
133133
</CommandLineArgument>
134+
<CommandLineArgument
135+
argument = "runtimes --include-betas"
136+
isEnabled = "NO">
137+
</CommandLineArgument>
134138
<CommandLineArgument
135139
argument = "install --help"
136140
isEnabled = "NO">

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Pull requests are the best way to propose changes to the codebase We actively w
2323
## Any contributions you make will be under the MIT Software License
2424
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
2525

26-
## Report bugs using Github's [issues](https://github.com/robotsandpencils/xcodes/issues)
26+
## Report bugs using Github's [issues](https://github.com/xcodesorg/xcodes/issues)
2727
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy!
2828

2929
## Write bug reports with detail, background, and sample code

Package.resolved

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ let package = Package(
2323
.package(url: "https://github.com/xcodereleases/data", revision: "fcf527b187817f67c05223676341f3ab69d4214d"),
2424
.package(url: "https://github.com/onevcat/Rainbow.git", .upToNextMinor(from: "3.2.0")),
2525
.package(url: "https://github.com/jpsim/Yams", .upToNextMinor(from: "5.0.1")),
26+
.package(url: "https://github.com/xcodesOrg/swift-srp", branch: "main")
2627
],
2728
targets: [
2829
.executableTarget(
@@ -50,7 +51,7 @@ let package = Package(
5051
"Version",
5152
.product(name: "XCModel", package: "data"),
5253
"Rainbow",
53-
"Yams",
54+
"Yams"
5455
]),
5556
.testTarget(
5657
name: "XcodesKitTests",
@@ -68,6 +69,7 @@ let package = Package(
6869
"PromiseKit",
6970
.product(name: "PMKFoundation", package: "Foundation"),
7071
"Rainbow",
72+
.product(name: "SRP", package: "swift-srp")
7173
]),
7274
.testTarget(
7375
name: "AppleAPITests",

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ git push --follow-tags
214214

215215
# Edit the draft release created by Release Drafter to point at the new tag
216216
# Set the release title to the new version
217-
# Add the xcodes.zip and xcodes-$VERSION.mojave.tar.gz files to the release
217+
# Duplicate xcodes-$VERSION.mojave.tar.gz and rename to xcodes-$VERSION.arm64_mojave.tar.gz, also create `xcodes-$VERSION.macos.i386.tar.gz` and `xcodes-$VERSION.macos.arm64.tar.gz`
218+
# Add the xcodes.zip, xcodes-$VERSION.mojave.tar.gz, xcodes-$VERSION.arm64_mojave.tar.gz files to the release
218219
# Publish the release
219220

220221
# Update the Homebrew Bottle: https://github.com/XcodesOrg/homebrew-made/blob/master/Formula/xcodes.rb

Sources/AppleAPI/Client.swift

Lines changed: 167 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import Foundation
22
import PromiseKit
33
import PMKFoundation
44
import Rainbow
5+
import SRP
6+
import Crypto
7+
import CommonCrypto
58

69
public class Client {
710
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
@@ -15,18 +18,23 @@ public class Client {
1518
case incorrectSecurityCode
1619
case unexpectedSignInResponse(statusCode: Int, message: String?)
1720
case appleIDAndPrivacyAcknowledgementRequired
21+
case serviceTemporarilyUnavailable
1822
case noTrustedPhoneNumbers
1923
case notAuthenticated
2024
case invalidHashcash
2125
case missingSecurityCodeInfo
2226
case accountUsesHardwareKey
27+
case srpInvalidPublicKey
28+
case srpError(String)
2329

2430
public var errorDescription: String? {
2531
switch self {
2632
case .invalidUsernameOrPassword(let username):
2733
return "Invalid username and password combination. Attempted to sign in with username \(username)."
2834
case .appleIDAndPrivacyAcknowledgementRequired:
2935
return "You must sign in to https://appstoreconnect.apple.com and acknowledge the Apple ID & Privacy agreement."
36+
case .serviceTemporarilyUnavailable:
37+
return "The service is temporarily unavailable. Please try again later."
3038
case .invalidPhoneNumberIndex(let min, let max, let given):
3139
return "Not a valid phone number index. Expecting a whole number between \(min)-\(max), but was given \(given ?? "nothing")."
3240
case .noTrustedPhoneNumbers:
@@ -56,6 +64,102 @@ public class Client {
5664
}
5765
}
5866

67+
/// SRPLogin - Secure Remote Password
68+
/// https://tools.ietf.org/html/rfc2945
69+
/// Forked from https://github.com/adam-fowler/swift-srp that provides the algorithm
70+
public func srpLogin(accountName: String, password: String) -> Promise<Void> {
71+
var serviceKey: String!
72+
let client = SRPClient(configuration: SRPConfiguration<SHA256>(.N2048))
73+
let clientKeys = client.generateKeys()
74+
let a = clientKeys.public
75+
76+
// Get the Service Key needed from olympus session needed in headers
77+
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
78+
Current.network.dataTask(with: URLRequest.itcServiceKey)
79+
}
80+
.then { (data, _) -> Promise<(serviceKey: String, hashcash: String)> in
81+
struct ServiceKeyResponse: Decodable {
82+
let authServiceKey: String?
83+
}
84+
85+
let response = try JSONDecoder().decode(ServiceKeyResponse.self, from: data)
86+
serviceKey = response.authServiceKey
87+
88+
/// Load a hashcash of the account name
89+
return self.loadHashcash(accountName: accountName, serviceKey: serviceKey).map { (serviceKey, $0) }
90+
}
91+
.then { (serviceKey, hashcash) -> Promise<(serviceKey: String, hashcash: String, data: Data)> in
92+
/// Call the SRP /init endpoint to start the login
93+
return Current.network.dataTask(with: URLRequest.SRPInit(serviceKey: serviceKey, a: Data(a.bytes).base64EncodedString(), accountName: accountName)).map { (serviceKey, hashcash, $0.data)}
94+
}
95+
.then { (serviceKey, hashcash, data) -> Promise<(data: Data, response: URLResponse)> in
96+
let srpInit = try JSONDecoder().decode(ServerSRPInitResponse.self, from: data)
97+
98+
guard let decodedB = Data(base64Encoded: srpInit.b) else {
99+
throw Error.srpInvalidPublicKey
100+
}
101+
guard let decodedSalt = Data(base64Encoded: srpInit.salt) else {
102+
throw Error.srpInvalidPublicKey
103+
}
104+
105+
let iterations = srpInit.iteration
106+
107+
do {
108+
guard let encryptedPassword = self.pbkdf2(password: password, saltData: decodedSalt, keyByteCount: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), rounds: iterations) else {
109+
throw Error.srpInvalidPublicKey
110+
}
111+
112+
let sharedSecret = try client.calculateSharedSecret(password: encryptedPassword, salt: [UInt8](decodedSalt), clientKeys: clientKeys, serverPublicKey: .init([UInt8](decodedB)))
113+
114+
let m1 = client.calculateClientProof(username: accountName, salt: [UInt8](decodedSalt), clientPublicKey: a, serverPublicKey: .init([UInt8](decodedB)), sharedSecret: .init(sharedSecret.bytes))
115+
let m2 = client.calculateServerProof(clientPublicKey: a, clientProof: m1, sharedSecret: .init([UInt8](sharedSecret.bytes)))
116+
117+
/// call the /complete endpoint passing in the hashcash, servicekey, and the calculated proof.
118+
return Current.network.dataTask(with: URLRequest.SRPComplete(serviceKey: serviceKey, hashcash: hashcash, accountName: accountName, c: srpInit.c, m1: Data(m1).base64EncodedString(), m2: Data(m2).base64EncodedString()))
119+
} catch {
120+
throw Error.srpError(error.localizedDescription)
121+
}
122+
}
123+
.then { (data, response) -> Promise<Void> in
124+
struct SignInResponse: Decodable {
125+
let authType: String?
126+
let serviceErrors: [ServiceError]?
127+
128+
struct ServiceError: Decodable, CustomStringConvertible {
129+
let code: String
130+
let message: String
131+
132+
var description: String {
133+
return "\(code): \(message)"
134+
}
135+
}
136+
}
137+
138+
let httpResponse = response as! HTTPURLResponse
139+
do {
140+
let responseBody = try JSONDecoder().decode(SignInResponse.self, from: data)
141+
switch httpResponse.statusCode {
142+
case 200:
143+
return Current.network.dataTask(with: URLRequest.olympusSession).asVoid()
144+
case 401:
145+
throw Error.invalidUsernameOrPassword(username: accountName)
146+
case 409:
147+
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
148+
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
149+
throw Error.appleIDAndPrivacyAcknowledgementRequired
150+
default:
151+
throw Error.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
152+
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", "))
153+
}
154+
} catch DecodingError.dataCorrupted where httpResponse.statusCode == 503 {
155+
throw Error.serviceTemporarilyUnavailable
156+
} catch {
157+
throw error
158+
}
159+
}
160+
}
161+
162+
@available(*, deprecated, message: "Please use srpLogin")
59163
public func login(accountName: String, password: String) -> Promise<Void> {
60164
var serviceKey: String!
61165

@@ -92,20 +196,25 @@ public class Client {
92196
}
93197

94198
let httpResponse = response as! HTTPURLResponse
95-
let responseBody = try JSONDecoder().decode(SignInResponse.self, from: data)
96-
97-
switch httpResponse.statusCode {
98-
case 200:
99-
return Current.network.dataTask(with: URLRequest.olympusSession).asVoid()
100-
case 401:
101-
throw Error.invalidUsernameOrPassword(username: accountName)
102-
case 409:
103-
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
104-
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
105-
throw Error.appleIDAndPrivacyAcknowledgementRequired
106-
default:
107-
throw Error.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
108-
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", "))
199+
do {
200+
let responseBody = try JSONDecoder().decode(SignInResponse.self, from: data)
201+
switch httpResponse.statusCode {
202+
case 200:
203+
return Current.network.dataTask(with: URLRequest.olympusSession).asVoid()
204+
case 401:
205+
throw Error.invalidUsernameOrPassword(username: accountName)
206+
case 409:
207+
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
208+
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
209+
throw Error.appleIDAndPrivacyAcknowledgementRequired
210+
default:
211+
throw Error.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
212+
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", "))
213+
}
214+
} catch DecodingError.dataCorrupted where httpResponse.statusCode == 503 {
215+
throw Error.serviceTemporarilyUnavailable
216+
} catch {
217+
throw error
109218
}
110219
}
111220
}
@@ -264,6 +373,43 @@ public class Client {
264373
return .value(hashcash)
265374
}
266375
}
376+
377+
private func sha256(data : Data) -> Data {
378+
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
379+
data.withUnsafeBytes {
380+
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
381+
}
382+
return Data(hash)
383+
}
384+
385+
private func pbkdf2(password: String, saltData: Data, keyByteCount: Int, prf: CCPseudoRandomAlgorithm, rounds: Int) -> Data? {
386+
guard let passwordData = password.data(using: .utf8) else { return nil }
387+
let hashedPasswordData = sha256(data: passwordData)
388+
389+
var derivedKeyData = Data(repeating: 0, count: keyByteCount)
390+
let derivedCount = derivedKeyData.count
391+
let derivationStatus: Int32 = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in
392+
let keyBuffer: UnsafeMutablePointer<UInt8> =
393+
derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
394+
return saltData.withUnsafeBytes { saltBytes -> Int32 in
395+
let saltBuffer: UnsafePointer<UInt8> = saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
396+
return hashedPasswordData.withUnsafeBytes { hashedPasswordBytes -> Int32 in
397+
let passwordBuffer: UnsafePointer<UInt8> = hashedPasswordBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
398+
return CCKeyDerivationPBKDF(
399+
CCPBKDFAlgorithm(kCCPBKDF2),
400+
passwordBuffer,
401+
hashedPasswordData.count,
402+
saltBuffer,
403+
saltData.count,
404+
prf,
405+
UInt32(rounds),
406+
keyBuffer,
407+
derivedCount)
408+
}
409+
}
410+
}
411+
return derivationStatus == kCCSuccess ? derivedKeyData : nil
412+
}
267413
}
268414

269415
public extension Promise where T == (data: Data, response: URLResponse) {
@@ -363,3 +509,10 @@ enum SecurityCode {
363509
}
364510
}
365511
}
512+
513+
public struct ServerSRPInitResponse: Decodable {
514+
let iteration: Int
515+
let salt: String
516+
let b: String
517+
let c: String
518+
}

0 commit comments

Comments
 (0)