Skip to content

Commit e04ed02

Browse files
committed
(wip) SRP Login implementation
1 parent aca4e0a commit e04ed02

20 files changed

Lines changed: 1726 additions & 3 deletions

File tree

Xcodes.xcodeproj/project.pbxproj

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
archiveVersion = 1;
44
classes = {
55
};
6-
objectVersion = 54;
6+
objectVersion = 60;
77
objects = {
88

99
/* Begin PBXBuildFile section */
@@ -122,6 +122,7 @@
122122
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */; };
123123
E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F532B333864003F3959 /* PlatformsListView.swift */; };
124124
E84E4F572B335094003F3959 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E84E4F562B335094003F3959 /* OrderedCollections */; };
125+
E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */ = {isa = PBXBuildFile; productRef = E862D43A2CC8B26F00BAA376 /* SRP */; };
125126
E86671272B309D2F0048559A /* PlatformsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86671262B309D2F0048559A /* PlatformsView.swift */; };
126127
E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; };
127128
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
@@ -358,6 +359,7 @@
358359
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */,
359360
CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */,
360361
E83FDC442CBB649100679C6B /* Sparkle in Frameworks */,
362+
E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */,
361363
CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */,
362364
E8C0EB1A291EF43E0081528A /* XcodesKit in Frameworks */,
363365
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */,
@@ -723,6 +725,7 @@
723725
E84E4F562B335094003F3959 /* OrderedCollections */,
724726
E83FDC432CBB649100679C6B /* Sparkle */,
725727
334A932B2CA885A400A5E079 /* LibFido2Swift */,
728+
E862D43A2CC8B26F00BAA376 /* SRP */,
726729
);
727730
productName = XcodesMac;
728731
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
@@ -812,6 +815,7 @@
812815
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */,
813816
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */,
814817
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */,
818+
E862D4392CC8B26F00BAA376 /* XCLocalSwiftPackageReference "xcodes-srp" */,
815819
);
816820
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
817821
projectDirPath = "";
@@ -1480,6 +1484,13 @@
14801484
};
14811485
/* End XCConfigurationList section */
14821486

