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:
Mechiel Lukkien
2023-02-05 12:30:14 +01:00
parent 49dd5b7ba9
commit 642a328ae1
7 changed files with 156 additions and 78 deletions

View File

@ -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")
}

View File

@ -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