mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-29 23:49:25 +01:00
152 lines
4.4 KiB
Go
152 lines
4.4 KiB
Go
|
// Copyright 2018 by David A. Golden. All rights reserved.
|
||
|
//
|
||
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||
|
// not use this file except in compliance with the License. You may obtain
|
||
|
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
||
|
package scram
|
||
|
|
||
|
import (
|
||
|
"crypto/hmac"
|
||
|
"encoding/base64"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
)
|
||
|
|
||
|
type serverState int
|
||
|
|
||
|
const (
|
||
|
serverFirst serverState = iota
|
||
|
serverFinal
|
||
|
serverDone
|
||
|
)
|
||
|
|
||
|
// ServerConversation implements the server-side of an authentication
|
||
|
// conversation with a client. A new conversation must be created for
|
||
|
// each authentication attempt.
|
||
|
type ServerConversation struct {
|
||
|
nonceGen NonceGeneratorFcn
|
||
|
hashGen HashGeneratorFcn
|
||
|
credentialCB CredentialLookup
|
||
|
state serverState
|
||
|
credential StoredCredentials
|
||
|
valid bool
|
||
|
gs2Header string
|
||
|
username string
|
||
|
authzID string
|
||
|
nonce string
|
||
|
c1b string
|
||
|
s1 string
|
||
|
}
|
||
|
|
||
|
// Step takes a string provided from a client and attempts to move the
|
||
|
// authentication conversation forward. It returns a string to be sent to the
|
||
|
// client or an error if the client message is invalid. Calling Step after a
|
||
|
// conversation completes is also an error.
|
||
|
func (sc *ServerConversation) Step(challenge string) (response string, err error) {
|
||
|
switch sc.state {
|
||
|
case serverFirst:
|
||
|
sc.state = serverFinal
|
||
|
response, err = sc.firstMsg(challenge)
|
||
|
case serverFinal:
|
||
|
sc.state = serverDone
|
||
|
response, err = sc.finalMsg(challenge)
|
||
|
default:
|
||
|
response, err = "", errors.New("Conversation already completed")
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Done returns true if the conversation is completed or has errored.
|
||
|
func (sc *ServerConversation) Done() bool {
|
||
|
return sc.state == serverDone
|
||
|
}
|
||
|
|
||
|
// Valid returns true if the conversation successfully authenticated the
|
||
|
// client.
|
||
|
func (sc *ServerConversation) Valid() bool {
|
||
|
return sc.valid
|
||
|
}
|
||
|
|
||
|
// Username returns the client-provided username. This is valid to call
|
||
|
// if the first conversation Step() is successful.
|
||
|
func (sc *ServerConversation) Username() string {
|
||
|
return sc.username
|
||
|
}
|
||
|
|
||
|
// AuthzID returns the (optional) client-provided authorization identity, if
|
||
|
// any. If one was not provided, it returns the empty string. This is valid
|
||
|
// to call if the first conversation Step() is successful.
|
||
|
func (sc *ServerConversation) AuthzID() string {
|
||
|
return sc.authzID
|
||
|
}
|
||
|
|
||
|
func (sc *ServerConversation) firstMsg(c1 string) (string, error) {
|
||
|
msg, err := parseClientFirst(c1)
|
||
|
if err != nil {
|
||
|
sc.state = serverDone
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
sc.gs2Header = msg.gs2Header
|
||
|
sc.username = msg.username
|
||
|
sc.authzID = msg.authzID
|
||
|
|
||
|
sc.credential, err = sc.credentialCB(msg.username)
|
||
|
if err != nil {
|
||
|
sc.state = serverDone
|
||
|
return "e=unknown-user", err
|
||
|
}
|
||
|
|
||
|
sc.nonce = msg.nonce + sc.nonceGen()
|
||
|
sc.c1b = msg.c1b
|
||
|
sc.s1 = fmt.Sprintf("r=%s,s=%s,i=%d",
|
||
|
sc.nonce,
|
||
|
base64.StdEncoding.EncodeToString([]byte(sc.credential.Salt)),
|
||
|
sc.credential.Iters,
|
||
|
)
|
||
|
|
||
|
return sc.s1, nil
|
||
|
}
|
||
|
|
||
|
// For errors, returns server error message as well as non-nil error. Callers
|
||
|
// can choose whether to send server error or not.
|
||
|
func (sc *ServerConversation) finalMsg(c2 string) (string, error) {
|
||
|
msg, err := parseClientFinal(c2)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
// Check channel binding matches what we expect; in this case, we expect
|
||
|
// just the gs2 header we received as we don't support channel binding
|
||
|
// with a data payload. If we add binding, we need to independently
|
||
|
// compute the header to match here.
|
||
|
if string(msg.cbind) != sc.gs2Header {
|
||
|
return "e=channel-bindings-dont-match", fmt.Errorf("channel binding received '%s' doesn't match expected '%s'", msg.cbind, sc.gs2Header)
|
||
|
}
|
||
|
|
||
|
// Check nonce received matches what we sent
|
||
|
if msg.nonce != sc.nonce {
|
||
|
return "e=other-error", errors.New("nonce received did not match nonce sent")
|
||
|
}
|
||
|
|
||
|
// Create auth message
|
||
|
authMsg := sc.c1b + "," + sc.s1 + "," + msg.c2wop
|
||
|
|
||
|
// Retrieve ClientKey from proof and verify it
|
||
|
clientSignature := computeHMAC(sc.hashGen, sc.credential.StoredKey, []byte(authMsg))
|
||
|
clientKey := xorBytes([]byte(msg.proof), clientSignature)
|
||
|
storedKey := computeHash(sc.hashGen, clientKey)
|
||
|
|
||
|
// Compare with constant-time function
|
||
|
if !hmac.Equal(storedKey, sc.credential.StoredKey) {
|
||
|
return "e=invalid-proof", errors.New("challenge proof invalid")
|
||
|
}
|
||
|
|
||
|
sc.valid = true
|
||
|
|
||
|
// Compute and return server verifier
|
||
|
serverSignature := computeHMAC(sc.hashGen, sc.credential.ServerKey, []byte(authMsg))
|
||
|
return "v=" + base64.StdEncoding.EncodeToString(serverSignature), nil
|
||
|
}
|