mirror of https://github.com/tidwall/tile38.git
Add sasl
This commit is contained in:
parent
4490eba0fc
commit
7dddca165c
1
go.mod
1
go.mod
|
@ -27,6 +27,7 @@ require (
|
||||||
github.com/tidwall/rhh v1.1.1
|
github.com/tidwall/rhh v1.1.1
|
||||||
github.com/tidwall/rtree v1.2.7
|
github.com/tidwall/rtree v1.2.7
|
||||||
github.com/tidwall/sjson v1.1.6
|
github.com/tidwall/sjson v1.1.6
|
||||||
|
github.com/xdg/scram v1.0.3
|
||||||
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da
|
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
|
||||||
|
|
3
go.sum
3
go.sum
|
@ -374,6 +374,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.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
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 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/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/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg=
|
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg=
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -75,6 +76,15 @@ func (conn *KafkaConn) Send(msg string) error {
|
||||||
}
|
}
|
||||||
cfg.Net.TLS.Enable = true
|
cfg.Net.TLS.Enable = true
|
||||||
cfg.Net.TLS.Config = tlsConfig
|
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
|
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
|
@ -189,6 +189,11 @@ github.com/tidwall/rtree
|
||||||
github.com/tidwall/sjson
|
github.com/tidwall/sjson
|
||||||
# github.com/tidwall/tinyqueue v0.1.1
|
# github.com/tidwall/tinyqueue v0.1.1
|
||||||
github.com/tidwall/tinyqueue
|
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
|
# github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da
|
||||||
## explicit
|
## explicit
|
||||||
github.com/yuin/gopher-lua
|
github.com/yuin/gopher-lua
|
||||||
|
|
Loading…
Reference in New Issue