1// Package SASL implements a client for Simple Authentication and Security Layer, RFC 4422.
2//
3// Supported authentication mechanisms:
4//
5// - EXTERNAL
6// - SCRAM-SHA-256-PLUS
7// - SCRAM-SHA-1-PLUS
8// - SCRAM-SHA-256
9// - SCRAM-SHA-1
10// - CRAM-MD5
11// - PLAIN
12// - LOGIN
13package sasl
14
15import (
16 "crypto/md5"
17 "crypto/sha1"
18 "crypto/sha256"
19 "crypto/tls"
20 "fmt"
21 "hash"
22 "strings"
23
24 "golang.org/x/text/secure/precis"
25
26 "github.com/mjl-/mox/scram"
27)
28
29// Client is a SASL client.
30//
31// A SASL client can be used for authentication in IMAP, SMTP and other protocols.
32// A client and server exchange messages in step lock. In IMAP and SMTP, these
33// messages are encoded with base64. Each SASL mechanism has predefined steps, but
34// the transaction can be aborted by either side at any time. An IMAP or SMTP
35// client must choose a SASL mechanism, instantiate a SASL client, and call Next
36// with a nil parameter. The resulting data must be written to the server, properly
37// encoded. The client must then read the response from the server and feed it to
38// the SASL client, which will return more data to send, or an error.
39type Client interface {
40 // Name as used in SMTP or IMAP authentication, e.g. PLAIN, CRAM-MD5,
41 // SCRAM-SHA-256. cleartextCredentials indicates if credentials are exchanged in
42 // clear text, which can be used to decide if the exchange is logged.
43 Info() (name string, cleartextCredentials bool)
44
45 // Next must be called for each step of the SASL transaction. The first call has a
46 // nil fromServer and serves to get a possible "initial response" from the client
47 // to the server. When last is true, the message from client to server is the last
48 // one, and the server must send a verdict. If err is set, the transaction must be
49 // aborted.
50 //
51 // For the first toServer ("initial response"), a nil toServer indicates there is
52 // no data, which is different from a non-nil zero-length toServer.
53 Next(fromServer []byte) (toServer []byte, last bool, err error)
54}
55
56type clientPlain struct {
57 Username, Password string
58 step int
59}
60
61var _ Client = (*clientPlain)(nil)
62
63// NewClientPlain returns a client for SASL PLAIN authentication.
64//
65// PLAIN is specified in RFC 4616, The PLAIN Simple Authentication and Security
66// Layer (SASL) Mechanism.
67func NewClientPlain(username, password string) Client {
68 // No "precis" processing, remote can clean password up. ../rfc/8265:679
69 return &clientPlain{username, password, 0}
70}
71
72func (a *clientPlain) Info() (name string, hasCleartextCredentials bool) {
73 return "PLAIN", true
74}
75
76func (a *clientPlain) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
77 defer func() { a.step++ }()
78 switch a.step {
79 case 0:
80 return []byte(fmt.Sprintf("\u0000%s\u0000%s", a.Username, a.Password)), true, nil
81 default:
82 return nil, false, fmt.Errorf("invalid step %d", a.step)
83 }
84}
85
86type clientLogin struct {
87 Username, Password string
88 step int
89}
90
91var _ Client = (*clientLogin)(nil)
92
93// NewClientLogin returns a client for the obsolete SASL LOGIN authentication.
94//
95// See https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
96func NewClientLogin(username, password string) Client {
97 // No "precis" processing, remote can clean password up. ../rfc/8265:679
98 return &clientLogin{username, password, 0}
99}
100
101func (a *clientLogin) Info() (name string, hasCleartextCredentials bool) {
102 return "LOGIN", true
103}
104
105func (a *clientLogin) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
106 defer func() { a.step++ }()
107 switch a.step {
108 case 0:
109 return []byte(a.Username), false, nil
110 case 1:
111 return []byte(a.Password), true, nil
112 default:
113 return nil, false, fmt.Errorf("invalid step %d", a.step)
114 }
115}
116
117// Cleanup password with precis, like remote should have done. If the password
118// appears invalid, we'll return the original, there is a chance the server also
119// doesn't enforce requirements and accepts it. ../rfc/8265:679
120func precisPassword(password string) string {
121 pw, err := precis.OpaqueString.String(password)
122 if err != nil {
123 return password
124 }
125 return pw
126}
127
128type clientCRAMMD5 struct {
129 Username, Password string
130 step int
131}
132
133var _ Client = (*clientCRAMMD5)(nil)
134
135// NewClientCRAMMD5 returns a client for SASL CRAM-MD5 authentication.
136//
137// CRAM-MD5 is specified in RFC 2195, IMAP/POP AUTHorize Extension for Simple
138// Challenge/Response.
139func NewClientCRAMMD5(username, password string) Client {
140 password = precisPassword(password)
141 return &clientCRAMMD5{username, password, 0}
142}
143
144func (a *clientCRAMMD5) Info() (name string, hasCleartextCredentials bool) {
145 return "CRAM-MD5", false
146}
147
148func (a *clientCRAMMD5) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
149 defer func() { a.step++ }()
150 switch a.step {
151 case 0:
152 return nil, false, nil
153 case 1:
154 // Validate the challenge.
155 // ../rfc/2195:82
156 s := string(fromServer)
157 if !strings.HasPrefix(s, "<") || !strings.HasSuffix(s, ">") {
158 return nil, false, fmt.Errorf("invalid challenge, missing angle brackets")
159 }
160 t := strings.SplitN(s, ".", 2)
161 if len(t) != 2 || t[0] == "" {
162 return nil, false, fmt.Errorf("invalid challenge, missing dot or random digits")
163 }
164 t = strings.Split(t[1], "@")
165 if len(t) == 1 || t[0] == "" || t[len(t)-1] == "" {
166 return nil, false, fmt.Errorf("invalid challenge, empty timestamp or empty hostname")
167 }
168
169 // ../rfc/2195:138
170 key := []byte(a.Password)
171 if len(key) > 64 {
172 t := md5.Sum(key)
173 key = t[:]
174 }
175 ipad := make([]byte, md5.BlockSize)
176 opad := make([]byte, md5.BlockSize)
177 copy(ipad, key)
178 copy(opad, key)
179 for i := range ipad {
180 ipad[i] ^= 0x36
181 opad[i] ^= 0x5c
182 }
183 ipadh := md5.New()
184 ipadh.Write(ipad)
185 ipadh.Write([]byte(fromServer))
186
187 opadh := md5.New()
188 opadh.Write(opad)
189 opadh.Write(ipadh.Sum(nil))
190
191 // ../rfc/2195:88
192 return []byte(fmt.Sprintf("%s %x", a.Username, opadh.Sum(nil))), true, nil
193
194 default:
195 return nil, false, fmt.Errorf("invalid step %d", a.step)
196 }
197}
198
199type clientSCRAMSHA struct {
200 Username, Password string
201
202 hash func() hash.Hash
203
204 plus bool
205 cs tls.ConnectionState
206
207 // When not doing PLUS variant, this field indicates whether that is because the
208 // server doesn't support the PLUS variant. Used for detecting MitM attempts.
209 noServerPlus bool
210
211 name string
212 step int
213 scram *scram.Client
214}
215
216var _ Client = (*clientSCRAMSHA)(nil)
217
218// NewClientSCRAMSHA1 returns a client for SASL SCRAM-SHA-1 authentication.
219//
220// Clients should prefer using the PLUS-variant with TLS channel binding, if
221// supported by a server. If noServerPlus is set, this mechanism was chosen because
222// the PLUS-variant was not supported by the server. If the server actually does
223// implement the PLUS variant, this can indicate a MitM attempt, which is detected
224// by the server and causes the authentication attempt to be aborted.
225//
226// SCRAM-SHA-1 is specified in RFC 5802, "Salted Challenge Response Authentication
227// Mechanism (SCRAM) SASL and GSS-API Mechanisms".
228func NewClientSCRAMSHA1(username, password string, noServerPlus bool) Client {
229 password = precisPassword(password)
230 return &clientSCRAMSHA{username, password, sha1.New, false, tls.ConnectionState{}, noServerPlus, "SCRAM-SHA-1", 0, nil}
231}
232
233// NewClientSCRAMSHA1PLUS returns a client for SASL SCRAM-SHA-1-PLUS authentication.
234//
235// The PLUS-variant binds the authentication exchange to the TLS connection,
236// detecting any MitM attempt.
237//
238// SCRAM-SHA-1-PLUS is specified in RFC 5802, "Salted Challenge Response
239// Authentication Mechanism (SCRAM) SASL and GSS-API Mechanisms".
240func NewClientSCRAMSHA1PLUS(username, password string, cs tls.ConnectionState) Client {
241 password = precisPassword(password)
242 return &clientSCRAMSHA{username, password, sha1.New, true, cs, false, "SCRAM-SHA-1-PLUS", 0, nil}
243}
244
245// NewClientSCRAMSHA256 returns a client for SASL SCRAM-SHA-256 authentication.
246//
247// Clients should prefer using the PLUS-variant with TLS channel binding, if
248// supported by a server. If noServerPlus is set, this mechanism was chosen because
249// the PLUS-variant was not supported by the server. If the server actually does
250// implement the PLUS variant, this can indicate a MitM attempt, which is detected
251// by the server and causes the authentication attempt to be aborted.
252//
253// SCRAM-SHA-256 is specified in RFC 7677, "SCRAM-SHA-256 and SCRAM-SHA-256-PLUS
254// Simple Authentication and Security Layer (SASL) Mechanisms".
255func NewClientSCRAMSHA256(username, password string, noServerPlus bool) Client {
256 password = precisPassword(password)
257 return &clientSCRAMSHA{username, password, sha256.New, false, tls.ConnectionState{}, noServerPlus, "SCRAM-SHA-256", 0, nil}
258}
259
260// NewClientSCRAMSHA256PLUS returns a client for SASL SCRAM-SHA-256-PLUS authentication.
261//
262// The PLUS-variant binds the authentication exchange to the TLS connection,
263// detecting any MitM attempt.
264//
265// SCRAM-SHA-256-PLUS is specified in RFC 7677, "SCRAM-SHA-256 and SCRAM-SHA-256-PLUS
266// Simple Authentication and Security Layer (SASL) Mechanisms".
267func NewClientSCRAMSHA256PLUS(username, password string, cs tls.ConnectionState) Client {
268 password = precisPassword(password)
269 return &clientSCRAMSHA{username, password, sha256.New, true, cs, false, "SCRAM-SHA-256-PLUS", 0, nil}
270}
271
272func (a *clientSCRAMSHA) Info() (name string, hasCleartextCredentials bool) {
273 return a.name, false
274}
275
276func (a *clientSCRAMSHA) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
277 defer func() { a.step++ }()
278 switch a.step {
279 case 0:
280 var cs *tls.ConnectionState
281 if a.plus {
282 cs = &a.cs
283 }
284 a.scram = scram.NewClient(a.hash, a.Username, "", a.noServerPlus, cs)
285 toserver, err := a.scram.ClientFirst()
286 return []byte(toserver), false, err
287
288 case 1:
289 clientFinal, err := a.scram.ServerFirst(fromServer, a.Password)
290 return []byte(clientFinal), false, err
291
292 case 2:
293 err := a.scram.ServerFinal(fromServer)
294 return nil, true, err
295
296 default:
297 return nil, false, fmt.Errorf("invalid step %d", a.step)
298 }
299}
300
301type clientExternal struct {
302 Username string
303 step int
304}
305
306var _ Client = (*clientExternal)(nil)
307
308// NewClientExternal returns a client for SASL EXTERNAL authentication.
309//
310// Username is optional.
311func NewClientExternal(username string) Client {
312 return &clientExternal{username, 0}
313}
314
315func (a *clientExternal) Info() (name string, hasCleartextCredentials bool) {
316 return "EXTERNAL", false
317}
318
319func (a *clientExternal) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
320 defer func() { a.step++ }()
321 switch a.step {
322 case 0:
323 return []byte(a.Username), true, nil
324 default:
325 return nil, false, fmt.Errorf("invalid step %d", a.step)
326 }
327}
328