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 "fmt"
9 "hash"
10 "strings"
11
12 "github.com/mjl-/mox/scram"
13)
14
15// Client is a SASL client
16type Client interface {
17 // Name as used in SMTP AUTH, e.g. PLAIN, CRAM-MD5, SCRAM-SHA-256.
18 // cleartextCredentials indicates if credentials are exchanged in clear text, which influences whether they are logged.
19 Info() (name string, cleartextCredentials bool)
20
21 // Next is called for each step of the SASL communication. The first call has a nil
22 // fromServer and serves to get a possible "initial response" from the client. If
23 // the client sends its final message it indicates so with last. Returning an error
24 // aborts the authentication attempt.
25 // For the first toServer ("initial response"), a nil toServer indicates there is
26 // no data, which is different from a non-nil zero-length toServer.
27 Next(fromServer []byte) (toServer []byte, last bool, err error)
28}
29
30type clientPlain struct {
31 Username, Password string
32 step int
33}
34
35var _ Client = (*clientPlain)(nil)
36
37// NewClientPlain returns a client for SASL PLAIN authentication.
38func NewClientPlain(username, password string) Client {
39 return &clientPlain{username, password, 0}
40}
41
42func (a *clientPlain) Info() (name string, hasCleartextCredentials bool) {
43 return "PLAIN", true
44}
45
46func (a *clientPlain) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
47 defer func() { a.step++ }()
48 switch a.step {
49 case 0:
50 return []byte(fmt.Sprintf("\u0000%s\u0000%s", a.Username, a.Password)), true, nil
51 default:
52 return nil, false, fmt.Errorf("invalid step %d", a.step)
53 }
54}
55
56type clientCRAMMD5 struct {
57 Username, Password string
58 step int
59}
60
61var _ Client = (*clientCRAMMD5)(nil)
62
63// NewClientCRAMMD5 returns a client for SASL CRAM-MD5 authentication.
64func NewClientCRAMMD5(username, password string) Client {
65 return &clientCRAMMD5{username, password, 0}
66}
67
68func (a *clientCRAMMD5) Info() (name string, hasCleartextCredentials bool) {
69 return "CRAM-MD5", false
70}
71
72func (a *clientCRAMMD5) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
73 defer func() { a.step++ }()
74 switch a.step {
75 case 0:
76 return nil, false, nil
77 case 1:
78 // Validate the challenge.
79 // ../rfc/2195:82
80 s := string(fromServer)
81 if !strings.HasPrefix(s, "<") || !strings.HasSuffix(s, ">") {
82 return nil, false, fmt.Errorf("invalid challenge, missing angle brackets")
83 }
84 t := strings.SplitN(s, ".", 2)
85 if len(t) != 2 || t[0] == "" {
86 return nil, false, fmt.Errorf("invalid challenge, missing dot or random digits")
87 }
88 t = strings.Split(t[1], "@")
89 if len(t) == 1 || t[0] == "" || t[len(t)-1] == "" {
90 return nil, false, fmt.Errorf("invalid challenge, empty timestamp or empty hostname")
91 }
92
93 // ../rfc/2195:138
94 key := []byte(a.Password)
95 if len(key) > 64 {
96 t := md5.Sum(key)
97 key = t[:]
98 }
99 ipad := make([]byte, md5.BlockSize)
100 opad := make([]byte, md5.BlockSize)
101 copy(ipad, key)
102 copy(opad, key)
103 for i := range ipad {
104 ipad[i] ^= 0x36
105 opad[i] ^= 0x5c
106 }
107 ipadh := md5.New()
108 ipadh.Write(ipad)
109 ipadh.Write([]byte(fromServer))
110
111 opadh := md5.New()
112 opadh.Write(opad)
113 opadh.Write(ipadh.Sum(nil))
114
115 // ../rfc/2195:88
116 return []byte(fmt.Sprintf("%s %x", a.Username, opadh.Sum(nil))), true, nil
117
118 default:
119 return nil, false, fmt.Errorf("invalid step %d", a.step)
120 }
121}
122
123type clientSCRAMSHA struct {
124 Username, Password string
125
126 name string
127 step int
128 scram *scram.Client
129}
130
131var _ Client = (*clientSCRAMSHA)(nil)
132
133// NewClientSCRAMSHA1 returns a client for SASL SCRAM-SHA-1 authentication.
134func NewClientSCRAMSHA1(username, password string) Client {
135 return &clientSCRAMSHA{username, password, "SCRAM-SHA-1", 0, nil}
136}
137
138// NewClientSCRAMSHA256 returns a client for SASL SCRAM-SHA-256 authentication.
139func NewClientSCRAMSHA256(username, password string) Client {
140 return &clientSCRAMSHA{username, password, "SCRAM-SHA-256", 0, nil}
141}
142
143func (a *clientSCRAMSHA) Info() (name string, hasCleartextCredentials bool) {
144 return a.name, false
145}
146
147func (a *clientSCRAMSHA) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
148 defer func() { a.step++ }()
149 switch a.step {
150 case 0:
151 var h func() hash.Hash
152 switch a.name {
153 case "SCRAM-SHA-1":
154 h = sha1.New
155 case "SCRAM-SHA-256":
156 h = sha256.New
157 default:
158 return nil, false, fmt.Errorf("invalid SCRAM-SHA variant %q", a.name)
159 }
160
161 a.scram = scram.NewClient(h, a.Username, "")
162 toserver, err := a.scram.ClientFirst()
163 return []byte(toserver), false, err
164
165 case 1:
166 clientFinal, err := a.scram.ServerFirst(fromServer, a.Password)
167 return []byte(clientFinal), false, err
168
169 case 2:
170 err := a.scram.ServerFinal(fromServer)
171 return nil, true, err
172
173 default:
174 return nil, false, fmt.Errorf("invalid step %d", a.step)
175 }
176}
177