-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #291 from xmidt-org/feature/password-hashing
Feature/password hashing
- Loading branch information
Showing
16 changed files
with
515 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package basculehash | ||
|
||
import ( | ||
"io" | ||
|
||
"golang.org/x/crypto/bcrypt" | ||
) | ||
|
||
// Bcrypt is a Hasher and Comparer based around the bcrypt hashing | ||
// algorithm. | ||
type Bcrypt struct { | ||
// Cost is the cost parameter for bcrypt. If unset, the internal | ||
// bcrypt cost is used. If this value is higher than the max, | ||
// Hash will return an error. | ||
// | ||
// See: https://pkg.go.dev/golang.org/x/crypto/bcrypt#pkg-constants | ||
Cost int | ||
} | ||
|
||
// Hash executes the bcrypt algorithm and write the output to dst. | ||
func (b Bcrypt) Hash(dst io.Writer, plaintext []byte) (n int, err error) { | ||
hashed, err := bcrypt.GenerateFromPassword(plaintext, b.Cost) | ||
if err == nil { | ||
n, err = dst.Write(hashed) | ||
} | ||
|
||
return | ||
} | ||
|
||
// Matches attempts to match a plaintext against its bcrypt hashed value. | ||
func (b Bcrypt) Matches(plaintext, hash []byte) (ok bool, err error) { | ||
err = bcrypt.CompareHashAndPassword(hash, plaintext) | ||
ok = (err == nil) | ||
return | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package basculehash | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/suite" | ||
"golang.org/x/crypto/bcrypt" | ||
) | ||
|
||
const bcryptPlaintext string = "bcrypt plaintext" | ||
|
||
type BcryptTestSuite struct { | ||
suite.Suite | ||
} | ||
|
||
// goodHash returns a hash that is expected to be successful. | ||
// The plaintext() is hashed with the given cost. | ||
func (suite *BcryptTestSuite) goodHash(cost int) []byte { | ||
var ( | ||
b bytes.Buffer | ||
hasher = Bcrypt{Cost: cost} | ||
_, err = hasher.Hash(&b, []byte(bcryptPlaintext)) | ||
) | ||
|
||
suite.Require().NoError(err) | ||
return b.Bytes() | ||
} | ||
|
||
func (suite *BcryptTestSuite) TestHash() { | ||
suite.Run("DefaultCost", func() { | ||
var ( | ||
o strings.Builder | ||
hasher = Bcrypt{} | ||
|
||
n, err = hasher.Hash(&o, []byte(bcryptPlaintext)) | ||
) | ||
|
||
suite.NoError(err) | ||
suite.Equal(o.Len(), n) | ||
}) | ||
|
||
suite.Run("CustomCost", func() { | ||
var ( | ||
o strings.Builder | ||
hasher = Bcrypt{Cost: 12} | ||
|
||
n, err = hasher.Hash(&o, []byte(bcryptPlaintext)) | ||
) | ||
|
||
suite.NoError(err) | ||
suite.Equal(o.Len(), n) | ||
}) | ||
|
||
suite.Run("CostTooHigh", func() { | ||
var ( | ||
o strings.Builder | ||
hasher = Bcrypt{Cost: bcrypt.MaxCost + 100} | ||
|
||
_, err = hasher.Hash(&o, []byte(bcryptPlaintext)) | ||
) | ||
|
||
suite.Error(err) | ||
}) | ||
} | ||
|
||
func (suite *BcryptTestSuite) TestMatches() { | ||
suite.Run("Success", func() { | ||
for _, cost := range []int{0 /* default */, 4, 8} { | ||
suite.Run(fmt.Sprintf("cost=%d", cost), func() { | ||
var ( | ||
hashed = suite.goodHash(cost) | ||
hasher = Bcrypt{Cost: cost} | ||
ok, err = hasher.Matches([]byte(bcryptPlaintext), hashed) | ||
) | ||
|
||
suite.True(ok) | ||
suite.NoError(err) | ||
}) | ||
} | ||
}) | ||
|
||
suite.Run("Fail", func() { | ||
for _, cost := range []int{0 /* default */, 4, 8} { | ||
suite.Run(fmt.Sprintf("cost=%d", cost), func() { | ||
var ( | ||
hashed = suite.goodHash(cost) | ||
hasher = Bcrypt{Cost: cost} | ||
ok, err = hasher.Matches([]byte("a different plaintext"), hashed) | ||
) | ||
|
||
suite.False(ok) | ||
suite.Error(err) | ||
}) | ||
} | ||
}) | ||
} | ||
|
||
func TestBcrypt(t *testing.T) { | ||
suite.Run(t, new(BcryptTestSuite)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package basculehash | ||
|
||
// Comparer is a strategy for comparing plaintext values with a | ||
// hash value from a Hasher. | ||
type Comparer interface { | ||
// Matches tests if the given plaintext matches the given hash. | ||
// For example, this method can test if a password matches the | ||
// one-way hashed password from a config file or database. | ||
// | ||
// If this method returns true, the error will always be nil. | ||
// If this method returns false, the error may be non-nil to | ||
// indicate that the match failed due to a problem, such as | ||
// the hash not being parseable. Client code that is just | ||
// interested in a yes/no answer can disregard the error return. | ||
Matches(plaintext, hash []byte) (bool, error) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
/* | ||
Package basculehash provides basic hash support for things like passwords | ||
or other sensitive data that needs to be stored externally to the application. | ||
*/ | ||
package basculehash |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package basculehash | ||
|
||
import ( | ||
"io" | ||
) | ||
|
||
// Hasher is a strategy for one-way hashing. | ||
type Hasher interface { | ||
// Hash writes the hash of a plaintext to a writer. The number of | ||
// bytes written along with any error is returned. | ||
// | ||
// The format of the written hash must be ASCII. The recommended | ||
// format is the modular crypt format, which bcrypt uses. | ||
Hash(dst io.Writer, plaintext []byte) (int, error) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package main | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/alecthomas/kong" | ||
"github.com/xmidt-org/bascule/basculehash" | ||
"golang.org/x/crypto/bcrypt" | ||
) | ||
|
||
const ( | ||
// MaxBcryptPlaintextLength is the maximum length of the input that | ||
// bcrypt will operate on. This value isn't exposed via the | ||
// golang.org/x/crypto/bcrypt package. | ||
MaxBcryptPlaintextLength = 72 | ||
) | ||
|
||
// Bcrypt is the subcommand for the bcrypt algorithm. | ||
type Bcrypt struct { | ||
Cost int `default:"10" short:"c" help:"the cost parameter for bcrypt. Must be between 4 and 31, inclusive."` | ||
Plaintext string `arg:"" required:"" help:"the plaintext (e.g. password) to hash. This cannot exceed 72 bytes in length."` | ||
} | ||
|
||
func (cmd *Bcrypt) Validate() error { | ||
switch { | ||
case cmd.Cost < bcrypt.MinCost: | ||
return fmt.Errorf("Cost cannot be less than %d", bcrypt.MinCost) | ||
|
||
case cmd.Cost > bcrypt.MaxCost: | ||
return fmt.Errorf("Cost cannot be greater than %d", bcrypt.MaxCost) | ||
|
||
case len(cmd.Plaintext) > MaxBcryptPlaintextLength: | ||
return fmt.Errorf("Plaintext length cannot exceed %d bytes", MaxBcryptPlaintextLength) | ||
|
||
default: | ||
return nil | ||
} | ||
} | ||
|
||
func (cmd *Bcrypt) Run(kong *kong.Kong) error { | ||
hasher := basculehash.Bcrypt{ | ||
Cost: cmd.Cost, | ||
} | ||
|
||
_, err := hasher.Hash(kong.Stdout, []byte(cmd.Plaintext)) | ||
return err | ||
} | ||
|
||
// CLI is the top grammar node for the command-line tool. | ||
type CLI struct { | ||
// Bcrypt is the bcrypt subcommand. This is the only supported hash | ||
// algorithm right now. | ||
Bcrypt Bcrypt `cmd:""` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package main | ||
|
||
import ( | ||
"os" | ||
|
||
"github.com/alecthomas/kong" | ||
) | ||
|
||
func newKong(extra ...kong.Option) (*kong.Kong, error) { | ||
return kong.New( | ||
new(CLI), | ||
append( | ||
[]kong.Option{ | ||
kong.UsageOnError(), | ||
kong.Description("hashes plaintext using bascule's infrastructure"), | ||
}, | ||
extra..., | ||
)..., | ||
) | ||
} | ||
|
||
func run(args []string, extra ...kong.Option) { | ||
var ctx *kong.Context | ||
k, err := newKong(extra...) | ||
if err == nil { | ||
ctx, err = k.Parse(args) | ||
} | ||
|
||
if err == nil { | ||
err = ctx.Run() | ||
} | ||
|
||
k.FatalIfErrorf(err) | ||
} | ||
|
||
func main() { | ||
run(os.Args[1:]) | ||
} |
Oops, something went wrong.