1package imapserver
2
3import (
4 "crypto/hmac"
5 "crypto/md5"
6 "crypto/sha1"
7 "crypto/sha256"
8 "encoding/base64"
9 "errors"
10 "fmt"
11 "hash"
12 "strings"
13 "testing"
14
15 "golang.org/x/text/secure/precis"
16
17 "github.com/mjl-/mox/scram"
18)
19
20func TestAuthenticateLogin(t *testing.T) {
21 // NFD username and PRECIS-cleaned password.
22 tc := start(t)
23 tc.client.Login("mo\u0301x@mox.example", password1)
24 tc.close()
25}
26
27func TestAuthenticatePlain(t *testing.T) {
28 tc := start(t)
29
30 tc.transactf("no", "authenticate bogus ")
31 tc.transactf("bad", "authenticate plain not base64...")
32 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000baduser\u0000badpass")))
33 tc.xcode("AUTHENTICATIONFAILED")
34 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000badpass")))
35 tc.xcode("AUTHENTICATIONFAILED")
36 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl\u0000badpass"))) // Need email, not account.
37 tc.xcode("AUTHENTICATIONFAILED")
38 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test")))
39 tc.xcode("AUTHENTICATIONFAILED")
40 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test"+password0)))
41 tc.xcode("AUTHENTICATIONFAILED")
42 tc.transactf("bad", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000")))
43 tc.xcode("")
44 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("other\u0000mjl@mox.example\u0000"+password0)))
45 tc.xcode("AUTHORIZATIONFAILED")
46 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
47 tc.close()
48
49 tc = start(t)
50 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example\u0000mjl@mox.example\u0000"+password0)))
51 tc.close()
52
53 // NFD username and PRECIS-cleaned password.
54 tc = start(t)
55 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mo\u0301x@mox.example\u0000mo\u0301x@mox.example\u0000"+password1)))
56 tc.close()
57
58 tc = start(t)
59 tc.client.AuthenticatePlain("mjl@mox.example", password0)
60 tc.close()
61
62 tc = start(t)
63 defer tc.close()
64
65 tc.cmdf("", "authenticate plain")
66 tc.readprefixline("+ ")
67 tc.writelinef("*") // Aborts.
68 tc.readstatus("bad")
69
70 tc.cmdf("", "authenticate plain")
71 tc.readprefixline("+ ")
72 tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
73 tc.readstatus("ok")
74}
75
76func TestAuthenticateSCRAMSHA1(t *testing.T) {
77 testAuthenticateSCRAM(t, false, "SCRAM-SHA-1", sha1.New)
78}
79
80func TestAuthenticateSCRAMSHA256(t *testing.T) {
81 testAuthenticateSCRAM(t, false, "SCRAM-SHA-256", sha256.New)
82}
83
84func TestAuthenticateSCRAMSHA1PLUS(t *testing.T) {
85 testAuthenticateSCRAM(t, true, "SCRAM-SHA-1-PLUS", sha1.New)
86}
87
88func TestAuthenticateSCRAMSHA256PLUS(t *testing.T) {
89 testAuthenticateSCRAM(t, true, "SCRAM-SHA-256-PLUS", sha256.New)
90}
91
92func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash.Hash) {
93 tc := startArgs(t, true, tls, true, true, "mjl")
94 tc.client.AuthenticateSCRAM(method, h, "mjl@mox.example", password0)
95 tc.close()
96
97 auth := func(status string, serverFinalError error, username, password string) {
98 t.Helper()
99
100 noServerPlus := false
101 sc := scram.NewClient(h, username, "", noServerPlus, tc.client.TLSConnectionState())
102 clientFirst, err := sc.ClientFirst()
103 tc.check(err, "scram clientFirst")
104 tc.client.LastTag = "x001"
105 tc.writelinef("%s authenticate %s %s", tc.client.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
106
107 xreadContinuation := func() []byte {
108 line, _, result, rerr := tc.client.ReadContinuation()
109 tc.check(rerr, "read continuation")
110 if result.Status != "" {
111 tc.t.Fatalf("expected continuation")
112 }
113 buf, err := base64.StdEncoding.DecodeString(line)
114 tc.check(err, "parsing base64 from remote")
115 return buf
116 }
117
118 serverFirst := xreadContinuation()
119 clientFinal, err := sc.ServerFirst(serverFirst, password)
120 tc.check(err, "scram clientFinal")
121 tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(clientFinal)))
122
123 serverFinal := xreadContinuation()
124 err = sc.ServerFinal(serverFinal)
125 if serverFinalError == nil {
126 tc.check(err, "scram serverFinal")
127 } else if err == nil || !errors.Is(err, serverFinalError) {
128 t.Fatalf("server final, got err %#v, expected %#v", err, serverFinalError)
129 }
130 if serverFinalError != nil {
131 tc.writelinef("*")
132 } else {
133 tc.writelinef("")
134 }
135 _, result, err := tc.client.Response()
136 tc.check(err, "read response")
137 if string(result.Status) != strings.ToUpper(status) {
138 tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status))
139 }
140 }
141
142 tc = startArgs(t, true, tls, true, true, "mjl")
143 auth("no", scram.ErrInvalidProof, "mjl@mox.example", "badpass")
144 auth("no", scram.ErrInvalidProof, "mjl@mox.example", "")
145 // todo: server aborts due to invalid username. we should probably make client continue with fake determinisitically generated salt and result in error in the end.
146 // auth("no", nil, "other@mox.example", password0)
147
148 tc.transactf("no", "authenticate bogus ")
149 tc.transactf("bad", "authenticate %s not base64...", method)
150 tc.transactf("no", "authenticate %s %s", method, base64.StdEncoding.EncodeToString([]byte("bad data")))
151
152 // NFD username, with PRECIS-cleaned password.
153 auth("ok", nil, "mo\u0301x@mox.example", password1)
154
155 tc.close()
156}
157
158func TestAuthenticateCRAMMD5(t *testing.T) {
159 tc := start(t)
160
161 tc.transactf("no", "authenticate bogus ")
162 tc.transactf("bad", "authenticate CRAM-MD5 not base64...")
163 tc.transactf("bad", "authenticate CRAM-MD5 %s", base64.StdEncoding.EncodeToString([]byte("baddata")))
164 tc.transactf("bad", "authenticate CRAM-MD5 %s", base64.StdEncoding.EncodeToString([]byte("bad data")))
165
166 auth := func(status string, username, password string) {
167 t.Helper()
168
169 tc.client.LastTag = "x001"
170 tc.writelinef("%s authenticate CRAM-MD5", tc.client.LastTag)
171
172 xreadContinuation := func() []byte {
173 line, _, result, rerr := tc.client.ReadContinuation()
174 tc.check(rerr, "read continuation")
175 if result.Status != "" {
176 tc.t.Fatalf("expected continuation")
177 }
178 buf, err := base64.StdEncoding.DecodeString(line)
179 tc.check(err, "parsing base64 from remote")
180 return buf
181 }
182
183 chal := xreadContinuation()
184 pw, err := precis.OpaqueString.String(password)
185 if err == nil {
186 password = pw
187 }
188 h := hmac.New(md5.New, []byte(password))
189 h.Write([]byte(chal))
190 resp := fmt.Sprintf("%s %x", username, h.Sum(nil))
191 tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(resp)))
192
193 _, result, err := tc.client.Response()
194 tc.check(err, "read response")
195 if string(result.Status) != strings.ToUpper(status) {
196 tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status))
197 }
198 }
199
200 auth("no", "mjl@mox.example", "badpass")
201 auth("no", "mjl@mox.example", "")
202 auth("no", "other@mox.example", password0)
203
204 auth("ok", "mjl@mox.example", password0)
205
206 tc.close()
207
208 // NFD username, with PRECIS-cleaned password.
209 tc = start(t)
210 auth("ok", "mo\u0301x@mox.example", password1)
211 tc.close()
212}
213