bcrypt: add cost parameter to hash-password (#7149)

* feat: add bcrypt cost parameter to hash-password

* revert: typos

* refactor: take the cost out of interface

* fix: default bcrypt cost to 14

* fix: follow bcrypt library for min and max cost

* doc: mention defaultBcryptCost in cost parameter description

* chore: gci format

* fix: more specific bcrypt cost algorithm flag

* feat: bcrypt cost provisioning

* Revert "feat: bcrypt cost provisioning"

This reverts commit e09d4bd036e7657588ed7785afd2c5388b29fb2a.

* chore: gci format

* chore: gci format

* chore: gci format

* chore: golangcilint fmted

---------

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
This commit is contained in:
GreyXor 2025-08-11 13:26:18 +02:00 committed by GitHub
parent 19ff47a63b
commit 49dac61b07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 30 additions and 8 deletions

View File

@ -60,7 +60,8 @@ func (Authentication) CaddyModule() caddy.ModuleInfo {
}
}
// Provision sets up a.
// Provision sets up an Authentication module by initializing its logger,
// loading and registering all configured authentication providers.
func (a *Authentication) Provision(ctx caddy.Context) error {
a.logger = ctx.Logger()
a.Providers = make(map[string]Authenticator)

View File

@ -32,7 +32,7 @@ import (
func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "hash-password",
Usage: "[--plaintext <password>] [--algorithm <name>]",
Usage: "[--plaintext <password>] [--algorithm <name>] [--cost <difficulty>]",
Short: "Hashes a password and writes base64",
Long: `
Convenient way to hash a plaintext password. The resulting
@ -43,10 +43,17 @@ Caddy is attached to a controlling tty, the plaintext will
not be echoed.
--algorithm currently only supports 'bcrypt', and is the default.
--cost sets the bcrypt hashing difficulty.
Higher values increase security by making the hash computation slower and more CPU-intensive.
If the provided cost is not within the valid range [bcrypt.MinCost, bcrypt.MaxCost],
the default value (defaultBcryptCost) will be used instead.
Note: Higher cost values can significantly degrade performance on slower systems.
`,
CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("plaintext", "p", "", "The plaintext password")
cmd.Flags().StringP("algorithm", "a", "bcrypt", "Name of the hash algorithm")
cmd.Flags().Int("bcrypt-cost", defaultBcryptCost, "Bcrypt hashing cost (only used with 'bcrypt' algorithm)")
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword)
},
})
@ -57,6 +64,7 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
algorithm := fs.String("algorithm")
plaintext := []byte(fs.String("plaintext"))
bcryptCost := fs.Int("bcrypt-cost")
if len(plaintext) == 0 {
fd := int(os.Stdin.Fd())
@ -108,7 +116,7 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
var hashString string
switch algorithm {
case "bcrypt":
hash, err = BcryptHash{}.Hash(plaintext)
hash, err = BcryptHash{cost: bcryptCost}.Hash(plaintext)
hashString = string(hash)
default:
return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)

View File

@ -15,6 +15,8 @@
package caddyauth
import (
"errors"
"golang.org/x/crypto/bcrypt"
"github.com/caddyserver/caddy/v2"
@ -24,8 +26,15 @@ func init() {
caddy.RegisterModule(BcryptHash{})
}
// defaultBcryptCost cost 14 strikes a solid balance between security, usability, and hardware performance
const defaultBcryptCost = 14
// BcryptHash implements the bcrypt hash.
type BcryptHash struct{}
type BcryptHash struct {
// cost is the bcrypt hashing difficulty factor (work factor).
// Higher values increase computation time and security.
cost int
}
// CaddyModule returns the Caddy module information.
func (BcryptHash) CaddyModule() caddy.ModuleInfo {
@ -38,7 +47,7 @@ func (BcryptHash) CaddyModule() caddy.ModuleInfo {
// Compare compares passwords.
func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) {
err := bcrypt.CompareHashAndPassword(hashed, plaintext)
if err == bcrypt.ErrMismatchedHashAndPassword {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return false, nil
}
if err != nil {
@ -48,8 +57,13 @@ func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) {
}
// Hash hashes plaintext using a random salt.
func (BcryptHash) Hash(plaintext []byte) ([]byte, error) {
return bcrypt.GenerateFromPassword(plaintext, 14)
func (b BcryptHash) Hash(plaintext []byte) ([]byte, error) {
cost := b.cost
if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
cost = defaultBcryptCost
}
return bcrypt.GenerateFromPassword(plaintext, cost)
}
// FakeHash returns a fake hash.

View File

@ -122,7 +122,6 @@ func TestPreferOrder(t *testing.T) {
}
}
func TestValidate(t *testing.T) {
type testCase struct {
name string