fix(commands): hash-password usage instructions (#3437)

This fixes the hash-password usage instructions and ensures it uses mostly a configuration source based config. In addition it updates our recommended argon2id parameters with the RFC recommendations.
pull/3460/head
James Elliott 2022-06-02 09:18:45 +10:00 committed by GitHub
parent 0d3ee8e730
commit 2037a0ee4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 199 additions and 122 deletions

View File

@ -23,13 +23,13 @@ authentication_backend:
path: /config/users.yml
password:
algorithm: argon2id
iterations: 1
iterations: 3
salt_length: 16
parallelism: 8
key_length: 32
parallelism: 4
memory: 64
```
## Format
The format of the users file is as follows.
@ -100,9 +100,9 @@ required: no
Controls the number of hashing iterations done by the other hashing settings.
When using `argon2id` the minimum is 1, which is also the recommended value.
When using `argon2id` the minimum is 3, which is also the recommended and default value.
When using `sha512` the minimum is 1000, and 50000 is the recommended value.
When using `sha512` the minimum is 1000, and 50000 is the recommended and default value.
#### salt_length
@ -123,7 +123,7 @@ and there is no documented reason why you'd set it to anything other than this,
<div markdown="1">
type: integer
{: .label .label-config .label-purple }
default: 8
default: 4
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
@ -134,13 +134,20 @@ which affects the effective cost of hashing.
#### memory
<div markdown="1">
type: integer
{: .label .label-config .label-purple }
default: 64
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
This setting is specific to `argon2id` and unused with `sha512`. Sets the amount of memory allocated to a single
password hashing action. This memory is released by go after the hashing process completes, however the operating system
may not reclaim it until it needs the memory which may make Authelia appear to be using more memory than it technically
is.
## Passwords
The file contains hashed passwords instead of plain text passwords for security reasons.
@ -149,13 +156,13 @@ You can use Authelia binary or docker image to generate the hash of any password
hash-password command has many tunable options, you can view them with the
`authelia hash-password --help` command. For example if you wanted to improve the entropy
you could generate a 16 byte salt and provide it with the `--salt` flag.
Example: `authelia hash-password --salt abcdefghijklhijl`. For argon2id the salt must
Example: `authelia hash-password --salt abcdefghijklhijl -- 'yourpassword'`. For argon2id the salt must
always be valid for base64 decoding (characters a through z, A through Z, 0 through 9, and +/).
Passwords passed to `hash-password` should be single quoted if using special characters to prevent parameter substitution.
For instance to generate a hash with the docker image just run:
$ docker run authelia/authelia:latest authelia hash-password 'yourpassword'
$ docker run authelia/authelia:latest authelia hash-password -- 'yourpassword'
Password hash: $argon2id$v=19$m=65536$3oc26byQuSkQqksq$zM1QiTvVPrMfV6BVLs2t4gM+af5IN7euO0VB6+Q8ZFs
You may also use the `--config` flag to point to your existing configuration. When used, the values defined in the config will be used instead.
@ -166,17 +173,18 @@ Full CLI Help Documentation:
Hash a password to be used in file-based users database. Default algorithm is argon2id.
Usage:
authelia hash-password [password] [flags]
authelia hash-password [flags] -- <password>
Flags:
-c, --config strings Configuration files
-h, --help help for hash-password
-i, --iterations int set the number of hashing iterations (default 1)
-i, --iterations int set the number of hashing iterations (default 3)
-k, --key-length int [argon2id] set the key length param (default 32)
-m, --memory int [argon2id] set the amount of memory param (in MB) (default 64)
-p, --parallelism int [argon2id] set the parallelism param (default 8)
-p, --parallelism int [argon2id] set the parallelism param (default 4)
-s, --salt string set the salt string
-l, --salt-length int set the auto-generated salt length (default 16)
-z, --sha512 use sha512 as the algorithm (defaults iterations to 50000, change with -i)
-z, --sha512 use sha512 as the algorithm (changes iterations to 50000, change with -i)
```
### Password hash algorithm
@ -209,7 +217,6 @@ If this is not desirable we recommend investigating the following options in ord
2. adjusting the [memory](#memory) parameter
3. changing the [algorithm](#algorithm)
### Password hash algorithm tuning
All algorithm tuning for Argon2id is supported. The only configuration variables that affect
@ -220,26 +227,21 @@ to cater for a reasonable system, if you're unsure about which settings to tune,
parameters below, or for a more in depth understanding see the referenced documentation in
[Argon2 links](./file.md#argon2-links).
#### Recommended Parameters: Argon2id
#### Examples for specific systems
These examples have been tested against a single system to make sure they roughly take
0.5 seconds each. Your results may vary depending on individual specification and
utilization, but they are a good guide to get started. You should however read the
linked documents in [Argon2 links](./file.md#argon2-links).
| System |Iterations|Parallelism|Memory |
|:------------: |:--------:|:---------:|:-----:|
|Raspberry Pi 2 | 1 | 8 | 64 |
|Raspberry Pi 3 | 1 | 8 | 128 |
|Raspberry Pi 4 | 1 | 8 | 128 |
|Intel G5 i5 NUC| 1 | 8 | 1024 |
This table is adapted from [RFC9106 Parameter Choice]:
| Situation | Iterations (t) | Parallelism (p) | Memory (m) | Salt Size | Key Size |
|:-----------:|:--------------:|:---------------:|:----------:|:---------:|:--------:|
| Low Memory | 3 | 4 | 64 | 16 | 32 |
| Recommended | 1 | 4 | 2048 | 16 | 32 |
## Argon2 Links
[How to choose the right parameters for Argon2](https://www.twelve21.io/how-to-choose-the-right-parameters-for-argon2/)
- [Go Documentation](https://godoc.org/golang.org/x/crypto/argon2)
- Argon2 Specification [RFC9106]
- [OWASP Password Storage Cheatsheet]
[Go Documentation](https://godoc.org/golang.org/x/crypto/argon2)
[IETF Draft](https://tools.ietf.org/id/draft-irtf-cfrg-argon2-09.html)
[RFC9106]: https://www.rfc-editor.org/rfc/rfc9106.html
[RFC9106 Parameter Choice]: https://www.rfc-editor.org/rfc/rfc9106.html#section-4
[OWASP Password Storage Cheatsheet]: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html

View File

@ -38,7 +38,7 @@ func TestShouldHashArgon2idPassword(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, argon2id, code)
assert.Equal(t, "BpLnfgDsc2WD8F2q", salt)
assert.Equal(t, "f+Y+KaS12gkNHN0Llc9kqDZuk1OYvoXj8t+5DcPbgY4", key)
assert.Equal(t, "kYempka60N8ETZ+EedP+Fn3z83mEPMl08RQEXTwY6u0", key)
assert.Equal(t, schema.DefaultCIPasswordConfiguration.Iterations, parameters.GetInt("t", HashingDefaultArgon2idTime))
assert.Equal(t, schema.DefaultCIPasswordConfiguration.Memory*1024, parameters.GetInt("m", HashingDefaultArgon2idMemory))
assert.Equal(t, schema.DefaultCIPasswordConfiguration.Parallelism, parameters.GetInt("p", HashingDefaultArgon2idParallelism))
@ -217,7 +217,7 @@ func TestShouldNotParseArgon2idHashWithWrongKeyLength(t *testing.T) {
}
func TestShouldParseArgon2idHash(t *testing.T) {
passwordHash, err := ParseHash("$argon2id$v=19$m=65536,t=1,p=8$NEwwcVNuQWlQMFpkMndxdg$LlHjiLxPB94pdmOiNwr7Bgy+uy3huSv6y9phCQ+mLls")
passwordHash, err := ParseHash("$argon2id$v=19$m=65536,t=3,p=4$NEwwcVNuQWlQMFpkMndxdg$LlHjiLxPB94pdmOiNwr7Bgy+uy3huSv6y9phCQ+mLls")
assert.NoError(t, err)
assert.Equal(t, schema.DefaultCIPasswordConfiguration.Iterations, passwordHash.Iterations)
assert.Equal(t, schema.DefaultCIPasswordConfiguration.Parallelism, passwordHash.Parallelism)

View File

@ -9,16 +9,16 @@ import (
"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/logging"
"github.com/authelia/authelia/v4/internal/configuration/validator"
)
// NewHashPasswordCmd returns a new Hash Password Cmd.
func NewHashPasswordCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "hash-password [password]",
Use: "hash-password [flags] -- <password>",
Short: "Hash a password to be used in file-based users database. Default algorithm is argon2id.",
Args: cobra.MinimumNArgs(1),
Run: cmdHashPasswordRun,
RunE: cmdHashPasswordRunE,
}
cmd.Flags().BoolP("sha512", "z", false, fmt.Sprintf("use sha512 as the algorithm (changes iterations to %d, change with -i)", schema.DefaultPasswordSHA512Configuration.Iterations))
@ -33,34 +33,44 @@ func NewHashPasswordCmd() (cmd *cobra.Command) {
return cmd
}
func cmdHashPasswordRun(cmd *cobra.Command, args []string) {
logger := logging.Logger()
sha512, _ := cmd.Flags().GetBool("sha512")
iterations, _ := cmd.Flags().GetInt("iterations")
func cmdHashPasswordRunE(cmd *cobra.Command, args []string) (err error) {
salt, _ := cmd.Flags().GetString("salt")
keyLength, _ := cmd.Flags().GetInt("key-length")
saltLength, _ := cmd.Flags().GetInt("salt-length")
memory, _ := cmd.Flags().GetInt("memory")
parallelism, _ := cmd.Flags().GetInt("parallelism")
sha512, _ := cmd.Flags().GetBool("sha512")
configs, _ := cmd.Flags().GetStringSlice("config")
if len(configs) > 0 {
mapDefaults := map[string]interface{}{
"authentication_backend.file.password.algorithm": schema.DefaultPasswordConfiguration.Algorithm,
"authentication_backend.file.password.iterations": schema.DefaultPasswordConfiguration.Iterations,
"authentication_backend.file.password.key_length": schema.DefaultPasswordConfiguration.KeyLength,
"authentication_backend.file.password.salt_length": schema.DefaultPasswordConfiguration.SaltLength,
"authentication_backend.file.password.parallelism": schema.DefaultPasswordConfiguration.Parallelism,
"authentication_backend.file.password.memory": schema.DefaultPasswordConfiguration.Memory,
}
if sha512 {
mapDefaults["authentication_backend.file.password.algorithm"] = schema.DefaultPasswordSHA512Configuration.Algorithm
mapDefaults["authentication_backend.file.password.iterations"] = schema.DefaultPasswordSHA512Configuration.Iterations
mapDefaults["authentication_backend.file.password.salt_length"] = schema.DefaultPasswordSHA512Configuration.SaltLength
}
mapCLI := map[string]string{
"iterations": "authentication_backend.file.password.iterations",
"key-length": "authentication_backend.file.password.key_length",
"salt-length": "authentication_backend.file.password.salt_length",
"parallelism": "authentication_backend.file.password.parallelism",
"memory": "authentication_backend.file.password.memory",
}
sources := configuration.NewDefaultSourcesWithDefaults(configs,
configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter,
configuration.NewMapSource(mapDefaults),
configuration.NewCommandLineSourceWithMapping(cmd.Flags(), mapCLI, false, false),
)
val := schema.NewStructValidator()
_, config, err := configuration.Load(val, configuration.NewDefaultSources(configs, configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter)...)
if err != nil {
logger.Fatalf("Error occurred loading configuration: %v", err)
}
if config.AuthenticationBackend.File != nil && config.AuthenticationBackend.File.Password != nil {
sha512 = config.AuthenticationBackend.File.Password.Algorithm == "sha512"
iterations = config.AuthenticationBackend.File.Password.Iterations
keyLength = config.AuthenticationBackend.File.Password.KeyLength
saltLength = config.AuthenticationBackend.File.Password.SaltLength
memory = config.AuthenticationBackend.File.Password.Memory
parallelism = config.AuthenticationBackend.File.Password.Parallelism
}
if _, config, err = configuration.Load(val, sources...); err != nil {
return fmt.Errorf("error occurred loading configuration: %w", err)
}
var (
@ -68,24 +78,41 @@ func cmdHashPasswordRun(cmd *cobra.Command, args []string) {
algorithm authentication.CryptAlgo
)
if sha512 {
if iterations == schema.DefaultPasswordConfiguration.Iterations {
iterations = schema.DefaultPasswordSHA512Configuration.Iterations
p := config.AuthenticationBackend.File.Password
switch p.Algorithm {
case "sha512":
algorithm = authentication.HashingAlgorithmSHA512
default:
algorithm = authentication.HashingAlgorithmArgon2id
}
algorithm = authentication.HashingAlgorithmSHA512
} else {
algorithm = authentication.HashingAlgorithmArgon2id
validator.ValidatePasswordConfiguration(p, 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 fmt.Errorf("errors occurred validating the password configuration: %w", err)
}
if salt != "" {
salt = crypt.Base64Encoding.EncodeToString([]byte(salt))
}
hash, err := authentication.HashPassword(args[0], salt, algorithm, iterations, memory*1024, parallelism, keyLength, saltLength)
if err != nil {
logging.Logger().Fatalf("Error occurred during hashing: %v\n", err)
if hash, err = authentication.HashPassword(args[0], salt, algorithm, p.Iterations, p.Memory*1024, p.Parallelism, p.KeyLength, p.SaltLength); err != nil {
return fmt.Errorf("error during password hashing: %w", err)
}
fmt.Printf("Password hash: %s\n", hash)
return nil
}

View File

@ -166,8 +166,8 @@ func ToTimeDurationHookFunc() mapstructure.DecodeHookFuncType {
}
}
// StringToRegexpFunc decodes a string into a *regexp.Regexp or regexp.Regexp.
func StringToRegexpFunc() mapstructure.DecodeHookFuncType {
// StringToRegexpHookFunc decodes a string into a *regexp.Regexp or regexp.Regexp.
func StringToRegexpHookFunc() mapstructure.DecodeHookFuncType {
return func(f reflect.Type, t reflect.Type, data interface{}) (value interface{}, err error) {
var ptr bool

View File

@ -585,7 +585,7 @@ func TestStringToRegexpFunc(t *testing.T) {
},
}
hook := configuration.StringToRegexpFunc()
hook := configuration.StringToRegexpHookFunc()
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
@ -698,7 +698,7 @@ func TestStringToRegexpFuncPointers(t *testing.T) {
},
}
hook := configuration.StringToRegexpFunc()
hook := configuration.StringToRegexpHookFunc()
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {

View File

@ -45,9 +45,9 @@ func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o inte
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToSliceHookFunc(","),
StringToMailAddressHookFunc(),
ToTimeDurationHookFunc(),
StringToURLHookFunc(),
StringToRegexpFunc(),
StringToRegexpHookFunc(),
ToTimeDurationHookFunc(),
),
Metadata: nil,
Result: o,

View File

@ -66,22 +66,22 @@ type PasswordResetAuthenticationBackendConfiguration struct {
// DefaultPasswordConfiguration represents the default configuration related to Argon2id hashing.
var DefaultPasswordConfiguration = PasswordConfiguration{
Iterations: 1,
Iterations: 3,
KeyLength: 32,
SaltLength: 16,
Algorithm: argon2id,
Memory: 64,
Parallelism: 8,
Parallelism: 4,
}
// DefaultCIPasswordConfiguration represents the default configuration related to Argon2id hashing for CI.
var DefaultCIPasswordConfiguration = PasswordConfiguration{
Iterations: 1,
Iterations: 3,
KeyLength: 32,
SaltLength: 16,
Algorithm: argon2id,
Memory: 64,
Parallelism: 8,
Parallelism: 4,
}
// DefaultPasswordSHA512Configuration represents the default configuration related to SHA512 hashing.

View File

@ -6,6 +6,7 @@ import (
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag"
@ -144,8 +145,31 @@ func (s *CommandLineSource) Load(_ *schema.StructValidator) (err error) {
return s.koanf.Load(posflag.Provider(s.flags, ".", s.koanf), nil)
}
// NewMapSource returns a new map[string]interface{} source.
func NewMapSource(m map[string]interface{}) (source *MapSource) {
return &MapSource{
m: m,
koanf: koanf.New(constDelimiter),
}
}
// Name of the Source.
func (s MapSource) Name() (name string) {
return "map"
}
// Merge the CommandLineSource koanf.Koanf into the provided one.
func (s *MapSource) Merge(ko *koanf.Koanf, val *schema.StructValidator) (err error) {
return ko.Merge(s.koanf)
}
// Load the Source into the YAMLFileSource koanf.Koanf.
func (s *MapSource) Load(_ *schema.StructValidator) (err error) {
return s.koanf.Load(confmap.Provider(s.m, constDelimiter), nil)
}
// NewDefaultSources returns a slice of Source configured to load from specified YAML files.
func NewDefaultSources(filePaths []string, prefix, delimiter string) (sources []Source) {
func NewDefaultSources(filePaths []string, prefix, delimiter string, additionalSources ...Source) (sources []Source) {
fileSources := NewYAMLFileSources(filePaths)
for _, source := range fileSources {
sources = append(sources, source)
@ -154,5 +178,18 @@ func NewDefaultSources(filePaths []string, prefix, delimiter string) (sources []
sources = append(sources, NewEnvironmentSource(prefix, delimiter))
sources = append(sources, NewSecretsSource(prefix, delimiter))
if len(additionalSources) != 0 {
sources = append(sources, additionalSources...)
}
return sources
}
// NewDefaultSourcesWithDefaults returns a slice of Source configured to load from specified YAML files with additional sources.
func NewDefaultSourcesWithDefaults(filePaths []string, prefix, delimiter string, defaults Source, additionalSources ...Source) (sources []Source) {
sources = []Source{defaults}
sources = append(sources, NewDefaultSources(filePaths, prefix, delimiter, additionalSources...)...)
return sources
}

View File

@ -40,3 +40,9 @@ type CommandLineSource struct {
flags *pflag.FlagSet
callback func(flag *pflag.Flag) (string, interface{})
}
// MapSource loads configuration from the command line flags.
type MapSource struct {
m map[string]interface{}
koanf *koanf.Koanf
}

View File

@ -53,63 +53,68 @@ func validateFileAuthenticationBackend(config *schema.FileAuthenticationBackendC
if config.Password == nil {
config.Password = &schema.DefaultPasswordConfiguration
} else {
// Salt Length.
switch {
case config.Password.SaltLength == 0:
config.Password.SaltLength = schema.DefaultPasswordConfiguration.SaltLength
case config.Password.SaltLength < 8:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordSaltLength, config.Password.SaltLength))
ValidatePasswordConfiguration(config.Password, validator)
}
}
switch config.Password.Algorithm {
// ValidatePasswordConfiguration validates the file auth backend password configuration.
func ValidatePasswordConfiguration(config *schema.PasswordConfiguration, validator *schema.StructValidator) {
// Salt Length.
switch {
case config.SaltLength == 0:
config.SaltLength = schema.DefaultPasswordConfiguration.SaltLength
case config.SaltLength < 8:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordSaltLength, config.SaltLength))
}
switch config.Algorithm {
case "":
config.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm
config.Algorithm = schema.DefaultPasswordConfiguration.Algorithm
fallthrough
case hashArgon2id:
validateFileAuthenticationBackendArgon2id(config, validator)
case hashSHA512:
validateFileAuthenticationBackendSHA512(config)
default:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordUnknownAlg, config.Password.Algorithm))
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordUnknownAlg, config.Algorithm))
}
if config.Password.Iterations < 1 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordInvalidIterations, config.Password.Iterations))
}
if config.Iterations < 1 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordInvalidIterations, config.Iterations))
}
}
func validateFileAuthenticationBackendSHA512(config *schema.FileAuthenticationBackendConfiguration) {
func validateFileAuthenticationBackendSHA512(config *schema.PasswordConfiguration) {
// Iterations (time).
if config.Password.Iterations == 0 {
config.Password.Iterations = schema.DefaultPasswordSHA512Configuration.Iterations
if config.Iterations == 0 {
config.Iterations = schema.DefaultPasswordSHA512Configuration.Iterations
}
}
func validateFileAuthenticationBackendArgon2id(config *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) {
func validateFileAuthenticationBackendArgon2id(config *schema.PasswordConfiguration, validator *schema.StructValidator) {
// Iterations (time).
if config.Password.Iterations == 0 {
config.Password.Iterations = schema.DefaultPasswordConfiguration.Iterations
if config.Iterations == 0 {
config.Iterations = schema.DefaultPasswordConfiguration.Iterations
}
// Parallelism.
if config.Password.Parallelism == 0 {
config.Password.Parallelism = schema.DefaultPasswordConfiguration.Parallelism
} else if config.Password.Parallelism < 1 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidParallelism, config.Password.Parallelism))
if config.Parallelism == 0 {
config.Parallelism = schema.DefaultPasswordConfiguration.Parallelism
} else if config.Parallelism < 1 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidParallelism, config.Parallelism))
}
// Memory.
if config.Password.Memory == 0 {
config.Password.Memory = schema.DefaultPasswordConfiguration.Memory
} else if config.Password.Memory < config.Password.Parallelism*8 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidMemory, config.Password.Parallelism, config.Password.Parallelism*8, config.Password.Memory))
if config.Memory == 0 {
config.Memory = schema.DefaultPasswordConfiguration.Memory
} else if config.Memory < config.Parallelism*8 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidMemory, config.Parallelism, config.Parallelism*8, config.Memory))
}
// Key Length.
if config.Password.KeyLength == 0 {
config.Password.KeyLength = schema.DefaultPasswordConfiguration.KeyLength
} else if config.Password.KeyLength < 16 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidKeyLength, config.Password.KeyLength))
if config.KeyLength == 0 {
config.KeyLength = schema.DefaultPasswordConfiguration.KeyLength
} else if config.KeyLength < 16 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidKeyLength, config.KeyLength))
}
}

View File

@ -85,7 +85,7 @@ func (s *CLISuite) TestShouldFailValidateConfig() {
func (s *CLISuite) TestShouldHashPasswordArgon2id() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-m", "32", "-s", "test1234"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Password hash: $argon2id$v=19$m=32768,t=1,p=8")
s.Assert().Contains(output, "Password hash: $argon2id$v=19$m=32768,t=3,p=4$")
}
func (s *CLISuite) TestShouldHashPasswordSHA512() {