Skip to content

Commit

Permalink
Merge pull request #393 from XcodesOrg/matt/supportSRPLogin
Browse files Browse the repository at this point in the history
Add support for SRP Apple login
  • Loading branch information
MattKiazyk authored Oct 30, 2024
2 parents 74516ad + 72540e9 commit 6a619e7
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 2 deletions.
27 changes: 27 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"pins" : [
{
"identity" : "big-num",
"kind" : "remoteSourceControl",
"location" : "https://github.com/adam-fowler/big-num",
"state" : {
"revision" : "5c5511ad06aeb2b97d0868f7394e14a624bfb1c7",
"version" : "2.0.2"
}
},
{
"identity" : "data",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -71,6 +80,24 @@
"version" : "1.1.4"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto",
"state" : {
"revision" : "ddb07e896a2a8af79512543b1c7eb9797f8898a5",
"version" : "1.1.7"
}
},
{
"identity" : "swift-srp",
"kind" : "remoteSourceControl",
"location" : "https://github.com/xcodesorg/swift-srp",
"state" : {
"branch" : "main",
"revision" : "543aa0122a0257b992f6c7d62d18a26e3dffb8fe"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ let package = Package(
.package(url: "https://github.com/xcodereleases/data", revision: "fcf527b187817f67c05223676341f3ab69d4214d"),
.package(url: "https://github.com/onevcat/Rainbow.git", .upToNextMinor(from: "3.2.0")),
.package(url: "https://github.com/jpsim/Yams", .upToNextMinor(from: "5.0.1")),
.package(url: "https://github.com/xcodesOrg/swift-srp", branch: "main")
],
targets: [
.executableTarget(
Expand Down Expand Up @@ -50,7 +51,7 @@ let package = Package(
"Version",
.product(name: "XCModel", package: "data"),
"Rainbow",
"Yams",
"Yams"
]),
.testTarget(
name: "XcodesKitTests",
Expand All @@ -68,6 +69,7 @@ let package = Package(
"PromiseKit",
.product(name: "PMKFoundation", package: "Foundation"),
"Rainbow",
.product(name: "SRP", package: "swift-srp")
]),
.testTarget(
name: "AppleAPITests",
Expand Down
140 changes: 140 additions & 0 deletions Sources/AppleAPI/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import Foundation
import PromiseKit
import PMKFoundation
import Rainbow
import SRP
import Crypto
import CommonCrypto

public class Client {
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
Expand All @@ -20,6 +23,8 @@ public class Client {
case invalidHashcash
case missingSecurityCodeInfo
case accountUsesHardwareKey
case srpInvalidPublicKey
case srpError(String)

public var errorDescription: String? {
switch self {
Expand Down Expand Up @@ -56,6 +61,97 @@ public class Client {
}
}

/// SRPLogin - Secure Remote Password
/// https://tools.ietf.org/html/rfc2945
/// Forked from https://github.com/adam-fowler/swift-srp that provides the algorithm
public func srpLogin(accountName: String, password: String) -> Promise<Void> {
var serviceKey: String!
let client = SRPClient(configuration: SRPConfiguration<SHA256>(.N2048))
let clientKeys = client.generateKeys()
let a = clientKeys.public

// Get the Service Key needed from olympus session needed in headers
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
Current.network.dataTask(with: URLRequest.itcServiceKey)
}
.then { (data, _) -> Promise<(serviceKey: String, hashcash: String)> in
struct ServiceKeyResponse: Decodable {
let authServiceKey: String?
}

let response = try JSONDecoder().decode(ServiceKeyResponse.self, from: data)
serviceKey = response.authServiceKey

/// Load a hashcash of the account name
return self.loadHashcash(accountName: accountName, serviceKey: serviceKey).map { (serviceKey, $0) }
}
.then { (serviceKey, hashcash) -> Promise<(serviceKey: String, hashcash: String, data: Data)> in
/// Call the SRP /init endpoint to start the login
return Current.network.dataTask(with: URLRequest.SRPInit(serviceKey: serviceKey, a: Data(a.bytes).base64EncodedString(), accountName: accountName)).map { (serviceKey, hashcash, $0.data)}
}
.then { (serviceKey, hashcash, data) -> Promise<(data: Data, response: URLResponse)> in
let srpInit = try JSONDecoder().decode(ServerSRPInitResponse.self, from: data)

guard let decodedB = Data(base64Encoded: srpInit.b) else {
throw Error.srpInvalidPublicKey
}
guard let decodedSalt = Data(base64Encoded: srpInit.salt) else {
throw Error.srpInvalidPublicKey
}

let iterations = srpInit.iteration

do {
guard let encryptedPassword = self.pbkdf2(password: password, saltData: decodedSalt, keyByteCount: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), rounds: iterations) else {
throw Error.srpInvalidPublicKey
}

let sharedSecret = try client.calculateSharedSecret(password: encryptedPassword, salt: [UInt8](decodedSalt), clientKeys: clientKeys, serverPublicKey: .init([UInt8](decodedB)))

let m1 = client.calculateClientProof(username: accountName, salt: [UInt8](decodedSalt), clientPublicKey: a, serverPublicKey: .init([UInt8](decodedB)), sharedSecret: .init(sharedSecret.bytes))
let m2 = client.calculateServerProof(clientPublicKey: a, clientProof: m1, sharedSecret: .init([UInt8](sharedSecret.bytes)))

/// call the /complete endpoint passing in the hashcash, servicekey, and the calculated proof.
return Current.network.dataTask(with: URLRequest.SRPComplete(serviceKey: serviceKey, hashcash: hashcash, accountName: accountName, c: srpInit.c, m1: Data(m1).base64EncodedString(), m2: Data(m2).base64EncodedString()))
} catch {
throw Error.srpError(error.localizedDescription)
}
}
.then { (data, response) -> Promise<Void> in
struct SignInResponse: Decodable {
let authType: String?
let serviceErrors: [ServiceError]?

struct ServiceError: Decodable, CustomStringConvertible {
let code: String
let message: String

var description: String {
return "\(code): \(message)"
}
}
}

let httpResponse = response as! HTTPURLResponse
let responseBody = try JSONDecoder().decode(SignInResponse.self, from: data)

switch httpResponse.statusCode {
case 200:
return Current.network.dataTask(with: URLRequest.olympusSession).asVoid()
case 401:
throw Error.invalidUsernameOrPassword(username: accountName)
case 409:
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
throw Error.appleIDAndPrivacyAcknowledgementRequired
default:
throw Error.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", "))
}
}
}

@available(*, deprecated, message: "Please use srpLogin")
public func login(accountName: String, password: String) -> Promise<Void> {
var serviceKey: String!

Expand Down Expand Up @@ -264,6 +360,43 @@ public class Client {
return .value(hashcash)
}
}

private func sha256(data : Data) -> Data {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
}
return Data(hash)
}

private func pbkdf2(password: String, saltData: Data, keyByteCount: Int, prf: CCPseudoRandomAlgorithm, rounds: Int) -> Data? {
guard let passwordData = password.data(using: .utf8) else { return nil }
let hashedPasswordData = sha256(data: passwordData)

var derivedKeyData = Data(repeating: 0, count: keyByteCount)
let derivedCount = derivedKeyData.count
let derivationStatus: Int32 = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in
let keyBuffer: UnsafeMutablePointer<UInt8> =
derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return saltData.withUnsafeBytes { saltBytes -> Int32 in
let saltBuffer: UnsafePointer<UInt8> = saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return hashedPasswordData.withUnsafeBytes { hashedPasswordBytes -> Int32 in
let passwordBuffer: UnsafePointer<UInt8> = hashedPasswordBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
passwordBuffer,
hashedPasswordData.count,
saltBuffer,
saltData.count,
prf,
UInt32(rounds),
keyBuffer,
derivedCount)
}
}
}
return derivationStatus == kCCSuccess ? derivedKeyData : nil
}
}

