authelia/internal/commands/crypto_hash.go

528 lines
17 KiB
Go

package commands
import (
"fmt"
"strings"
"syscall"
"github.com/go-crypt/crypt"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/term"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/configuration"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/configuration/validator"
"github.com/authelia/authelia/v4/internal/utils"
)
func newHashPasswordCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: cmdUseHashPassword,
Short: cmdAutheliaHashPasswordShort,
Long: cmdAutheliaHashPasswordLong,
Example: cmdAutheliaHashPasswordExample,
Args: cobra.MaximumNArgs(1),
RunE: cmdHashPasswordRunE,
DisableAutoGenTag: true,
}
cmdFlagConfig(cmd)
cmd.Flags().BoolP(cmdFlagNameSHA512, "z", false, fmt.Sprintf("use sha512 as the algorithm (changes iterations to %d, change with -i)", schema.DefaultPasswordConfig.SHA2Crypt.Iterations))
cmd.Flags().IntP(cmdFlagNameIterations, "i", schema.DefaultPasswordConfig.Argon2.Iterations, "set the number of hashing iterations")
cmd.Flags().IntP(cmdFlagNameMemory, "m", schema.DefaultPasswordConfig.Argon2.Memory, "[argon2id] set the amount of memory param (in MB)")
cmd.Flags().IntP(cmdFlagNameParallelism, "p", schema.DefaultPasswordConfig.Argon2.Parallelism, "[argon2id] set the parallelism param")
cmd.Flags().IntP("key-length", "k", schema.DefaultPasswordConfig.Argon2.KeyLength, "[argon2id] set the key length param")
cmd.Flags().IntP("salt-length", "l", schema.DefaultPasswordConfig.Argon2.SaltLength, "set the auto-generated salt length")
cmd.Flags().Bool(cmdFlagNameNoConfirm, false, "skip the password confirmation prompt")
return cmd
}
func cmdHashPasswordRunE(cmd *cobra.Command, args []string) (err error) {
var (
flagsMap map[string]string
sha512 bool
)
if sha512, err = cmd.Flags().GetBool(cmdFlagNameSHA512); err != nil {
return err
}
switch {
case sha512:
flagsMap = map[string]string{
cmdFlagNameIterations: prefixFilePassword + ".sha2crypt.iterations",
"salt-length": prefixFilePassword + ".sha2crypt.salt_length",
}
default:
flagsMap = map[string]string{
cmdFlagNameIterations: prefixFilePassword + ".argon2.iterations",
"key-length": prefixFilePassword + ".argon2.key_length",
"salt-length": prefixFilePassword + ".argon2.salt_length",
cmdFlagNameParallelism: prefixFilePassword + ".argon2.parallelism",
cmdFlagNameMemory: prefixFilePassword + ".argon2.memory",
}
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func newCryptoHashCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: cmdUseHash,
Short: cmdAutheliaCryptoHashShort,
Long: cmdAutheliaCryptoHashLong,
Example: cmdAutheliaCryptoHashExample,
Args: cobra.NoArgs,
DisableAutoGenTag: true,
}
cmd.AddCommand(
newCryptoHashValidateCmd(),
newCryptoHashGenerateCmd(),
)
return cmd
}
func newCryptoHashGenerateCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: cmdUseGenerate,
Short: cmdAutheliaCryptoHashGenerateShort,
Long: cmdAutheliaCryptoHashGenerateLong,
Example: cmdAutheliaCryptoHashGenerateExample,
RunE: func(cmd *cobra.Command, args []string) error {
return cmdCryptoHashGenerateFinish(cmd, args, map[string]string{})
},
DisableAutoGenTag: true,
}
cmdFlagConfig(cmd)
cmdFlagPassword(cmd, true)
for _, use := range []string{cmdUseHashArgon2, cmdUseHashSHA2Crypt, cmdUseHashPBKDF2, cmdUseHashBCrypt, cmdUseHashSCrypt} {
cmd.AddCommand(newCryptoHashGenerateSubCmd(use))
}
return cmd
}
func newCryptoHashGenerateSubCmd(use string) (cmd *cobra.Command) {
useFmt := fmtCryptoHashUse(use)
cmd = &cobra.Command{
Use: use,
Short: fmt.Sprintf(fmtCmdAutheliaCryptoHashGenerateSubShort, useFmt),
Long: fmt.Sprintf(fmtCmdAutheliaCryptoHashGenerateSubLong, useFmt, useFmt),
Example: fmt.Sprintf(fmtCmdAutheliaCryptoHashGenerateSubExample, use),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
DisableAutoGenTag: true,
}
cmdFlagConfig(cmd)
cmdFlagPassword(cmd, true)
cmdFlagRandomPassword(cmd)
switch use {
case cmdUseHashArgon2:
cmdFlagIterations(cmd, schema.DefaultPasswordConfig.Argon2.Iterations)
cmdFlagParallelism(cmd, schema.DefaultPasswordConfig.Argon2.Parallelism)
cmdFlagKeySize(cmd, schema.DefaultPasswordConfig.Argon2.KeyLength)
cmdFlagSaltSize(cmd, schema.DefaultPasswordConfig.Argon2.SaltLength)
cmd.Flags().StringP(cmdFlagNameVariant, "v", schema.DefaultPasswordConfig.Argon2.Variant, "variant, options are 'argon2id', 'argon2i', and 'argon2d'")
cmd.Flags().IntP(cmdFlagNameMemory, "m", schema.DefaultPasswordConfig.Argon2.Memory, "memory in kibibytes")
cmd.Flags().String(cmdFlagNameProfile, "", "profile to use, options are low-memory and recommended")
cmd.RunE = cryptoHashGenerateArgon2RunE
case cmdUseHashSHA2Crypt:
cmdFlagIterations(cmd, schema.DefaultPasswordConfig.SHA2Crypt.Iterations)
cmdFlagSaltSize(cmd, schema.DefaultPasswordConfig.SHA2Crypt.SaltLength)
cmd.Flags().StringP(cmdFlagNameVariant, "v", schema.DefaultPasswordConfig.SHA2Crypt.Variant, "variant, options are sha256 and sha512")
cmd.RunE = cryptoHashGenerateSHA2CryptRunE
case cmdUseHashPBKDF2:
cmdFlagIterations(cmd, schema.DefaultPasswordConfig.PBKDF2.Iterations)
cmdFlagSaltSize(cmd, schema.DefaultPasswordConfig.PBKDF2.SaltLength)
cmd.Flags().StringP(cmdFlagNameVariant, "v", schema.DefaultPasswordConfig.PBKDF2.Variant, "variant, options are 'sha1', 'sha224', 'sha256', 'sha384', and 'sha512'")
cmd.RunE = cryptoHashGeneratePBKDF2RunE
case cmdUseHashBCrypt:
cmd.Flags().StringP(cmdFlagNameVariant, "v", schema.DefaultPasswordConfig.BCrypt.Variant, "variant, options are 'standard' and 'sha256'")
cmd.Flags().IntP(cmdFlagNameCost, "i", schema.DefaultPasswordConfig.BCrypt.Cost, "hashing cost")
cmd.RunE = cryptoHashGenerateBCryptRunE
case cmdUseHashSCrypt:
cmdFlagIterations(cmd, schema.DefaultPasswordConfig.SCrypt.Iterations)
cmdFlagKeySize(cmd, schema.DefaultPasswordConfig.SCrypt.KeyLength)
cmdFlagSaltSize(cmd, schema.DefaultPasswordConfig.SCrypt.SaltLength)
cmdFlagParallelism(cmd, schema.DefaultPasswordConfig.SCrypt.Parallelism)
cmd.Flags().IntP(cmdFlagNameBlockSize, "r", schema.DefaultPasswordConfig.SCrypt.BlockSize, "block size")
cmd.RunE = cryptoHashGenerateSCryptRunE
}
return cmd
}
func cryptoHashGenerateArgon2RunE(cmd *cobra.Command, args []string) (err error) {
flagsMap := map[string]string{
cmdFlagNameVariant: prefixFilePassword + ".argon2.variant",
cmdFlagNameIterations: prefixFilePassword + ".argon2.iterations",
cmdFlagNameMemory: prefixFilePassword + ".argon2.memory",
cmdFlagNameParallelism: prefixFilePassword + ".argon2.parallelism",
cmdFlagNameKeySize: prefixFilePassword + ".argon2.key_length",
cmdFlagNameSaltSize: prefixFilePassword + ".argon2.salt_length",
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func cryptoHashGenerateSHA2CryptRunE(cmd *cobra.Command, args []string) (err error) {
flagsMap := map[string]string{
cmdFlagNameVariant: prefixFilePassword + ".sha2crypt.variant",
cmdFlagNameIterations: prefixFilePassword + ".sha2crypt.iterations",
cmdFlagNameSaltSize: prefixFilePassword + ".sha2crypt.salt_length",
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func cryptoHashGeneratePBKDF2RunE(cmd *cobra.Command, args []string) (err error) {
flagsMap := map[string]string{
cmdFlagNameVariant: prefixFilePassword + ".pbkdf2.variant",
cmdFlagNameIterations: prefixFilePassword + ".pbkdf2.iterations",
cmdFlagNameKeySize: prefixFilePassword + ".pbkdf2.key_length",
cmdFlagNameSaltSize: prefixFilePassword + ".pbkdf2.salt_length",
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func cryptoHashGenerateBCryptRunE(cmd *cobra.Command, args []string) (err error) {
flagsMap := map[string]string{
cmdFlagNameVariant: prefixFilePassword + ".bcrypt.variant",
cmdFlagNameCost: prefixFilePassword + ".bcrypt.cost",
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func cryptoHashGenerateSCryptRunE(cmd *cobra.Command, args []string) (err error) {
flagsMap := map[string]string{
cmdFlagNameIterations: prefixFilePassword + ".scrypt.iterations",
cmdFlagNameBlockSize: prefixFilePassword + ".scrypt.block_size",
cmdFlagNameParallelism: prefixFilePassword + ".scrypt.parallelism",
cmdFlagNameKeySize: prefixFilePassword + ".scrypt.key_length",
cmdFlagNameSaltSize: prefixFilePassword + ".scrypt.salt_length",
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func newCryptoHashValidateCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: fmt.Sprintf(cmdUseFmtValidate, cmdUseValidate),
Short: cmdAutheliaCryptoHashValidateShort,
Long: cmdAutheliaCryptoHashValidateLong,
Example: cmdAutheliaCryptoHashValidateExample,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) (err error) {
var (
password string
valid bool
)
if password, _, err = cmdCryptoHashGetPassword(cmd, args, false, false); err != nil {
return fmt.Errorf("error occurred trying to obtain the password: %w", err)
}
if len(password) == 0 {
return fmt.Errorf("no password provided")
}
if valid, err = crypt.CheckPassword(password, args[0]); err != nil {
return fmt.Errorf("error occurred trying to validate the password against the digest: %w", err)
}
switch {
case valid:
fmt.Println("The password matches the digest.")
default:
fmt.Println("The password does not match the digest.")
}
return nil
},
DisableAutoGenTag: true,
}
cmdFlagPassword(cmd, false)
return cmd
}
func cmdCryptoHashGenerateFinish(cmd *cobra.Command, args []string, flagsMap map[string]string) (err error) {
var (
algorithm string
configs []string
c schema.Password
)
if configs, err = cmd.Flags().GetStringSlice(cmdFlagNameConfig); err != nil {
return err
}
// Skip config if the flag wasn't set and the default is non-existent.
if !cmd.Flags().Changed(cmdFlagNameConfig) {
configs = configFilterExisting(configs)
}
legacy := cmd.Use == cmdUseHashPassword
switch {
case cmd.Use == cmdUseGenerate:
break
case legacy:
if sha512, _ := cmd.Flags().GetBool(cmdFlagNameSHA512); sha512 {
algorithm = cmdUseHashSHA2Crypt
} else {
algorithm = cmdUseHashArgon2
}
default:
algorithm = cmd.Use
}
if c, err = cmdCryptoHashGetConfig(algorithm, configs, cmd.Flags(), flagsMap); err != nil {
return err
}
if legacy && algorithm == cmdUseHashArgon2 && cmd.Flags().Changed(cmdFlagNameMemory) {
c.Argon2.Memory *= 1024
}
var (
hash crypt.Hash
digest crypt.Digest
password string
random bool
)
if password, random, err = cmdCryptoHashGetPassword(cmd, args, legacy, !legacy); err != nil {
return err
}
if len(password) == 0 {
return fmt.Errorf("no password provided")
}
if hash, err = authentication.NewFileCryptoHashFromConfig(c); err != nil {
return err
}
if digest, err = hash.Hash(password); err != nil {
return err
}
if random {
fmt.Printf("Random Password: %s\n", password)
}
fmt.Printf("Digest: %s\n", digest.Encode())
return nil
}
func cmdCryptoHashGetConfig(algorithm string, configs []string, flags *pflag.FlagSet, flagsMap map[string]string) (c schema.Password, err error) {
mapDefaults := map[string]interface{}{
prefixFilePassword + ".algorithm": schema.DefaultPasswordConfig.Algorithm,
prefixFilePassword + ".argon2.variant": schema.DefaultPasswordConfig.Argon2.Variant,
prefixFilePassword + ".argon2.iterations": schema.DefaultPasswordConfig.Argon2.Iterations,
prefixFilePassword + ".argon2.memory": schema.DefaultPasswordConfig.Argon2.Memory,
prefixFilePassword + ".argon2.parallelism": schema.DefaultPasswordConfig.Argon2.Parallelism,
prefixFilePassword + ".argon2.key_length": schema.DefaultPasswordConfig.Argon2.KeyLength,
prefixFilePassword + ".argon2.salt_length": schema.DefaultPasswordConfig.Argon2.SaltLength,
prefixFilePassword + ".sha2crypt.variant": schema.DefaultPasswordConfig.SHA2Crypt.Variant,
prefixFilePassword + ".sha2crypt.iterations": schema.DefaultPasswordConfig.SHA2Crypt.Iterations,
prefixFilePassword + ".sha2crypt.salt_length": schema.DefaultPasswordConfig.SHA2Crypt.SaltLength,
prefixFilePassword + ".pbkdf2.variant": schema.DefaultPasswordConfig.PBKDF2.Variant,
prefixFilePassword + ".pbkdf2.iterations": schema.DefaultPasswordConfig.PBKDF2.Iterations,
prefixFilePassword + ".pbkdf2.salt_length": schema.DefaultPasswordConfig.PBKDF2.SaltLength,
prefixFilePassword + ".bcrypt.variant": schema.DefaultPasswordConfig.BCrypt.Variant,
prefixFilePassword + ".bcrypt.cost": schema.DefaultPasswordConfig.BCrypt.Cost,
prefixFilePassword + ".scrypt.iterations": schema.DefaultPasswordConfig.SCrypt.Iterations,
prefixFilePassword + ".scrypt.block_size": schema.DefaultPasswordConfig.SCrypt.BlockSize,
prefixFilePassword + ".scrypt.parallelism": schema.DefaultPasswordConfig.SCrypt.Parallelism,
prefixFilePassword + ".scrypt.key_length": schema.DefaultPasswordConfig.SCrypt.KeyLength,
prefixFilePassword + ".scrypt.salt_length": schema.DefaultPasswordConfig.SCrypt.SaltLength,
}
sources := configuration.NewDefaultSourcesWithDefaults(configs,
configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter,
configuration.NewMapSource(mapDefaults),
configuration.NewCommandLineSourceWithMapping(flags, flagsMap, false, false),
)
if algorithm != "" {
alg := map[string]interface{}{prefixFilePassword + ".algorithm": algorithm}
sources = append(sources, configuration.NewMapSource(alg))
}
val := schema.NewStructValidator()
if _, err = configuration.LoadAdvanced(val, prefixFilePassword, &c, sources...); err != nil {
return schema.Password{}, fmt.Errorf("error occurred loading configuration: %w", err)
}
validator.ValidatePasswordConfiguration(&c, val)
errs := val.Errors()
if len(errs) != 0 {
for i, e := range errs {
if i == 0 {
err = e
continue
}
err = fmt.Errorf("%v, %w", err, e)
}
return schema.Password{}, fmt.Errorf("errors occurred validating the password configuration: %w", err)
}
return c, nil
}
func cmdCryptoHashGetPassword(cmd *cobra.Command, args []string, useArgs, useRandom bool) (password string, random bool, err error) {
if useRandom {
if random, err = cmd.Flags().GetBool(cmdFlagNameRandom); err != nil {
return
}
}
switch {
case random:
var length int
if length, err = cmd.Flags().GetInt(cmdFlagNameRandomLength); err != nil {
return
}
password = utils.RandomString(length, utils.CharSetAlphaNumeric, true)
return
case cmd.Flags().Changed(cmdFlagNamePassword):
password, err = cmd.Flags().GetString(cmdFlagNamePassword)
return
case useArgs && len(args) != 0:
password, err = strings.Join(args, " "), nil
return
}
var (
data []byte
noConfirm bool
)
if data, err = hashReadPasswordWithPrompt("Enter Password: "); err != nil {
err = fmt.Errorf("failed to read the password from the terminal: %w", err)
return
}
password = string(data)
if cmd.Use == fmt.Sprintf(cmdUseFmtValidate, cmdUseValidate) {
fmt.Println("")
return
}
if noConfirm, err = cmd.Flags().GetBool(cmdFlagNameNoConfirm); err == nil && !noConfirm {
if data, err = hashReadPasswordWithPrompt("Confirm Password: "); err != nil {
err = fmt.Errorf("failed to read the password from the terminal: %w", err)
return
}
if password != string(data) {
fmt.Println("")
err = fmt.Errorf("the password did not match the confirmation password")
return
}
}
fmt.Println("")
return
}
func hashReadPasswordWithPrompt(prompt string) (data []byte, err error) {
fmt.Print(prompt)
if data, err = term.ReadPassword(int(syscall.Stdin)); err != nil { //nolint:unconvert,nolintlint
if err.Error() == "inappropriate ioctl for device" {
return nil, fmt.Errorf("the terminal doesn't appear to be interactive either use the '--password' flag or use an interactive terminal: %w", err)
}
return nil, err
}
fmt.Println("")
return data, nil
}
func cmdFlagConfig(cmd *cobra.Command) {
cmd.Flags().StringSliceP(cmdFlagNameConfig, "c", []string{"configuration.yml"}, "configuration files to load")
}
func cmdFlagPassword(cmd *cobra.Command, noConfirm bool) {
cmd.Flags().String(cmdFlagNamePassword, "", "manually supply the password rather than using the terminal prompt")
if noConfirm {
cmd.Flags().Bool(cmdFlagNameNoConfirm, false, "skip the password confirmation prompt")
}
}
func cmdFlagRandomPassword(cmd *cobra.Command) {
cmd.Flags().Bool(cmdFlagNameRandom, false, "uses a randomly generated password")
cmd.Flags().Int(cmdFlagNameRandomLength, 72, "when using a randomly generated password it configures the length")
}
func cmdFlagIterations(cmd *cobra.Command, value int) {
cmd.Flags().IntP(cmdFlagNameIterations, "i", value, "number of iterations")
}
func cmdFlagKeySize(cmd *cobra.Command, value int) {
cmd.Flags().IntP(cmdFlagNameKeySize, "k", value, "key size in bytes")
}
func cmdFlagSaltSize(cmd *cobra.Command, value int) {
cmd.Flags().IntP(cmdFlagNameSaltSize, "s", value, "salt size in bytes")
}
func cmdFlagParallelism(cmd *cobra.Command, value int) {
cmd.Flags().IntP(cmdFlagNameParallelism, "p", value, "parallelism or threads")
}