You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
149 lines
4.0 KiB
149 lines
4.0 KiB
// 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" |
|
"strings" |
|
) |
|
|
|
type clientState int |
|
|
|
const ( |
|
clientStarting clientState = iota |
|
clientFirst |
|
clientFinal |
|
clientDone |
|
) |
|
|
|
// ClientConversation implements the client-side of an authentication |
|
// conversation with a server. A new conversation must be created for |
|
// each authentication attempt. |
|
type ClientConversation struct { |
|
client *Client |
|
nonceGen NonceGeneratorFcn |
|
hashGen HashGeneratorFcn |
|
minIters int |
|
state clientState |
|
valid bool |
|
gs2 string |
|
nonce string |
|
c1b string |
|
serveSig []byte |
|
} |
|
|
|
// Step takes a string provided from a server (or just an empty string for the |
|
// very first conversation step) and attempts to move the authentication |
|
// conversation forward. It returns a string to be sent to the server or an |
|
// error if the server message is invalid. Calling Step after a conversation |
|
// completes is also an error. |
|
func (cc *ClientConversation) Step(challenge string) (response string, err error) { |
|
switch cc.state { |
|
case clientStarting: |
|
cc.state = clientFirst |
|
response, err = cc.firstMsg() |
|
case clientFirst: |
|
cc.state = clientFinal |
|
response, err = cc.finalMsg(challenge) |
|
case clientFinal: |
|
cc.state = clientDone |
|
response, err = cc.validateServer(challenge) |
|
default: |
|
response, err = "", errors.New("Conversation already completed") |
|
} |
|
return |
|
} |
|
|
|
// Done returns true if the conversation is completed or has errored. |
|
func (cc *ClientConversation) Done() bool { |
|
return cc.state == clientDone |
|
} |
|
|
|
// Valid returns true if the conversation successfully authenticated with the |
|
// server, including counter-validation that the server actually has the |
|
// user's stored credentials. |
|
func (cc *ClientConversation) Valid() bool { |
|
return cc.valid |
|
} |
|
|
|
func (cc *ClientConversation) firstMsg() (string, error) { |
|
// Values are cached for use in final message parameters |
|
cc.gs2 = cc.gs2Header() |
|
cc.nonce = cc.client.nonceGen() |
|
cc.c1b = fmt.Sprintf("n=%s,r=%s", encodeName(cc.client.username), cc.nonce) |
|
|
|
return cc.gs2 + cc.c1b, nil |
|
} |
|
|
|
func (cc *ClientConversation) finalMsg(s1 string) (string, error) { |
|
msg, err := parseServerFirst(s1) |
|
if err != nil { |
|
return "", err |
|
} |
|
|
|
// Check nonce prefix and update |
|
if !strings.HasPrefix(msg.nonce, cc.nonce) { |
|
return "", errors.New("server nonce did not extend client nonce") |
|
} |
|
cc.nonce = msg.nonce |
|
|
|
// Check iteration count vs minimum |
|
if msg.iters < cc.minIters { |
|
return "", fmt.Errorf("server requested too few iterations (%d)", msg.iters) |
|
} |
|
|
|
// Create client-final-message-without-proof |
|
c2wop := fmt.Sprintf( |
|
"c=%s,r=%s", |
|
base64.StdEncoding.EncodeToString([]byte(cc.gs2)), |
|
cc.nonce, |
|
) |
|
|
|
// Create auth message |
|
authMsg := cc.c1b + "," + s1 + "," + c2wop |
|
|
|
// Get derived keys from client cache |
|
dk := cc.client.getDerivedKeys(KeyFactors{Salt: string(msg.salt), Iters: msg.iters}) |
|
|
|
// Create proof as clientkey XOR clientsignature |
|
clientSignature := computeHMAC(cc.hashGen, dk.StoredKey, []byte(authMsg)) |
|
clientProof := xorBytes(dk.ClientKey, clientSignature) |
|
proof := base64.StdEncoding.EncodeToString(clientProof) |
|
|
|
// Cache ServerSignature for later validation |
|
cc.serveSig = computeHMAC(cc.hashGen, dk.ServerKey, []byte(authMsg)) |
|
|
|
return fmt.Sprintf("%s,p=%s", c2wop, proof), nil |
|
} |
|
|
|
func (cc *ClientConversation) validateServer(s2 string) (string, error) { |
|
msg, err := parseServerFinal(s2) |
|
if err != nil { |
|
return "", err |
|
} |
|
|
|
if len(msg.err) > 0 { |
|
return "", fmt.Errorf("server error: %s", msg.err) |
|
} |
|
|
|
if !hmac.Equal(msg.verifier, cc.serveSig) { |
|
return "", errors.New("server validation failed") |
|
} |
|
|
|
cc.valid = true |
|
return "", nil |
|
} |
|
|
|
func (cc *ClientConversation) gs2Header() string { |
|
if cc.client.authzID == "" { |
|
return "n,," |
|
} |
|
return fmt.Sprintf("n,%s,", encodeName(cc.client.authzID)) |
|
}
|
|
|