public extension Promise where T == (data: Data, response: URLResponse) {
Expand Down Expand Up @@ -363,3 +496,10 @@ enum SecurityCode {
}
}
}

public struct ServerSRPInitResponse: Decodable {
let iteration: Int
let salt: String
let b: String
let c: String
}
48 changes: 48 additions & 0 deletions Sources/AppleAPI/URLRequest+Apple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ extension URL {
static func submitSecurityCode(_ code: SecurityCode) -> URL { URL(string: "https://idmsa.apple.com/appleauth/auth/verify/\(code.urlPathComponent)/securitycode")! }
static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")!
static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")!

static let srpInit = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/init")!
static let srpComplete = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/complete?isRememberMeEnabled=false")!
}

extension URLRequest {
Expand Down Expand Up @@ -129,4 +132,49 @@ extension URLRequest {

return request
}

static func SRPInit(serviceKey: String, a: String, accountName: String) -> URLRequest {
struct ServerSRPInitRequest: Encodable {
public let a: String
public let accountName: String
public let protocols: [SRPProtocol]
}

var request = URLRequest(url: .srpInit)
request.httpMethod = "POST"
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
request.allHTTPHeaderFields?["Accept"] = "application/json"
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey

request.httpBody = try? JSONEncoder().encode(ServerSRPInitRequest(a: a, accountName: accountName, protocols: [.s2k, .s2k_fo]))
return request
}

static func SRPComplete(serviceKey: String, hashcash: String, accountName: String, c: String, m1: String, m2: String) -> URLRequest {
struct ServerSRPCompleteRequest: Encodable {
let accountName: String
let c: String
let m1: String
let m2: String
let rememberMe: Bool
}

var request = URLRequest(url: .srpComplete)
request.httpMethod = "POST"
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
request.allHTTPHeaderFields?["Accept"] = "application/json"
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
request.allHTTPHeaderFields?["X-Apple-HC"] = hashcash

request.httpBody = try? JSONEncoder().encode(ServerSRPCompleteRequest(accountName: accountName, c: c, m1: m1, m2: m2, rememberMe: false))
return request
}
}

public enum SRPProtocol: String, Codable {
case s2k, s2k_fo
}
2 changes: 1 addition & 1 deletion Sources/XcodesKit/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ public struct Network {

public var validateSession: () -> Promise<Void> = client.validateSession

public var login: (String, String) -> Promise<Void> = { client.login(accountName: $0, password: $1) }
public var login: (String, String) -> Promise<Void> = { client.srpLogin(accountName: $0, password: $1) }
public func login(accountName: String, password: String) -> Promise<Void> {
login(accountName, password)
}
Expand Down

0 comments on commit 6a619e7

Please sign in to comment.