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