summaryrefslogtreecommitdiff
path: root/vendor/mellium.im/sasl/scram.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/mellium.im/sasl/scram.go')
-rw-r--r--vendor/mellium.im/sasl/scram.go227
1 files changed, 227 insertions, 0 deletions
diff --git a/vendor/mellium.im/sasl/scram.go b/vendor/mellium.im/sasl/scram.go
new file mode 100644
index 000000000..6e6bdc3c2
--- /dev/null
+++ b/vendor/mellium.im/sasl/scram.go
@@ -0,0 +1,227 @@
+// Copyright 2016 The Mellium Contributors.
+// Use of this source code is governed by the BSD 2-clause license that can be
+// found in the LICENSE file.
+
+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")
+)
+
+// The number of random bytes to generate for a nonce.
+const noncerandlen = 16
+
+func getGS2Header(name string, n *Negotiator) (gs2Header []byte) {
+ _, _, identity := n.Credentials()
+ switch {
+ case n.TLSState() == nil || !strings.HasSuffix(name, "-PLUS"):
+ // We do not support channel binding
+ gs2Header = []byte(gs2HeaderNoCBSupport)
+ case n.State()&RemoteCB == RemoteCB:
+ // We support channel binding and the server does too
+ gs2Header = []byte(gs2HeaderCBSupport)
+ case n.State()&RemoteCB != RemoteCB:
+ // We support channel binding but the server does not
+ 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 {
+ // BUG(ssw): We need a way to cache the SCRAM client and server key
+ // calculations.
+ return Mechanism{
+ Name: name,
+ Start: func(m *Negotiator) (bool, []byte, interface{}, error) {
+ user, _, _ := m.Credentials()
+
+ // Escape "=" and ",". This is mostly the same as bytes.Replace but
+ // faster because we can do both replacements in a single pass.
+ 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':
+ // RFC 5802:
+ // m: This attribute is reserved for future extensibility. In this
+ // version of SCRAM, its presence in a client or a server message
+ // MUST cause authentication failure when the attribute is parsed by
+ // the other end.
+ 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
+ }
+ // Success!
+ return false, nil, nil, nil
+ }
+ err = ErrInvalidState
+ return
+}