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