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 { func (a *Authentication) Provision(ctx caddy.Context) error {
a.logger = ctx.Logger() a.logger = ctx.Logger()
a.Providers = make(map[string]Authenticator) a.Providers = make(map[string]Authenticator)

View File

@ -32,7 +32,7 @@ import (
func init() { func init() {
caddycmd.RegisterCommand(caddycmd.Command{ caddycmd.RegisterCommand(caddycmd.Command{
Name: "hash-password", Name: "hash-password",
Usage: "[--plaintext <password>] [--algorithm <name>]", Usage: "[--plaintext <password>] [--algorithm <name>] [--cost <difficulty>]",
Short: "Hashes a password and writes base64", Short: "Hashes a password and writes base64",
Long: ` Long: `
Convenient way to hash a plaintext password. The resulting 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. not be echoed.
--algorithm currently only supports 'bcrypt', and is the default. --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) { CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("plaintext", "p", "", "The plaintext password") cmd.Flags().StringP("plaintext", "p", "", "The plaintext password")
cmd.Flags().StringP("algorithm", "a", "bcrypt", "Name of the hash algorithm") 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) cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword)
}, },
}) })
@ -57,6 +64,7 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
algorithm := fs.String("algorithm") algorithm := fs.String("algorithm")
plaintext := []byte(fs.String("plaintext")) plaintext := []byte(fs.String("plaintext"))
bcryptCost := fs.Int("bcrypt-cost")
if len(plaintext) == 0 { if len(plaintext) == 0 {
fd := int(os.Stdin.Fd()) fd := int(os.Stdin.Fd())
@ -108,7 +116,7 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
var hashString string var hashString string
switch algorithm { switch algorithm {
case "bcrypt": case "bcrypt":
hash, err = BcryptHash{}.Hash(plaintext) hash, err = BcryptHash{cost: bcryptCost}.Hash(plaintext)
hashString = string(hash) hashString = string(hash)
default: default:
return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm) return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)

View File

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

View File

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