// Copyright 2015 Matthew Holt and The Caddy Authors // // 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 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package caddyauth import ( "bufio" "bytes" "context" "encoding/base64" "errors" "fmt" "io" "os" "os/signal" "github.com/spf13/cobra" "golang.org/x/term" caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2" ) func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "hash-password", Usage: "[--plaintext ] [--algorithm ] [--bcrypt-cost ] [--argon2id-time ] [--argon2id-memory ] [--argon2id-threads ] [--argon2id-keylen ]", Short: "Hashes a password and writes base64", Long: ` Convenient way to hash a plaintext password. The resulting hash is written to stdout as a base64 string. --plaintext The password to hash. If omitted, it will be read from stdin. If Caddy is attached to a controlling TTY, the input will not be echoed. --algorithm Selects the hashing algorithm. Valid options are: * 'argon2id' (recommended for modern security) * 'bcrypt' (legacy, slower, configurable cost) * the name of another installed http.authentication.hashes module that can generate hashes with its default configuration; its output is base64-encoded bcrypt-specific parameters: --bcrypt-cost Sets the bcrypt hashing difficulty. Higher values increase security by making the hash computation slower and more CPU-intensive. Must be within the valid range [bcrypt.MinCost, bcrypt.MaxCost]. If omitted or invalid, the default cost is used. Argon2id-specific parameters: --argon2id-time Number of iterations to perform. Increasing this makes hashing slower and more resistant to brute-force attacks. --argon2id-memory Amount of memory to use during hashing. Larger values increase resistance to GPU/ASIC attacks. --argon2id-threads Number of CPU threads to use. Increase for faster hashing on multi-core systems. --argon2id-keylen Length of the resulting hash in bytes. Longer keys increase security but slightly increase storage size. `, CobraFunc: func(cmd *cobra.Command) { cmd.Flags().StringP("plaintext", "p", "", "The plaintext password") cmd.Flags().StringP("algorithm", "a", bcryptName, "Name of the hash algorithm") cmd.Flags().Int("bcrypt-cost", defaultBcryptCost, "Bcrypt hashing cost (only used with 'bcrypt' algorithm)") cmd.Flags().Uint32("argon2id-time", defaultArgon2idTime, "Number of iterations for Argon2id hashing. Increasing this makes the hash slower and more resistant to brute-force attacks.") cmd.Flags().Uint32("argon2id-memory", defaultArgon2idMemory, "Memory to use in KiB for Argon2id hashing. Larger values increase resistance to GPU/ASIC attacks.") cmd.Flags().Uint8("argon2id-threads", defaultArgon2idThreads, "Number of CPU threads to use for Argon2id hashing. Increase for faster hashing on multi-core systems.") cmd.Flags().Uint32("argon2id-keylen", defaultArgon2idKeylen, "Length of the resulting Argon2id hash in bytes. Longer hashes increase security but slightly increase storage size.") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword) }, }) } func cmdHashPassword(fs caddycmd.Flags) (int, error) { var err error algorithm := fs.String("algorithm") plaintext := []byte(fs.String("plaintext")) bcryptCost := fs.Int("bcrypt-cost") if len(plaintext) == 0 { fd := int(os.Stdin.Fd()) if term.IsTerminal(fd) { // ensure the terminal state is restored on SIGINT state, _ := term.GetState(fd) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) go func() { <-c _ = term.Restore(fd, state) os.Exit(caddy.ExitCodeFailedStartup) }() defer signal.Stop(c) fmt.Fprint(os.Stderr, "Enter password: ") plaintext, err = term.ReadPassword(fd) fmt.Fprintln(os.Stderr) if err != nil { return caddy.ExitCodeFailedStartup, err } fmt.Fprint(os.Stderr, "Confirm password: ") confirmation, err := term.ReadPassword(fd) fmt.Fprintln(os.Stderr) if err != nil { return caddy.ExitCodeFailedStartup, err } if !bytes.Equal(plaintext, confirmation) { return caddy.ExitCodeFailedStartup, fmt.Errorf("password does not match") } } else { rd := bufio.NewReader(os.Stdin) line, err := rd.ReadBytes('\n') if err != nil && !errors.Is(err, io.EOF) { return caddy.ExitCodeFailedStartup, err } // drop the line ending (\n or \r\n), if any; a lone trailing \r is kept if trimmed, ok := bytes.CutSuffix(line, []byte("\n")); ok { line = bytes.TrimSuffix(trimmed, []byte("\r")) } plaintext = line } if len(plaintext) == 0 { return caddy.ExitCodeFailedStartup, fmt.Errorf("plaintext is required") } } var hash []byte var hashString string switch algorithm { case bcryptName: hash, err = BcryptHash{cost: bcryptCost}.Hash(plaintext) hashString = string(hash) case argon2idName: time, err := fs.GetUint32("argon2id-time") if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id time parameter: %w", err) } memory, err := fs.GetUint32("argon2id-memory") if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id memory parameter: %w", err) } threads, err := fs.GetUint8("argon2id-threads") if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id threads parameter: %w", err) } keyLen, err := fs.GetUint32("argon2id-keylen") if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id keylen parameter: %w", err) } hash, _ = Argon2idHash{ time: time, memory: memory, threads: threads, keyLen: keyLen, }.Hash(plaintext) hashString = string(hash) default: hashString, err = hashWithModule(algorithm, plaintext) } if err != nil { return caddy.ExitCodeFailedStartup, err } fmt.Println(hashString) return 0, nil } // hashWithModule hashes plaintext using the installed password // hashing module with the given name, loaded with an empty // configuration. The module must implement Hasher to be usable for // generating hashes. Its output is arbitrary bytes, so it is always // base64-encoded, which is the form HTTPBasicAuth reads passwords in // when they do not start with "$". func hashWithModule(algorithm string, plaintext []byte) (string, error) { modInfo, err := hashModuleInfo(algorithm) if err != nil { return "", err } ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) defer cancel() mod, err := ctx.LoadModuleByID(string(modInfo.ID), nil) if err != nil { return "", err } hasher, ok := mod.(Hasher) if !ok { return "", fmt.Errorf("hash algorithm %s does not support generating hashes", algorithm) } hash, err := hasher.Hash(plaintext) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(hash), nil }