123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- // 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))
- }
|