mirror of
https://github.com/mjl-/mox.git
synced 2024-12-27 08:53:48 +03:00
add support for SCRAM-SHA-1
the idea is that clients may not support SCRAM-SHA-256, but may support SCRAM-SHA-1. if they do support the 256 variant, they'll use it. unfortunately, thunderbird does not support scram-sha-1 either.
This commit is contained in:
parent
49dd5b7ba9
commit
642a328ae1
7 changed files with 156 additions and 78 deletions
|
@ -5,6 +5,7 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -60,17 +61,17 @@ func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate with SCRAM-SHA-256, where the password is not exchanged in original
|
// Authenticate with SCRAM-SHA-1 or SCRAM-SHA-256, where the password is not
|
||||||
// plaintext form, but only derived hashes are exchanged by both parties as proof
|
// exchanged in original plaintext form, but only derived hashes are exchanged by
|
||||||
// of knowledge of password.
|
// both parties as proof of knowledge of password.
|
||||||
func (c *Conn) AuthenticateSCRAMSHA256(username, password string) (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, password string) (untagged []Untagged, result Result, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr)
|
||||||
|
|
||||||
sc := scram.NewClient(username, "")
|
sc := scram.NewClient(h, username, "")
|
||||||
clientFirst, err := sc.ClientFirst()
|
clientFirst, err := sc.ClientFirst()
|
||||||
c.xcheckf(err, "scram clientFirst")
|
c.xcheckf(err, "scram clientFirst")
|
||||||
c.LastTag = c.nextTag()
|
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")
|
c.xcheckf(err, "writing command line")
|
||||||
|
|
||||||
xreadContinuation := func() []byte {
|
xreadContinuation := func() []byte {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package imapserver
|
package imapserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"hash"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -53,19 +56,27 @@ func TestAuthenticatePlain(t *testing.T) {
|
||||||
tc.readstatus("ok")
|
tc.readstatus("ok")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthenticateSCRAMSHA1(t *testing.T) {
|
||||||
|
testAuthenticateSCRAM(t, "SCRAM-SHA-1", sha1.New)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAuthenticateSCRAMSHA256(t *testing.T) {
|
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 := start(t)
|
||||||
tc.client.AuthenticateSCRAMSHA256("mjl@mox.example", "testtest")
|
tc.client.AuthenticateSCRAM(method, h, "mjl@mox.example", "testtest")
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
auth := func(status string, serverFinalError error, username, password string) {
|
auth := func(status string, serverFinalError error, username, password string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
sc := scram.NewClient(username, "")
|
sc := scram.NewClient(h, username, "")
|
||||||
clientFirst, err := sc.ClientFirst()
|
clientFirst, err := sc.ClientFirst()
|
||||||
tc.check(err, "scram clientFirst")
|
tc.check(err, "scram clientFirst")
|
||||||
tc.client.LastTag = "x001"
|
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 {
|
xreadContinuation := func() []byte {
|
||||||
line, _, result, rerr := tc.client.ReadContinuation()
|
line, _, result, rerr := tc.client.ReadContinuation()
|
||||||
|
@ -109,7 +120,7 @@ func TestAuthenticateSCRAMSHA256(t *testing.T) {
|
||||||
// auth("no", nil, "other@mox.example", "testtest")
|
// auth("no", nil, "other@mox.example", "testtest")
|
||||||
|
|
||||||
tc.transactf("no", "authenticate bogus ")
|
tc.transactf("no", "authenticate bogus ")
|
||||||
tc.transactf("bad", "authenticate scram-sha-256 not base64...")
|
tc.transactf("bad", "authenticate %s not base64...", method)
|
||||||
tc.transactf("bad", "authenticate scram-sha-256 %s", base64.StdEncoding.EncodeToString([]byte("bad data")))
|
tc.transactf("bad", "authenticate %s %s", method, base64.StdEncoding.EncodeToString([]byte("bad data")))
|
||||||
tc.close()
|
tc.close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,10 +39,13 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
@ -114,8 +117,9 @@ var (
|
||||||
// LIST-STATUS: ../rfc/5819
|
// LIST-STATUS: ../rfc/5819
|
||||||
// ID: ../rfc/2971
|
// ID: ../rfc/2971
|
||||||
// AUTH=SCRAM-SHA-256: ../rfc/7677 ../rfc/5802
|
// 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
|
// 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 {
|
type conn struct {
|
||||||
cid int64
|
cid int64
|
||||||
|
@ -1388,16 +1392,22 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
|
||||||
c.username = authc
|
c.username = authc
|
||||||
authResult = "ok"
|
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: 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
|
// 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.
|
// No plaintext credentials, we can log these normally.
|
||||||
|
|
||||||
c0 := xreadInitial()
|
c0 := xreadInitial()
|
||||||
ss, err := scram.NewServer(c0)
|
ss, err := scram.NewServer(h, c0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xsyntaxErrorf("starting scram: %w", err)
|
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 {
|
if ss.Authorization != "" && ss.Authorization != ss.Authentication {
|
||||||
xuserErrorf("authentication with authorization for different user not supported")
|
xuserErrorf("authentication with authorization for different user not supported")
|
||||||
}
|
}
|
||||||
var password store.Password
|
var xscram store.SCRAM
|
||||||
acc.WithRLock(func() {
|
acc.WithRLock(func() {
|
||||||
err := acc.DB.Read(func(tx *bstore.Tx) error {
|
err := acc.DB.Read(func(tx *bstore.Tx) error {
|
||||||
password, err = bstore.QueryTx[store.Password](tx).Get()
|
password, err := bstore.QueryTx[store.Password](tx).Get()
|
||||||
xsc := password.SCRAMSHA256
|
if authVariant == "scram-sha-1" {
|
||||||
if err == bstore.ErrAbsent || err == nil && (len(xsc.Salt) == 0 || xsc.Iterations == 0 || len(xsc.SaltedPassword) == 0) {
|
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")
|
xuserErrorf("scram not possible")
|
||||||
}
|
}
|
||||||
xcheckf(err, "fetching credentials")
|
xcheckf(err, "fetching credentials")
|
||||||
|
@ -1431,11 +1445,11 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
|
||||||
})
|
})
|
||||||
xcheckf(err, "read tx")
|
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")
|
xcheckf(err, "scram first server step")
|
||||||
c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
|
c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
|
||||||
c2 := xreadContinuation()
|
c2 := xreadContinuation()
|
||||||
s3, err := ss.Finish(c2, password.SCRAMSHA256.SaltedPassword)
|
s3, err := ss.Finish(c2, xscram.SaltedPassword)
|
||||||
if len(s3) > 0 {
|
if len(s3) > 0 {
|
||||||
c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
|
c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// SCRAM-SHA-256 and SCRAM-SHA-1 allow a client to authenticate to a server using a
|
||||||
// without handing plaintext password over to the server. The client also
|
// password without handing plaintext password over to the server. The client also
|
||||||
// verifies the server knows (a derivative of) the password.
|
// verifies the server knows (a derivative of) the password.
|
||||||
package scram
|
package scram
|
||||||
|
|
||||||
|
@ -13,10 +13,10 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
cryptorand "crypto/rand"
|
cryptorand "crypto/rand"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/crypto/pbkdf2"
|
"golang.org/x/crypto/pbkdf2"
|
||||||
|
@ -83,14 +83,14 @@ func MakeRandom() []byte {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaltPassword returns a salted password.
|
// 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)
|
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.
|
// HMAC returns the hmac with key over msg.
|
||||||
func HMAC(key []byte, msg string) []byte {
|
func HMAC(h func() hash.Hash, key []byte, msg string) []byte {
|
||||||
mac := hmac.New(sha256.New, key)
|
mac := hmac.New(h, key)
|
||||||
mac.Write([]byte(msg))
|
mac.Write([]byte(msg))
|
||||||
return mac.Sum(nil)
|
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 {
|
type Server struct {
|
||||||
Authentication string // Username for authentication, "authc". Always set and non-empty.
|
Authentication string // Username for authentication, "authc". Always set and non-empty.
|
||||||
Authorization string // If set, role of user to assume after authentication, "authz".
|
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.
|
// Messages used in hash calculations.
|
||||||
clientFirstBare string
|
clientFirstBare string
|
||||||
serverFirst 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 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.
|
// - 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)
|
p := newParser(clientFirst)
|
||||||
defer p.recover(&rerr)
|
defer p.recover(&rerr)
|
||||||
|
|
||||||
server = &Server{}
|
server = &Server{h: h}
|
||||||
|
|
||||||
// ../rfc/5802:949 ../rfc/5802:910
|
// ../rfc/5802:949 ../rfc/5802:910
|
||||||
gs2cbindFlag := p.xbyte()
|
gs2cbindFlag := p.xbyte()
|
||||||
|
@ -209,18 +211,19 @@ func (s *Server) Finish(clientFinal []byte, saltedPassword []byte) (serverFinal
|
||||||
|
|
||||||
msg := s.clientFirstBare + "," + s.serverFirst + "," + s.clientFinalWithoutProof
|
msg := s.clientFirstBare + "," + s.serverFirst + "," + s.clientFinalWithoutProof
|
||||||
|
|
||||||
clientKey := HMAC(saltedPassword, "Client Key")
|
clientKey := HMAC(s.h, saltedPassword, "Client Key")
|
||||||
storedKey0 := sha256.Sum256(clientKey)
|
h := s.h()
|
||||||
storedKey := storedKey0[:]
|
h.Write(clientKey)
|
||||||
|
storedKey := h.Sum(nil)
|
||||||
|
|
||||||
clientSig := HMAC(storedKey, msg)
|
clientSig := HMAC(s.h, storedKey, msg)
|
||||||
xor(clientSig, clientKey) // Now clientProof.
|
xor(clientSig, clientKey) // Now clientProof.
|
||||||
if !bytes.Equal(clientSig, proof) {
|
if !bytes.Equal(clientSig, proof) {
|
||||||
return "e=" + string(ErrInvalidProof), ErrInvalidProof
|
return "e=" + string(ErrInvalidProof), ErrInvalidProof
|
||||||
}
|
}
|
||||||
|
|
||||||
serverKey := HMAC(saltedPassword, "Server Key")
|
serverKey := HMAC(s.h, saltedPassword, "Server Key")
|
||||||
serverSig := HMAC(serverKey, msg)
|
serverSig := HMAC(s.h, serverKey, msg)
|
||||||
return fmt.Sprintf("v=%s", base64.StdEncoding.EncodeToString(serverSig)), nil
|
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)
|
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 {
|
type Client struct {
|
||||||
authc string
|
authc string
|
||||||
authz string
|
authz string
|
||||||
|
|
||||||
|
h func() hash.Hash // sha1.New or sha256.New
|
||||||
|
|
||||||
// Messages used in hash calculations.
|
// Messages used in hash calculations.
|
||||||
clientFirstBare string
|
clientFirstBare string
|
||||||
serverFirst string
|
serverFirst string
|
||||||
|
@ -248,17 +253,17 @@ type Client struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a client for authentication authc, optionally for
|
// 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:
|
// The sequence for data and calls on a client:
|
||||||
//
|
//
|
||||||
// - ClientFirst, write result to server.
|
// - ClientFirst, write result to server.
|
||||||
// - Read response from server, feed to ServerFirst, write response to server.
|
// - Read response from server, feed to ServerFirst, write response to server.
|
||||||
// - Read response from server, feed to ServerFinal.
|
// - 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)
|
authc = norm.NFC.String(authc)
|
||||||
authz = norm.NFC.String(authz)
|
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.
|
// 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.authMessage = c.clientFirstBare + "," + c.serverFirst + "," + c.clientFinalWithoutProof
|
||||||
|
|
||||||
c.saltedPassword = SaltPassword(password, salt, iterations)
|
c.saltedPassword = SaltPassword(c.h, password, salt, iterations)
|
||||||
clientKey := HMAC(c.saltedPassword, "Client Key")
|
clientKey := HMAC(c.h, c.saltedPassword, "Client Key")
|
||||||
storedKey0 := sha256.Sum256(clientKey)
|
h := c.h()
|
||||||
storedKey := storedKey0[:]
|
h.Write(clientKey)
|
||||||
clientSig := HMAC(storedKey, c.authMessage)
|
storedKey := h.Sum(nil)
|
||||||
|
clientSig := HMAC(c.h, storedKey, c.authMessage)
|
||||||
xor(clientSig, clientKey) // Now clientProof.
|
xor(clientSig, clientKey) // Now clientProof.
|
||||||
clientProof := clientSig
|
clientProof := clientSig
|
||||||
|
|
||||||
|
@ -344,8 +350,8 @@ func (c *Client) ServerFinal(serverFinal []byte) (rerr error) {
|
||||||
p.xtake("v=")
|
p.xtake("v=")
|
||||||
verifier := p.xbase64()
|
verifier := p.xbase64()
|
||||||
|
|
||||||
serverKey := HMAC(c.saltedPassword, "Server Key")
|
serverKey := HMAC(c.h, c.saltedPassword, "Server Key")
|
||||||
serverSig := HMAC(serverKey, c.authMessage)
|
serverSig := HMAC(c.h, serverKey, c.authMessage)
|
||||||
if !bytes.Equal(verifier, serverSig) {
|
if !bytes.Equal(verifier, serverSig) {
|
||||||
return fmt.Errorf("incorrect server signature")
|
return fmt.Errorf("incorrect server signature")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package scram
|
package scram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"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
|
// Test vector from ../rfc/7677:122
|
||||||
salt := base64Decode("W22ZaJ0SNY7soEsUEjb6gQ==")
|
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"
|
server.serverNonceOverride = "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0"
|
||||||
tcheck(t, err, "newserver")
|
tcheck(t, err, "newserver")
|
||||||
resp, err := server.ServerFirst(4096, salt)
|
resp, err := server.ServerFirst(4096, salt)
|
||||||
|
@ -44,9 +66,9 @@ func TestScramServer(t *testing.T) {
|
||||||
// Bad attempt with wrong password.
|
// Bad attempt with wrong password.
|
||||||
func TestScramServerBadPassword(t *testing.T) {
|
func TestScramServerBadPassword(t *testing.T) {
|
||||||
salt := base64Decode("W22ZaJ0SNY7soEsUEjb6gQ==")
|
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"
|
server.serverNonceOverride = "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0"
|
||||||
tcheck(t, err, "newserver")
|
tcheck(t, err, "newserver")
|
||||||
_, err = server.ServerFirst(4096, salt)
|
_, err = server.ServerFirst(4096, salt)
|
||||||
|
@ -60,9 +82,9 @@ func TestScramServerBadPassword(t *testing.T) {
|
||||||
// Bad attempt with different number of rounds.
|
// Bad attempt with different number of rounds.
|
||||||
func TestScramServerBadIterations(t *testing.T) {
|
func TestScramServerBadIterations(t *testing.T) {
|
||||||
salt := base64Decode("W22ZaJ0SNY7soEsUEjb6gQ==")
|
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"
|
server.serverNonceOverride = "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0"
|
||||||
tcheck(t, err, "newserver")
|
tcheck(t, err, "newserver")
|
||||||
_, err = server.ServerFirst(4096, salt)
|
_, err = server.ServerFirst(4096, salt)
|
||||||
|
@ -76,9 +98,9 @@ func TestScramServerBadIterations(t *testing.T) {
|
||||||
// Another attempt but with a randomly different nonce.
|
// Another attempt but with a randomly different nonce.
|
||||||
func TestScramServerBad(t *testing.T) {
|
func TestScramServerBad(t *testing.T) {
|
||||||
salt := base64Decode("W22ZaJ0SNY7soEsUEjb6gQ==")
|
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")
|
tcheck(t, err, "newserver")
|
||||||
_, err = server.ServerFirst(4096, salt)
|
_, err = server.ServerFirst(4096, salt)
|
||||||
tcheck(t, err, "server first")
|
tcheck(t, err, "server first")
|
||||||
|
@ -89,7 +111,7 @@ func TestScramServerBad(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestScramClient(t *testing.T) {
|
func TestScramClient(t *testing.T) {
|
||||||
c := NewClient("user", "")
|
c := NewClient(sha256.New, "user", "")
|
||||||
c.clientNonce = "rOprNGfwEbeRWgbNEkqO"
|
c.clientNonce = "rOprNGfwEbeRWgbNEkqO"
|
||||||
clientFirst, err := c.ClientFirst()
|
clientFirst, err := c.ClientFirst()
|
||||||
tcheck(t, err, "ClientFirst")
|
tcheck(t, err, "ClientFirst")
|
||||||
|
@ -129,14 +151,14 @@ func TestScram(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
salt := MakeRandom()
|
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
|
client.clientNonce = clientNonce
|
||||||
clientFirst, err := client.ClientFirst()
|
clientFirst, err := client.ClientFirst()
|
||||||
xerr(err, "client.ClientFirst")
|
xerr(err, "client.ClientFirst")
|
||||||
|
|
||||||
server, err := NewServer([]byte(clientFirst))
|
server, err := NewServer(sha256.New, []byte(clientFirst))
|
||||||
xerr(err, "NewServer")
|
xerr(err, "NewServer")
|
||||||
server.serverNonceOverride = serverNonce
|
server.serverNonceOverride = serverNonce
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,13 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
@ -679,7 +682,7 @@ func (c *conn) cmdHello(p *parser, ehlo bool) {
|
||||||
if c.submission {
|
if c.submission {
|
||||||
// ../rfc/4954:123
|
// ../rfc/4954:123
|
||||||
if c.tls || !c.requireTLSForAuth {
|
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 {
|
} else {
|
||||||
c.bwritelinef("250-AUTH ")
|
c.bwritelinef("250-AUTH ")
|
||||||
}
|
}
|
||||||
|
@ -861,16 +864,22 @@ func (c *conn) cmdAuth(p *parser) {
|
||||||
// ../rfc/4954:276
|
// ../rfc/4954:276
|
||||||
c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
|
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: 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
|
// 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.
|
// Passwords cannot be retrieved or replayed from the trace.
|
||||||
|
|
||||||
c0 := xreadInitial()
|
c0 := xreadInitial()
|
||||||
ss, err := scram.NewServer(c0)
|
ss, err := scram.NewServer(h, c0)
|
||||||
xcheckf(err, "starting scram")
|
xcheckf(err, "starting scram")
|
||||||
c.log.Info("scram auth", mlog.Field("authentication", ss.Authentication))
|
c.log.Info("scram auth", mlog.Field("authentication", ss.Authentication))
|
||||||
acc, _, err := store.OpenEmail(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 {
|
if ss.Authorization != "" && ss.Authorization != ss.Authentication {
|
||||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
|
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
|
||||||
}
|
}
|
||||||
var password store.Password
|
var xscram store.SCRAM
|
||||||
acc.WithRLock(func() {
|
acc.WithRLock(func() {
|
||||||
err := acc.DB.Read(func(tx *bstore.Tx) error {
|
err := acc.DB.Read(func(tx *bstore.Tx) error {
|
||||||
password, err = bstore.QueryTx[store.Password](tx).Get()
|
password, err := bstore.QueryTx[store.Password](tx).Get()
|
||||||
xsc := password.SCRAMSHA256
|
if authVariant == "scram-sha-1" {
|
||||||
if err == bstore.ErrAbsent || err == nil && (len(xsc.Salt) == 0 || xsc.Iterations == 0 || len(xsc.SaltedPassword) == 0) {
|
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")
|
xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
|
||||||
}
|
}
|
||||||
xcheckf(err, "fetching credentials")
|
xcheckf(err, "fetching credentials")
|
||||||
|
@ -904,11 +917,11 @@ func (c *conn) cmdAuth(p *parser) {
|
||||||
})
|
})
|
||||||
xcheckf(err, "read tx")
|
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")
|
xcheckf(err, "scram first server step")
|
||||||
c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) // ../rfc/4954:187
|
c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) // ../rfc/4954:187
|
||||||
c2 := xreadContinuation()
|
c2 := xreadContinuation()
|
||||||
s3, err := ss.Finish(c2, password.SCRAMSHA256.SaltedPassword)
|
s3, err := ss.Finish(c2, xscram.SaltedPassword)
|
||||||
if len(s3) > 0 {
|
if len(s3) > 0 {
|
||||||
c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) // ../rfc/4954:187
|
c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) // ../rfc/4954:187
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,8 @@ package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -62,14 +64,17 @@ var subjectpassRand = mox.NewRand()
|
||||||
|
|
||||||
var InitialMailboxes = []string{"Inbox", "Sent", "Archive", "Trash", "Drafts", "Junk"}
|
var InitialMailboxes = []string{"Inbox", "Sent", "Archive", "Trash", "Drafts", "Junk"}
|
||||||
|
|
||||||
// Password holds a bcrypt hash for logging in with SMTP/IMAP/admin.
|
type SCRAM struct {
|
||||||
type Password struct {
|
|
||||||
Hash string
|
|
||||||
SCRAMSHA256 struct {
|
|
||||||
Salt []byte
|
Salt []byte
|
||||||
Iterations int
|
Iterations int
|
||||||
SaltedPassword []byte
|
SaltedPassword []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Password holds a bcrypt hash for logging in with SMTP/IMAP/admin.
|
||||||
|
type Password struct {
|
||||||
|
Hash string
|
||||||
|
SCRAMSHA1 SCRAM
|
||||||
|
SCRAMSHA256 SCRAM
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subjectpass holds the secret key used to sign subjectpass tokens.
|
// Subjectpass holds the secret key used to sign subjectpass tokens.
|
||||||
|
@ -609,9 +614,15 @@ func (a *Account) SetPassword(password string) error {
|
||||||
}
|
}
|
||||||
var pw Password
|
var pw Password
|
||||||
pw.Hash = string(hash)
|
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.Salt = scram.MakeRandom()
|
||||||
pw.SCRAMSHA256.Iterations = 4096
|
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 {
|
if err := tx.Insert(&pw); err != nil {
|
||||||
return fmt.Errorf("inserting new password: %v", err)
|
return fmt.Errorf("inserting new password: %v", err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue