forked from vapor/postgres-nio
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSASLAuthenticationManager.swift
171 lines (145 loc) · 7.21 KB
/
SASLAuthenticationManager.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
import Crypto
public final class SASLAuthenticationManager<M: SASLAuthenticationMechanism> {
private enum Role {
case client, server
}
private enum State {
/// Client: Waiting to send initial request. May transition to `waitingNextStep`.
/// Server: Waiting for initial request. May transition to `waitingNextStep`, `done`.
case waitingForInitial
/// Client: Initial request sent, waiting for next challenge. May transition to `done`.
/// Server: Latest challenge sent, waiting for next response. May transition to `done`.
case waitingNextStep
/// Client: Received success or failure. No more operations permitted.
/// Server: Sent success or failure. No more operations permitted.
case done
}
private let mechanism: M
private let role: Role
private var state: State = .waitingForInitial
public init(asClientSpeaking mechanism: M) {
self.mechanism = mechanism
self.role = .client
}
public init(asServerAccepting mechanism: M) {
self.mechanism = mechanism
self.role = .server
}
/// Handle an incoming message via the provided mechanism. The `sender`
/// closure will be invoked with any data that should be transmitted to the
/// other side of the negotiation. An error thrown from the closure will
/// immediately result in an authentication failure state. The closure may
/// be invoked even if authentication otherwise fails (such as for
/// mechanisms which send failure responses). On authentication failure, an
/// error is thrown. Otherwise, `true` is returned to indicate that
/// authentication has successfully completed. `false` is returned to
/// indicate that further steps are required by the current mechanism.
///
/// Pass a `nil` message to start the initial request from a client. It is
/// invalid to do this for a server.
public func handle(message: [UInt8]?, sender: ([UInt8]) throws -> Void) throws -> Bool {
guard self.state != .done else {
// Already did whatever we were gonna do.
throw SASLAuthenticationError.resultAlreadyDelivered
}
if message == nil {
guard self.role == .client else {
// Can't respond to `nil` as server
self.state = .done
throw SASLAuthenticationError.serverRoleRequiresMessage
}
guard self.state == .waitingForInitial else {
// Can't respond to `nil` as client twice.
self.state = .done
throw SASLAuthenticationError.initialRequestAlreadySent
}
} else if self.role == .client && state == .waitingForInitial {
// Must respond to `nil` as client first and exactly once.
self.state = .done
throw SASLAuthenticationError.initialRequestNotSent
}
switch self.mechanism.step(message: message) {
case .continue(let response):
if let response = response {
try sender(response)
}
self.state = .waitingNextStep
return false
case .succeed(let response):
if let response = response {
try sender(response)
}
self.state = .done
return true
case .fail(let response, let error):
if let response = response {
try sender(response)
}
self.state = .done
if let error = error {
throw error
} else {
throw SASLAuthenticationError.genericAuthenticationFailure
}
}
}
}
/// Various errors that can occur during SASL negotiation that are not specific
/// to the particular SASL mechanism in use.
public enum SASLAuthenticationError: Error {
/// A server can not handle a nonexistent message. Only an initial-state
/// client can do that, and even then it's really just a proxy for the API
/// having difficulty expressing "this must be done once and then never
/// again" clearly.
case serverRoleRequiresMessage
/// A client may only receive a nonexistent message once during the initial
/// state. This is a proxy for the API not being good at expressing a "must
/// do this first and only once."
case initialRequestAlreadySent
/// A client must receive a nonexistent message exactly once before doing
/// anything else. This is ALSO a proxy for the API just being bad at
/// expressing the requirement.
case initialRequestNotSent
/// Authentication failed, and the underlying mechanism declined to provide
/// a more specific error message.
case genericAuthenticationFailure
/// This `SASLAuthenticationManager` has already delivered a success or
/// failure result (which may include a fatal state management error). It
/// can not be reused.
case resultAlreadyDelivered
}
/// Signifies an action to be taken as the result of a single step of a SASL
/// mechanism.
public enum SASLAuthenticationStepResult {
/// More steps are needed. Assume neither success nor failure. If data is
/// provided, send it. A value of `nil` signifies sending no response at
/// all, whereas a value of `[]` signifies sending an empty response, which
/// may not be the same action depending on the underlying protocol
case `continue`(response: [UInt8]? = nil)
/// Signal authentication success. If data is provided, send it. A value of
/// `nil` signifies sending no response at all, whereas a value of `[]`
/// signifies sending an empty response, which may not be the same action
/// depending on the underlying protocol.
case succeed(response: [UInt8]? = nil)
/// Signal authentication failure. If data is provided, send it. A value of
/// `nil` signifies sending no response at all, whereas a value of `[]`
/// signifies sending an empty response, which may not be the same action
/// depending on the underlying protocol. The provided error, if any, is
/// surfaced. If none is provided, a generic failure is surfaced instead.
case fail(response: [UInt8]? = nil, error: Error? = nil)
}
/// The protocol to which all SASL mechanism implementations must conform. It is
/// the responsibility of each individual implementation to provide an API for
/// creating instances of itself which are able to retrieve information from the
/// caller (such as usernames and passwords) by some mechanism.
public protocol SASLAuthenticationMechanism {
/// The IANA-registered SASL mechanism name. This may be a family prefix or
/// a specific mechanism name. It is explicitly suitable for use in
/// negotiation via whatever underlying application-specific protocol is in
/// use for the purpose.
static var name: String { get }
/// Single-step the mechanism. The message may be `nil` in particular when
/// the local side of the negotiation is a client starting its initial
/// authentication request.
func step(message: [UInt8]?) -> SASLAuthenticationStepResult
}