Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
2.0.0
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Alamofire
import Shared
import Foundation
import FxA
import Deferred
import SwiftyJSON
public let FxAClientErrorDomain = "org.mozilla.fxa.error"
public let FxAClientUnknownError = NSError(domain: FxAClientErrorDomain, code: 999,
userInfo: [NSLocalizedDescriptionKey: "Invalid server response"])
let KeyLength: Int = 32
public struct FxALoginResponse {
public let remoteEmail: String
public let uid: String
public let verified: Bool
public let sessionToken: Data
public let keyFetchToken: Data
}
public struct FxAccountRemoteError {
static let AttemptToOperateOnAnUnverifiedAccount: Int32 = 104
static let InvalidAuthenticationToken: Int32 = 110
static let EndpointIsNoLongerSupported: Int32 = 116
static let IncorrectLoginMethodForThisAccount: Int32 = 117
static let IncorrectKeyRetrievalMethodForThisAccount: Int32 = 118
static let IncorrectAPIVersionForThisAccount: Int32 = 119
static let UnknownDevice: Int32 = 123
static let DeviceSessionConflict: Int32 = 124
static let UnknownError: Int32 = 999
}
public struct FxAKeysResponse {
let kA: Data
let wrapkB: Data
}
public struct FxASignResponse {
let certificate: String
}
public struct FxAStatusResponse {
let exists: Bool
}
public struct FxADevicesResponse {
let devices: [FxADevice]
}
public struct FxANotifyResponse {
let success: Bool
}
public struct FxAOAuthResponse {
let accessToken: String
}
public struct FxAProfileResponse {
let email: String
let uid: String
let avatarURL: String?
let displayName: String?
}
public struct FxADeviceDestroyResponse {
let success: Bool
}
// fxa-auth-server produces error details like:
// {
// "code": 400, // matches the HTTP status code
// "errno": 107, // stable application-level error number
// "error": "Bad Request", // string description of the error type
// "message": "the value of salt is not allowed to be undefined",
// "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error
// }
public enum FxAClientError {
case remote(RemoteError)
case local(NSError)
}
// Be aware that string interpolation doesn't work: rdar://17318018, much good that it will do.
extension FxAClientError: MaybeErrorType {
public var description: String {
switch self {
case let .remote(error):
let errorString = error.error ?? NSLocalizedString("Missing error", comment: "Error for a missing remote error number")
let messageString = error.message ?? NSLocalizedString("Missing message", comment: "Error for a missing remote error message")
return "<FxAClientError.Remote \(error.code)/\(error.errno): \(errorString) (\(messageString))>"
case let .local(error):
return "<FxAClientError.Local Error Domain=\(error.domain) Code=\(error.code) \"\(error.localizedDescription)\">"
}
}
}
public struct RemoteError {
let code: Int32
let errno: Int32
let error: String?
let message: String?
let info: String?
var isUpgradeRequired: Bool {
return errno == FxAccountRemoteError.EndpointIsNoLongerSupported
|| errno == FxAccountRemoteError.IncorrectLoginMethodForThisAccount
|| errno == FxAccountRemoteError.IncorrectKeyRetrievalMethodForThisAccount
|| errno == FxAccountRemoteError.IncorrectAPIVersionForThisAccount
}
var isInvalidAuthentication: Bool {
return code == 401
}
var isUnverified: Bool {
return errno == FxAccountRemoteError.AttemptToOperateOnAnUnverifiedAccount
}
}
open class FxAClient10 {
let authURL: URL
let oauthURL: URL
let profileURL: URL
public init(authEndpoint: URL? = nil, oauthEndpoint: URL? = nil, profileEndpoint: URL? = nil) {
self.authURL = authEndpoint ?? ProductionFirefoxAccountConfiguration().authEndpointURL as URL
self.oauthURL = oauthEndpoint ?? ProductionFirefoxAccountConfiguration().oauthEndpointURL as URL
self.profileURL = profileEndpoint ?? ProductionFirefoxAccountConfiguration().profileEndpointURL as URL
}
open class func KW(_ kw: String) -> Data {
return ("identity.mozilla.com/picl/v1/" + kw).utf8EncodedData
}
/**
* The token server accepts an X-Client-State header, which is the
* lowercase-hex-encoded first 16 bytes of the SHA-256 hash of the
* bytes of kB.
*/
open class func computeClientState(_ kB: Data) -> String {
return kB.sha256.subdata(in: 0..<16).hexEncodedString
}
open class func deriveKSync(_ kB: Data) -> Data {
let salt = Data()
let contextInfo = FxAClient10.KW("oldsync")
let len: UInt = 64 // KeyLength + KeyLength, without type nonsense.
return (kB as NSData).deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: len)!
}
open class func quickStretchPW(_ email: Data, password: Data) -> Data {
var salt = KW("quickStretch")
salt.append(":".utf8EncodedData)
salt.append(email)
return (password as NSData).derivePBKDF2HMACSHA256Key(withSalt: salt as Data!, iterations: 1000, length: 32)
}
open class func computeUnwrapKey(_ stretchedPW: Data) -> Data {
let salt: Data = Data()
let contextInfo: Data = KW("unwrapBkey")
let bytes = (stretchedPW as NSData).deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(KeyLength))
return bytes!
}
fileprivate class func remoteError(fromJSON json: JSON, statusCode: Int) -> RemoteError? {
if json.error != nil || 200 <= statusCode && statusCode <= 299 {
return nil
}
if let code = json["code"].int32 {
if let errno = json["errno"].int32 {
return RemoteError(code: code, errno: errno,
error: json["error"].string,
message: json["message"].string,
info: json["info"].string)
}
}
return nil
}
fileprivate class func loginResponse(fromJSON json: JSON) -> FxALoginResponse? {
guard json.error == nil,
let uid = json["uid"].string,
let verified = json["verified"].bool,
let sessionToken = json["sessionToken"].string,
let keyFetchToken = json["keyFetchToken"].string else {
return nil
}
return FxALoginResponse(remoteEmail: "", uid: uid, verified: verified,
sessionToken: sessionToken.hexDecodedData, keyFetchToken: keyFetchToken.hexDecodedData)
}
fileprivate class func keysResponse(fromJSON keyRequestKey: Data, json: JSON) -> FxAKeysResponse? {
guard json.error == nil,
let bundle = json["bundle"].string else {
return nil
}
let data = bundle.hexDecodedData
guard data.count == 3 * KeyLength else {
return nil
}
let ciphertext = data.subdata(in: 0..<(2 * KeyLength))
let MAC = data.subdata(in: (2 * KeyLength)..<(3 * KeyLength))
let salt: Data = Data()
let contextInfo: Data = KW("account/keys")
let bytes = (keyRequestKey as NSData).deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(3 * KeyLength))
let respHMACKey = bytes?.subdata(in: 0..<KeyLength)
let respXORKey = bytes?.subdata(in: KeyLength..<(3 * KeyLength))
guard let hmacKey = respHMACKey,
ciphertext.hmacSha256WithKey(hmacKey) == MAC else {
NSLog("Bad HMAC in /keys response!")
return nil
}
guard let xorKey = respXORKey,
let xoredBytes = ciphertext.xoredWith(xorKey) else {
return nil
}
let kA = xoredBytes.subdata(in: 0..<KeyLength)
let wrapkB = xoredBytes.subdata(in: KeyLength..<(2 * KeyLength))
return FxAKeysResponse(kA: kA, wrapkB: wrapkB)
}
fileprivate class func signResponse(fromJSON json: JSON) -> FxASignResponse? {
guard json.error == nil,
let cert = json["cert"].string else {
return nil
}
return FxASignResponse(certificate: cert)
}
fileprivate class func statusResponse(fromJSON json: JSON) -> FxAStatusResponse? {
guard json.error == nil,
let exists = json["exists"].bool else {
return nil
}
return FxAStatusResponse(exists: exists)
}
fileprivate class func devicesResponse(fromJSON json: JSON) -> FxADevicesResponse? {
guard json.error == nil,
let jsonDevices = json.array else {
return nil
}
let devices = jsonDevices.compactMap { (jsonDevice) -> FxADevice? in
return FxADevice.fromJSON(jsonDevice)
}
return FxADevicesResponse(devices: devices)
}
fileprivate class func notifyResponse(fromJSON json: JSON) -> FxANotifyResponse {
return FxANotifyResponse(success: json.error == nil)
}
fileprivate class func deviceDestroyResponse(fromJSON json: JSON) -> FxADeviceDestroyResponse {
return FxADeviceDestroyResponse(success: json.error == nil)
}
fileprivate class func oauthResponse(fromJSON json: JSON) -> FxAOAuthResponse? {
guard json.error == nil,
let accessToken = json["access_token"].string else {
return nil
}
return FxAOAuthResponse(accessToken: accessToken)
}
fileprivate class func profileResponse(fromJSON json: JSON) -> FxAProfileResponse? {
guard json.error == nil,
let uid = json["uid"].string,
let email = json["email"].string else {
return nil
}
let avatarURL = json["avatar"].string
let displayName = json["displayName"].string
return FxAProfileResponse(email: email, uid: uid, avatarURL: avatarURL, displayName: displayName)
}
lazy fileprivate var alamofire: SessionManager = {
let ua = UserAgent.fxaUserAgent
let configuration = URLSessionConfiguration.ephemeral
var defaultHeaders = SessionManager.default.session.configuration.httpAdditionalHeaders ?? [:]
defaultHeaders["User-Agent"] = ua
configuration.httpAdditionalHeaders = defaultHeaders
return SessionManager(configuration: configuration)
}()
open func login(_ emailUTF8: Data, quickStretchedPW: Data, getKeys: Bool) -> Deferred<Maybe<FxALoginResponse>> {
let authPW = (quickStretchedPW as NSData).deriveHKDFSHA256Key(withSalt: Data(), contextInfo: FxAClient10.KW("authPW"), length: 32) as NSData
let parameters = [
"email": NSString(data: emailUTF8, encoding: String.Encoding.utf8.rawValue)!,
"authPW": authPW.base16EncodedString(options: NSDataBase16EncodingOptions.lowerCase) as NSString,
]
var URL: URL = self.authURL.appendingPathComponent("/account/login")
if getKeys {
var components = URLComponents(url: URL, resolvingAgainstBaseURL: false)!
components.query = "keys=true"
URL = components.url!
}
var mutableURLRequest = URLRequest(url: URL)
mutableURLRequest.httpMethod = HTTPMethod.post.rawValue
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
mutableURLRequest.httpBody = JSON(parameters).stringValue()?.utf8EncodedData
return makeRequest(mutableURLRequest, responseHandler: FxAClient10.loginResponse)
}
open func status(forUID uid: String) -> Deferred<Maybe<FxAStatusResponse>> {
let statusURL = self.authURL.appendingPathComponent("/account/status").withQueryParam("uid", value: uid)
var mutableURLRequest = URLRequest(url: statusURL)
mutableURLRequest.httpMethod = HTTPMethod.get.rawValue
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
return makeRequest(mutableURLRequest, responseHandler: FxAClient10.statusResponse)
}
open func devices(withSessionToken sessionToken: NSData) -> Deferred<Maybe<FxADevicesResponse>> {
let URL = self.authURL.appendingPathComponent("/account/devices")
var mutableURLRequest = URLRequest(url: URL)
mutableURLRequest.httpMethod = HTTPMethod.get.rawValue
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
let salt: Data = Data()
let contextInfo: Data = FxAClient10.KW("sessionToken")
let key = sessionToken.deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))!
mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key)
return makeRequest(mutableURLRequest, responseHandler: FxAClient10.devicesResponse)
}
open func notify(deviceIDs: [GUID], collectionsChanged collections: [String], reason: String, withSessionToken sessionToken: NSData) -> Deferred<Maybe<FxANotifyResponse>> {
let httpBody = JSON([
"to": deviceIDs,
"payload": [
"version": 1,
"command": "sync:collection_changed",
"data": [
"collections": collections,
"reason": reason
]
]
])
return self.notify(httpBody: httpBody, withSessionToken: sessionToken)
}
open func notifyAll(ownDeviceId: GUID, collectionsChanged collections: [String], reason: String, withSessionToken sessionToken: NSData) -> Deferred<Maybe<FxANotifyResponse>> {
let httpBody = JSON([
"to": "all",
"excluded": [ownDeviceId],
"payload": [
"version": 1,
"command": "sync:collection_changed",
"data": [
"collections": collections,
"reason": reason
]
]
])
return self.notify(httpBody: httpBody, withSessionToken: sessionToken)
}
fileprivate func notify(httpBody: JSON, withSessionToken sessionToken: NSData) -> Deferred<Maybe<FxANotifyResponse>> {
let URL = self.authURL.appendingPathComponent("/account/devices/notify")
var mutableURLRequest = URLRequest(url: URL)
mutableURLRequest.httpMethod = HTTPMethod.post.rawValue
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
mutableURLRequest.httpBody = httpBody.stringValue()?.utf8EncodedData
let salt: Data = Data()
let contextInfo: Data = FxAClient10.KW("sessionToken")
let key = sessionToken.deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))!
mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key)
return makeRequest(mutableURLRequest, responseHandler: FxAClient10.notifyResponse)
}
open func destroyDevice(ownDeviceId: GUID, withSessionToken sessionToken: NSData) -> Deferred<Maybe<FxADeviceDestroyResponse>> {
let URL = self.authURL.appendingPathComponent("/account/device/destroy")
var mutableURLRequest = URLRequest(url: URL)
let httpBody: JSON = JSON(["id": ownDeviceId])
mutableURLRequest.httpMethod = HTTPMethod.post.rawValue
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
mutableURLRequest.httpBody = httpBody.stringValue()?.utf8EncodedData
let salt: Data = Data()
let contextInfo: Data = FxAClient10.KW("sessionToken")
let key = sessionToken.deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))!
mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key)
return makeRequest(mutableURLRequest, responseHandler: FxAClient10.deviceDestroyResponse)
}
open func registerOrUpdate(device: FxADevice, withSessionToken sessionToken: NSData) -> Deferred<Maybe<FxADevice>> {
let URL = self.authURL.appendingPathComponent("/account/device")
var mutableURLRequest = URLRequest(url: URL)
mutableURLRequest.httpMethod = HTTPMethod.post.rawValue
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
mutableURLRequest.httpBody = device.toJSON().stringValue()?.utf8EncodedData
let salt: Data = Data()
let contextInfo: Data = FxAClient10.KW("sessionToken")
let key = sessionToken.deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))!
mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key)
return makeRequest(mutableURLRequest, responseHandler: FxADevice.fromJSON)
}
open func oauthAuthorize(withSessionToken sessionToken: NSData, keyPair: RSAKeyPair, certificate: String) -> Deferred<Maybe<FxAOAuthResponse>> {
let audience = self.getAudience(forURL: self.oauthURL)
let assertion = JSONWebTokenUtils.createAssertionWithPrivateKeyToSign(with: keyPair.privateKey,
certificate: certificate,
audience: audience)
let oauthAuthorizationURL = self.oauthURL.appendingPathComponent("/authorization")
var mutableURLRequest = URLRequest(url: oauthAuthorizationURL)
mutableURLRequest.httpMethod = HTTPMethod.post.rawValue
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
let parameters = [
"assertion": assertion,
"client_id": AppConstants.FxAiOSClientId,
"response_type": "token",
"scope": "profile",
"ttl": "300"
]
let salt: Data = Data()
let contextInfo: Data = FxAClient10.KW("sessionToken")
let key = sessionToken.deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))!
guard let httpBody = JSON(parameters as NSDictionary).stringValue()?.utf8EncodedData else {
return deferMaybe(FxAClientError.local(FxAClientUnknownError))
}
mutableURLRequest.httpBody = httpBody
mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key)
return makeRequest(mutableURLRequest, responseHandler: FxAClient10.oauthResponse)
}
open func getProfile(withSessionToken sessionToken: NSData) -> Deferred<Maybe<FxAProfileResponse>> {
let keyPair = RSAKeyPair.generate(withModulusSize: 1024)!
return self.sign(sessionToken as Data, publicKey: keyPair.publicKey) >>== { signResult in
return self.oauthAuthorize(withSessionToken: sessionToken, keyPair: keyPair, certificate: signResult.certificate) >>== { oauthResult in
let profileURL = self.profileURL.appendingPathComponent("/profile")
var mutableURLRequest = URLRequest(url: profileURL)
mutableURLRequest.httpMethod = HTTPMethod.get.rawValue
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
mutableURLRequest.setValue("Bearer " + oauthResult.accessToken, forHTTPHeaderField: "Authorization")
return self.makeRequest(mutableURLRequest, responseHandler: FxAClient10.profileResponse)
}
}
}
open func getAudience(forURL URL: URL) -> String {
if let port = URL.port {
return "\(URL.scheme!)://\(URL.host!):\(port)"
} else {
return "\(URL.scheme!)://\(URL.host!)"
}
}
fileprivate func makeRequest<T>(_ request: URLRequest, responseHandler: @escaping (JSON) -> T?) -> Deferred<Maybe<T>> {
let deferred = Deferred<Maybe<T>>()
alamofire.request(request)
.validate(contentType: ["application/json"])
.responseJSON { response in
withExtendedLifetime(self.alamofire) {
if let error = response.result.error {
deferred.fill(Maybe(failure: FxAClientError.local(error as NSError)))
return
}
if let data = response.result.value {
let json = JSON(data)
if let remoteError = FxAClient10.remoteError(fromJSON: json, statusCode: response.response!.statusCode) {
deferred.fill(Maybe(failure: FxAClientError.remote(remoteError)))
return
}
if let response = responseHandler(json) {
deferred.fill(Maybe(success: response))
return
}
}
deferred.fill(Maybe(failure: FxAClientError.local(FxAClientUnknownError)))
}
}
return deferred
}
}
extension FxAClient10: FxALoginClient {
func keyPair() -> Deferred<Maybe<KeyPair>> {
let result = RSAKeyPair.generate(withModulusSize: 2048)! // TODO: debate key size and extract this constant.
return Deferred(value: Maybe(success: result))
}
open func keys(_ keyFetchToken: Data) -> Deferred<Maybe<FxAKeysResponse>> {
let URL = self.authURL.appendingPathComponent("/account/keys")
var mutableURLRequest = URLRequest(url: URL)
mutableURLRequest.httpMethod = HTTPMethod.get.rawValue
let salt: Data = Data()
let contextInfo: Data = FxAClient10.KW("keyFetchToken")
let key = (keyFetchToken as NSData).deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(3 * KeyLength))!
mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key)
let rangeStart = 2 * KeyLength
let keyRequestKey = key.subdata(in: rangeStart..<(rangeStart + KeyLength))
return makeRequest(mutableURLRequest) { FxAClient10.keysResponse(fromJSON: keyRequestKey, json: $0) }
}
open func sign(_ sessionToken: Data, publicKey: PublicKey) -> Deferred<Maybe<FxASignResponse>> {
let parameters = [
"publicKey": publicKey.jsonRepresentation() as NSDictionary,
"duration": NSNumber(value: OneDayInMilliseconds), // The maximum the server will allow.
]
let url = self.authURL.appendingPathComponent("/certificate/sign")
var mutableURLRequest = URLRequest(url: url)
mutableURLRequest.httpMethod = HTTPMethod.post.rawValue
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
mutableURLRequest.httpBody = JSON(parameters as NSDictionary).stringValue()?.utf8EncodedData
let salt: Data = Data()
let contextInfo: Data = FxAClient10.KW("sessionToken")
let key = (sessionToken as NSData).deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))!
mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key)
return makeRequest(mutableURLRequest, responseHandler: FxAClient10.signResponse)
}
}