blob: 834056889e8ec7b20d345dbd1b8fb828f7ee79ad [file] [log] [blame]
Don Newton379ae252019-04-01 12:17:06 -04001// Copyright 2018 by David A. Golden. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may
4// not use this file except in compliance with the License. You may obtain
5// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
7package scram
8
9import (
10 "crypto/hmac"
11 "encoding/base64"
12 "errors"
13 "fmt"
14 "strings"
15)
16
17type clientState int
18
19const (
20 clientStarting clientState = iota
21 clientFirst
22 clientFinal
23 clientDone
24)
25
26// ClientConversation implements the client-side of an authentication
27// conversation with a server. A new conversation must be created for
28// each authentication attempt.
29type ClientConversation struct {
30 client *Client
31 nonceGen NonceGeneratorFcn
32 hashGen HashGeneratorFcn
33 minIters int
34 state clientState
35 valid bool
36 gs2 string
37 nonce string
38 c1b string
39 serveSig []byte
40}
41
42// Step takes a string provided from a server (or just an empty string for the
43// very first conversation step) and attempts to move the authentication
44// conversation forward. It returns a string to be sent to the server or an
45// error if the server message is invalid. Calling Step after a conversation
46// completes is also an error.
47func (cc *ClientConversation) Step(challenge string) (response string, err error) {
48 switch cc.state {
49 case clientStarting:
50 cc.state = clientFirst
51 response, err = cc.firstMsg()
52 case clientFirst:
53 cc.state = clientFinal
54 response, err = cc.finalMsg(challenge)
55 case clientFinal:
56 cc.state = clientDone
57 response, err = cc.validateServer(challenge)
58 default:
59 response, err = "", errors.New("Conversation already completed")
60 }
61 return
62}
63
64// Done returns true if the conversation is completed or has errored.
65func (cc *ClientConversation) Done() bool {
66 return cc.state == clientDone
67}
68
69// Valid returns true if the conversation successfully authenticated with the
70// server, including counter-validation that the server actually has the
71// user's stored credentials.
72func (cc *ClientConversation) Valid() bool {
73 return cc.valid
74}
75
76func (cc *ClientConversation) firstMsg() (string, error) {
77 // Values are cached for use in final message parameters
78 cc.gs2 = cc.gs2Header()
79 cc.nonce = cc.client.nonceGen()
80 cc.c1b = fmt.Sprintf("n=%s,r=%s", encodeName(cc.client.username), cc.nonce)
81
82 return cc.gs2 + cc.c1b, nil
83}
84
85func (cc *ClientConversation) finalMsg(s1 string) (string, error) {
86 msg, err := parseServerFirst(s1)
87 if err != nil {
88 return "", err
89 }
90
91 // Check nonce prefix and update
92 if !strings.HasPrefix(msg.nonce, cc.nonce) {
93 return "", errors.New("server nonce did not extend client nonce")
94 }
95 cc.nonce = msg.nonce
96
97 // Check iteration count vs minimum
98 if msg.iters < cc.minIters {
99 return "", fmt.Errorf("server requested too few iterations (%d)", msg.iters)
100 }
101
102 // Create client-final-message-without-proof
103 c2wop := fmt.Sprintf(
104 "c=%s,r=%s",
105 base64.StdEncoding.EncodeToString([]byte(cc.gs2)),
106 cc.nonce,
107 )
108
109 // Create auth message
110 authMsg := cc.c1b + "," + s1 + "," + c2wop
111
112 // Get derived keys from client cache
113 dk := cc.client.getDerivedKeys(KeyFactors{Salt: string(msg.salt), Iters: msg.iters})
114
115 // Create proof as clientkey XOR clientsignature
116 clientSignature := computeHMAC(cc.hashGen, dk.StoredKey, []byte(authMsg))
117 clientProof := xorBytes(dk.ClientKey, clientSignature)
118 proof := base64.StdEncoding.EncodeToString(clientProof)
119
120 // Cache ServerSignature for later validation
121 cc.serveSig = computeHMAC(cc.hashGen, dk.ServerKey, []byte(authMsg))
122
123 return fmt.Sprintf("%s,p=%s", c2wop, proof), nil
124}
125
126func (cc *ClientConversation) validateServer(s2 string) (string, error) {
127 msg, err := parseServerFinal(s2)
128 if err != nil {
129 return "", err
130 }
131
132 if len(msg.err) > 0 {
133 return "", fmt.Errorf("server error: %s", msg.err)
134 }
135
136 if !hmac.Equal(msg.verifier, cc.serveSig) {
137 return "", errors.New("server validation failed")
138 }
139
140 cc.valid = true
141 return "", nil
142}
143
144func (cc *ClientConversation) gs2Header() string {
145 if cc.client.authzID == "" {
146 return "n,,"
147 }
148 return fmt.Sprintf("n,%s,", encodeName(cc.client.authzID))
149}