mirror of https://github.com/tidwall/tile38.git
Add sasl
This commit is contained in:
parent
a087852d47
commit
55e503c378
1
go.mod
1
go.mod
|
@ -28,6 +28,7 @@ require (
|
|||
github.com/tidwall/rhh v1.1.1
|
||||
github.com/tidwall/rtree v1.2.7
|
||||
github.com/tidwall/sjson v1.1.6
|
||||
github.com/xdg/scram v1.0.3
|
||||
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
|
||||
|
|
3
go.sum
3
go.sum
|
@ -430,6 +430,9 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1
|
|||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||
github.com/xdg/scram v1.0.3 h1:nTadYh2Fs4BK2xdldEa2g5bbaZp0/+1nJMMPtPxS/to=
|
||||
github.com/xdg/scram v1.0.3/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||
github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
|
||||
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg=
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -75,6 +76,15 @@ func (conn *KafkaConn) Send(msg string) error {
|
|||
}
|
||||
cfg.Net.TLS.Enable = true
|
||||
cfg.Net.TLS.Config = tlsConfig
|
||||
} else {
|
||||
log.Debugf("building kafka sasl config")
|
||||
cfg.ClientID = "sasl_scram_client"
|
||||
cfg.Net.SASL.Enable = true
|
||||
cfg.Net.SASL.User = os.Getenv("KAFKA_USERNAME")
|
||||
cfg.Net.SASL.Password = os.Getenv("KAFKA_PASSWORD")
|
||||
cfg.Net.SASL.Handshake = true
|
||||
cfg.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient { return &XDGSCRAMClient{HashGeneratorFcn: SHA512} }
|
||||
cfg.Net.SASL.Mechanism = sarama.SASLTypeSCRAMSHA512
|
||||
}
|
||||
|
||||
cfg.Net.DialTimeout = time.Second
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package endpoint
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
|
||||
"github.com/xdg/scram"
|
||||
)
|
||||
|
||||
var (
|
||||
SHA256 scram.HashGeneratorFcn = sha256.New
|
||||
SHA512 scram.HashGeneratorFcn = sha512.New
|
||||
)
|
||||
|
||||
type XDGSCRAMClient struct {
|
||||
*scram.Client
|
||||
*scram.ClientConversation
|
||||
scram.HashGeneratorFcn
|
||||
}
|
||||
|
||||
func (x *XDGSCRAMClient) Begin(userName, password, authzID string) (err error) {
|
||||
x.Client, err = x.HashGeneratorFcn.NewClient(userName, password, authzID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
x.ClientConversation = x.Client.NewConversation()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *XDGSCRAMClient) Step(challenge string) (response string, err error) {
|
||||
response, err = x.ClientConversation.Step(challenge)
|
||||
return
|
||||
}
|
||||
|
||||
func (x *XDGSCRAMClient) Done() bool {
|
||||
return x.ClientConversation.Done()
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- "1.7"
|
||||
- "1.8"
|
||||
- "1.9"
|
||||
- "1.10"
|
||||
- master
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
|
@ -0,0 +1,175 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
|
@ -0,0 +1 @@
|
|||
This was renamed to [`xdg-go/scram`](https://github.com/xdg/stringprep) in October 2018. This didn't break dependencies at the time because Github redirected requests. In March 2021, I made `xdg-go/scram` a module, which can't be used as `xdg/scram` with Github redirects. This repository has been recreated to support legacy dependencies.
|
|
@ -0,0 +1,130 @@
|
|||
// 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"
|
||||
|
||||
"golang.org/x/crypto/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
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
// 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))
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
// 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"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NonceGeneratorFcn defines a function that returns a string of high-quality
|
||||
// random printable ASCII characters EXCLUDING the comma (',') character. The
|
||||
// default nonce generator provides Base64 encoding of 24 bytes from
|
||||
// crypto/rand.
|
||||
type NonceGeneratorFcn func() string
|
||||
|
||||
// derivedKeys collects the three cryptographically derived values
|
||||
// into one struct for caching.
|
||||
type derivedKeys struct {
|
||||
ClientKey []byte
|
||||
StoredKey []byte
|
||||
ServerKey []byte
|
||||
}
|
||||
|
||||
// KeyFactors represent the two server-provided factors needed to compute
|
||||
// client credentials for authentication. Salt is decoded bytes (i.e. not
|
||||
// base64), but in string form so that KeyFactors can be used as a map key for
|
||||
// cached credentials.
|
||||
type KeyFactors struct {
|
||||
Salt string
|
||||
Iters int
|
||||
}
|
||||
|
||||
// StoredCredentials are the values that a server must store for a given
|
||||
// username to allow authentication. They include the salt and iteration
|
||||
// count, plus the derived values to authenticate a client and for the server
|
||||
// to authenticate itself back to the client.
|
||||
//
|
||||
// NOTE: these are specific to a given hash function. To allow a user to
|
||||
// authenticate with either SCRAM-SHA-1 or SCRAM-SHA-256, two sets of
|
||||
// StoredCredentials must be created and stored, one for each hash function.
|
||||
type StoredCredentials struct {
|
||||
KeyFactors
|
||||
StoredKey []byte
|
||||
ServerKey []byte
|
||||
}
|
||||
|
||||
// CredentialLookup is a callback to provide StoredCredentials for a given
|
||||
// username. This is used to configure Server objects.
|
||||
//
|
||||
// NOTE: these are specific to a given hash function. The callback provided
|
||||
// to a Server with a given hash function must provide the corresponding
|
||||
// StoredCredentials.
|
||||
type CredentialLookup func(string) (StoredCredentials, error)
|
||||
|
||||
func defaultNonceGenerator() string {
|
||||
raw := make([]byte, 24)
|
||||
nonce := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
||||
rand.Read(raw)
|
||||
base64.StdEncoding.Encode(nonce, raw)
|
||||
return string(nonce)
|
||||
}
|
||||
|
||||
func encodeName(s string) string {
|
||||
return strings.Replace(strings.Replace(s, "=", "=3D", -1), ",", "=2C", -1)
|
||||
}
|
||||
|
||||
func decodeName(s string) (string, error) {
|
||||
// TODO Check for = not followed by 2C or 3D
|
||||
return strings.Replace(strings.Replace(s, "=2C", ",", -1), "=3D", "=", -1), nil
|
||||
}
|
||||
|
||||
func computeHash(hg HashGeneratorFcn, b []byte) []byte {
|
||||
h := hg()
|
||||
h.Write(b)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func computeHMAC(hg HashGeneratorFcn, key, data []byte) []byte {
|
||||
mac := hmac.New(hg, key)
|
||||
mac.Write(data)
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
func xorBytes(a, b []byte) []byte {
|
||||
// TODO check a & b are same length, or just xor to smallest
|
||||
xor := make([]byte, len(a))
|
||||
for i := range a {
|
||||
xor[i] = a[i] ^ b[i]
|
||||
}
|
||||
return xor
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// 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 provides client and server implementations of the Salted
|
||||
// Challenge Response Authentication Mechanism (SCRAM) described in RFC-5802
|
||||
// and RFC-7677.
|
||||
//
|
||||
// Usage
|
||||
//
|
||||
// The scram package provides two variables, `SHA1` and `SHA256`, that are
|
||||
// used to construct Client or Server objects.
|
||||
//
|
||||
// clientSHA1, err := scram.SHA1.NewClient(username, password, authID)
|
||||
// clientSHA256, err := scram.SHA256.NewClient(username, password, authID)
|
||||
//
|
||||
// serverSHA1, err := scram.SHA1.NewServer(credentialLookupFcn)
|
||||
// serverSHA256, err := scram.SHA256.NewServer(credentialLookupFcn)
|
||||
//
|
||||
// These objects are used to construct ClientConversation or
|
||||
// ServerConversation objects that are used to carry out authentication.
|
||||
package scram
|
|
@ -0,0 +1,205 @@
|
|||
// 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 (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type c1Msg struct {
|
||||
gs2Header string
|
||||
authzID string
|
||||
username string
|
||||
nonce string
|
||||
c1b string
|
||||
}
|
||||
|
||||
type c2Msg struct {
|
||||
cbind []byte
|
||||
nonce string
|
||||
proof []byte
|
||||
c2wop string
|
||||
}
|
||||
|
||||
type s1Msg struct {
|
||||
nonce string
|
||||
salt []byte
|
||||
iters int
|
||||
}
|
||||
|
||||
type s2Msg struct {
|
||||
verifier []byte
|
||||
err string
|
||||
}
|
||||
|
||||
func parseField(s, k string) (string, error) {
|
||||
t := strings.TrimPrefix(s, k+"=")
|
||||
if t == s {
|
||||
return "", fmt.Errorf("error parsing '%s' for field '%s'", s, k)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func parseGS2Flag(s string) (string, error) {
|
||||
if s[0] == 'p' {
|
||||
return "", fmt.Errorf("channel binding requested but not supported")
|
||||
}
|
||||
|
||||
if s == "n" || s == "y" {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("error parsing '%s' for gs2 flag", s)
|
||||
}
|
||||
|
||||
func parseFieldBase64(s, k string) ([]byte, error) {
|
||||
raw, err := parseField(s, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dec, err := base64.StdEncoding.DecodeString(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dec, nil
|
||||
}
|
||||
|
||||
func parseFieldInt(s, k string) (int, error) {
|
||||
raw, err := parseField(s, k)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
num, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error parsing field '%s': %v", k, err)
|
||||
}
|
||||
|
||||
return num, nil
|
||||
}
|
||||
|
||||
func parseClientFirst(c1 string) (msg c1Msg, err error) {
|
||||
|
||||
fields := strings.Split(c1, ",")
|
||||
if len(fields) < 4 {
|
||||
err = errors.New("not enough fields in first server message")
|
||||
return
|
||||
}
|
||||
|
||||
gs2flag, err := parseGS2Flag(fields[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 'a' field is optional
|
||||
if len(fields[1]) > 0 {
|
||||
msg.authzID, err = parseField(fields[1], "a")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Recombine and save the gs2 header
|
||||
msg.gs2Header = gs2flag + "," + msg.authzID + ","
|
||||
|
||||
// Check for unsupported extensions field "m".
|
||||
if strings.HasPrefix(fields[2], "m=") {
|
||||
err = errors.New("SCRAM message extensions are not supported")
|
||||
return
|
||||
}
|
||||
|
||||
msg.username, err = parseField(fields[2], "n")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg.nonce, err = parseField(fields[3], "r")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg.c1b = strings.Join(fields[2:], ",")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func parseClientFinal(c2 string) (msg c2Msg, err error) {
|
||||
fields := strings.Split(c2, ",")
|
||||
if len(fields) < 3 {
|
||||
err = errors.New("not enough fields in first server message")
|
||||
return
|
||||
}
|
||||
|
||||
msg.cbind, err = parseFieldBase64(fields[0], "c")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg.nonce, err = parseField(fields[1], "r")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Extension fields may come between nonce and proof, so we
|
||||
// grab the *last* fields as proof.
|
||||
msg.proof, err = parseFieldBase64(fields[len(fields)-1], "p")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg.c2wop = c2[:strings.LastIndex(c2, ",")]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func parseServerFirst(s1 string) (msg s1Msg, err error) {
|
||||
|
||||
// Check for unsupported extensions field "m".
|
||||
if strings.HasPrefix(s1, "m=") {
|
||||
err = errors.New("SCRAM message extensions are not supported")
|
||||
return
|
||||
}
|
||||
|
||||
fields := strings.Split(s1, ",")
|
||||
if len(fields) < 3 {
|
||||
err = errors.New("not enough fields in first server message")
|
||||
return
|
||||
}
|
||||
|
||||
msg.nonce, err = parseField(fields[0], "r")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg.salt, err = parseFieldBase64(fields[1], "s")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg.iters, err = parseFieldInt(fields[2], "i")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func parseServerFinal(s2 string) (msg s2Msg, err error) {
|
||||
fields := strings.Split(s2, ",")
|
||||
|
||||
msg.verifier, err = parseFieldBase64(fields[0], "v")
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg.err, err = parseField(fields[0], "e")
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// 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/sha1"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"hash"
|
||||
|
||||
"github.com/xdg/stringprep"
|
||||
)
|
||||
|
||||
// HashGeneratorFcn abstracts a factory function that returns a hash.Hash
|
||||
// value to be used for SCRAM operations. Generally, one would use the
|
||||
// provided package variables, `scram.SHA1` and `scram.SHA256`, for the most
|
||||
// common forms of SCRAM.
|
||||
type HashGeneratorFcn func() hash.Hash
|
||||
|
||||
// SHA1 is a function that returns a crypto/sha1 hasher and should be used to
|
||||
// create Client objects configured for SHA-1 hashing.
|
||||
var SHA1 HashGeneratorFcn = func() hash.Hash { return sha1.New() }
|
||||
|
||||
// SHA256 is a function that returns a crypto/sha256 hasher and should be used
|
||||
// to create Client objects configured for SHA-256 hashing.
|
||||
var SHA256 HashGeneratorFcn = func() hash.Hash { return sha256.New() }
|
||||
|
||||
// NewClient constructs a SCRAM client component based on a given hash.Hash
|
||||
// factory receiver. This constructor will normalize the username, password
|
||||
// and authzID via the SASLprep algorithm, as recommended by RFC-5802. If
|
||||
// SASLprep fails, the method returns an error.
|
||||
func (f HashGeneratorFcn) NewClient(username, password, authzID string) (*Client, error) {
|
||||
var userprep, passprep, authprep string
|
||||
var err error
|
||||
|
||||
if userprep, err = stringprep.SASLprep.Prepare(username); err != nil {
|
||||
return nil, fmt.Errorf("Error SASLprepping username '%s': %v", username, err)
|
||||
}
|
||||
if passprep, err = stringprep.SASLprep.Prepare(password); err != nil {
|
||||
return nil, fmt.Errorf("Error SASLprepping password '%s': %v", password, err)
|
||||
}
|
||||
if authprep, err = stringprep.SASLprep.Prepare(authzID); err != nil {
|
||||
return nil, fmt.Errorf("Error SASLprepping authzID '%s': %v", authzID, err)
|
||||
}
|
||||
|
||||
return newClient(userprep, passprep, authprep, f), nil
|
||||
}
|
||||
|
||||
// NewClientUnprepped acts like NewClient, except none of the arguments will
|
||||
// be normalized via SASLprep. This is not generally recommended, but is
|
||||
// provided for users that may have custom normalization needs.
|
||||
func (f HashGeneratorFcn) NewClientUnprepped(username, password, authzID string) (*Client, error) {
|
||||
return newClient(username, password, authzID, f), nil
|
||||
}
|
||||
|
||||
// NewServer constructs a SCRAM server component based on a given hash.Hash
|
||||
// factory receiver. To be maximally generic, it uses dependency injection to
|
||||
// handle credential lookup, which is the process of turning a username string
|
||||
// into a struct with stored credentials for authentication.
|
||||
func (f HashGeneratorFcn) NewServer(cl CredentialLookup) (*Server, error) {
|
||||
return newServer(cl, f)
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// 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"
|
||||
|
||||
// Server implements the server side of SCRAM authentication. It holds
|
||||
// configuration values needed to initialize new server-side conversations.
|
||||
// Generally, this can be persistent within an application.
|
||||
type Server struct {
|
||||
sync.RWMutex
|
||||
credentialCB CredentialLookup
|
||||
nonceGen NonceGeneratorFcn
|
||||
hashGen HashGeneratorFcn
|
||||
}
|
||||
|
||||
func newServer(cl CredentialLookup, fcn HashGeneratorFcn) (*Server, error) {
|
||||
return &Server{
|
||||
credentialCB: cl,
|
||||
nonceGen: defaultNonceGenerator,
|
||||
hashGen: fcn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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 (s *Server) WithNonceGenerator(ng NonceGeneratorFcn) *Server {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.nonceGen = ng
|
||||
return s
|
||||
}
|
||||
|
||||
// NewConversation constructs a server-side authentication conversation.
|
||||
// Conversations cannot be reused, so this must be called for each new
|
||||
// authentication attempt.
|
||||
func (s *Server) NewConversation() *ServerConversation {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
return &ServerConversation{
|
||||
nonceGen: s.nonceGen,
|
||||
hashGen: s.hashGen,
|
||||
credentialCB: s.credentialCB,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
// 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
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.7
|
||||
- 1.8
|
||||
- 1.9
|
||||
- master
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
|
@ -0,0 +1,175 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
|
@ -0,0 +1,27 @@
|
|||
[![GoDoc](https://godoc.org/github.com/xdg/stringprep?status.svg)](https://godoc.org/github.com/xdg/stringprep)
|
||||
[![Build Status](https://travis-ci.org/xdg/stringprep.svg?branch=master)](https://travis-ci.org/xdg/stringprep)
|
||||
|
||||
# stringprep – Go implementation of RFC-3454 stringprep and RFC-4013 SASLprep
|
||||
|
||||
## Synopsis
|
||||
|
||||
```
|
||||
import "github.com/xdg/stringprep"
|
||||
|
||||
prepped := stringprep.SASLprep.Prepare("TrustNô1")
|
||||
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
This library provides an implementation of the stringprep algorithm
|
||||
(RFC-3454) in Go, including all data tables.
|
||||
|
||||
A pre-built SASLprep (RFC-4013) profile is provided as well.
|
||||
|
||||
## Copyright and License
|
||||
|
||||
Copyright 2018 by David A. Golden. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"). You may
|
||||
obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
@ -0,0 +1,73 @@
|
|||
// 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 stringprep
|
||||
|
||||
var errHasLCat = "BiDi string can't have runes from category L"
|
||||
var errFirstRune = "BiDi string first rune must have category R or AL"
|
||||
var errLastRune = "BiDi string last rune must have category R or AL"
|
||||
|
||||
// Check for prohibited characters from table C.8
|
||||
func checkBiDiProhibitedRune(s string) error {
|
||||
for _, r := range s {
|
||||
if TableC8.Contains(r) {
|
||||
return Error{Msg: errProhibited, Rune: r}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for LCat characters from table D.2
|
||||
func checkBiDiLCat(s string) error {
|
||||
for _, r := range s {
|
||||
if TableD2.Contains(r) {
|
||||
return Error{Msg: errHasLCat, Rune: r}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check first and last characters are in table D.1; requires non-empty string
|
||||
func checkBadFirstAndLastRandALCat(s string) error {
|
||||
rs := []rune(s)
|
||||
if !TableD1.Contains(rs[0]) {
|
||||
return Error{Msg: errFirstRune, Rune: rs[0]}
|
||||
}
|
||||
n := len(rs) - 1
|
||||
if !TableD1.Contains(rs[n]) {
|
||||
return Error{Msg: errLastRune, Rune: rs[n]}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Look for RandALCat characters from table D.1
|
||||
func hasBiDiRandALCat(s string) bool {
|
||||
for _, r := range s {
|
||||
if TableD1.Contains(r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that BiDi rules are satisfied ; let empty string pass this rule
|
||||
func passesBiDiRules(s string) error {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := checkBiDiProhibitedRune(s); err != nil {
|
||||
return err
|
||||
}
|
||||
if hasBiDiRandALCat(s) {
|
||||
if err := checkBiDiLCat(s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkBadFirstAndLastRandALCat(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// 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 stringprep provides data tables and algorithms for RFC-3454,
|
||||
// including errata (as of 2018-02). It also provides a profile for
|
||||
// SASLprep as defined in RFC-4013.
|
||||
package stringprep
|
|
@ -0,0 +1,14 @@
|
|||
package stringprep
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Error describes problems encountered during stringprep, including what rune
|
||||
// was problematic.
|
||||
type Error struct {
|
||||
Msg string
|
||||
Rune rune
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return fmt.Sprintf("%s (rune: '\\u%04x')", e.Msg, e.Rune)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// 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 stringprep
|
||||
|
||||
// Mapping represents a stringprep mapping, from a single rune to zero or more
|
||||
// runes.
|
||||
type Mapping map[rune][]rune
|
||||
|
||||
// Map maps a rune to a (possibly empty) rune slice via a stringprep Mapping.
|
||||
// The ok return value is false if the rune was not found.
|
||||
func (m Mapping) Map(r rune) (replacement []rune, ok bool) {
|
||||
rs, ok := m[r]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return rs, true
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package stringprep
|
||||
|
||||
import (
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// Profile represents a stringprep profile.
|
||||
type Profile struct {
|
||||
Mappings []Mapping
|
||||
Normalize bool
|
||||
Prohibits []Set
|
||||
CheckBiDi bool
|
||||
}
|
||||
|
||||
var errProhibited = "prohibited character"
|
||||
|
||||
// Prepare transforms an input string to an output string following
|
||||
// the rules defined in the profile as defined by RFC-3454.
|
||||
func (p Profile) Prepare(s string) (string, error) {
|
||||
// Optimistically, assume output will be same length as input
|
||||
temp := make([]rune, 0, len(s))
|
||||
|
||||
// Apply maps
|
||||
for _, r := range s {
|
||||
rs, ok := p.applyMaps(r)
|
||||
if ok {
|
||||
temp = append(temp, rs...)
|
||||
} else {
|
||||
temp = append(temp, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize
|
||||
var out string
|
||||
if p.Normalize {
|
||||
out = norm.NFKC.String(string(temp))
|
||||
} else {
|
||||
out = string(temp)
|
||||
}
|
||||
|
||||
// Check prohibited
|
||||
for _, r := range out {
|
||||
if p.runeIsProhibited(r) {
|
||||
return "", Error{Msg: errProhibited, Rune: r}
|
||||
}
|
||||
}
|
||||
|
||||
// Check BiDi allowed
|
||||
if p.CheckBiDi {
|
||||
if err := passesBiDiRules(out); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p Profile) applyMaps(r rune) ([]rune, bool) {
|
||||
for _, m := range p.Mappings {
|
||||
rs, ok := m.Map(r)
|
||||
if ok {
|
||||
return rs, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (p Profile) runeIsProhibited(r rune) bool {
|
||||
for _, s := range p.Prohibits {
|
||||
if s.Contains(r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package stringprep
|
||||
|
||||
var mapNonASCIISpaceToASCIISpace = Mapping{
|
||||
0x00A0: []rune{0x0020},
|
||||
0x1680: []rune{0x0020},
|
||||
0x2000: []rune{0x0020},
|
||||
0x2001: []rune{0x0020},
|
||||
0x2002: []rune{0x0020},
|
||||
0x2003: []rune{0x0020},
|
||||
0x2004: []rune{0x0020},
|
||||
0x2005: []rune{0x0020},
|
||||
0x2006: []rune{0x0020},
|
||||
0x2007: []rune{0x0020},
|
||||
0x2008: []rune{0x0020},
|
||||
0x2009: []rune{0x0020},
|
||||
0x200A: []rune{0x0020},
|
||||
0x200B: []rune{0x0020},
|
||||
0x202F: []rune{0x0020},
|
||||
0x205F: []rune{0x0020},
|
||||
0x3000: []rune{0x0020},
|
||||
}
|
||||
|
||||
// SASLprep is a pre-defined stringprep profile for user names and passwords
|
||||
// as described in RFC-4013.
|
||||
//
|
||||
// Because the stringprep distinction between query and stored strings was
|
||||
// intended for compatibility across profile versions, but SASLprep was never
|
||||
// updated and is now deprecated, this profile only operates in stored
|
||||
// strings mode, prohibiting unassigned code points.
|
||||
var SASLprep Profile = saslprep
|
||||
|
||||
var saslprep = Profile{
|
||||
Mappings: []Mapping{
|
||||
TableB1,
|
||||
mapNonASCIISpaceToASCIISpace,
|
||||
},
|
||||
Normalize: true,
|
||||
Prohibits: []Set{
|
||||
TableA1,
|
||||
TableC1_2,
|
||||
TableC2_1,
|
||||
TableC2_2,
|
||||
TableC3,
|
||||
TableC4,
|
||||
TableC5,
|
||||
TableC6,
|
||||
TableC7,
|
||||
TableC8,
|
||||
TableC9,
|
||||
},
|
||||
CheckBiDi: true,
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// 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 stringprep
|
||||
|
||||
import "sort"
|
||||
|
||||
// RuneRange represents a close-ended range of runes: [N,M]. For a range
|
||||
// consisting of a single rune, N and M will be equal.
|
||||
type RuneRange [2]rune
|
||||
|
||||
// Contains returns true if a rune is within the bounds of the RuneRange.
|
||||
func (rr RuneRange) Contains(r rune) bool {
|
||||
return rr[0] <= r && r <= rr[1]
|
||||
}
|
||||
|
||||
func (rr RuneRange) isAbove(r rune) bool {
|
||||
return r <= rr[0]
|
||||
}
|
||||
|
||||
// Set represents a stringprep data table used to identify runes of a
|
||||
// particular type.
|
||||
type Set []RuneRange
|
||||
|
||||
// Contains returns true if a rune is within any of the RuneRanges in the
|
||||
// Set.
|
||||
func (s Set) Contains(r rune) bool {
|
||||
i := sort.Search(len(s), func(i int) bool { return s[i].Contains(r) || s[i].isAbove(r) })
|
||||
if i < len(s) && s[i].Contains(r) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -220,6 +220,11 @@ github.com/tidwall/rtree
|
|||
github.com/tidwall/sjson
|
||||
# github.com/tidwall/tinyqueue v0.1.1
|
||||
github.com/tidwall/tinyqueue
|
||||
# github.com/xdg/scram v1.0.3
|
||||
## explicit
|
||||
github.com/xdg/scram
|
||||
# github.com/xdg/stringprep v1.0.0
|
||||
github.com/xdg/stringprep
|
||||
# github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da
|
||||
## explicit
|
||||
github.com/yuin/gopher-lua
|
||||
|
|
Loading…
Reference in New Issue