diff --git a/imapclient/cmds.go b/imapclient/cmds.go index 9b6025d..7577f95 100644 --- a/imapclient/cmds.go +++ b/imapclient/cmds.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "encoding/base64" "fmt" + "hash" "strings" "time" @@ -60,17 +61,17 @@ func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged return } -// Authenticate with SCRAM-SHA-256, where the password is not exchanged in original -// plaintext form, but only derived hashes are exchanged by both parties as proof -// of knowledge of password. -func (c *Conn) AuthenticateSCRAMSHA256(username, password string) (untagged []Untagged, result Result, rerr error) { +// Authenticate with SCRAM-SHA-1 or SCRAM-SHA-256, where the password is not +// exchanged in original plaintext form, but only derived hashes are exchanged by +// both parties as proof of knowledge of password. +func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, password string) (untagged []Untagged, result Result, rerr error) { defer c.recover(&rerr) - sc := scram.NewClient(username, "") + sc := scram.NewClient(h, username, "") clientFirst, err := sc.ClientFirst() c.xcheckf(err, "scram clientFirst") c.LastTag = c.nextTag() - err = c.Writelinef("%s authenticate scram-sha-256 %s", c.LastTag, base64.StdEncoding.EncodeToString([]byte(clientFirst))) + err = c.Writelinef("%s authenticate %s %s", c.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst))) c.xcheckf(err, "writing command line") xreadContinuation := func() []byte { diff --git a/imapserver/authenticate_test.go b/imapserver/authenticate_test.go index bf25657..0dae24f 100644 --- a/imapserver/authenticate_test.go +++ b/imapserver/authenticate_test.go @@ -1,8 +1,11 @@ package imapserver import ( + "crypto/sha1" + "crypto/sha256" "encoding/base64" "errors" + "hash" "strings" "testing" @@ -53,19 +56,27 @@ func TestAuthenticatePlain(t *testing.T) { tc.readstatus("ok") } +func TestAuthenticateSCRAMSHA1(t *testing.T) { + testAuthenticateSCRAM(t, "SCRAM-SHA-1", sha1.New) +} + func TestAuthenticateSCRAMSHA256(t *testing.T) { + testAuthenticateSCRAM(t, "SCRAM-SHA-256", sha256.New) +} + +func testAuthenticateSCRAM(t *testing.T, method string, h func() hash.Hash) { tc := start(t) - tc.client.AuthenticateSCRAMSHA256("mjl@mox.example", "testtest") + tc.client.AuthenticateSCRAM(method, h, "mjl@mox.example", "testtest") tc.close() auth := func(status string, serverFinalError error, username, password string) { t.Helper() - sc := scram.NewClient(username, "") + sc := scram.NewClient(h, username, "") clientFirst, err := sc.ClientFirst() tc.check(err, "scram clientFirst") tc.client.LastTag = "x001" - tc.writelinef("%s authenticate scram-sha-256 %s", tc.client.LastTag, base64.StdEncoding.EncodeToString([]byte(clientFirst))) + tc.writelinef("%s authenticate %s %s", tc.client.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst))) xreadContinuation := func() []byte { line, _, result, rerr := tc.client.ReadContinuation() @@ -109,7 +120,7 @@ func TestAuthenticateSCRAMSHA256(t *testing.T) { // auth("no", nil, "other@mox.example", "testtest") tc.transactf("no", "authenticate bogus ") - tc.transactf("bad", "authenticate scram-sha-256 not base64...") - tc.transactf("bad", "authenticate scram-sha-256 %s", base64.StdEncoding.EncodeToString([]byte("bad data"))) + tc.transactf("bad", "authenticate %s not base64...", method) + tc.transactf("bad", "authenticate %s %s", method, base64.StdEncoding.EncodeToString([]byte("bad data"))) tc.close() } diff --git a/imapserver/server.go b/imapserver/server.go index e532759..1d42bed 100644 --- a/imapserver/server.go +++ b/imapserver/server.go @@ -39,10 +39,13 @@ import ( "bufio" "bytes" "context" + "crypto/sha1" + "crypto/sha256" "crypto/tls" "encoding/base64" "errors" "fmt" + "hash" "io" "net" "os" @@ -114,8 +117,9 @@ var ( // LIST-STATUS: ../rfc/5819 // ID: ../rfc/2971 // AUTH=SCRAM-SHA-256: ../rfc/7677 ../rfc/5802 +// AUTH=SCRAM-SHA-1: ../rfc/5802 // APPENDLIMIT, we support the max possible size, 1<<63 - 1: ../rfc/7889:129 -const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ONLY LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256 ID APPENDLIMIT=9223372036854775807" +const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ONLY LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1 ID APPENDLIMIT=9223372036854775807" type conn struct { cid int64 @@ -1388,16 +1392,22 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) { c.username = authc authResult = "ok" - case "SCRAM-SHA-256": + case "SCRAM-SHA-1", "SCRAM-SHA-256": // todo: improve handling of errors during scram. e.g. invalid parameters. should we abort the imap command, or continue until the end and respond with a scram-level error? // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go - authVariant = "scram-sha-256" + authVariant = strings.ToLower(authType) + var h func() hash.Hash + if authVariant == "scram-sha-1" { + h = sha1.New + } else { + h = sha256.New + } // No plaintext credentials, we can log these normally. c0 := xreadInitial() - ss, err := scram.NewServer(c0) + ss, err := scram.NewServer(h, c0) if err != nil { xsyntaxErrorf("starting scram: %w", err) } @@ -1418,12 +1428,16 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) { if ss.Authorization != "" && ss.Authorization != ss.Authentication { xuserErrorf("authentication with authorization for different user not supported") } - var password store.Password + var xscram store.SCRAM acc.WithRLock(func() { err := acc.DB.Read(func(tx *bstore.Tx) error { - password, err = bstore.QueryTx[store.Password](tx).Get() - xsc := password.SCRAMSHA256 - if err == bstore.ErrAbsent || err == nil && (len(xsc.Salt) == 0 || xsc.Iterations == 0 || len(xsc.SaltedPassword) == 0) { + password, err := bstore.QueryTx[store.Password](tx).Get() + if authVariant == "scram-sha-1" { + xscram = password.SCRAMSHA1 + } else { + xscram = password.SCRAMSHA256 + } + if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) { xuserErrorf("scram not possible") } xcheckf(err, "fetching credentials") @@ -1431,11 +1445,11 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) { }) xcheckf(err, "read tx") }) - s1, err := ss.ServerFirst(password.SCRAMSHA256.Iterations, password.SCRAMSHA256.Salt) + s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt) xcheckf(err, "scram first server step") c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1))) c2 := xreadContinuation() - s3, err := ss.Finish(c2, password.SCRAMSHA256.SaltedPassword) + s3, err := ss.Finish(c2, xscram.SaltedPassword) if len(s3) > 0 { c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3))) } diff --git a/scram/scram.go b/scram/scram.go index 98dd1b9..627e907 100644 --- a/scram/scram.go +++ b/scram/scram.go @@ -1,7 +1,7 @@ -// Package scram implements the SCRAM-SHA256 SASL authentication mechanism, RFC 7677. +// Package scram implements the SCRAM-SHA-* SASL authentication mechanism, RFC 7677 and RFC 5802. // -// SCRAM-SHA256 allows a client to authenticate to a server using a password -// without handing plaintext password over to the server. The client also +// SCRAM-SHA-256 and SCRAM-SHA-1 allow a client to authenticate to a server using a +// password without handing plaintext password over to the server. The client also // verifies the server knows (a derivative of) the password. package scram @@ -13,10 +13,10 @@ import ( "bytes" "crypto/hmac" cryptorand "crypto/rand" - "crypto/sha256" "encoding/base64" "errors" "fmt" + "hash" "strings" "golang.org/x/crypto/pbkdf2" @@ -83,14 +83,14 @@ func MakeRandom() []byte { } // SaltPassword returns a salted password. -func SaltPassword(password string, salt []byte, iterations int) []byte { +func SaltPassword(h func() hash.Hash, password string, salt []byte, iterations int) []byte { password = norm.NFC.String(password) - return pbkdf2.Key([]byte(password), salt, iterations, sha256.Size, sha256.New) + return pbkdf2.Key([]byte(password), salt, iterations, h().Size(), h) } // HMAC returns the hmac with key over msg. -func HMAC(key []byte, msg string) []byte { - mac := hmac.New(sha256.New, key) +func HMAC(h func() hash.Hash, key []byte, msg string) []byte { + mac := hmac.New(h, key) mac.Write([]byte(msg)) return mac.Sum(nil) } @@ -101,11 +101,13 @@ func xor(a, b []byte) { } } -// Server represents the server-side of a SCRAM-SHA-256 authentication. +// Server represents the server-side of a SCRAM-SHA-* authentication. type Server struct { Authentication string // Username for authentication, "authc". Always set and non-empty. Authorization string // If set, role of user to assume after authentication, "authz". + h func() hash.Hash // sha1.New or sha256.New + // Messages used in hash calculations. clientFirstBare string serverFirst string @@ -123,11 +125,11 @@ type Server struct { // // - Read initial data from client, call NewServer (this call), then ServerFirst and write to the client. // - Read response from client, call Finish or FinishFinal and write the resulting string. -func NewServer(clientFirst []byte) (server *Server, rerr error) { +func NewServer(h func() hash.Hash, clientFirst []byte) (server *Server, rerr error) { p := newParser(clientFirst) defer p.recover(&rerr) - server = &Server{} + server = &Server{h: h} // ../rfc/5802:949 ../rfc/5802:910 gs2cbindFlag := p.xbyte() @@ -209,18 +211,19 @@ func (s *Server) Finish(clientFinal []byte, saltedPassword []byte) (serverFinal msg := s.clientFirstBare + "," + s.serverFirst + "," + s.clientFinalWithoutProof - clientKey := HMAC(saltedPassword, "Client Key") - storedKey0 := sha256.Sum256(clientKey) - storedKey := storedKey0[:] + clientKey := HMAC(s.h, saltedPassword, "Client Key") + h := s.h() + h.Write(clientKey) + storedKey := h.Sum(nil) - clientSig := HMAC(storedKey, msg) + clientSig := HMAC(s.h, storedKey, msg) xor(clientSig, clientKey) // Now clientProof. if !bytes.Equal(clientSig, proof) { return "e=" + string(ErrInvalidProof), ErrInvalidProof } - serverKey := HMAC(saltedPassword, "Server Key") - serverSig := HMAC(serverKey, msg) + serverKey := HMAC(s.h, saltedPassword, "Server Key") + serverSig := HMAC(s.h, serverKey, msg) return fmt.Sprintf("v=%s", base64.StdEncoding.EncodeToString(serverSig)), nil } @@ -230,11 +233,13 @@ func (s *Server) FinishError(err Error) string { return "e=" + string(err) } -// Client represents the client-side of a SCRAM-SHA-256 authentication. +// Client represents the client-side of a SCRAM-SHA-* authentication. type Client struct { authc string authz string + h func() hash.Hash // sha1.New or sha256.New + // Messages used in hash calculations. clientFirstBare string serverFirst string @@ -248,17 +253,17 @@ type Client struct { } // NewClient returns a client for authentication authc, optionally for -// authorization with role authz. +// authorization with role authz, for the hash (sha1.New or sha256.New). // // The sequence for data and calls on a client: // // - ClientFirst, write result to server. // - Read response from server, feed to ServerFirst, write response to server. // - Read response from server, feed to ServerFinal. -func NewClient(authc, authz string) *Client { +func NewClient(h func() hash.Hash, authc, authz string) *Client { authc = norm.NFC.String(authc) authz = norm.NFC.String(authz) - return &Client{authc: authc, authz: authz} + return &Client{authc: authc, authz: authz, h: h} } // ClientFirst returns the first client message to write to the server. @@ -315,11 +320,12 @@ func (c *Client) ServerFirst(serverFirst []byte, password string) (clientFinal s c.authMessage = c.clientFirstBare + "," + c.serverFirst + "," + c.clientFinalWithoutProof - c.saltedPassword = SaltPassword(password, salt, iterations) - clientKey := HMAC(c.saltedPassword, "Client Key") - storedKey0 := sha256.Sum256(clientKey) - storedKey := storedKey0[:] - clientSig := HMAC(storedKey, c.authMessage) + c.saltedPassword = SaltPassword(c.h, password, salt, iterations) + clientKey := HMAC(c.h, c.saltedPassword, "Client Key") + h := c.h() + h.Write(clientKey) + storedKey := h.Sum(nil) + clientSig := HMAC(c.h, storedKey, c.authMessage) xor(clientSig, clientKey) // Now clientProof. clientProof := clientSig @@ -344,8 +350,8 @@ func (c *Client) ServerFinal(serverFinal []byte) (rerr error) { p.xtake("v=") verifier := p.xbase64() - serverKey := HMAC(c.saltedPassword, "Server Key") - serverSig := HMAC(serverKey, c.authMessage) + serverKey := HMAC(c.h, c.saltedPassword, "Server Key") + serverSig := HMAC(c.h, serverKey, c.authMessage) if !bytes.Equal(verifier, serverSig) { return fmt.Errorf("incorrect server signature") } diff --git a/scram/scram_test.go b/scram/scram_test.go index 69334d3..1a315a1 100644 --- a/scram/scram_test.go +++ b/scram/scram_test.go @@ -1,6 +1,8 @@ package scram import ( + "crypto/sha1" + "crypto/sha256" "encoding/base64" "errors" "testing" @@ -21,12 +23,32 @@ func tcheck(t *testing.T, err error, msg string) { } } -func TestScramServer(t *testing.T) { +func TestSCRAMSHA1Server(t *testing.T) { + // Test vector from ../rfc/5802:496 + salt := base64Decode("QSXCR+Q6sek8bf92") + saltedPassword := SaltPassword(sha1.New, "pencil", salt, 4096) + + server, err := NewServer(sha1.New, []byte("n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL")) + server.serverNonceOverride = "3rfcNHYJY1ZVvWVs7j" + tcheck(t, err, "newserver") + resp, err := server.ServerFirst(4096, salt) + tcheck(t, err, "server first") + if resp != "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096" { + t.Fatalf("bad server first") + } + serverFinal, err := server.Finish([]byte("c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts="), saltedPassword) + tcheck(t, err, "finish") + if serverFinal != "v=rmF9pqV8S7suAoZWja4dJRkFsKQ=" { + t.Fatalf("bad server final") + } +} + +func TestSCRAMSHA256Server(t *testing.T) { // Test vector from ../rfc/7677:122 salt := base64Decode("W22ZaJ0SNY7soEsUEjb6gQ==") - saltedPassword := SaltPassword("pencil", salt, 4096) + saltedPassword := SaltPassword(sha256.New, "pencil", salt, 4096) - server, err := NewServer([]byte("n,,n=user,r=rOprNGfwEbeRWgbNEkqO")) + server, err := NewServer(sha256.New, []byte("n,,n=user,r=rOprNGfwEbeRWgbNEkqO")) server.serverNonceOverride = "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0" tcheck(t, err, "newserver") resp, err := server.ServerFirst(4096, salt) @@ -44,9 +66,9 @@ func TestScramServer(t *testing.T) { // Bad attempt with wrong password. func TestScramServerBadPassword(t *testing.T) { salt := base64Decode("W22ZaJ0SNY7soEsUEjb6gQ==") - saltedPassword := SaltPassword("marker", salt, 4096) + saltedPassword := SaltPassword(sha256.New, "marker", salt, 4096) - server, err := NewServer([]byte("n,,n=user,r=rOprNGfwEbeRWgbNEkqO")) + server, err := NewServer(sha256.New, []byte("n,,n=user,r=rOprNGfwEbeRWgbNEkqO")) server.serverNonceOverride = "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0" tcheck(t, err, "newserver") _, err = server.ServerFirst(4096, salt) @@ -60,9 +82,9 @@ func TestScramServerBadPassword(t *testing.T) { // Bad attempt with different number of rounds. func TestScramServerBadIterations(t *testing.T) { salt := base64Decode("W22ZaJ0SNY7soEsUEjb6gQ==") - saltedPassword := SaltPassword("pencil", salt, 2048) + saltedPassword := SaltPassword(sha256.New, "pencil", salt, 2048) - server, err := NewServer([]byte("n,,n=user,r=rOprNGfwEbeRWgbNEkqO")) + server, err := NewServer(sha256.New, []byte("n,,n=user,r=rOprNGfwEbeRWgbNEkqO")) server.serverNonceOverride = "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0" tcheck(t, err, "newserver") _, err = server.ServerFirst(4096, salt) @@ -76,9 +98,9 @@ func TestScramServerBadIterations(t *testing.T) { // Another attempt but with a randomly different nonce. func TestScramServerBad(t *testing.T) { salt := base64Decode("W22ZaJ0SNY7soEsUEjb6gQ==") - saltedPassword := SaltPassword("pencil", salt, 4096) + saltedPassword := SaltPassword(sha256.New, "pencil", salt, 4096) - server, err := NewServer([]byte("n,,n=user,r=rOprNGfwEbeRWgbNEkqO")) + server, err := NewServer(sha256.New, []byte("n,,n=user,r=rOprNGfwEbeRWgbNEkqO")) tcheck(t, err, "newserver") _, err = server.ServerFirst(4096, salt) tcheck(t, err, "server first") @@ -89,7 +111,7 @@ func TestScramServerBad(t *testing.T) { } func TestScramClient(t *testing.T) { - c := NewClient("user", "") + c := NewClient(sha256.New, "user", "") c.clientNonce = "rOprNGfwEbeRWgbNEkqO" clientFirst, err := c.ClientFirst() tcheck(t, err, "ClientFirst") @@ -129,14 +151,14 @@ func TestScram(t *testing.T) { } salt := MakeRandom() - saltedPassword := SaltPassword(password, salt, iterations) + saltedPassword := SaltPassword(sha256.New, password, salt, iterations) - client := NewClient(username, "") + client := NewClient(sha256.New, username, "") client.clientNonce = clientNonce clientFirst, err := client.ClientFirst() xerr(err, "client.ClientFirst") - server, err := NewServer([]byte(clientFirst)) + server, err := NewServer(sha256.New, []byte(clientFirst)) xerr(err, "NewServer") server.serverNonceOverride = serverNonce diff --git a/smtpserver/server.go b/smtpserver/server.go index 8ffa5f9..be76222 100644 --- a/smtpserver/server.go +++ b/smtpserver/server.go @@ -6,10 +6,13 @@ import ( "bytes" "context" "crypto/rsa" + "crypto/sha1" + "crypto/sha256" "crypto/tls" "encoding/base64" "errors" "fmt" + "hash" "io" "net" "os" @@ -679,7 +682,7 @@ func (c *conn) cmdHello(p *parser, ehlo bool) { if c.submission { // ../rfc/4954:123 if c.tls || !c.requireTLSForAuth { - c.bwritelinef("250-AUTH PLAIN SCRAM-SHA-256") + c.bwritelinef("250-AUTH PLAIN SCRAM-SHA-256 SCRAM-SHA-1") } else { c.bwritelinef("250-AUTH ") } @@ -861,16 +864,22 @@ func (c *conn) cmdAuth(p *parser) { // ../rfc/4954:276 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil) - case "SCRAM-SHA-256": + case "SCRAM-SHA-1", "SCRAM-SHA-256": // todo: improve handling of errors during scram. e.g. invalid parameters. should we abort the imap command, or continue until the end and respond with a scram-level error? // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go - authVariant = "scram-sha-256" + authVariant = strings.ToLower(mech) + var h func() hash.Hash + if authVariant == "scram-sha-1" { + h = sha1.New + } else { + h = sha256.New + } // Passwords cannot be retrieved or replayed from the trace. c0 := xreadInitial() - ss, err := scram.NewServer(c0) + ss, err := scram.NewServer(h, c0) xcheckf(err, "starting scram") c.log.Info("scram auth", mlog.Field("authentication", ss.Authentication)) acc, _, err := store.OpenEmail(ss.Authentication) @@ -891,12 +900,16 @@ func (c *conn) cmdAuth(p *parser) { if ss.Authorization != "" && ss.Authorization != ss.Authentication { xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported") } - var password store.Password + var xscram store.SCRAM acc.WithRLock(func() { err := acc.DB.Read(func(tx *bstore.Tx) error { - password, err = bstore.QueryTx[store.Password](tx).Get() - xsc := password.SCRAMSHA256 - if err == bstore.ErrAbsent || err == nil && (len(xsc.Salt) == 0 || xsc.Iterations == 0 || len(xsc.SaltedPassword) == 0) { + password, err := bstore.QueryTx[store.Password](tx).Get() + if authVariant == "scram-sha-1" { + xscram = password.SCRAMSHA1 + } else { + xscram = password.SCRAMSHA256 + } + if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) { xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible") } xcheckf(err, "fetching credentials") @@ -904,11 +917,11 @@ func (c *conn) cmdAuth(p *parser) { }) xcheckf(err, "read tx") }) - s1, err := ss.ServerFirst(password.SCRAMSHA256.Iterations, password.SCRAMSHA256.Salt) + s1, err := ss.ServerFirst(xscram.Iterations, xscram.Salt) xcheckf(err, "scram first server step") c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) // ../rfc/4954:187 c2 := xreadContinuation() - s3, err := ss.Finish(c2, password.SCRAMSHA256.SaltedPassword) + s3, err := ss.Finish(c2, xscram.SaltedPassword) if len(s3) > 0 { c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) // ../rfc/4954:187 } diff --git a/store/account.go b/store/account.go index 8278c3c..146eacc 100644 --- a/store/account.go +++ b/store/account.go @@ -24,6 +24,8 @@ package store import ( "context" + "crypto/sha1" + "crypto/sha256" "encoding/json" "errors" "fmt" @@ -62,14 +64,17 @@ var subjectpassRand = mox.NewRand() var InitialMailboxes = []string{"Inbox", "Sent", "Archive", "Trash", "Drafts", "Junk"} +type SCRAM struct { + Salt []byte + Iterations int + SaltedPassword []byte +} + // Password holds a bcrypt hash for logging in with SMTP/IMAP/admin. type Password struct { Hash string - SCRAMSHA256 struct { - Salt []byte - Iterations int - SaltedPassword []byte - } + SCRAMSHA1 SCRAM + SCRAMSHA256 SCRAM } // Subjectpass holds the secret key used to sign subjectpass tokens. @@ -609,9 +614,15 @@ func (a *Account) SetPassword(password string) error { } var pw Password pw.Hash = string(hash) + + pw.SCRAMSHA1.Salt = scram.MakeRandom() + pw.SCRAMSHA1.Iterations = 2 * 4096 + pw.SCRAMSHA1.SaltedPassword = scram.SaltPassword(sha1.New, password, pw.SCRAMSHA1.Salt, pw.SCRAMSHA1.Iterations) + pw.SCRAMSHA256.Salt = scram.MakeRandom() pw.SCRAMSHA256.Iterations = 4096 - pw.SCRAMSHA256.SaltedPassword = scram.SaltPassword(password, pw.SCRAMSHA256.Salt, pw.SCRAMSHA256.Iterations) + pw.SCRAMSHA256.SaltedPassword = scram.SaltPassword(sha256.New, password, pw.SCRAMSHA256.Salt, pw.SCRAMSHA256.Iterations) + if err := tx.Insert(&pw); err != nil { return fmt.Errorf("inserting new password: %v", err) }