Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SRP Apple login #393

Merged
merged 3 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {
MattKiazyk marked this conversation as resolved.
Show resolved Hide resolved
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
MattKiazyk marked this conversation as resolved.
Show resolved Hide resolved
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) }
MattKiazyk marked this conversation as resolved.
Show resolved Hide resolved
public func login(accountName: String, password: String) -> Promise<Void> {
login(accountName, password)
}
Expand Down
Loading