Skip to content

Commit 3a8722c

Browse files
authored
Merge pull request #640 from XcodesOrg/matt/SRPLogin
Support SRP Login
2 parents a75c54f + 29bf770 commit 3a8722c

File tree

9 files changed

+730
-9
lines changed

9 files changed

+730
-9
lines changed

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ XcodesApp is now part of the `XcodesOrg` - [read more here](nextstep.md)
2222
- Just click a button to make a version active with `xcode-select`.
2323
- View release notes, OS compatibility, included SDKs and compilers from [Xcode Releases](https://xcodereleases.com).
2424
- Dark/Light Mode supported
25+
- Security Key Authentication supported
2526

2627
## Platforms/Runtimes
2728

2829
- Xcodes supports downloading the Apple runtimes via the app. Simply click on the Platform, and Xcodes will install automatically for you.
2930

31+
**Note: iOS 18+, tvOS 18+, watchOS 11+, visionOS 2+ requires that Xcode 16.1 Beta 3+ be installed and active.**
32+
3033
## Experiments
3134

3235
- Thanks to the wonderful work of [https://github.com/saagarjha/unxip](https://github.com/saagarjha/unxip), turn on the experiment to increase your unxipping time by up to 70%! More can be found on his repo, but bugs, high memory may occur if used.
@@ -160,7 +163,8 @@ popd
160163
# Attach the zip that was created in the Product directory to the release
161164
# Publish the release
162165

163-
# Update the [Homebrew Cask](https://github.com/RobotsAndPencils/homebrew-cask/blob/master/Casks/xcodes.rb).
166+
shasum -a 256 xcodes.zip
167+
# Update the [Homebrew Cask](https://github.com/XcodesOrg/homebrew-cask/blob/master/Casks/x/xcodes.rb).
164168
```
165169
</details>
166170

Xcodes.xcodeproj/project.pbxproj

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */; };
1011
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 334A932B2CA885A400A5E079 /* LibFido2Swift */; };
1112
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; };
1213
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */; };
@@ -122,6 +123,7 @@
122123
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */; };
123124
E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F532B333864003F3959 /* PlatformsListView.swift */; };
124125
E84E4F572B335094003F3959 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E84E4F562B335094003F3959 /* OrderedCollections */; };
126+
E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */ = {isa = PBXBuildFile; productRef = E862D43A2CC8B26F00BAA376 /* SRP */; };
125127
E86671272B309D2F0048559A /* PlatformsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86671262B309D2F0048559A /* PlatformsView.swift */; };
126128
E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; };
127129
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
@@ -195,6 +197,7 @@
195197
/* End PBXCopyFilesBuildPhase section */
196198

197199
/* Begin PBXFileReference section */
200+
15F5B88F2CCF09B900705E2F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
198201
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyPinView.swift; sourceTree = "<group>"; };
199202
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyTouchView.swift; sourceTree = "<group>"; };
200203
36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPreferencePane.swift; sourceTree = "<group>"; };
@@ -351,13 +354,15 @@
351354
isa = PBXFrameworksBuildPhase;
352355
buildActionMask = 2147483647;
353356
files = (
357+
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */,
354358
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */,
355359
CABFA9E42592F08E00380FEE /* Version in Frameworks */,
356360
CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */,
357361
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */,
358362
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */,
359363
CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */,
360364
E83FDC442CBB649100679C6B /* Sparkle in Frameworks */,
365+
E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */,
361366
CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */,
362367
E8C0EB1A291EF43E0081528A /* XcodesKit in Frameworks */,
363368
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */,
@@ -414,6 +419,7 @@
414419
CA538A12255A4F7C00E64DD7 /* Frameworks */ = {
415420
isa = PBXGroup;
416421
children = (
422+
15F5B88F2CCF09B900705E2F /* CryptoKit.framework */,
417423
);
418424
name = Frameworks;
419425
sourceTree = "<group>";
@@ -723,6 +729,7 @@
723729
E84E4F562B335094003F3959 /* OrderedCollections */,
724730
E83FDC432CBB649100679C6B /* Sparkle */,
725731
334A932B2CA885A400A5E079 /* LibFido2Swift */,
732+
E862D43A2CC8B26F00BAA376 /* SRP */,
726733
);
727734
productName = XcodesMac;
728735
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
@@ -1646,6 +1653,10 @@
16461653
package = E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */;
16471654
productName = OrderedCollections;
16481655
};
1656+
E862D43A2CC8B26F00BAA376 /* SRP */ = {
1657+
isa = XCSwiftPackageProductDependency;
1658+
productName = SRP;
1659+
};
16491660
E8C0EB19291EF43E0081528A /* XcodesKit */ = {
16501661
isa = XCSwiftPackageProductDependency;
16511662
productName = XcodesKit;

Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

+27
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
"version": null
1111
}
1212
},
13+
{
14+
"package": "big-num",
15+
"repositoryURL": "https://github.com/adam-fowler/big-num",
16+
"state": {
17+
"branch": null,
18+
"revision": "5c5511ad06aeb2b97d0868f7394e14a624bfb1c7",
19+
"version": "2.0.2"
20+
}
21+
},
1322
{
1423
"package": "CombineExpectations",
1524
"repositoryURL": "https://github.com/groue/CombineExpectations",
@@ -100,6 +109,24 @@
100109
"version": "1.0.5"
101110
}
102111
},
112+
{
113+
"package": "swift-crypto",
114+
"repositoryURL": "https://github.com/apple/swift-crypto",
115+
"state": {
116+
"branch": null,
117+
"revision": "ddb07e896a2a8af79512543b1c7eb9797f8898a5",
118+
"version": "1.1.7"
119+
}
120+
},
121+
{
122+
"package": "swift-srp",
123+
"repositoryURL": "https://github.com/xcodesOrg/swift-srp",
124+
"state": {
125+
"branch": "main",
126+
"revision": "543aa0122a0257b992f6c7d62d18a26e3dffb8fe",
127+
"version": null
128+
}
129+
},
103130
{
104131
"package": "SwiftSoup",
105132
"repositoryURL": "https://github.com/scinfu/SwiftSoup",

Xcodes/AppleAPI/Package.swift

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
// swift-tools-version:5.3
1+
// swift-tools-version:5.7
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
55

66
let package = Package(
77
name: "AppleAPI",
8-
platforms: [.macOS(.v10_15)],
8+
platforms: [.macOS(.v11)],
99
products: [
1010
// Products define the executables and libraries a package produces, and make them visible to other packages.
1111
.library(
1212
name: "AppleAPI",
1313
targets: ["AppleAPI"]),
1414
],
15-
dependencies: [],
15+
dependencies: [
16+
.package(url: "https://github.com/xcodesOrg/swift-srp", branch: "main")
17+
],
1618
targets: [
1719
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
1820
// Targets can depend on other targets in this package, and on products in packages this package depends on.
1921
.target(
2022
name: "AppleAPI",
21-
dependencies: []),
23+
dependencies: [.product(name: "SRP", package: "swift-srp")]),
2224
.testTarget(
2325
name: "AppleAPITests",
2426
dependencies: ["AppleAPI"]),

Xcodes/AppleAPI/Sources/AppleAPI/Client.swift

+106-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import Foundation
22
import Combine
3+
import SRP
4+
import Crypto
5+
import CommonCrypto
6+
37

48
public class Client {
59
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
@@ -8,8 +12,12 @@ public class Client {
812

913
// MARK: - Login
1014

11-
public func login(accountName: String, password: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
15+
public func srpLogin(accountName: String, password: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
1216
var serviceKey: String!
17+
18+
let client = SRPClient(configuration: SRPConfiguration<SHA256>(.N2048))
19+
let clientKeys = client.generateKeys()
20+
let a = clientKeys.public
1321

1422
return Current.network.dataTask(with: URLRequest.itcServiceKey)
1523
.map(\.data)
@@ -24,11 +32,45 @@ public class Client {
2432
.map { return (serviceKey, $0)}
2533
.eraseToAnyPublisher()
2634
}
27-
.flatMap { (serviceKey, hashcash) -> AnyPublisher<URLSession.DataTaskPublisher.Output, Swift.Error> in
35+
.flatMap { (serviceKey, hashcash) -> AnyPublisher<(String, String, ServerSRPInitResponse), Swift.Error> in
36+
37+
return Current.network.dataTask(with: URLRequest.SRPInit(serviceKey: serviceKey, a: Data(a.bytes).base64EncodedString(), accountName: accountName))
38+
.map(\.data)
39+
.decode(type: ServerSRPInitResponse.self, decoder: JSONDecoder())
40+
.map { return (serviceKey, hashcash, $0) }
41+
.eraseToAnyPublisher()
42+
}
43+
.flatMap { (serviceKey, hashcash, srpInit) -> AnyPublisher<URLSession.DataTaskPublisher.Output, Swift.Error> in
44+
guard let decodedB = Data(base64Encoded: srpInit.b) else {
45+
return Fail(error: AuthenticationError.srpInvalidPublicKey)
46+
.eraseToAnyPublisher()
47+
}
48+
49+
guard let decodedSalt = Data(base64Encoded: srpInit.salt) else {
50+
return Fail(error: AuthenticationError.srpInvalidPublicKey)
51+
.eraseToAnyPublisher()
52+
}
53+
54+
let iterations = srpInit.iteration
55+
56+
do {
57+
guard let encryptedPassword = self.pbkdf2(password: password, saltData: decodedSalt, keyByteCount: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), rounds: iterations) else {
58+
return Fail(error: AuthenticationError.srpInvalidPublicKey)
59+
.eraseToAnyPublisher()
60+
}
61+
62+
let sharedSecret = try client.calculateSharedSecret(password: encryptedPassword, salt: [UInt8](decodedSalt), clientKeys: clientKeys, serverPublicKey: .init([UInt8](decodedB)))
2863

29-
return Current.network.dataTask(with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password, hashcash: hashcash))
64+
let m1 = client.calculateClientProof(username: accountName, salt: [UInt8](decodedSalt), clientPublicKey: a, serverPublicKey: .init([UInt8](decodedB)), sharedSecret: .init(sharedSecret.bytes))
65+
let m2 = client.calculateServerProof(clientPublicKey: a, clientProof: m1, sharedSecret: .init([UInt8](sharedSecret.bytes)))
66+
67+
return Current.network.dataTask(with: URLRequest.SRPComplete(serviceKey: serviceKey, hashcash: hashcash, accountName: accountName, c: srpInit.c, m1: Data(m1).base64EncodedString(), m2: Data(m2).base64EncodedString()))
3068
.mapError { $0 as Swift.Error }
3169
.eraseToAnyPublisher()
70+
} catch {
71+
return Fail(error: AuthenticationError.srpInvalidPublicKey)
72+
.eraseToAnyPublisher()
73+
}
3274
}
3375
.flatMap { result -> AnyPublisher<AuthenticationState, Swift.Error> in
3476
let (data, response) = result
@@ -257,6 +299,44 @@ public class Client {
257299
.mapError { $0 as Error }
258300
.eraseToAnyPublisher()
259301
}
302+
303+
func sha256(data : Data) -> Data {
304+
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
305+
data.withUnsafeBytes {
306+
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
307+
}
308+
return Data(hash)
309+
}
310+
311+
private func pbkdf2(password: String, saltData: Data, keyByteCount: Int, prf: CCPseudoRandomAlgorithm, rounds: Int) -> Data? {
312+
guard let passwordData = password.data(using: .utf8) else { return nil }
313+
let hashedPasswordData = sha256(data: passwordData)
314+
315+
var derivedKeyData = Data(repeating: 0, count: keyByteCount)
316+
let derivedCount = derivedKeyData.count
317+
let derivationStatus: Int32 = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in
318+
let keyBuffer: UnsafeMutablePointer<UInt8> =
319+
derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
320+
return saltData.withUnsafeBytes { saltBytes -> Int32 in
321+
let saltBuffer: UnsafePointer<UInt8> = saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
322+
return hashedPasswordData.withUnsafeBytes { hashedPasswordBytes -> Int32 in
323+
let passwordBuffer: UnsafePointer<UInt8> = hashedPasswordBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
324+
return CCKeyDerivationPBKDF(
325+
CCPBKDFAlgorithm(kCCPBKDF2),
326+
passwordBuffer,
327+
hashedPasswordData.count,
328+
saltBuffer,
329+
saltData.count,
330+
prf,
331+
UInt32(rounds),
332+
keyBuffer,
333+
derivedCount)
334+
}
335+
}
336+
}
337+
return derivationStatus == kCCSuccess ? derivedKeyData : nil
338+
}
339+
260340
}
261341

262342
// MARK: - Types
@@ -282,6 +362,7 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
282362
case notDeveloperAppleId
283363
case notAuthorized
284364
case invalidResult(resultString: String?)
365+
case srpInvalidPublicKey
285366

286367
public var errorDescription: String? {
287368
switch self {
@@ -316,6 +397,8 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
316397
return "You are not authorized. Please Sign in with your Apple ID first."
317398
case let .invalidResult(resultString):
318399
return resultString ?? "If you continue to have problems, please submit a bug report in the Help menu."
400+
case .srpInvalidPublicKey:
401+
return "Invalid Key"
319402
}
320403
}
321404
}
@@ -495,3 +578,23 @@ public struct AppleProvider: Decodable, Equatable {
495578
public struct AppleUser: Decodable, Equatable {
496579
public let fullName: String
497580
}
581+
582+
public struct ServerSRPInitResponse: Decodable {
583+
let iteration: Int
584+
let salt: String
585+
let b: String
586+
let c: String
587+
}
588+
589+
590+
591+
extension String {
592+
func base64ToU8Array() -> Data {
593+
return Data(base64Encoded: self) ?? Data()
594+
}
595+
}
596+
extension Data {
597+
func hexEncodedString() -> String {
598+
return map { String(format: "%02hhx", $0) }.joined()
599+
}
600+
}

Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift

+51
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ public extension URL {
1010
static let federate = URL(string: "https://idmsa.apple.com/appleauth/auth/federate")!
1111
static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")!
1212
static let keyAuth = URL(string: "https://idmsa.apple.com/appleauth/auth/verify/security/key")!
13+
14+
static let srpInit = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/init")!
15+
static let srpComplete = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/complete?isRememberMeEnabled=false")!
16+
1317
}
1418

1519
public extension URLRequest {
@@ -150,4 +154,51 @@ public extension URLRequest {
150154

151155
return request
152156
}
157+
158+
static func SRPInit(serviceKey: String, a: String, accountName: String) -> URLRequest {
159+
struct ServerSRPInitRequest: Encodable {
160+
public let a: String
161+
public let accountName: String
162+
public let protocols: [SRPProtocol]
163+
}
164+
165+
var request = URLRequest(url: .srpInit)
166+
request.httpMethod = "POST"
167+
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
168+
request.allHTTPHeaderFields?["Accept"] = "application/json"
169+
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
170+
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
171+
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
172+
173+
request.httpBody = try? JSONEncoder().encode(ServerSRPInitRequest(a: a, accountName: accountName, protocols: [.s2k, .s2k_fo]))
174+
return request
175+
}
176+
177+
static func SRPComplete(serviceKey: String, hashcash: String, accountName: String, c: String, m1: String, m2: String) -> URLRequest {
178+
struct ServerSRPCompleteRequest: Encodable {
179+
let accountName: String
180+
let c: String
181+
let m1: String
182+
let m2: String
183+
let rememberMe: Bool
184+
}
185+
186+
var request = URLRequest(url: .srpComplete)
187+
request.httpMethod = "POST"
188+
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
189+
request.allHTTPHeaderFields?["Accept"] = "application/json"
190+
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
191+
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
192+
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
193+
request.allHTTPHeaderFields?["X-Apple-HC"] = hashcash
194+
195+
request.httpBody = try? JSONEncoder().encode(ServerSRPCompleteRequest(accountName: accountName, c: c, m1: m1, m2: m2, rememberMe: false))
196+
return request
197+
}
153198
}
199+
200+
public enum SRPProtocol: String, Codable {
201+
case s2k, s2k_fo
202+
}
203+
204+

Xcodes/Backend/AppState.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ class AppState: ObservableObject {
290290
Current.defaults.set(username, forKey: "username")
291291

292292
isProcessingAuthRequest = true
293-
return client.login(accountName: username, password: password)
293+
return client.srpLogin(accountName: username, password: password)
294294
.receive(on: DispatchQueue.main)
295295
.handleEvents(
296296
receiveOutput: { authenticationState in

0 commit comments

Comments
 (0)