mirror of
https://github.com/caddyserver/caddy.git
synced 2026-11-11 19:37:55 +00:00
* caddyauth: Support third-party http.authentication.hashes modules basic_auth and hash-password only accepted the built-in bcrypt and argon2id algorithms, even though the http.authentication.hashes namespace is documented as extensible. Look up any other algorithm name among the installed hash modules instead, and list the available ones when the name is not recognized. The built-in algorithms keep their own hash-password flags; other modules are provisioned and must implement Hasher to generate hashes. Closes #6670 * caddyauth: Base64-encode non-MCF hashes from hash-password modules hash-password printed module hashes as raw bytes, but basic_auth base64-decodes any password that isn't in Modular Crypt Format, so the output couldn't be used as-is. Encode non-MCF hashes as base64 and add a round-trip test that provisions HTTPBasicAuth with the command output. * caddyauth: always base64-encode third-party hash output
227 lines
7.3 KiB
Go
227 lines
7.3 KiB
Go
// 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 <password>] [--algorithm <argon2id|bcrypt|...>] [--bcrypt-cost <difficulty>] [--argon2id-time <iterations>] [--argon2id-memory <KiB>] [--argon2id-threads <n>] [--argon2id-keylen <bytes>]",
|
|
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
|
|
}
|