package sasl
import (
"bytes"
"crypto/hmac"
"encoding/base64"
"errors"
"hash"
"strconv"
"strings"
"golang.org/x/crypto/pbkdf2"
)
const (
gs2HeaderCBSupport = "p=tls-unique,"
gs2HeaderNoServerCBSupport = "y,"
gs2HeaderNoCBSupport = "n,"
)
var (
clientKeyInput = []byte ("Client Key" )
serverKeyInput = []byte ("Server Key" )
)
const noncerandlen = 16
func getGS2Header (name string , n *Negotiator ) (gs2Header []byte ) {
_ , _ , identity := n .Credentials ()
switch {
case n .TLSState () == nil || !strings .HasSuffix (name , "-PLUS" ):
gs2Header = []byte (gs2HeaderNoCBSupport )
case n .State ()&RemoteCB == RemoteCB :
gs2Header = []byte (gs2HeaderCBSupport )
case n .State ()&RemoteCB != RemoteCB :
gs2Header = []byte (gs2HeaderNoServerCBSupport )
}
if len (identity ) > 0 {
gs2Header = append (gs2Header , []byte (`a=` )...)
gs2Header = append (gs2Header , identity ...)
}
gs2Header = append (gs2Header , ',' )
return
}
func scram (name string , fn func () hash .Hash ) Mechanism {
return Mechanism {
Name : name ,
Start : func (m *Negotiator ) (bool , []byte , interface {}, error ) {
user , _ , _ := m .Credentials ()
n := bytes .Count (user , []byte {'=' }) + bytes .Count (user , []byte {',' })
username := make ([]byte , len (user )+(n *2 ))
w := 0
start := 0
for i := 0 ; i < n ; i ++ {
j := start
j += bytes .IndexAny (user [start :], "=," )
w += copy (username [w :], user [start :j ])
switch user [j ] {
case '=' :
w += copy (username [w :], "=3D" )
case ',' :
w += copy (username [w :], "=2C" )
}
start = j + 1
}
copy (username [w :], user [start :])
clientFirstMessage := make ([]byte , 5 +len (m .Nonce ())+len (username ))
copy (clientFirstMessage , "n=" )
copy (clientFirstMessage [2 :], username )
copy (clientFirstMessage [2 +len (username ):], ",r=" )
copy (clientFirstMessage [5 +len (username ):], m .Nonce ())
return true , append (getGS2Header (name , m ), clientFirstMessage ...), clientFirstMessage , nil
},
Next : func (m *Negotiator , challenge []byte , data interface {}) (more bool , resp []byte , cache interface {}, err error ) {
if challenge == nil || len (challenge ) == 0 {
return more , resp , cache , ErrInvalidChallenge
}
if m .State ()&Receiving == Receiving {
panic ("not yet implemented" )
}
return scramClientNext (name , fn , m , challenge , data )
},
}
}
func scramClientNext (name string , fn func () hash .Hash , m *Negotiator , challenge []byte , data interface {}) (more bool , resp []byte , cache interface {}, err error ) {
_ , password , _ := m .Credentials ()
state := m .State ()
switch state & StepMask {
case AuthTextSent :
iter := -1
var salt , nonce []byte
for _ , field := range bytes .Split (challenge , []byte {',' }) {
if len (field ) < 3 || (len (field ) >= 2 && field [1 ] != '=' ) {
continue
}
switch field [0 ] {
case 'i' :
ival := string (bytes .TrimRight (field [2 :], "\x00" ))
if iter , err = strconv .Atoi (ival ); err != nil {
return
}
case 's' :
salt = make ([]byte , base64 .StdEncoding .DecodedLen (len (field )-2 ))
var n int
n , err = base64 .StdEncoding .Decode (salt , field [2 :])
salt = salt [:n ]
if err != nil {
return
}
case 'r' :
nonce = field [2 :]
case 'm' :
err = errors .New ("Server sent reserved attribute `m'" )
return
}
}
switch {
case iter < 0 :
err = errors .New ("Iteration count is missing" )
return
case iter < 0 :
err = errors .New ("Iteration count is invalid" )
return
case nonce == nil || !bytes .HasPrefix (nonce , m .Nonce ()):
err = errors .New ("Server nonce does not match client nonce" )
return
case salt == nil :
err = errors .New ("Server sent empty salt" )
return
}
gs2Header := getGS2Header (name , m )
tlsState := m .TLSState ()
var channelBinding []byte
if tlsState != nil && strings .HasSuffix (name , "-PLUS" ) {
channelBinding = make (
[]byte ,
2 +base64 .StdEncoding .EncodedLen (len (gs2Header )+len (tlsState .TLSUnique )),
)
base64 .StdEncoding .Encode (channelBinding [2 :], append (gs2Header , tlsState .TLSUnique ...))
channelBinding [0 ] = 'c'
channelBinding [1 ] = '='
} else {
channelBinding = make (
[]byte ,
2 +base64 .StdEncoding .EncodedLen (len (gs2Header )),
)
base64 .StdEncoding .Encode (channelBinding [2 :], gs2Header )
channelBinding [0 ] = 'c'
channelBinding [1 ] = '='
}
clientFinalMessageWithoutProof := append (channelBinding , []byte (",r=" )...)
clientFinalMessageWithoutProof = append (clientFinalMessageWithoutProof , nonce ...)
clientFirstMessage := data .([]byte )
authMessage := append (clientFirstMessage , ',' )
authMessage = append (authMessage , challenge ...)
authMessage = append (authMessage , ',' )
authMessage = append (authMessage , clientFinalMessageWithoutProof ...)
saltedPassword := pbkdf2 .Key (password , salt , iter , fn ().Size (), fn )
h := hmac .New (fn , saltedPassword )
h .Write (serverKeyInput )
serverKey := h .Sum (nil )
h .Reset ()
h .Write (clientKeyInput )
clientKey := h .Sum (nil )
h = hmac .New (fn , serverKey )
h .Write (authMessage )
serverSignature := h .Sum (nil )
h = fn ()
h .Write (clientKey )
storedKey := h .Sum (nil )
h = hmac .New (fn , storedKey )
h .Write (authMessage )
clientSignature := h .Sum (nil )
clientProof := make ([]byte , len (clientKey ))
xorBytes (clientProof , clientKey , clientSignature )
encodedClientProof := make ([]byte , base64 .StdEncoding .EncodedLen (len (clientProof )))
base64 .StdEncoding .Encode (encodedClientProof , clientProof )
clientFinalMessage := append (clientFinalMessageWithoutProof , []byte (",p=" )...)
clientFinalMessage = append (clientFinalMessage , encodedClientProof ...)
return true , clientFinalMessage , serverSignature , nil
case ResponseSent :
clientCalculatedServerFinalMessage := "v=" + base64 .StdEncoding .EncodeToString (data .([]byte ))
if clientCalculatedServerFinalMessage != string (challenge ) {
err = ErrAuthn
return
}
return false , nil , nil , nil
}
err = ErrInvalidState
return
}
The pages are generated with Golds v0.3.6 . (GOOS=darwin GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds .