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") }