/
FxALoginStateMachine.swift
182 lines (167 loc) · 9.4 KB
/
FxALoginStateMachine.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/* 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 Foundation
import FxA
import Shared
import XCGLogger
import Deferred
// TODO: log to an FxA-only, persistent log file.
private let log = Logger.syncLogger
// TODO: fill this in!
private let KeyUnwrappingError = NSError(domain: "org.mozilla", code: 1, userInfo: nil)
protocol FxALoginClient {
func keyPair() -> Deferred<Maybe<KeyPair>>
func keys(_ keyFetchToken: Data) -> Deferred<Maybe<FxAKeysResponse>>
func sign(_ sessionToken: Data, publicKey: PublicKey) -> Deferred<Maybe<FxASignResponse>>
}
class FxALoginStateMachine {
let client: FxALoginClient
// The keys are used as a set, to prevent cycles in the state machine.
var stateLabelsSeen = [FxAStateLabel: Bool]()
init(client: FxALoginClient) {
self.client = client
}
func advance(fromState state: FxAState, now: Timestamp) -> Deferred<FxAState> {
stateLabelsSeen.updateValue(true, forKey: state.label)
return self.advanceOne(fromState: state, now: now).bind { (newState: FxAState) in
let labelAlreadySeen = self.stateLabelsSeen.updateValue(true, forKey: newState.label) != nil
if labelAlreadySeen {
// Last stop!
return Deferred(value: newState)
}
return self.advance(fromState: newState, now: now)
}
}
fileprivate func advanceOne(fromState state: FxAState, now: Timestamp) -> Deferred<FxAState> {
// For convenience. Without type annotation, Swift complains about types not being exact.
let separated: Deferred<FxAState> = Deferred(value: SeparatedState())
let doghouse: Deferred<FxAState> = Deferred(value: DoghouseState())
let same: Deferred<FxAState> = Deferred(value: state)
log.info("Advancing from state: \(state.label.rawValue)")
switch state.label {
case .married:
let state = state as! MarriedState
log.debug("Checking key pair freshness.")
if state.isKeyPairExpired(now) {
log.info("Key pair has expired; transitioning to CohabitingBeforeKeyPair.")
return advanceOne(fromState: state.withoutKeyPair(), now: now)
}
log.debug("Checking certificate freshness.")
if state.isCertificateExpired(now) {
log.info("Certificate has expired; transitioning to CohabitingAfterKeyPair.")
return advanceOne(fromState: state.withoutCertificate(), now: now)
}
log.info("Key pair and certificate are fresh; staying Married.")
return same
case .cohabitingBeforeKeyPair:
let state = state as! CohabitingBeforeKeyPairState
log.debug("Generating key pair.")
return self.client.keyPair().bind { result in
if let keyPair = result.successValue {
log.info("Generated key pair! Transitioning to CohabitingAfterKeyPair.")
let newState = CohabitingAfterKeyPairState(sessionToken: state.sessionToken,
kA: state.kA, kB: state.kB,
keyPair: keyPair, keyPairExpiresAt: now + OneMonthInMilliseconds)
return Deferred(value: newState)
} else {
log.error("Failed to generate key pair! Something is horribly wrong; transitioning to Separated in the hope that the error is transient.")
return separated
}
}
case .cohabitingAfterKeyPair:
let state = state as! CohabitingAfterKeyPairState
log.debug("Signing public key.")
return client.sign(state.sessionToken, publicKey: state.keyPair.publicKey).bind { result in
if let response = result.successValue {
log.info("Signed public key! Transitioning to Married.")
let newState = MarriedState(sessionToken: state.sessionToken,
kA: state.kA, kB: state.kB,
keyPair: state.keyPair, keyPairExpiresAt: state.keyPairExpiresAt,
certificate: response.certificate, certificateExpiresAt: now + OneDayInMilliseconds)
return Deferred(value: newState)
} else {
if let error = result.failureValue as? FxAClientError {
switch error {
case let .remote(remoteError):
if remoteError.isUpgradeRequired {
log.error("Upgrade required: \(error.description)! Transitioning to Doghouse.")
return doghouse
} else if remoteError.isInvalidAuthentication {
log.error("Invalid authentication: \(error.description)! Transitioning to Separated.")
return separated
} else if remoteError.code < 200 || remoteError.code >= 300 {
log.error("Unsuccessful HTTP request: \(error.description)! Assuming error is transient and not transitioning.")
return same
} else {
log.error("Unknown error: \(error.description). Transitioning to Separated.")
return separated
}
case let .local(localError) where localError.domain == NSURLErrorDomain:
log.warning("Local networking error: \(result.failureValue!). Assuming transient and not transitioning.")
return same
default:
break
}
}
log.error("Unknown error: \(result.failureValue!). Transitioning to Separated.")
return separated
}
}
case .engagedBeforeVerified, .engagedAfterVerified:
let state = state as! ReadyForKeys
log.debug("Fetching keys.")
return client.keys(state.keyFetchToken).bind { result in
if let response = result.successValue {
if let kB = response.wrapkB.xoredWith(state.unwrapkB) {
log.info("Unwrapped keys response. Transition to CohabitingBeforeKeyPair.")
self.notifyAccountVerified()
let newState = CohabitingBeforeKeyPairState(sessionToken: state.sessionToken,
kA: response.kA, kB: kB)
return Deferred(value: newState)
} else {
log.error("Failed to unwrap keys response! Transitioning to Separated in order to fetch new initial datum.")
return separated
}
} else {
if let error = result.failureValue as? FxAClientError {
log.error("Error \(error.description) \(error.description)")
switch error {
case let .remote(remoteError):
if remoteError.isUpgradeRequired {
log.error("Upgrade required: \(error.description)! Transitioning to Doghouse.")
return doghouse
} else if remoteError.isInvalidAuthentication {
log.error("Invalid authentication: \(error.description)! Transitioning to Separated in order to fetch new initial datum.")
return separated
} else if remoteError.isUnverified {
log.warning("Account is not yet verified; not transitioning.")
return same
} else if remoteError.code < 200 || remoteError.code >= 300 {
log.error("Unsuccessful HTTP request: \(error.description)! Assuming error is transient and not transitioning.")
return same
} else {
log.error("Unknown error: \(error.description). Transitioning to Separated.")
return separated
}
case let .local(localError) where localError.domain == NSURLErrorDomain:
log.warning("Local networking error: \(result.failureValue!). Assuming transient and not transitioning.")
return same
default:
break
}
}
log.error("Unknown error: \(result.failureValue!). Transitioning to Separated.")
return separated
}
}
case .separated, .doghouse:
// We can't advance from the separated state (we need user input) or the doghouse (we need a client upgrade).
log.warning("User interaction required; not transitioning.")
return same
}
}
fileprivate func notifyAccountVerified() {
NotificationCenter.default.post(name: NotificationFirefoxAccountVerified, object: nil, userInfo: nil)
}
}