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