131 lines
3.8 KiB
Go
131 lines
3.8 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 (
|
||
|
"sync"
|
||
|
|
||
|
"github.com/xdg-go/pbkdf2"
|
||
|
)
|
||
|
|
||
|
// Client implements the client side of SCRAM authentication. It holds
|
||
|
// configuration values needed to initialize new client-side conversations for
|
||
|
// a specific username, password and authorization ID tuple. Client caches
|
||
|
// the computationally-expensive parts of a SCRAM conversation as described in
|
||
|
// RFC-5802. If repeated authentication conversations may be required for a
|
||
|
// user (e.g. disconnect/reconnect), the user's Client should be preserved.
|
||
|
//
|
||
|
// For security reasons, Clients have a default minimum PBKDF2 iteration count
|
||
|
// of 4096. If a server requests a smaller iteration count, an authentication
|
||
|
// conversation will error.
|
||
|
//
|
||
|
// A Client can also be used by a server application to construct the hashed
|
||
|
// authentication values to be stored for a new user. See StoredCredentials()
|
||
|
// for more.
|
||
|
type Client struct {
|
||
|
sync.RWMutex
|
||
|
username string
|
||
|
password string
|
||
|
authzID string
|
||
|
minIters int
|
||
|
nonceGen NonceGeneratorFcn
|
||
|
hashGen HashGeneratorFcn
|
||
|
cache map[KeyFactors]derivedKeys
|
||
|
}
|
||
|
|
||
|
func newClient(username, password, authzID string, fcn HashGeneratorFcn) *Client {
|
||
|
return &Client{
|
||
|
username: username,
|
||
|
password: password,
|
||
|
authzID: authzID,
|
||
|
minIters: 4096,
|
||
|
nonceGen: defaultNonceGenerator,
|
||
|
hashGen: fcn,
|
||
|
cache: make(map[KeyFactors]derivedKeys),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// WithMinIterations changes minimum required PBKDF2 iteration count.
|
||
|
func (c *Client) WithMinIterations(n int) *Client {
|
||
|
c.Lock()
|
||
|
defer c.Unlock()
|
||
|
c.minIters = n
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
// WithNonceGenerator replaces the default nonce generator (base64 encoding of
|
||
|
// 24 bytes from crypto/rand) with a custom generator. This is provided for
|
||
|
// testing or for users with custom nonce requirements.
|
||
|
func (c *Client) WithNonceGenerator(ng NonceGeneratorFcn) *Client {
|
||
|
c.Lock()
|
||
|
defer c.Unlock()
|
||
|
c.nonceGen = ng
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
// NewConversation constructs a client-side authentication conversation.
|
||
|
// Conversations cannot be reused, so this must be called for each new
|
||
|
// authentication attempt.
|
||
|
func (c *Client) NewConversation() *ClientConversation {
|
||
|
c.RLock()
|
||
|
defer c.RUnlock()
|
||
|
return &ClientConversation{
|
||
|
client: c,
|
||
|
nonceGen: c.nonceGen,
|
||
|
hashGen: c.hashGen,
|
||
|
minIters: c.minIters,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Client) getDerivedKeys(kf KeyFactors) derivedKeys {
|
||
|
dk, ok := c.getCache(kf)
|
||
|
if !ok {
|
||
|
dk = c.computeKeys(kf)
|
||
|
c.setCache(kf, dk)
|
||
|
}
|
||
|
return dk
|
||
|
}
|
||
|
|
||
|
// GetStoredCredentials takes a salt and iteration count structure and
|
||
|
// provides the values that must be stored by a server to authentication a
|
||
|
// user. These values are what the Server credential lookup function must
|
||
|
// return for a given username.
|
||
|
func (c *Client) GetStoredCredentials(kf KeyFactors) StoredCredentials {
|
||
|
dk := c.getDerivedKeys(kf)
|
||
|
return StoredCredentials{
|
||
|
KeyFactors: kf,
|
||
|
StoredKey: dk.StoredKey,
|
||
|
ServerKey: dk.ServerKey,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Client) computeKeys(kf KeyFactors) derivedKeys {
|
||
|
h := c.hashGen()
|
||
|
saltedPassword := pbkdf2.Key([]byte(c.password), []byte(kf.Salt), kf.Iters, h.Size(), c.hashGen)
|
||
|
clientKey := computeHMAC(c.hashGen, saltedPassword, []byte("Client Key"))
|
||
|
|
||
|
return derivedKeys{
|
||
|
ClientKey: clientKey,
|
||
|
StoredKey: computeHash(c.hashGen, clientKey),
|
||
|
ServerKey: computeHMAC(c.hashGen, saltedPassword, []byte("Server Key")),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Client) getCache(kf KeyFactors) (derivedKeys, bool) {
|
||
|
c.RLock()
|
||
|
defer c.RUnlock()
|
||
|
dk, ok := c.cache[kf]
|
||
|
return dk, ok
|
||
|
}
|
||
|
|
||
|
func (c *Client) setCache(kf KeyFactors, dk derivedKeys) {
|
||
|
c.Lock()
|
||
|
defer c.Unlock()
|
||
|
c.cache[kf] = dk
|
||
|
return
|
||
|
}
|