1487+
/* Begin XCLocalSwiftPackageReference section */
1488+
E862D4392CC8B26F00BAA376 /* XCLocalSwiftPackageReference "xcodes-srp" */ = {
1489+
isa = XCLocalSwiftPackageReference;
1490+
relativePath = "xcodes-srp";
1491+
};
1492+
/* End XCLocalSwiftPackageReference section */
1493+
14831494
/* Begin XCRemoteSwiftPackageReference section */
14841495
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */ = {
14851496
isa = XCRemoteSwiftPackageReference;
@@ -1646,6 +1657,10 @@
16461657
package = E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */;
16471658
productName = OrderedCollections;
16481659
};
1660+
E862D43A2CC8B26F00BAA376 /* SRP */ = {
1661+
isa = XCSwiftPackageProductDependency;
1662+
productName = SRP;
1663+
};
16491664
E8C0EB19291EF43E0081528A /* XcodesKit */ = {
16501665
isa = XCSwiftPackageProductDependency;
16511666
productName = XcodesKit;

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

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

Xcodes/AppleAPI/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ 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(

Xcodes/AppleAPI/Sources/AppleAPI/Client.swift

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import Foundation
22
import Combine
3+
import SRP
4+
import Crypto
5+
import CommonCrypto
36

47
public class Client {
58
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
@@ -8,6 +11,144 @@ public class Client {
811

912
// MARK: - Login
1013

14+
public func srpLogin(accountName: String, password: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
15+
var serviceKey: String!
16+
17+
let config = SRPConfiguration<SHA256>(.N2048)
18+
let client = SRPClient(configuration: config)
19+
let clientKeys = client.generateKeys()
20+
21+
return Current.network.dataTask(with: URLRequest.itcServiceKey)
22+
.map(\.data)
23+
.decode(type: ServiceKeyResponse.self, decoder: JSONDecoder())
24+
.flatMap { serviceKeyResponse -> AnyPublisher<(String, String), Swift.Error> in
25+
serviceKey = serviceKeyResponse.authServiceKey
26+
27+
// Fixes issue https://github.com/RobotsAndPencils/XcodesApp/issues/360
28+
// On 2023-02-23, Apple added a custom implementation of hashcash to their auth flow
29+
// Without this addition, Apple ID's would get set to locked
30+
return self.loadHashcash(accountName: accountName, serviceKey: serviceKey)
31+
.map { return (serviceKey, $0)}
32+
.eraseToAnyPublisher()
33+
}
34+
.flatMap { (serviceKey, hashcash) -> AnyPublisher<(String, String, ServerSRPInitResponse), Swift.Error> in
35+
36+
return Current.network.dataTask(with: URLRequest.SRPInit(serviceKey: serviceKey, a: clientKeys.private.hex, accountName: accountName))
37+
.map(\.data)
38+
.decode(type: ServerSRPInitResponse.self, decoder: JSONDecoder())
39+
.map { return (serviceKey, hashcash, $0) }
40+
.eraseToAnyPublisher()
41+
}
42+
.flatMap { (serviceKey, hashcash, srpInit) -> AnyPublisher<URLSession.DataTaskPublisher.Output, Swift.Error> in
43+
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+
let serverPublic = SRPKey([UInt8](decodedB))
56+
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+
63+
let encryptedPasswordArray = encryptedPassword.hexEncodedString()
64+
65+
print("EncryptedPassword: \(encryptedPasswordArray)")
66+
print("EncryptedPassword: \([UInt8](encryptedPassword))")
67+
do {
68+
69+
// this calculates "S"
70+
let clientSharedSecret = try client.calculateSharedSecret(
71+
encryptedPassword: encryptedPasswordArray,
72+
salt: [UInt8](decodedSalt),
73+
clientKeys: clientKeys,
74+
serverPublicKey: serverPublic
75+
)
76+
print("SharedSecret: \(clientSharedSecret)")
77+
78+
let m1 = client.calculateClientProof(
79+
username: accountName,
80+
salt: [UInt8](decodedSalt),
81+
clientPublicKey: clientKeys.public,
82+
serverPublicKey: serverPublic,
83+
sharedSecret: clientSharedSecret
84+
)
85+
86+
let m2 = client.serverProof(clientProof: m1, clientKeys: clientKeys, sharedSecret: clientSharedSecret)
87+
88+
89+
print("M1: \(Data(m1).base64EncodedString())")
90+
print("M2: \(Data(m2).base64EncodedString())")
91+
92+
return Current.network.dataTask(with: URLRequest.SRPComplete(serviceKey: serviceKey, hashcash: hashcash, accountName: accountName, c: srpInit.c, m1: Data(m1).base64EncodedString(), m2: Data(m2).base64EncodedString()))
93+
.mapError { $0 as Swift.Error }
94+
.eraseToAnyPublisher()
95+
} catch {
96+
print("Error: calculateSharedSecret \(error)")
97+
return Fail(error: AuthenticationError.srpInvalidPublicKey)
98+
.eraseToAnyPublisher()
99+
}
100+
}
101+
.flatMap { result -> AnyPublisher<AuthenticationState, Swift.Error> in
102+
let (data, response) = result
103+
return Just(data)
104+
.decode(type: SignInResponse.self, decoder: JSONDecoder())
105+
.flatMap { responseBody -> AnyPublisher<AuthenticationState, Swift.Error> in
106+
let httpResponse = response as! HTTPURLResponse
107+
108+
switch httpResponse.statusCode {
109+
case 200:
110+
return Current.network.dataTask(with: URLRequest.olympusSession)
111+
.map { _ in AuthenticationState.authenticated }
112+
.mapError { $0 as Swift.Error }
113+
.eraseToAnyPublisher()
114+
case 401:
115+
return Fail(error: AuthenticationError.invalidUsernameOrPassword(username: accountName))
116+
.eraseToAnyPublisher()
117+
case 403:
118+
let errorMessage = responseBody.serviceErrors?.first?.description.replacingOccurrences(of: "-20209: ", with: "") ?? ""
119+
return Fail(error: AuthenticationError.accountLocked(errorMessage))
120+
.eraseToAnyPublisher()
121+
case 409:
122+
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
123+
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
124+
return Fail(error: AuthenticationError.appleIDAndPrivacyAcknowledgementRequired)
125+
.eraseToAnyPublisher()
126+
default:
127+
return Fail(error: AuthenticationError.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
128+
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", ")))
129+
.eraseToAnyPublisher()
130+
}
131+
}
132+
.eraseToAnyPublisher()
133+
}
134+
// .map(\.data)
135+
// .decode(type: ServerSRPInitResponse.self, decoder: JSONDecoder())
136+
//
137+
//
138+
//
139+
// .flatMap { result -> AnyPublisher<AuthenticationState, Swift.Error> in
140+
// return ("")
141+
// }
142+
// .flatMap { serverResponse -> AnyPublisher<AuthenticationState, Error> in
143+
// print(serverResponse)
144+
// return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication)
145+
// .eraseToAnyPublisher()
146+
// }
147+
.mapError { $0 as Swift.Error }
148+
.eraseToAnyPublisher()
149+
}
150+
151+
11152
public func login(accountName: String, password: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
12153
var serviceKey: String!
13154

@@ -257,6 +398,32 @@ public class Client {
257398
.mapError { $0 as Error }
258399
.eraseToAnyPublisher()
259400
}
401+
402+
private func pbkdf2(password: String, saltData: Data, keyByteCount: Int, prf: CCPseudoRandomAlgorithm, rounds: Int) -> Data? {
403+
guard let passwordData = password.data(using: .utf8) else { return nil }
404+
405+
var derivedKeyData = Data(repeating: 0, count: keyByteCount)
406+
let derivedCount = derivedKeyData.count
407+
let derivationStatus: Int32 = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in
408+
let keyBuffer: UnsafeMutablePointer<UInt8> =
409+
derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
410+
return saltData.withUnsafeBytes { saltBytes -> Int32 in
411+
let saltBuffer: UnsafePointer<UInt8> = saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
412+
return CCKeyDerivationPBKDF(
413+
CCPBKDFAlgorithm(kCCPBKDF2),
414+
password,
415+
passwordData.count,
416+
saltBuffer,
417+
saltData.count,
418+
prf,
419+
UInt32(rounds),
420+
keyBuffer,
421+
derivedCount)
422+
}
423+
}
424+
return derivationStatus == kCCSuccess ? derivedKeyData : nil
425+
}
426+
260427
}
261428

262429
// MARK: - Types
@@ -282,6 +449,7 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
282449
case notDeveloperAppleId
283450
case notAuthorized
284451
case invalidResult(resultString: String?)
452+
case srpInvalidPublicKey
285453

286454
public var errorDescription: String? {
287455
switch self {
@@ -316,6 +484,8 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
316484
return "You are not authorized. Please Sign in with your Apple ID first."
317485
case let .invalidResult(resultString):
318486
return resultString ?? "If you continue to have problems, please submit a bug report in the Help menu."
487+
case .srpInvalidPublicKey:
488+
return "Invalid Key"
319489
}
320490
}
321491
}
@@ -495,3 +665,23 @@ public struct AppleProvider: Decodable, Equatable {
495665
public struct AppleUser: Decodable, Equatable {
496666
public let fullName: String
497667
}
668+
669+
public struct ServerSRPInitResponse: Decodable {
670+
let iteration: Int
671+
let salt: String
672+
let b: String
673+
let c: String
674+
}
675+
676+
677+
678+
extension String {
679+
func base64ToU8Array() -> Data {
680+
return Data(base64Encoded: self) ?? Data()
681+
}
682+
}
683+
extension Data {
684+
func hexEncodedString() -> String {
685+
return map { String(format: "%02hhx", $0) }.joined()
686+
}
687+
}

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

Lines changed: 51 additions & 0 deletions
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+

0 commit comments

Comments
 (0)