pax_global_header00006660000000000000000000000064140301551740014512gustar00rootroot0000000000000052 comment=2ad501880a7f7bcc9af290278baa3658c9a2f2cd scram-1.0.2/000077500000000000000000000000001403015517400126175ustar00rootroot00000000000000scram-1.0.2/.github/000077500000000000000000000000001403015517400141575ustar00rootroot00000000000000scram-1.0.2/.github/workflows/000077500000000000000000000000001403015517400162145ustar00rootroot00000000000000scram-1.0.2/.github/workflows/test.yml000066400000000000000000000013501403015517400177150ustar00rootroot00000000000000on: [push, pull_request] name: CI jobs: test: strategy: matrix: go-version: [1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x, 1.16.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - uses: actions/cache@v2 with: # In order: # * Module download cache # * Build cache (Linux) path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Test run: go test -race ./... scram-1.0.2/.gitignore000066400000000000000000000000001403015517400145750ustar00rootroot00000000000000scram-1.0.2/CHANGELOG.md000066400000000000000000000004441403015517400144320ustar00rootroot00000000000000# CHANGELOG ## v1.0.2 - 2021-03-28 - Switch PBKDF2 dependency to github.com/xdg-go/pbkdf2 to minimize transitive dependencies and support Go 1.9+. ## v1.0.1 - 2021-03-27 - Bump stringprep dependency to v1.0.2 for Go 1.11 support. ## v1.0.0 - 2021-03-27 - First release as a Go module scram-1.0.2/LICENSE000066400000000000000000000236361403015517400136360ustar00rootroot00000000000000 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. scram-1.0.2/README.md000066400000000000000000000045311403015517400141010ustar00rootroot00000000000000[![Go Reference](https://pkg.go.dev/badge/github.com/xdg-go/scram.svg)](https://pkg.go.dev/github.com/xdg-go/scram) [![Go Report Card](https://goreportcard.com/badge/github.com/xdg-go/scram)](https://goreportcard.com/report/github.com/xdg-go/scram) [![Github Actions](https://github.com/xdg-go/scram/actions/workflows/test.yml/badge.svg)](https://github.com/xdg-go/scram/actions/workflows/test.yml) # scram – Go implementation of RFC-5802 ## Description Package scram provides client and server implementations of the Salted Challenge Response Authentication Mechanism (SCRAM) described in [RFC-5802](https://tools.ietf.org/html/rfc5802) and [RFC-7677](https://tools.ietf.org/html/rfc7677). It includes both client and server side support. Channel binding and extensions are not (yet) supported. ## Examples ### Client side package main import "github.com/xdg-go/scram" func main() { // Get Client with username, password and (optional) authorization ID. clientSHA1, err := scram.SHA1.NewClient("mulder", "trustno1", "") if err != nil { panic(err) } // Prepare the authentication conversation. Use the empty string as the // initial server message argument to start the conversation. conv := clientSHA1.NewConversation() var serverMsg string // Get the first message, send it and read the response. firstMsg, err := conv.Step(serverMsg) if err != nil { panic(err) } serverMsg = sendClientMsg(firstMsg) // Get the second message, send it, and read the response. secondMsg, err := conv.Step(serverMsg) if err != nil { panic(err) } serverMsg = sendClientMsg(secondMsg) // Validate the server's final message. We have no further message to // send so ignore that return value. _, err = conv.Step(serverMsg) if err != nil { panic(err) } return } func sendClientMsg(s string) string { // A real implementation would send this to a server and read a reply. return "" } ## 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 scram-1.0.2/client.go000066400000000000000000000074001403015517400144250ustar00rootroot00000000000000// 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" "github.com/xdg-go/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 } scram-1.0.2/client_conv.go000066400000000000000000000077731403015517400154670ustar00rootroot00000000000000// 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)) } scram-1.0.2/client_conv_test.go000066400000000000000000000037461403015517400165220ustar00rootroot00000000000000// 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 ( "strconv" "testing" ) func TestClientConv(t *testing.T) { cases, err := getTestData("good", "bad-server") if err != nil { t.Fatal(err) } for _, v := range cases { t.Run(v.Label, genClientSubTest(v)) } } func genClientSubTest(c TestCase) func(t *testing.T) { return func(t *testing.T) { hgf, err := getHGF(c.Digest) if err != nil { t.Fatal(err) } var client *Client if c.SkipSASLprep { client, err = hgf.NewClientUnprepped(c.User, c.Pass, c.AuthzID) } else { client, err = hgf.NewClient(c.User, c.Pass, c.AuthzID) } if err != nil { t.Errorf("%s: expected no error from NewClient, but got '%v'", c.Label, err) } if c.ClientNonce != "" { client = client.WithNonceGenerator(func() string { return c.ClientNonce }) } conv := client.NewConversation() for i, s := range clientSteps(c) { if conv.Done() { t.Errorf("%s: Premature end of conversation before step %d", c.Label, i+1) return } got, err := conv.Step(s.Input) if s.IsError && err == nil { t.Errorf("%s: step %d: expected error but didn't get one", c.Label, i+1) return } else if !s.IsError && err != nil { t.Errorf("%s: step %d: expected no error but got '%v'", c.Label, i+1, err) return } if got != s.Expect { t.Errorf("%s: step %d: incorrect step message; got %s, expected %s", c.Label, i+1, strconv.QuoteToASCII(got), strconv.QuoteToASCII(s.Expect), ) return } } if c.Valid != conv.Valid() { t.Errorf("%s: Conversation Valid() incorrect: got '%v', expected '%v'", c.Label, conv.Valid(), c.Valid) return } if !conv.Done() { t.Errorf("%s: Conversation not marked done after last step", c.Label) } } } scram-1.0.2/common.go000066400000000000000000000056701403015517400144460ustar00rootroot00000000000000// 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 } scram-1.0.2/common_test.go000066400000000000000000000014001403015517400154700ustar00rootroot00000000000000// 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 "testing" func TestEncodeName(t *testing.T) { cases := []struct { input string expect string }{ {input: "arthur", expect: "arthur"}, {input: "doe,jane", expect: "doe=2Cjane"}, {input: "a,b,c,d", expect: "a=2Cb=2Cc=2Cd"}, {input: "a,b=c,d=", expect: "a=2Cb=3Dc=2Cd=3D"}, } for _, c := range cases { if got := encodeName(c.input); got != c.expect { t.Errorf("Failed encoding '%s', got '%s', expected '%s'", c.input, got, c.expect) } } } scram-1.0.2/doc.go000066400000000000000000000020241403015517400137110ustar00rootroot00000000000000// 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 scram-1.0.2/doc_test.go000066400000000000000000000021021403015517400147450ustar00rootroot00000000000000package scram_test import "github.com/xdg-go/scram" func Example() { // Get Client with username, password and (optional) authorization ID. clientSHA1, err := scram.SHA1.NewClient("mulder", "trustno1", "") if err != nil { panic(err) } // Prepare the authentication conversation. Use the empty string as the // initial server message argument to start the conversation. conv := clientSHA1.NewConversation() var serverMsg string // Get the first message, send it and read the response. firstMsg, err := conv.Step(serverMsg) if err != nil { panic(err) } serverMsg = sendClientMsg(firstMsg) // Get the second message, send it, and read the response. secondMsg, err := conv.Step(serverMsg) if err != nil { panic(err) } serverMsg = sendClientMsg(secondMsg) // Validate the server's final message. We have no further message to // send so ignore that return value. _, err = conv.Step(serverMsg) if err != nil { panic(err) } return } func sendClientMsg(s string) string { // A real implementation would send this to a server and read a reply. return "" } scram-1.0.2/go.mod000066400000000000000000000001731403015517400137260ustar00rootroot00000000000000module github.com/xdg-go/scram go 1.11 require ( github.com/xdg-go/pbkdf2 v1.0.0 github.com/xdg-go/stringprep v1.0.2 ) scram-1.0.2/go.sum000066400000000000000000000011341403015517400137510ustar00rootroot00000000000000github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= scram-1.0.2/parse.go000066400000000000000000000074071403015517400142700ustar00rootroot00000000000000// 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 } scram-1.0.2/scram.go000066400000000000000000000053711403015517400142610ustar00rootroot00000000000000// 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-go/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) } scram-1.0.2/server.go000066400000000000000000000030241403015517400144530ustar00rootroot00000000000000// 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, } } scram-1.0.2/server_conv.go000066400000000000000000000106661403015517400155120ustar00rootroot00000000000000// 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 } scram-1.0.2/server_conv_test.go000066400000000000000000000070061403015517400165430ustar00rootroot00000000000000// 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" "fmt" "strconv" "testing" "github.com/xdg-go/stringprep" ) func TestServerConv(t *testing.T) { cases, err := getTestData("good", "bad-client") if err != nil { t.Fatal(err) } for _, v := range cases { t.Run(v.Label, genServerSubTest(v)) } } // Prep user credential callback for the case from Client func genServerCallback(c TestCase) (CredentialLookup, error) { salt, err := base64.StdEncoding.DecodeString(c.Salt64) if err != nil { return nil, fmt.Errorf("error decoding salt: %v", err) } hgf, err := getHGF(c.Digest) if err != nil { return nil, fmt.Errorf("error getting digest for credential callback: %v", err) } kf := KeyFactors{Salt: string(salt), Iters: c.Iters} var client *Client var userprep string if c.SkipSASLprep { client, err = hgf.NewClientUnprepped(c.User, c.Pass, c.AuthzID) userprep = c.User } else { client, err = hgf.NewClient(c.User, c.Pass, c.AuthzID) if userprep, err = stringprep.SASLprep.Prepare(c.User); err != nil { return nil, fmt.Errorf("Error SASLprepping username '%s': %v", c.User, err) } } if err != nil { return nil, fmt.Errorf("error generating client for credential callback: %v", err) } stored := client.GetStoredCredentials(kf) cbFcn := func(s string) (StoredCredentials, error) { if s == userprep { return stored, nil } return StoredCredentials{}, fmt.Errorf("Unknown user %s", s) } return cbFcn, nil } func genServerSubTest(c TestCase) func(t *testing.T) { return func(t *testing.T) { hgf, err := getHGF(c.Digest) if err != nil { t.Fatal(err) } cbFcn, err := genServerCallback(c) if err != nil { t.Fatal(err) } server, err := hgf.NewServer(cbFcn) if err != nil { t.Fatalf("%s: expected no error from NewServer, but got '%v'", c.Label, err) } if c.ServerNonce != "" { server = server.WithNonceGenerator(func() string { return c.ServerNonce }) } conv := server.NewConversation() for i, s := range serverSteps(c) { if conv.Done() { t.Errorf("%s: Premature end of conversation before step %d", c.Label, i+1) return } got, err := conv.Step(s.Input) if s.IsError && err == nil { t.Errorf("%s: step %d: expected error but didn't get one", c.Label, i+1) return } else if !s.IsError && err != nil { t.Errorf("%s: step %d: expected no error but got '%v'", c.Label, i+1, err) return } if got != s.Expect { t.Errorf("%s: step %d: incorrect step message; got %s, expected %s", c.Label, i+1, strconv.QuoteToASCII(got), strconv.QuoteToASCII(s.Expect), ) return } } if c.Valid != conv.Valid() { t.Errorf("%s: Conversation Valid() incorrect: got '%v', expected '%v'", c.Label, conv.Valid(), c.Valid) return } if !conv.Done() { t.Errorf("%s: Conversation not marked done after last step", c.Label) } var expectedUser string if c.SkipSASLprep { expectedUser = c.User } else { if expectedUser, err = stringprep.SASLprep.Prepare(c.User); err != nil { t.Errorf("Error SASLprepping username '%s': %v", c.User, err) } } if conv.Valid() && conv.Username() != expectedUser { t.Errorf("%s: Conversation didn't record proper username: got '%s', expected '%s'", c.Label, conv.Username(), expectedUser) } } } scram-1.0.2/testdata/000077500000000000000000000000001403015517400144305ustar00rootroot00000000000000scram-1.0.2/testdata/bad-client/000077500000000000000000000000001403015517400164325ustar00rootroot00000000000000scram-1.0.2/testdata/bad-client/bad-user.json000066400000000000000000000006271403015517400210340ustar00rootroot00000000000000{ "label": "unknown user", "digest": "SHA-1", "user": "user", "pass": "pencil", "authID": "", "skipSASLprep": false, "salt64": "QSXCR+Q6sek8bf92", "iters": 4096, "clientNonce": "fyko+d2lbbFgONRv9qkxdawL", "serverNonce": "3rfcNHYJY1ZVvWVs7j", "valid": false, "steps" : [ "n,,n=doesntexist,r=fyko+d2lbbFgONRv9qkxdawL", "e=unknown-user" ] } scram-1.0.2/testdata/bad-client/rfc5802-bad-proof.json000066400000000000000000000011241403015517400222630ustar00rootroot00000000000000{ "label": "RFC 5802 example with bad proof", "digest": "SHA-1", "user": "user", "pass": "pencil", "authID": "", "skipSASLprep": false, "salt64": "QSXCR+Q6sek8bf92", "iters": 4096, "clientNonce": "fyko+d2lbbFgONRv9qkxdawL", "serverNonce": "3rfcNHYJY1ZVvWVs7j", "valid": false, "steps" : [ "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL", "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096", "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=AAAAAAAAAAAAAAAAAAAAAAAAAAA=", "e=invalid-proof" ] } scram-1.0.2/testdata/bad-client/rfc7677-bad-proof.json000066400000000000000000000012121403015517400222750ustar00rootroot00000000000000{ "label": "RFC 7677 example with bad proof", "digest": "SHA-256", "user": "user", "pass": "pencil", "authID": "", "skipSASLprep": false, "salt64": "W22ZaJ0SNY7soEsUEjb6gQ==", "iters": 4096, "clientNonce": "rOprNGfwEbeRWgbNEkqO", "serverNonce": "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0", "valid": false, "steps" : [ "n,,n=user,r=rOprNGfwEbeRWgbNEkqO", "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096", "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "e=invalid-proof" ] } scram-1.0.2/testdata/bad-server/000077500000000000000000000000001403015517400164625ustar00rootroot00000000000000scram-1.0.2/testdata/bad-server/rfc5802-bad-validator.json000066400000000000000000000011501403015517400231520ustar00rootroot00000000000000{ "label": "RFC 5802 example with bad validation", "digest": "SHA-1", "user": "user", "pass": "pencil", "authID": "", "skipSASLprep": false, "salt64": "QSXCR+Q6sek8bf92", "iters": 4096, "clientNonce": "fyko+d2lbbFgONRv9qkxdawL", "serverNonce": "3rfcNHYJY1ZVvWVs7j", "valid": false, "steps" : [ "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL", "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096", "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", "v=AAAAAAAAAAAAAAAAAAAAAAAAAAA=" ] } scram-1.0.2/testdata/bad-server/rfc7677-bad-validator.json000066400000000000000000000012561403015517400231750ustar00rootroot00000000000000{ "label": "RFC 7677 example with bad validation", "digest": "SHA-256", "user": "user", "pass": "pencil", "authID": "", "skipSASLprep": false, "salt64": "W22ZaJ0SNY7soEsUEjb6gQ==", "iters": 4096, "clientNonce": "rOprNGfwEbeRWgbNEkqO", "serverNonce": "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0", "valid": false, "steps" : [ "n,,n=user,r=rOprNGfwEbeRWgbNEkqO", "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096", "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=", "v=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" ] } scram-1.0.2/testdata/good/000077500000000000000000000000001403015517400153605ustar00rootroot00000000000000scram-1.0.2/testdata/good/rfc5802.json000066400000000000000000000011371403015517400173460ustar00rootroot00000000000000{ "label": "RFC 5802 example", "digest": "SHA-1", "user": "user", "pass": "pencil", "authID": "", "skipSASLprep": false, "salt64": "QSXCR+Q6sek8bf92", "iters": 4096, "clientNonce": "fyko+d2lbbFgONRv9qkxdawL", "serverNonce": "3rfcNHYJY1ZVvWVs7j", "valid": true, "steps" : [ "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL", "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096", "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", "v=rmF9pqV8S7suAoZWja4dJRkFsKQ=", "" ] } scram-1.0.2/testdata/good/rfc7677.json000066400000000000000000000012451403015517400173620ustar00rootroot00000000000000{ "label": "RFC 7677 example", "digest": "SHA-256", "user": "user", "pass": "pencil", "authID": "", "skipSASLprep": false, "salt64": "W22ZaJ0SNY7soEsUEjb6gQ==", "iters": 4096, "clientNonce": "rOprNGfwEbeRWgbNEkqO", "serverNonce": "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0", "valid": true, "steps" : [ "n,,n=user,r=rOprNGfwEbeRWgbNEkqO", "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096", "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=", "v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=", "" ] } scram-1.0.2/testdata/good/sha-1-ascii-pass.json000066400000000000000000000010271403015517400212160ustar00rootroot00000000000000{ "label" : "SHA-1 ASCII pass", "digest" : "SHA-1", "user" : "ram\u00f5n", "pass" : "pencil", "authID" : "", "skipSASLprep" : false, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=ram\u00f5n,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=kvH02DJiH7oHwk+SKpN4plfpF04=", "v=BoA2mAPlV/b9A5WPDbHmHZi3EGc=", "" ] } scram-1.0.2/testdata/good/sha-1-ascii-user.json000066400000000000000000000010201403015517400212170ustar00rootroot00000000000000{ "label" : "SHA-1 ASCII user", "digest" : "SHA-1", "user" : "user", "pass" : "p\u00e8ncil", "authID" : "", "skipSASLprep" : false, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=user,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=yn797N2/XhIwZBB29LhEs6D6XVw=", "v=a6QRQikpGygizEM4/rCOvkgdglI=", "" ] } scram-1.0.2/testdata/good/sha-1-ascii.json000066400000000000000000000010061403015517400202470ustar00rootroot00000000000000{ "label" : "SHA-1 ASCII", "digest" : "SHA-1", "user" : "user", "pass" : "pencil", "authID" : "", "skipSASLprep" : false, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=user,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=I4oktcY7BOL0Agn0NlWRXlRP1mg=", "v=oKPvB1bE/9ydptJ+kohMgL+NdM0=", "" ] } scram-1.0.2/testdata/good/sha-1-no-saslprep.json000066400000000000000000000010401403015517400214200ustar00rootroot00000000000000{ "label" : "SHA-1 no-SASLprep", "digest" : "SHA-1", "user" : "ramo\u0301n", "pass" : "p\u212bssword", "authID" : "", "skipSASLprep" : true, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=ramo\u0301n,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=zLg8AlljNXeGOwWk0G2ay6a6qiM=", "v=sVH5eR1tapz4QrMVCIGAlrUCAfc=", "" ] } scram-1.0.2/testdata/good/sha-1-saslprep-non-normal.json000066400000000000000000000010501403015517400230650ustar00rootroot00000000000000{ "label" : "SHA-1 SASLprep non-normal", "digest" : "SHA-1", "user" : "ramo\u0301n", "pass" : "p\u212bssword", "authID" : "", "skipSASLprep" : false, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=ram\u00f3n,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=92sLIo0pB5IdEBOhBXx+t6Ew4pA=", "v=xS0F7g5YU4fvigpFAb8jTE8/S0E=", "" ] } scram-1.0.2/testdata/good/sha-1-saslprep-normal.json000066400000000000000000000010441403015517400223000ustar00rootroot00000000000000{ "label" : "SHA-1 SASLprep normal", "digest" : "SHA-1", "user" : "ram\u00f5n", "pass" : "p\u00c5assword", "authID" : "", "skipSASLprep" : false, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=ram\u00f5n,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=A1/CIzRGDxwgLpXqQ0CHSSOKX08=", "v=aCt2W88clBMnoAQauVf677Rjpho=", "" ] } scram-1.0.2/testdata/good/sha-256-ascii-pass.json000066400000000000000000000010731403015517400213730ustar00rootroot00000000000000{ "label" : "SHA-256 ASCII pass", "digest" : "SHA-256", "user" : "ram\u00f5n", "pass" : "pencil", "authID" : "", "skipSASLprep" : false, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=ram\u00f5n,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=vRdD7SqiY5kMyAFX2enPOJK9BL+3YIVyuzCt1H2qc4o=", "v=sh7QPwVuquMatYobYpYOaPiNS+lqwTCmy3rdexRDDkE=", "" ] } scram-1.0.2/testdata/good/sha-256-ascii-user.json000066400000000000000000000010641403015517400214030ustar00rootroot00000000000000{ "label" : "SHA-256 ASCII user", "digest" : "SHA-256", "user" : "user", "pass" : "p\u00e8ncil", "authID" : "", "skipSASLprep" : false, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=user,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=o6rKPfQCKSGHClFxHjdSeiVCPA6K53++gpY3XlP8lI8=", "v=rsyNAwnHfclZKxAKx1tKfInH3xPVAzCy237DQo5n/N8=", "" ] } scram-1.0.2/testdata/good/sha-256-ascii.json000066400000000000000000000010521403015517400204240ustar00rootroot00000000000000{ "label" : "SHA-256 ASCII", "digest" : "SHA-256", "user" : "user", "pass" : "pencil", "authID" : "", "skipSASLprep" : false, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=user,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=ItXnHvCDW7VGij6H+4rv2o93HvkLwrQaLkfVjeSMfrc=", "v=P61v8wxOu6B9J7Uij+Sk4zewSK1e6en6f5rCFO4OUNE=", "" ] } scram-1.0.2/testdata/good/sha-256-no-saslprep.json000066400000000000000000000011041403015517400215750ustar00rootroot00000000000000{ "label" : "SHA-256 no-SASLprep", "digest" : "SHA-256", "user" : "ramo\u0301n", "pass" : "p\u212bssword", "authID" : "", "skipSASLprep" : true, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=ramo\u0301n,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=oTfTL+YxW2HglmsPRO5VLdQk+oVt48HHrKppt+kYP2Y=", "v=mtXS1UbPSI9Ks9flMJwHBDfnmwcUwjpI8A/NlAT5c98=", "" ] } scram-1.0.2/testdata/good/sha-256-saslprep-non-normal.json000066400000000000000000000011141403015517400232420ustar00rootroot00000000000000{ "label" : "SHA-256 SASLprep non-normal", "digest" : "SHA-256", "user" : "ramo\u0301n", "pass" : "p\u212bssword", "authID" : "", "skipSASLprep" : false, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=ram\u00f3n,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=KXgIc8B+d5k3zx1P4rfs4TiybIlv11O85Jl1TrzEsfI=", "v=zG9u+MI5GPTROhnW/W1PUCKV4Uvp2SHzwFOZV9Hth/c=", "" ] } scram-1.0.2/testdata/good/sha-256-saslprep-normal.json000066400000000000000000000011101403015517400224460ustar00rootroot00000000000000{ "label" : "SHA-256 SASLprep normal", "digest" : "SHA-256", "user" : "ram\u00f5n", "pass" : "p\u00c5assword", "authID" : "", "skipSASLprep" : false, "salt64" : "c2FsdFNBTFRzYWx0\n", "iters" : 4096, "clientNonce" : "clientNONCE", "serverNonce" : "serverNONCE", "valid" : true, "steps" : [ "n,,n=ram\u00f5n,r=clientNONCE", "r=clientNONCEserverNONCE,s=c2FsdFNBTFRzYWx0,i=4096", "c=biws,r=clientNONCEserverNONCE,p=Km2zqmf/GbLdkItzscNI5D0c1f+GmLDi2fScTPm6d4k=", "v=30soY0l2BiInoDyrHxIuamz2LBvci1lFKo/tOMpqo98=", "" ] } scram-1.0.2/testdata_test.go000066400000000000000000000070311403015517400160170ustar00rootroot00000000000000// 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/json" "fmt" "io/ioutil" "path/filepath" "strings" ) type TestCase struct { Label string Digest string User string Pass string AuthzID string SkipSASLprep bool Salt64 string Iters int ClientNonce string ServerNonce string Valid bool Steps []string } type testStep struct { Input string Expect string IsError bool } func getHGF(s string) (HashGeneratorFcn, error) { switch s { case "SHA-1": return SHA1, nil case "SHA-256": return SHA256, nil default: panic(fmt.Sprintf("Unknown hash function '%s'", s)) } } func decodeFile(s string) (TestCase, error) { var tc TestCase data, err := ioutil.ReadFile(s) if err != nil { return tc, err } err = json.Unmarshal(data, &tc) if err != nil { return tc, fmt.Errorf("error unmarshaling '%s': %v", s, err) } return tc, nil } func getTestFiles(dir string) ([]string, error) { subdir := filepath.Join("testdata", dir) files, err := ioutil.ReadDir(subdir) if err != nil { return nil, err } filenames := make([]string, len(files)) for i, v := range files { filenames[i] = filepath.Join(subdir, v.Name()) } return filenames, nil } func getTestData(dirs ...string) ([]TestCase, error) { var err error filenames := make([]string, 0) for _, v := range dirs { names, err := getTestFiles(v) if err != nil { return nil, err } filenames = append(filenames, names...) } cases := make([]TestCase, len(filenames)) for i, v := range filenames { cases[i], err = decodeFile(v) if err != nil { return nil, err } } return cases, nil } // Even steps are client messages; odd steps are server responses. func clientSteps(c TestCase) []testStep { n := len(c.Steps) // Test case requires at least two steps: the first client step // (which cannot fail) and the first server response -- after which // an error would prevent further client steps. if n < 2 { panic("Incomplete conversation for this test case") } // First step needs empty input. steps := []testStep{{Input: "", Expect: c.Steps[0]}} // From i==1 until end, construct conversations from pairs of steps. We // know that (n >= 2). If the last pair is incomplete (no client Expect) // that indicates error. last := n - 1 for i := 1; i <= last; i += 2 { steps = append(steps, assembleStep(c, i, last)) } return steps } // Even steps are client messages; odd steps are server responses. func serverSteps(c TestCase) []testStep { n := len(c.Steps) // Test case requires at least one step: the first client step // after which an error would prevent further server steps. if n == 0 { panic("Incomplete conversation for this test case") } steps := make([]testStep, 0, 1) // From i==0 until end, construct conversations from pairs of steps. We // know that (n >= 1). If the last pair is incomplete (no server Expect) // that indicates error. last := n - 1 for i := 0; i < last; i += 2 { ts := assembleStep(c, i, last) steps = append(steps, ts) } return steps } func assembleStep(c TestCase, i int, last int) testStep { ts := testStep{Input: c.Steps[i]} if i == last { ts.IsError = true } else { ts.Expect = c.Steps[i+1] if strings.HasPrefix(ts.Expect, "e=") { ts.IsError = true } } return ts }