[FEATURE] Support Argon2id password hasing and improved entropy (#679)

* [FEATURE] Support Argon2id Passwords

- Updated go module github.com/simia-tech/crypt
- Added Argon2id support for file based authentication backend
- Made it the default method
- Made it so backwards compatibility with SHA512 exists
- Force seeding of the random string generator used for salts to ensure they are all different
- Added command params to the authelia hash-password command
- Automatically remove {CRYPT} from hashes as they are updated
- Automatically change hashes when they are updated to the configured algorithm
- Made the hashing algorithm parameters completely configurable
- Added reasonably comprehensive test suites
- Updated docs
- Updated config template

* Adjust error output

* Fix unit test

* Add unit tests and argon2 version check

* Fix new unit tests

* Update docs, added tests

* Implement configurable values and more comprehensive testing

* Added cmd params to hash_password, updated docs, misc fixes

* More detailed error for cmd, fixed a typo

* Fixed cmd flag error, minor refactoring

* Requested Changes and Minor refactoring

* Increase entropy

* Update docs for entropy changes

* Refactor to reduce nesting and easier code maintenance

* Cleanup Errors (uniformity for the function call)

* Check salt length, fix docs

* Add Base64 string validation for argon2id

* Cleanup and Finalization
- Moved RandomString function from ./internal/authentication/password_hash.go to ./internal/utils/strings.go
- Added SplitStringToArrayOfStrings func that splits strings into an array with a fixed max string len
- Fixed an error in validator that would allow a zero salt length
- Added a test to verify the upstream crypt module supports our defined random salt chars
- Updated docs
- Removed unused "HashingAlgorithm" string type

* Update crypt go mod, support argon2id key length and major refactor

* Config Template Update, Final Tests

* Use schema defaults for hash-password cmd

* Iterations check

* Docs requested changes

* Test Coverage, suggested edits

* Wording edit

* Doc changes

* Default sanity changes

* Default sanity changes - docs

* CI Sanity changes

* Memory in MB
This commit is contained in:
James Elliott 2020-03-06 12:38:02 +11:00 committed by GitHub
parent 72a3f1e0d7
commit 26369fff3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1089 additions and 131 deletions

View File

@ -59,7 +59,7 @@ func startServer() {
var userProvider authentication.UserProvider var userProvider authentication.UserProvider
if config.AuthenticationBackend.File != nil { if config.AuthenticationBackend.File != nil {
userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File.Path) userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File)
} else if config.AuthenticationBackend.Ldap != nil { } else if config.AuthenticationBackend.Ldap != nil {
userProvider = authentication.NewLDAPUserProvider(*config.AuthenticationBackend.Ldap) userProvider = authentication.NewLDAPUserProvider(*config.AuthenticationBackend.Ldap)
} else { } else {
@ -120,7 +120,7 @@ func main() {
}, },
} }
rootCmd.AddCommand(versionCmd, commands.HashPasswordCmd, commands.MigrateCmd) rootCmd.AddCommand(versionCmd, commands.MigrateCmd, commands.HashPasswordCmd)
rootCmd.AddCommand(commands.CertificatesCmd) rootCmd.AddCommand(commands.CertificatesCmd)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -97,11 +97,21 @@ authentication_backend:
# which is updated when users reset their passwords. # which is updated when users reset their passwords.
# Therefore, this backend is meant to be used in a dev environment # Therefore, this backend is meant to be used in a dev environment
# and not in production since it prevents Authelia to be scaled to # and not in production since it prevents Authelia to be scaled to
# more than one instance. # more than one instance. The options under password_options have sane
# defaults, and as it has security implications it is highly recommended
# you leave the default values. Before considering changing these settings
# please read the docs page below:
# https://docs.authelia.com/configuration/authentication/file.html#password-hash-algorithm-tuning
# #
## file: ## file:
## path: ./users_database.yml ## path: ./users_database.yml
## password_options:
## algorithm: argon2id
## iterations: 1
## key_length: 32
## salt_length: 16
## memory: 1048576
## parallelism: 8
# Access Control # Access Control
# #
# Access control is a list of rules defining the authorizations applied for one # Access control is a list of rules defining the authorizations applied for one

View File

@ -18,59 +18,168 @@ file in the configuration file.
authentication_backend: authentication_backend:
file: file:
path: /var/lib/authelia/users.yml path: /var/lib/authelia/users.yml
password_hashing:
algorithm: argon2id
iterations: 1
salt_length: 16
parallelism: 8
memory: 1024
### Password hashing configuration settings
#### algorithm
- Value Type: String
- Possible Value: `argon2id` and `sha512`
- Recommended: `argon2id`
- What it Does: Changes the hashing algorithm
#### iterations
- Value Type: Int
- Possible Value: `1` or higher for argon2id and `1000` or higher for sha512
(will automatically be set to `1000` on lower settings)
- Recommended: `1` for the `argon2id` algorithm and `50000` for `sha512`
- What it Does: Adjusts the number of times we run the password through the hashing algorithm
#### key_length
- Value Type: Int
- Possible Value: `16` or higher.
- Recommended: `32` or higher.
- What it Does: Adjusts the length of the actual hash
#### salt_length
- Value Type: Int
- Possible Value: between `2` and `16`
- Recommended: `16`
- What it Does: Adjusts the length of the random salt we add to the password, there
is no reason not to set this to 16
#### parallelism
- Value Type: Int
- Possible Value: `1` or higher
- Recommended: `8` or twice your CPU cores
- What it Does: Sets the number of threads used for hashing
#### memory
- Value Type: Int
- Possible Value: at least `8` times the value of `parallelism`
- Recommended: `1024` (1GB) or as much RAM as you can afford to give to hashing
- What it Does: Sets the amount of RAM used in MB for hashing
#### 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
[How to choose the right parameters for Argon2].
| 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 |
## Format ## Format
The format of the users file is as follows.
The format of the file is as follows.
users: users:
john: john:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com email: john.doe@authelia.com
groups: groups:
- admins - admins
- dev - dev
harry: harry:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: harry.potter@authelia.com email: harry.potter@authelia.com
groups: [] groups: []
bob: bob:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: bob.dylan@authelia.com email: bob.dylan@authelia.com
groups: groups:
- dev - dev
james: james:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: james.dean@authelia.com email: james.dean@authelia.com
This file should be set with read/write permissions as it could be updated by users This file should be set with read/write permissions as it could be updated by users
resetting their passwords. resetting their passwords.
## Passwords ## Passwords
The file contains hash of passwords instead of plain text passwords for security reasons. The file contains hashed passwords instead of plain text passwords for security reasons.
You can use authelia binary or docker image to generate the hash of any password. You can use Authelia binary or docker image to generate the hash of any password. The
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
always be valid for base64 decoding (characters a through z, A through Z, 0 through 9, and +/).
For instance, with the docker image, just run 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
$6$rounds=50000$BpLnfgDsc2WD8F2q$be7OyobnQ8K09dyDiGjY.cULh4yDePMh6CUMpLwF4WHTJmLcPE2ijM2ZsqZL.hVAANojEfDu3sU9u9uD7AeBJ/ $ Password hash: $argon2id$v=19$m=65536$3oc26byQuSkQqksq$zM1QiTvVPrMfV6BVLs2t4gM+af5IN7euO0VB6+Q8ZFs
Full CLI Help Documentation:
## Password Hash Function ```
Hash a password to be used in file-based users database. Default algorithm is argon2id.
The only supported hash function is salted sha512 determined by the prefix `$6$` as described Usage:
in this [wiki](https://en.wikipedia.org/wiki/Crypt_(C)) page. authelia hash-password [password] [flags]
Although not the best hash function, Salted SHA512 is a decent algorithm given the number of rounds is big Flags:
enough. It's not the best because the difficulty to crack the hash does not on the performance of the machine. -h, --help help for hash-password
The best algorithm, [Argon2](https://en.wikipedia.org/wiki/Argon2) does though. It won the -i, --iterations int set the number of hashing iterations (default 1)
[Password Hashing Competition](https://en.wikipedia.org/wiki/Password_Hashing_Competition) in 2015 and is now -k, --key-length int [argon2id] set the key length param (default 32)
considered the best hashing function. There is an open [issue](https://github.com/authelia/authelia/issues/577) -m, --memory int [argon2id] set the amount of memory param (in MB) (default 1024)
to add support for this hashing function. -p, --parallelism int [argon2id] set the parallelism param (default 8)
-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)
```
## Password hash algorithm
The default hash algorithm is salted Argon2id version 19. Argon2id is currently considered
the best hashing algorithm, and in 2015 won the
[Password Hashing Competition](https://en.wikipedia.org/wiki/Password_Hashing_Competition).
It benefits from customizable parameters allowing the cost of computing a hash to scale
into the future which makes it harder to brute-force. Argon2id was implemented due to community
feedback as you can see in this closed [issue](https://github.com/authelia/authelia/issues/577).
Additionally SHA512 is supported for backwards compatibility and user choice. While it's a reasonable
hash function given high enough iterations, as hardware gets better it has a higher chance of being
brute-forced.
Hashes are identifiable as argon2id or SHA512 by their prefix of either `$argon2id$` and `$6$`
respectively, as described in this [wiki page](https://en.wikipedia.org/wiki/Crypt_(C)).
### Password hash algorithm tuning
All algorithm tuning is supported for Argon2id. The only configuration variables that affect
SHA512 are iterations and salt length. The configuration variables are unique to the file
authentication provider, thus they all exist in a key under the file authentication configuration
key called `password_hashing`. We have set what are considered as sane and recommended defaults
to cater for a reasonable system, if you're unsure about which settings to tune, please see the
parameters above, or for a more in depth understanding see the referenced documentation.
#### Argon2 Links
[How to choose the right parameters for Argon2]
[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)
[IETF Draft](https://tools.ietf.org/id/draft-irtf-cfrg-argon2-09.html)
[How to choose the right parameters for Argon2]: https://www.twelve21.io/how-to-choose-the-right-parameters-for-argon2/

View File

@ -13,7 +13,7 @@ can find the documentation of the configuration required for every supported
proxy. proxy.
If you are not aware of the workflow of an authentication request, reading this If you are not aware of the workflow of an authentication request, reading this
[documentation](./home/architecture) first is highly recommended. [documentation](../../home/architecture.md) first is highly recommended.
## How Authelia integrates with proxies? ## How Authelia integrates with proxies?
@ -22,8 +22,8 @@ Authelia takes authentication requests coming from the proxy and targeting the
`/api/verify` endpoint exposed by Authelia. Two pieces of information are required for `/api/verify` endpoint exposed by Authelia. Two pieces of information are required for
Authelia to be able to authenticate the user request: Authelia to be able to authenticate the user request:
* The session cookie or a `Proxy-Authorization` header (see [single factor authentication](./features/single-factor)). * The session cookie or a `Proxy-Authorization` header (see [single factor authentication](../../features/single-factor.md)).
* The target URL of the user request (used primarily for [access control](./features/access-control)). * The target URL of the user request (used primarily for [access control](../../features/access-control.md)).
The target URL can be provided using one of the following ways: The target URL can be provided using one of the following ways:
@ -45,4 +45,4 @@ login portal if not authenticated yet.
If no redirection parameter is provided, the response code is either 200 or 401. The If no redirection parameter is provided, the response code is either 200 or 401. The
redirection must then be handled by the proxy when an error is detected redirection must then be handled by the proxy when an error is detected
(see [nginx](./deployment/supported-proxies/nginx) example). (see [nginx](./nginx.md) example).

View File

@ -28,6 +28,23 @@ that the attacker must also require the certificate to retrieve the cookies.
Note that using [HSTS] has consequences. That's why you should read the blog Note that using [HSTS] has consequences. That's why you should read the blog
post nginx has written on [HSTS]. post nginx has written on [HSTS].
## Protections against password cracking (File authentication provider)
Authelia implements a variety of measures to prevent an attacker cracking passwords if they
somehow obtain the file used by the file authentication provider, this is unrelated to LDAP auth.
First and foremost Authelia only uses very secure hashing algorithms with sane and secure defaults.
The first and default hashing algorithm we use is Argon2id which is currently considered
the most secure hashing algorithm. We also support SHA512, which previously was the default.
Secondly Authelia uses salting with all hashing algorithms. These salts are generated with a random
string generator, which is seeded every time it's used by a cryptographically secure 1024bit prime number.
This ensures that even if an attacker obtains the file, each password has to be brute forced individually.
Lastly Authelia's implementation of Argon2id is highly tunable. You can tune the key length, salt
used, iterations (time), paralellism, and memory usage. To read more about this please read how to
[configure](../configuration/authentication/file.md) file authentication.
## Notifier security measures (SMTP) ## Notifier security measures (SMTP)
By default the SMTP Notifier implementation does not allow connections that are not secure. By default the SMTP Notifier implementation does not allow connections that are not secure.
@ -100,4 +117,4 @@ add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block"; add_header X-XSS-Protection "1; mode=block";
``` ```
[HSTS]: https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/ [HSTS]: https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/

2
go.mod
View File

@ -25,7 +25,7 @@ require (
github.com/onsi/gomega v1.7.1 // indirect github.com/onsi/gomega v1.7.1 // indirect
github.com/otiai10/copy v1.0.2 github.com/otiai10/copy v1.0.2
github.com/pquerna/otp v1.2.0 github.com/pquerna/otp v1.2.0
github.com/simia-tech/crypt v0.2.0 github.com/simia-tech/crypt v0.4.2
github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus v1.4.2
github.com/spf13/cobra v0.0.5 github.com/spf13/cobra v0.0.5
github.com/spf13/viper v1.6.2 github.com/spf13/viper v1.6.2

2
go.sum
View File

@ -187,6 +187,8 @@ github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500 h1:9Pi10H7E8E79/x2H
github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY= github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY=
github.com/simia-tech/crypt v0.2.0 h1:cU8qdqUYNuEFKSMq15yaB2aI1aC5vrn6dFOonT6Kg6o= github.com/simia-tech/crypt v0.2.0 h1:cU8qdqUYNuEFKSMq15yaB2aI1aC5vrn6dFOonT6Kg6o=
github.com/simia-tech/crypt v0.2.0/go.mod h1:DMwvjPTzsiHrjqHVW5HvIbF4vUUzMCYDKVLsPWmLdTo= github.com/simia-tech/crypt v0.2.0/go.mod h1:DMwvjPTzsiHrjqHVW5HvIbF4vUUzMCYDKVLsPWmLdTo=
github.com/simia-tech/crypt v0.4.2 h1:ZQFyCxgImhXpyxWNXEtBfAmV6T8dT1w481fpm8blQww=
github.com/simia-tech/crypt v0.4.2/go.mod h1:DMwvjPTzsiHrjqHVW5HvIbF4vUUzMCYDKVLsPWmLdTo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=

View File

@ -23,3 +23,22 @@ const (
// PossibleMethods is the set of all possible 2FA methods. // PossibleMethods is the set of all possible 2FA methods.
var PossibleMethods = []string{TOTP, U2F, Push} var PossibleMethods = []string{TOTP, U2F, Push}
const (
//Argon2id Hash Identifier
HashingAlgorithmArgon2id = "argon2id"
//SHA512 Hash Identifier
HashingAlgorithmSHA512 = "6"
)
// These are the default values from the upstream crypt module, we use them to for GetInt, and they need to be checked when updating github.com/simia-tech/crypt
const (
HashingDefaultArgon2idTime = 1
HashingDefaultArgon2idMemory = 32 * 1024
HashingDefaultArgon2idParallelism = 4
HashingDefaultArgon2idKeyLength = 32
HashingDefaultSHA512Iterations = 5000
)
// Valid Hashing runes
var HashingPossibleSaltCharacters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/")

View File

@ -1,21 +1,22 @@
package authentication package authentication
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"strings" "strings"
"sync" "sync"
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
"github.com/authelia/authelia/internal/configuration/schema"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// FileUserProvider is a provider reading details from a file. // FileUserProvider is a provider reading details from a file.
type FileUserProvider struct { type FileUserProvider struct {
path *string configuration *schema.FileAuthenticationBackendConfiguration
database *DatabaseModel database *DatabaseModel
lock *sync.Mutex lock *sync.Mutex
} }
// UserDetailsModel is the model of user details in the file database. // UserDetailsModel is the model of user details in the file database.
@ -31,8 +32,8 @@ type DatabaseModel struct {
} }
// NewFileUserProvider creates a new instance of FileUserProvider. // NewFileUserProvider creates a new instance of FileUserProvider.
func NewFileUserProvider(filepath string) *FileUserProvider { func NewFileUserProvider(configuration *schema.FileAuthenticationBackendConfiguration) *FileUserProvider {
database, err := readDatabase(filepath) database, err := readDatabase(configuration.Path)
if err != nil { if err != nil {
// Panic since the file does not exist when Authelia is starting. // Panic since the file does not exist when Authelia is starting.
panic(err.Error()) panic(err.Error())
@ -45,9 +46,9 @@ func NewFileUserProvider(filepath string) *FileUserProvider {
} }
return &FileUserProvider{ return &FileUserProvider{
path: &filepath, configuration: configuration,
database: database, database: database,
lock: &sync.Mutex{}, lock: &sync.Mutex{},
} }
} }
@ -114,9 +115,24 @@ func (p *FileUserProvider) UpdatePassword(username string, newPassword string) e
return fmt.Errorf("User '%s' does not exist in database", username) return fmt.Errorf("User '%s' does not exist in database", username)
} }
hash := HashPassword(newPassword, "") var algorithm string
details.HashedPassword = fmt.Sprintf("{CRYPT}%s", hash) if p.configuration.PasswordHashing.Algorithm == "argon2id" {
algorithm = HashingAlgorithmArgon2id
} else if p.configuration.PasswordHashing.Algorithm == "sha512" {
algorithm = HashingAlgorithmSHA512
} else {
return errors.New("Invalid algorithm in configuration. It should be `argon2id` or `sha512`")
}
hash, err := HashPassword(
newPassword, "", algorithm, p.configuration.PasswordHashing.Iterations,
p.configuration.PasswordHashing.Memory*1024, p.configuration.PasswordHashing.Parallelism,
p.configuration.PasswordHashing.KeyLength, p.configuration.PasswordHashing.SaltLength)
if err != nil {
return err
}
details.HashedPassword = hash
p.lock.Lock() p.lock.Lock()
p.database.Users[username] = details p.database.Users[username] = details
@ -125,7 +141,7 @@ func (p *FileUserProvider) UpdatePassword(username string, newPassword string) e
p.lock.Unlock() p.lock.Unlock()
return err return err
} }
err = ioutil.WriteFile(*p.path, b, 0644) err = ioutil.WriteFile(p.configuration.Path, b, 0644)
p.lock.Unlock() p.lock.Unlock()
return err return err
} }

View File

@ -4,8 +4,10 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"strings"
"testing" "testing"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -29,9 +31,11 @@ func WithDatabase(content []byte, f func(path string)) {
} }
} }
func TestShouldCheckUserPasswordIsCorrect(t *testing.T) { func TestShouldCheckUserArgon2idPasswordIsCorrect(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(UserDatabaseContent, func(path string) {
provider := NewFileUserProvider(path) config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "password") ok, err := provider.CheckUserPassword("john", "password")
assert.NoError(t, err) assert.NoError(t, err)
@ -39,9 +43,23 @@ func TestShouldCheckUserPasswordIsCorrect(t *testing.T) {
}) })
} }
func TestShouldCheckUserSHA512PasswordIsCorrect(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("harry", "password")
assert.NoError(t, err)
assert.True(t, ok)
})
}
func TestShouldCheckUserPasswordIsWrong(t *testing.T) { func TestShouldCheckUserPasswordIsWrong(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(UserDatabaseContent, func(path string) {
provider := NewFileUserProvider(path) config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "wrong_password") ok, err := provider.CheckUserPassword("john", "wrong_password")
assert.NoError(t, err) assert.NoError(t, err)
@ -51,7 +69,9 @@ func TestShouldCheckUserPasswordIsWrong(t *testing.T) {
func TestShouldCheckUserPasswordOfUnexistingUser(t *testing.T) { func TestShouldCheckUserPasswordOfUnexistingUser(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(UserDatabaseContent, func(path string) {
provider := NewFileUserProvider(path) config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
_, err := provider.CheckUserPassword("fake", "password") _, err := provider.CheckUserPassword("fake", "password")
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, "User 'fake' does not exist in database", err.Error()) assert.Equal(t, "User 'fake' does not exist in database", err.Error())
@ -60,7 +80,9 @@ func TestShouldCheckUserPasswordOfUnexistingUser(t *testing.T) {
func TestShouldRetrieveUserDetails(t *testing.T) { func TestShouldRetrieveUserDetails(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(UserDatabaseContent, func(path string) {
provider := NewFileUserProvider(path) config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
details, err := provider.GetDetails("john") details, err := provider.GetDetails("john")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, details.Emails, []string{"john.doe@authelia.com"}) assert.Equal(t, details.Emails, []string{"john.doe@authelia.com"})
@ -70,45 +92,125 @@ func TestShouldRetrieveUserDetails(t *testing.T) {
func TestShouldUpdatePassword(t *testing.T) { func TestShouldUpdatePassword(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(UserDatabaseContent, func(path string) {
provider := NewFileUserProvider(path) config := DefaultFileAuthenticationBackendConfiguration
err := provider.UpdatePassword("john", "newpassword") config.Path = path
provider := NewFileUserProvider(&config)
err := provider.UpdatePassword("harry", "newpassword")
assert.NoError(t, err) assert.NoError(t, err)
// Reset the provider to force a read from disk. // Reset the provider to force a read from disk.
provider = NewFileUserProvider(path) provider = NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "newpassword") ok, err := provider.CheckUserPassword("harry", "newpassword")
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, ok) assert.True(t, ok)
}) })
} }
// Checks both that the hashing algo changes and that it removes {CRYPT} from the start.
func TestShouldUpdatePasswordHashingAlgorithmToArgon2id(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].HashedPassword, "{CRYPT}$6$"))
err := provider.UpdatePassword("harry", "newpassword")
assert.NoError(t, err)
// Reset the provider to force a read from disk.
provider = NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("harry", "newpassword")
assert.NoError(t, err)
assert.True(t, ok)
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].HashedPassword, "$argon2id$"))
})
}
func TestShouldUpdatePasswordHashingAlgorithmToSHA512(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
config.PasswordHashing.Algorithm = "sha512"
config.PasswordHashing.Iterations = 50000
provider := NewFileUserProvider(&config)
assert.True(t, strings.HasPrefix(provider.database.Users["john"].HashedPassword, "{CRYPT}$argon2id$"))
err := provider.UpdatePassword("john", "newpassword")
assert.NoError(t, err)
// Reset the provider to force a read from disk.
provider = NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "newpassword")
assert.NoError(t, err)
assert.True(t, ok)
assert.True(t, strings.HasPrefix(provider.database.Users["john"].HashedPassword, "$6$"))
})
}
func TestShouldRaiseWhenLoadingMalformedDatabaseForFirstTime(t *testing.T) { func TestShouldRaiseWhenLoadingMalformedDatabaseForFirstTime(t *testing.T) {
WithDatabase(MalformedUserDatabaseContent, func(path string) { WithDatabase(MalformedUserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithValue(t, "Unable to parse database: yaml: line 4: mapping values are not allowed in this context", func() { assert.PanicsWithValue(t, "Unable to parse database: yaml: line 4: mapping values are not allowed in this context", func() {
NewFileUserProvider(path) NewFileUserProvider(&config)
}) })
}) })
} }
func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) { func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) {
WithDatabase(BadSchemaUserDatabaseContent, func(path string) { WithDatabase(BadSchemaUserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithValue(t, "Invalid schema of database: Users: non zero value required", func() { assert.PanicsWithValue(t, "Invalid schema of database: Users: non zero value required", func() {
NewFileUserProvider(path) NewFileUserProvider(&config)
}) })
}) })
} }
func TestShouldRaiseWhenLoadingDatabaseWithBadHashesForTheFirstTime(t *testing.T) { func TestShouldRaiseWhenLoadingDatabaseWithBadSHA512HashesForTheFirstTime(t *testing.T) {
WithDatabase(BadHashContent, func(path string) { WithDatabase(BadSHA512HashContent, func(path string) {
assert.PanicsWithValue(t, "Unable to parse hash of user john: Cannot match pattern 'rounds=<int>' to find the number of rounds", func() { config := DefaultFileAuthenticationBackendConfiguration
NewFileUserProvider(path) config.Path = path
assert.PanicsWithValue(t, "Unable to parse hash of user john: Hash key is not the last parameter, the hash is likely malformed ($6$rounds00000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/).", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSettingsForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashSettingsContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithValue(t, "Unable to parse hash of user john: Hash key is not the last parameter, the hash is likely malformed ($argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM).", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashKeyForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashKeyContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithValue(t, "Unable to parse hash of user john: Hash key contains invalid base64 characters.", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSaltForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashSaltContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithValue(t, "Unable to parse hash of user john: Salt contains invalid base64 characters.", func() {
NewFileUserProvider(&config)
}) })
}) })
} }
func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) { func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) {
WithDatabase(UserDatabaseWithouCryptContent, func(path string) { WithDatabase(UserDatabaseWithoutCryptContent, func(path string) {
provider := NewFileUserProvider(path) config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "password") ok, err := provider.CheckUserPassword("john", "password")
assert.NoError(t, err) assert.NoError(t, err)
@ -116,10 +218,24 @@ func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) {
}) })
} }
var (
DefaultFileAuthenticationBackendConfiguration = schema.FileAuthenticationBackendConfiguration{
Path: "",
PasswordHashing: &schema.PasswordHashingConfiguration{
Iterations: schema.DefaultCIPasswordOptionsConfiguration.Iterations,
KeyLength: schema.DefaultCIPasswordOptionsConfiguration.KeyLength,
SaltLength: schema.DefaultCIPasswordOptionsConfiguration.SaltLength,
Algorithm: schema.DefaultCIPasswordOptionsConfiguration.Algorithm,
Memory: schema.DefaultCIPasswordOptionsConfiguration.Memory,
Parallelism: schema.DefaultCIPasswordOptionsConfiguration.Parallelism,
},
}
)
var UserDatabaseContent = []byte(` var UserDatabaseContent = []byte(`
users: users:
john: john:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" password: "{CRYPT}$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com email: john.doe@authelia.com
groups: groups:
- admins - admins
@ -161,7 +277,7 @@ user:
- dev - dev
`) `)
var UserDatabaseWithouCryptContent = []byte(` var UserDatabaseWithoutCryptContent = []byte(`
users: users:
john: john:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
@ -174,7 +290,7 @@ users:
email: james.dean@authelia.com email: james.dean@authelia.com
`) `)
var BadHashContent = []byte(` var BadSHA512HashContent = []byte(`
users: users:
john: john:
password: "$6$rounds00000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" password: "$6$rounds00000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
@ -186,3 +302,35 @@ users:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: james.dean@authelia.com email: james.dean@authelia.com
`) `)
var BadArgon2idHashSettingsContent = []byte(`
users:
john:
password: "$argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com
groups:
- admins
- dev
james:
password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: james.dean@authelia.com
`)
var BadArgon2idHashKeyContent = []byte(`
users:
john:
password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$^^vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com
groups:
- admins
- dev
`)
var BadArgon2idHashSaltContent = []byte(`
users:
john:
password: "$argon2id$v=19$m=65536,t=3,p=2$^^LnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com
groups:
- admins
- dev
`)

View File

@ -3,87 +3,149 @@ package authentication
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"math/rand"
"strconv" "strconv"
"strings" "strings"
"github.com/authelia/authelia/internal/utils"
"github.com/simia-tech/crypt" "github.com/simia-tech/crypt"
) )
// PasswordHash represents all characteristics of a password hash. // PasswordHash represents all characteristics of a password hash.
// Authelia only supports salted SHA512 method, i.e., $6$ mode. // Authelia only supports salted SHA512 or salted argon2id method, i.e., $6$ mode or $argon2id$ mode.
type PasswordHash struct { type PasswordHash struct {
// The number of rounds. Algorithm string
Rounds int Iterations int
// The salt with a max size of 16 characters for SHA512. Salt string
Salt string Key string
// The password hash. KeyLength int
Hash string Memory int
Parallelism int
} }
// ParseHash extracts all characteristics of a hash given its string representation. // ParseHash extracts all characteristics of a hash given its string representation.
func ParseHash(hash string) (*PasswordHash, error) { func ParseHash(hash string) (passwordHash *PasswordHash, err error) {
parts := strings.Split(hash, "$") parts := strings.Split(hash, "$")
if len(parts) != 5 { // This error can be ignored as it's always nil
return nil, fmt.Errorf("Cannot parse the hash %s", hash) code, parameters, salt, key, _ := crypt.DecodeSettings(hash)
h := &PasswordHash{}
h.Salt = salt
h.Key = key
if h.Key != parts[len(parts)-1] {
return nil, fmt.Errorf("Hash key is not the last parameter, the hash is likely malformed (%s).", hash)
}
if h.Key == "" {
return nil, fmt.Errorf("Hash key contains no characters or the field length is invalid (%s)", hash)
} }
// Only supports salted sha 512. _, err = crypt.Base64Encoding.DecodeString(h.Salt)
if parts[1] != "6" {
return nil, fmt.Errorf("Authelia only supports salted SHA512 hashing ($6$), not $%s$", parts[1])
}
roundsKV := strings.Split(parts[2], "=")
if len(roundsKV) != 2 {
return nil, errors.New("Cannot match pattern 'rounds=<int>' to find the number of rounds")
}
rounds, err := strconv.ParseInt(roundsKV[1], 10, 0)
if err != nil { if err != nil {
return nil, fmt.Errorf("Cannot find the number of rounds from %s using pattern 'rounds=<int>'. Cause: %s", roundsKV[1], err.Error()) return nil, errors.New("Salt contains invalid base64 characters.")
} }
return &PasswordHash{ if code == HashingAlgorithmSHA512 {
Rounds: int(rounds), h.Iterations = parameters.GetInt("rounds", HashingDefaultSHA512Iterations)
Salt: parts[3], h.Algorithm = HashingAlgorithmSHA512
Hash: parts[4], if parameters["rounds"] != "" && parameters["rounds"] != strconv.Itoa(h.Iterations) {
}, nil return nil, fmt.Errorf("SHA512 iterations is not numeric (%s).", parameters["rounds"])
} }
} else if code == HashingAlgorithmArgon2id {
version := parameters.GetInt("v", 0)
if version < 19 {
if version == 0 {
return nil, fmt.Errorf("Argon2id version parameter not found (%s)", hash)
}
return nil, fmt.Errorf("Argon2id versions less than v19 are not supported (hash is version %d).", version)
} else if version > 19 {
return nil, fmt.Errorf("Argon2id versions greater than v19 are not supported (hash is version %d).", version)
}
h.Algorithm = HashingAlgorithmArgon2id
h.Memory = parameters.GetInt("m", HashingDefaultArgon2idMemory)
h.Iterations = parameters.GetInt("t", HashingDefaultArgon2idTime)
h.Parallelism = parameters.GetInt("p", HashingDefaultArgon2idParallelism)
h.KeyLength = parameters.GetInt("k", HashingDefaultArgon2idKeyLength)
// The set of letters RandomString can pick in. decodedKey, err := crypt.Base64Encoding.DecodeString(h.Key)
var possibleLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") if err != nil {
return nil, errors.New("Hash key contains invalid base64 characters.")
// RandomString generate a random string of n characters. }
func RandomString(n int) string { if len(decodedKey) != h.KeyLength {
b := make([]rune, n) return nil, fmt.Errorf("Argon2id key length parameter (%d) does not match the actual key length (%d).", h.KeyLength, len(decodedKey))
for i := range b { }
b[i] = possibleLetters[rand.Intn(len(possibleLetters))] } else {
return nil, fmt.Errorf("Authelia only supports salted SHA512 hashing ($6$) and salted argon2id ($argon2id$), not $%s$", code)
} }
return string(b) return h, nil
} }
// HashPassword generate a salt and hash the password with the salt and a constant // HashPassword generate a salt and hash the password with the salt and a constant
// number of rounds. // number of rounds.
func HashPassword(password string, salt string) string { func HashPassword(password, salt, algorithm string, iterations, memory, parallelism, keyLength, saltLength int) (hash string, err error) {
var settings string
if algorithm != HashingAlgorithmArgon2id && algorithm != HashingAlgorithmSHA512 {
return "", fmt.Errorf("Hashing algorithm input of '%s' is invalid, only values of %s and %s are supported.", algorithm, HashingAlgorithmArgon2id, HashingAlgorithmSHA512)
}
if salt == "" { if salt == "" {
salt = fmt.Sprintf("$6$rounds=50000$%s", RandomString(16)) if saltLength < 2 {
return "", fmt.Errorf("Salt length input of %d is invalid, it must be 2 or higher.", saltLength)
} else if saltLength > 16 {
return "", fmt.Errorf("Salt length input of %d is invalid, it must be 16 or lower.", saltLength)
}
} else if len(salt) > 16 {
return "", fmt.Errorf("Salt input of %s is invalid (%d characters), it must be 16 or fewer characters.", salt, len(salt))
} else if len(salt) < 2 {
return "", fmt.Errorf("Salt input of %s is invalid (%d characters), it must be 2 or more characters.", salt, len(salt))
} else if _, err = crypt.Base64Encoding.DecodeString(salt); err != nil {
return "", fmt.Errorf("Salt input of %s is invalid, only characters [a-zA-Z0-9+/] are valid for input.", salt)
} }
hash, err := crypt.Crypt(password, salt)
if err != nil { if algorithm == HashingAlgorithmArgon2id {
log.Fatal(err) // Caution: Increasing any of the values in the below block has a high chance in old passwords that cannot be verified.
if memory < 8 {
return "", fmt.Errorf("Memory (argon2id) input of %d is invalid, it must be 8 or higher.", memory)
}
if parallelism < 1 {
return "", fmt.Errorf("Parallelism (argon2id) input of %d is invalid, it must be 1 or higher.", parallelism)
}
if memory < parallelism*8 {
return "", fmt.Errorf("Memory (argon2id) input of %d is invalid with a paraellelism input of %d, it must be %d (parallelism * 8) or higher.", memory, parallelism, parallelism*8)
}
if keyLength < 16 {
return "", fmt.Errorf("Key length (argon2id) input of %d is invalid, it must be 16 or higher.", keyLength)
}
if iterations < 1 {
return "", fmt.Errorf("Iterations (argon2id) input of %d is invalid, it must be 1 or more.", iterations)
}
// Caution: Increasing any of the values in the above block has a high chance in old passwords that cannot be verified.
} }
return hash
if salt == "" {
salt = utils.RandomString(saltLength, HashingPossibleSaltCharacters)
}
if algorithm == HashingAlgorithmArgon2id {
settings, _ = crypt.Argon2idSettings(memory, iterations, parallelism, keyLength, salt)
} else if algorithm == HashingAlgorithmSHA512 {
settings = fmt.Sprintf("$6$rounds=%d$%s", iterations, salt)
}
// This error can be ignored because we check for it before a user gets here
hash, _ = crypt.Crypt(password, settings)
return hash, nil
} }
// CheckPassword check a password against a hash. // CheckPassword check a password against a hash.
func CheckPassword(password string, hash string) (bool, error) { func CheckPassword(password, hash string) (ok bool, err error) {
passwordHash, err := ParseHash(hash) passwordHash, err := ParseHash(hash)
if err != nil { if err != nil {
return false, err return false, err
} }
salt := fmt.Sprintf("$6$rounds=%d$%s$", passwordHash.Rounds, passwordHash.Salt) expectedHash, err := HashPassword(password, passwordHash.Salt, passwordHash.Algorithm, passwordHash.Iterations, passwordHash.Memory, passwordHash.Parallelism, passwordHash.KeyLength, len(passwordHash.Salt))
pHash := HashPassword(password, salt) if err != nil {
return pHash == hash, nil return false, err
}
return hash == expectedHash, nil
} }

View File

@ -1,55 +1,323 @@
package authentication package authentication
import ( import (
"fmt"
"testing" "testing"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/utils"
"github.com/simia-tech/crypt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestShouldHashPassword(t *testing.T) { func TestShouldHashSHA512Password(t *testing.T) {
hash := HashPassword("password", "$6$rounds=50000$aFr56HjK3DrB8t3S") hash, err := HashPassword("password", "aFr56HjK3DrB8t3S", HashingAlgorithmSHA512, 50000, 0, 0, 0, 16)
assert.Equal(t, "$6$rounds=50000$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1", hash)
assert.NoError(t, err)
code, parameters, salt, hash, err := crypt.DecodeSettings(hash)
assert.Equal(t, "6", code)
assert.Equal(t, "aFr56HjK3DrB8t3S", salt)
assert.Equal(t, "zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1", hash)
assert.Equal(t, schema.DefaultPasswordOptionsSHA512Configuration.Iterations, parameters.GetInt("rounds", HashingDefaultSHA512Iterations))
} }
func TestShouldCheckPassword(t *testing.T) { func TestShouldHashArgon2idPassword(t *testing.T) {
ok, err := CheckPassword("password", "$6$rounds=50000$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1") hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordOptionsConfiguration.Iterations, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024,
schema.DefaultCIPasswordOptionsConfiguration.Parallelism, schema.DefaultCIPasswordOptionsConfiguration.KeyLength,
schema.DefaultCIPasswordOptionsConfiguration.SaltLength)
assert.NoError(t, err) assert.NoError(t, err)
code, parameters, salt, key, err := crypt.DecodeSettings(hash)
assert.NoError(t, err)
assert.Equal(t, "argon2id", code)
assert.Equal(t, "BpLnfgDsc2WD8F2q", salt)
assert.Equal(t, "O126GHPeZ5fwj7OLSs7PndXsTbje76R+QW9/EGfhkJg", key)
assert.Equal(t, schema.DefaultCIPasswordOptionsConfiguration.Iterations, parameters.GetInt("t", HashingDefaultArgon2idTime))
assert.Equal(t, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024, parameters.GetInt("m", HashingDefaultArgon2idMemory))
assert.Equal(t, schema.DefaultCIPasswordOptionsConfiguration.Parallelism, parameters.GetInt("p", HashingDefaultArgon2idParallelism))
assert.Equal(t, schema.DefaultCIPasswordOptionsConfiguration.KeyLength, parameters.GetInt("k", HashingDefaultArgon2idKeyLength))
}
// This checks the method of hashing (for argon2id) supports all the characters we allow in Authelia's hash function
func TestArgon2idHashSaltValidValues(t *testing.T) {
data := string(HashingPossibleSaltCharacters)
datas := utils.SliceString(data, 16)
var hash string
var err error
for _, salt := range datas {
hash, err = HashPassword("password", salt, HashingAlgorithmArgon2id, 1, 8, 1, 32, 16)
assert.NoError(t, err)
assert.Equal(t, fmt.Sprintf("$argon2id$v=19$m=8,p=1$%s$", salt), hash[0:40])
}
}
// This checks the method of hashing (for sha512) supports all the characters we allow in Authelia's hash function
func TestSHA512HashSaltValidValues(t *testing.T) {
data := string(HashingPossibleSaltCharacters)
datas := utils.SliceString(data, 16)
var hash string
var err error
for _, salt := range datas {
hash, err = HashPassword("password", salt, HashingAlgorithmSHA512, 1000, 0, 0, 0, 16)
assert.NoError(t, err)
assert.Equal(t, fmt.Sprintf("$6$rounds=1000$%s$", salt), hash[0:32])
}
}
func TestShouldNotHashPasswordWithNonExistentAlgorithm(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", "bogus",
schema.DefaultCIPasswordOptionsConfiguration.Iterations, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024,
schema.DefaultCIPasswordOptionsConfiguration.Parallelism, schema.DefaultCIPasswordOptionsConfiguration.KeyLength,
schema.DefaultCIPasswordOptionsConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Hashing algorithm input of 'bogus' is invalid, only values of argon2id and 6 are supported.")
}
func TestShouldNotHashArgon2idPasswordDueToMemoryParallelismMismatch(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordOptionsConfiguration.Iterations, 8, 2,
schema.DefaultCIPasswordOptionsConfiguration.KeyLength, schema.DefaultCIPasswordOptionsConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Memory (argon2id) input of 8 is invalid with a paraellelism input of 2, it must be 16 (parallelism * 8) or higher.")
}
func TestShouldNotHashArgon2idPasswordDueToMemoryLessThanEight(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordOptionsConfiguration.Iterations, 1, schema.DefaultCIPasswordOptionsConfiguration.Parallelism,
schema.DefaultCIPasswordOptionsConfiguration.KeyLength, schema.DefaultCIPasswordOptionsConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Memory (argon2id) input of 1 is invalid, it must be 8 or higher.")
}
func TestShouldNotHashArgon2idPasswordDueToKeyLengthLessThanSixteen(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordOptionsConfiguration.Iterations, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024,
schema.DefaultCIPasswordOptionsConfiguration.Parallelism, 5, schema.DefaultCIPasswordOptionsConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Key length (argon2id) input of 5 is invalid, it must be 16 or higher.")
}
func TestShouldNotHashArgon2idPasswordDueParallelismLessThanOne(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordOptionsConfiguration.Iterations, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024, -1,
schema.DefaultCIPasswordOptionsConfiguration.KeyLength, schema.DefaultCIPasswordOptionsConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Parallelism (argon2id) input of -1 is invalid, it must be 1 or higher.")
}
func TestShouldNotHashArgon2idPasswordDueIterationsLessThanOne(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
0, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024, schema.DefaultCIPasswordOptionsConfiguration.Parallelism,
schema.DefaultCIPasswordOptionsConfiguration.KeyLength, schema.DefaultCIPasswordOptionsConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Iterations (argon2id) input of 0 is invalid, it must be 1 or more.")
}
func TestShouldNotHashPasswordDueToSaltLength(t *testing.T) {
hash, err := HashPassword("password", "", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordOptionsConfiguration.Iterations, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024,
schema.DefaultCIPasswordOptionsConfiguration.Parallelism, schema.DefaultCIPasswordOptionsConfiguration.KeyLength, 0)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Salt length input of 0 is invalid, it must be 2 or higher.")
hash, err = HashPassword("password", "", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordOptionsConfiguration.Iterations, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024,
schema.DefaultCIPasswordOptionsConfiguration.Parallelism, schema.DefaultCIPasswordOptionsConfiguration.KeyLength, 20)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Salt length input of 20 is invalid, it must be 16 or lower.")
}
func TestShouldNotHashPasswordDueToSaltCharLengthTooLong(t *testing.T) {
hash, err := HashPassword("password", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordOptionsConfiguration.Iterations, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024,
schema.DefaultCIPasswordOptionsConfiguration.Parallelism, schema.DefaultCIPasswordOptionsConfiguration.KeyLength,
schema.DefaultCIPasswordOptionsConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Salt input of abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 is invalid (62 characters), it must be 16 or fewer characters.")
}
func TestShouldNotHashPasswordDueToSaltCharLengthTooShort(t *testing.T) {
hash, err := HashPassword("password", "a", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordOptionsConfiguration.Iterations, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024,
schema.DefaultCIPasswordOptionsConfiguration.Parallelism, schema.DefaultCIPasswordOptionsConfiguration.KeyLength,
schema.DefaultCIPasswordOptionsConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Salt input of a is invalid (1 characters), it must be 2 or more characters.")
}
func TestShouldNotHashPasswordWithNonBase64CharsInSalt(t *testing.T) {
hash, err := HashPassword("password", "abc&123", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordOptionsConfiguration.Iterations, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024,
schema.DefaultCIPasswordOptionsConfiguration.Parallelism, schema.DefaultCIPasswordOptionsConfiguration.KeyLength,
schema.DefaultCIPasswordOptionsConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Salt input of abc&123 is invalid, only characters [a-zA-Z0-9+/] are valid for input.")
}
func TestShouldNotParseHashWithNoneBase64CharsInKey(t *testing.T) {
passwordHash, err := ParseHash("$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$^^vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.EqualError(t, err, "Hash key contains invalid base64 characters.")
assert.Nil(t, passwordHash)
}
func TestShouldNotParseHashWithNoneBase64CharsInSalt(t *testing.T) {
passwordHash, err := ParseHash("$argon2id$v=19$m=65536$^^wTFoFjITudo57a$Z4NH/EKkdv6PJ01Ye1twJ61fsmRJujZZn1IXdUOyrJY")
assert.EqualError(t, err, "Salt contains invalid base64 characters.")
assert.Nil(t, passwordHash)
}
func TestShouldNotParseWithMalformedHash(t *testing.T) {
hashExtraField := "$argon2id$v=19$m=65536,t=3,p=2$abc$BpLnfgDsc2WD8F2q$^^vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
hashMissingSaltAndParams := "$argon2id$v=1$2t9X8nNCN2n3/kFYJ3xWNBg5k/rO782Qr7JJoJIK7G4"
hashMissingSalt := "$argon2id$v=1$m=65536,t=3,p=2$2t9X8nNCN2n3/kFYJ3xWNBg5k/rO782Qr7JJoJIK7G4"
passwordHash, err := ParseHash(hashExtraField)
assert.EqualError(t, err, fmt.Sprintf("Hash key is not the last parameter, the hash is likely malformed (%s).", hashExtraField))
assert.Nil(t, passwordHash)
passwordHash, err = ParseHash(hashMissingSaltAndParams)
assert.EqualError(t, err, fmt.Sprintf("Hash key is not the last parameter, the hash is likely malformed (%s).", hashMissingSaltAndParams))
assert.Nil(t, passwordHash)
passwordHash, err = ParseHash(hashMissingSalt)
assert.EqualError(t, err, fmt.Sprintf("Hash key is not the last parameter, the hash is likely malformed (%s).", hashMissingSalt))
assert.Nil(t, passwordHash)
}
func TestShouldNotParseHashWithEmptyKey(t *testing.T) {
hash := "$argon2id$v=19$m=65536$fvwTFoFjITudo57a$"
passwordHash, err := ParseHash(hash)
assert.EqualError(t, err, fmt.Sprintf("Hash key contains no characters or the field length is invalid (%s)", hash))
assert.Nil(t, passwordHash)
}
func TestShouldNotParseArgon2idHashWithEmptyVersion(t *testing.T) {
hash := "$argon2id$m=65536$fvwTFoFjITudo57a$Z4NH/EKkdv6PJ01Ye1twJ61fsmRJujZZn1IXdUOyrJY"
passwordHash, err := ParseHash(hash)
assert.EqualError(t, err, fmt.Sprintf("Argon2id version parameter not found (%s)", hash))
assert.Nil(t, passwordHash)
}
func TestShouldNotParseArgon2idHashWithWrongKeyLength(t *testing.T) {
hash := "$argon2id$v=19$m=65536,k=50$fvwTFoFjITudo57a$Z4NH/EKkdv6PJ01Ye1twJ61fsmRJujZZn1IXdUOyrJY"
passwordHash, err := ParseHash(hash)
assert.EqualError(t, err, "Argon2id key length parameter (50) does not match the actual key length (32).")
assert.Nil(t, passwordHash)
}
func TestShouldParseArgon2idHash(t *testing.T) {
passwordHash, err := ParseHash("$argon2id$v=19$m=131072,t=1,p=8$BpLnfgDsc2WD8F2q$G4fD5nJwXHDMS+u0eEMKvU0LF23jxbSmJSxhSLTteHE")
assert.NoError(t, err)
assert.Equal(t, schema.DefaultCIPasswordOptionsConfiguration.Iterations, passwordHash.Iterations)
assert.Equal(t, schema.DefaultCIPasswordOptionsConfiguration.Parallelism, passwordHash.Parallelism)
assert.Equal(t, schema.DefaultCIPasswordOptionsConfiguration.KeyLength, passwordHash.KeyLength)
assert.Equal(t, schema.DefaultCIPasswordOptionsConfiguration.Memory*1024, passwordHash.Memory)
}
func TestShouldCheckSHA512Password(t *testing.T) {
ok, err := CheckPassword("password", "$6$rounds=50000$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1")
assert.NoError(t, err)
assert.True(t, ok) assert.True(t, ok)
} }
func TestCannotParseHash(t *testing.T) { func TestShouldCheckArgon2idPassword(t *testing.T) {
ok, err := CheckPassword("password", "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.NoError(t, err)
assert.True(t, ok)
}
func TestCannotParseSHA512Hash(t *testing.T) {
ok, err := CheckPassword("password", "$6$roSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1") ok, err := CheckPassword("password", "$6$roSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1")
assert.EqualError(t, err, "Cannot parse the hash $6$roSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1") assert.EqualError(t, err, "Hash key is not the last parameter, the hash is likely malformed ($6$roSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1).")
assert.False(t, ok) assert.False(t, ok)
} }
func TestOnlySupportSHA512(t *testing.T) { func TestCannotParseArgon2idHash(t *testing.T) {
ok, err := CheckPassword("password", "$argon2id$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.EqualError(t, err, "Hash key is not the last parameter, the hash is likely malformed ($argon2id$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM).")
assert.False(t, ok)
}
func TestOnlySupportSHA512AndArgon2id(t *testing.T) {
ok, err := CheckPassword("password", "$8$rounds=50000$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1") ok, err := CheckPassword("password", "$8$rounds=50000$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1")
assert.EqualError(t, err, "Authelia only supports salted SHA512 hashing ($6$), not $8$") assert.EqualError(t, err, "Authelia only supports salted SHA512 hashing ($6$) and salted argon2id ($argon2id$), not $8$")
assert.False(t, ok) assert.False(t, ok)
} }
func TestCannotFindNumberOfRounds(t *testing.T) { func TestCannotFindNumberOfRounds(t *testing.T) {
ok, err := CheckPassword("password", "$6$rounds50000$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1") hash := "$6$rounds50000$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1"
ok, err := CheckPassword("password", hash)
assert.EqualError(t, err, "Cannot match pattern 'rounds=<int>' to find the number of rounds") assert.EqualError(t, err, fmt.Sprintf("Hash key is not the last parameter, the hash is likely malformed (%s).", hash))
assert.False(t, ok)
}
func TestCannotMatchArgon2idParamPattern(t *testing.T) {
ok, err := CheckPassword("password", "$argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.EqualError(t, err, "Hash key is not the last parameter, the hash is likely malformed ($argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM).")
assert.False(t, ok)
}
func TestArgon2idVersionLessThanSupported(t *testing.T) {
ok, err := CheckPassword("password", "$argon2id$v=18$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.EqualError(t, err, "Argon2id versions less than v19 are not supported (hash is version 18).")
assert.False(t, ok)
}
func TestArgon2idVersionGreaterThanSupported(t *testing.T) {
ok, err := CheckPassword("password", "$argon2id$v=20$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.EqualError(t, err, "Argon2id versions greater than v19 are not supported (hash is version 20).")
assert.False(t, ok) assert.False(t, ok)
} }
func TestNumberOfRoundsNotInt(t *testing.T) { func TestNumberOfRoundsNotInt(t *testing.T) {
ok, err := CheckPassword("password", "$6$rounds=abc$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1") ok, err := CheckPassword("password", "$6$rounds=abc$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1")
assert.EqualError(t, err, "Cannot find the number of rounds from abc using pattern 'rounds=<int>'. Cause: strconv.ParseInt: parsing \"abc\": invalid syntax") assert.EqualError(t, err, "SHA512 iterations is not numeric (abc).")
assert.False(t, ok) assert.False(t, ok)
} }
func TestShouldCheckPasswordHashedWithAuthelia(t *testing.T) { func TestShouldCheckPasswordArgon2idHashedWithAuthelia(t *testing.T) {
password := "my;secure*password" password := "my;secure*password"
hash := HashPassword(password, "") hash, err := HashPassword(password, "", HashingAlgorithmArgon2id, schema.DefaultCIPasswordOptionsConfiguration.Iterations,
schema.DefaultCIPasswordOptionsConfiguration.Memory*1024, schema.DefaultCIPasswordOptionsConfiguration.Parallelism,
schema.DefaultCIPasswordOptionsConfiguration.KeyLength, schema.DefaultCIPasswordOptionsConfiguration.SaltLength)
assert.NoError(t, err)
equal, err := CheckPassword(password, hash)
require.NoError(t, err)
assert.True(t, equal)
}
func TestShouldCheckPasswordSHA512HashedWithAuthelia(t *testing.T) {
password := "my;secure*password"
hash, err := HashPassword(password, "", HashingAlgorithmSHA512, schema.DefaultPasswordOptionsSHA512Configuration.Iterations,
0, 0, 0, schema.DefaultPasswordOptionsSHA512Configuration.SaltLength)
assert.NoError(t, err)
equal, err := CheckPassword(password, hash) equal, err := CheckPassword(password, hash)
require.NoError(t, err) require.NoError(t, err)

View File

@ -4,14 +4,51 @@ import (
"fmt" "fmt"
"github.com/authelia/authelia/internal/authentication" "github.com/authelia/authelia/internal/authentication"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() {
HashPasswordCmd.Flags().BoolP("sha512", "z", false, fmt.Sprintf("use sha512 as the algorithm (changes iterations to %d, change with -i)", schema.DefaultPasswordOptionsSHA512Configuration.Iterations))
HashPasswordCmd.Flags().IntP("iterations", "i", schema.DefaultPasswordOptionsConfiguration.Iterations, "set the number of hashing iterations")
HashPasswordCmd.Flags().StringP("salt", "s", "", "set the salt string")
HashPasswordCmd.Flags().IntP("memory", "m", schema.DefaultPasswordOptionsConfiguration.Memory, "[argon2id] set the amount of memory param (in MB)")
HashPasswordCmd.Flags().IntP("parallelism", "p", schema.DefaultPasswordOptionsConfiguration.Parallelism, "[argon2id] set the parallelism param")
HashPasswordCmd.Flags().IntP("key-length", "k", schema.DefaultPasswordOptionsConfiguration.KeyLength, "[argon2id] set the key length param")
HashPasswordCmd.Flags().IntP("salt-length", "l", schema.DefaultPasswordOptionsConfiguration.SaltLength, "set the auto-generated salt length")
}
var HashPasswordCmd = &cobra.Command{ var HashPasswordCmd = &cobra.Command{
Use: "hash-password [password]", Use: "hash-password [password]",
Short: "Hash a password to be used in file-based users database", Short: "Hash a password to be used in file-based users database. Default algorithm is argon2id.",
Run: func(cobraCmd *cobra.Command, args []string) { Run: func(cobraCmd *cobra.Command, args []string) {
fmt.Println(authentication.HashPassword(args[0], "")) sha512, _ := cobraCmd.Flags().GetBool("sha512")
iterations, _ := cobraCmd.Flags().GetInt("iterations")
salt, _ := cobraCmd.Flags().GetString("salt")
keyLength, _ := cobraCmd.Flags().GetInt("key-length")
saltLength, _ := cobraCmd.Flags().GetInt("salt-length")
memory, _ := cobraCmd.Flags().GetInt("memory")
parallelism, _ := cobraCmd.Flags().GetInt("parallelism")
var err error
var hash string
var algorithm string
if sha512 {
if iterations == schema.DefaultPasswordOptionsConfiguration.Iterations {
iterations = schema.DefaultPasswordOptionsSHA512Configuration.Iterations
}
algorithm = authentication.HashingAlgorithmSHA512
} else {
algorithm = authentication.HashingAlgorithmArgon2id
}
hash, err = authentication.HashPassword(args[0], salt, algorithm, iterations, memory*1024, parallelism, keyLength, saltLength)
if err != nil {
fmt.Println(fmt.Sprintf("Error occured during hashing: %s", err))
} else {
fmt.Println(fmt.Sprintf("Password hash: %s", hash))
}
}, },
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
} }

View File

@ -17,7 +17,44 @@ type LDAPAuthenticationBackendConfiguration struct {
// FileAuthenticationBackendConfiguration represents the configuration related to file-based backend // FileAuthenticationBackendConfiguration represents the configuration related to file-based backend
type FileAuthenticationBackendConfiguration struct { type FileAuthenticationBackendConfiguration struct {
Path string `mapstructure:"path"` Path string `mapstructure:"path"`
PasswordHashing *PasswordHashingConfiguration `mapstructure:"password"`
}
type PasswordHashingConfiguration struct {
Iterations int `mapstructure:"iterations"`
KeyLength int `mapstructure:"key_length"`
SaltLength int `mapstructure:"salt_length"`
Algorithm string `mapstrucutre:"algorithm"`
Memory int `mapstructure:"memory"`
Parallelism int `mapstructure:"parallelism"`
}
// Default Argon2id Configuration
var DefaultPasswordOptionsConfiguration = PasswordHashingConfiguration{
Iterations: 1,
KeyLength: 32,
SaltLength: 16,
Algorithm: "argon2id",
Memory: 1024,
Parallelism: 8,
}
// Default Argon2id Configuration for CI testing when calling HashPassword()
var DefaultCIPasswordOptionsConfiguration = PasswordHashingConfiguration{
Iterations: 1,
KeyLength: 32,
SaltLength: 16,
Algorithm: "argon2id",
Memory: 128,
Parallelism: 8,
}
// Default SHA512 Cofniguration
var DefaultPasswordOptionsSHA512Configuration = PasswordHashingConfiguration{
Iterations: 50000,
SaltLength: 16,
Algorithm: "sha512",
} }
// AuthenticationBackendConfiguration represents the configuration related to the authentication backend. // AuthenticationBackendConfiguration represents the configuration related to the authentication backend.

View File

@ -16,6 +16,63 @@ func validateFileAuthenticationBackend(configuration *schema.FileAuthenticationB
if configuration.Path == "" { if configuration.Path == "" {
validator.Push(errors.New("Please provide a `path` for the users database in `authentication_backend`")) validator.Push(errors.New("Please provide a `path` for the users database in `authentication_backend`"))
} }
if configuration.PasswordHashing == nil {
configuration.PasswordHashing = &schema.DefaultPasswordOptionsConfiguration
} else {
if configuration.PasswordHashing.Algorithm == "" {
configuration.PasswordHashing.Algorithm = schema.DefaultPasswordOptionsConfiguration.Algorithm
} else {
configuration.PasswordHashing.Algorithm = strings.ToLower(configuration.PasswordHashing.Algorithm)
if configuration.PasswordHashing.Algorithm != "argon2id" && configuration.PasswordHashing.Algorithm != "sha512" {
validator.Push(fmt.Errorf("Unknown hashing algorithm supplied, valid values are argon2id and sha512, you configured '%s'", configuration.PasswordHashing.Algorithm))
}
}
// Iterations (time)
if configuration.PasswordHashing.Iterations == 0 {
if configuration.PasswordHashing.Algorithm == "argon2id" {
configuration.PasswordHashing.Iterations = schema.DefaultPasswordOptionsConfiguration.Iterations
} else {
configuration.PasswordHashing.Iterations = schema.DefaultPasswordOptionsSHA512Configuration.Iterations
}
} else if configuration.PasswordHashing.Iterations < 1 {
validator.Push(fmt.Errorf("The number of iterations specified is invalid, must be 1 or more, you configured %d", configuration.PasswordHashing.Iterations))
}
//Salt Length
if configuration.PasswordHashing.SaltLength == 0 {
configuration.PasswordHashing.SaltLength = schema.DefaultPasswordOptionsConfiguration.SaltLength
} else if configuration.PasswordHashing.SaltLength < 2 {
validator.Push(fmt.Errorf("The salt length must be 2 or more, you configured %d", configuration.PasswordHashing.SaltLength))
} else if configuration.PasswordHashing.SaltLength > 16 {
validator.Push(fmt.Errorf("The salt length must be 16 or less, you configured %d", configuration.PasswordHashing.SaltLength))
}
if configuration.PasswordHashing.Algorithm == "argon2id" {
// Parallelism
if configuration.PasswordHashing.Parallelism == 0 {
configuration.PasswordHashing.Parallelism = schema.DefaultPasswordOptionsConfiguration.Parallelism
} else if configuration.PasswordHashing.Parallelism < 1 {
validator.Push(fmt.Errorf("Parallelism for argon2id must be 1 or more, you configured %d", configuration.PasswordHashing.Parallelism))
}
// Memory
if configuration.PasswordHashing.Memory == 0 {
configuration.PasswordHashing.Memory = schema.DefaultPasswordOptionsConfiguration.Memory
} else if configuration.PasswordHashing.Memory < configuration.PasswordHashing.Parallelism*8 {
validator.Push(fmt.Errorf("Memory for argon2id must be %d or more (parallelism * 8), you configured memory as %d and parallelism as %d", configuration.PasswordHashing.Parallelism*8, configuration.PasswordHashing.Memory, configuration.PasswordHashing.Parallelism))
}
// Key Length
if configuration.PasswordHashing.KeyLength == 0 {
configuration.PasswordHashing.KeyLength = schema.DefaultPasswordOptionsConfiguration.KeyLength
} else if configuration.PasswordHashing.KeyLength < 16 {
validator.Push(fmt.Errorf("Key length for argon2id must be 16, you configured %d", configuration.PasswordHashing.KeyLength))
}
}
}
} }
func validateLdapURL(ldapURL string, validator *schema.StructValidator) string { func validateLdapURL(ldapURL string, validator *schema.StructValidator) string {

View File

@ -28,9 +28,16 @@ type FileBasedAuthenticationBackend struct {
func (suite *FileBasedAuthenticationBackend) SetupTest() { func (suite *FileBasedAuthenticationBackend) SetupTest() {
suite.validator = schema.NewStructValidator() suite.validator = schema.NewStructValidator()
suite.configuration = schema.AuthenticationBackendConfiguration{} suite.configuration = schema.AuthenticationBackendConfiguration{}
suite.configuration.File = &schema.FileAuthenticationBackendConfiguration{Path: "/a/path"} suite.configuration.File = &schema.FileAuthenticationBackendConfiguration{Path: "/a/path", PasswordHashing: &schema.PasswordHashingConfiguration{
Algorithm: schema.DefaultPasswordOptionsConfiguration.Algorithm,
Iterations: schema.DefaultPasswordOptionsConfiguration.Iterations,
Parallelism: schema.DefaultPasswordOptionsConfiguration.Parallelism,
Memory: schema.DefaultPasswordOptionsConfiguration.Memory,
KeyLength: schema.DefaultPasswordOptionsConfiguration.KeyLength,
SaltLength: schema.DefaultPasswordOptionsConfiguration.SaltLength,
}}
suite.configuration.File.PasswordHashing.Algorithm = schema.DefaultPasswordOptionsConfiguration.Algorithm
} }
func (suite *FileBasedAuthenticationBackend) TestShouldValidateCompleteConfiguration() { func (suite *FileBasedAuthenticationBackend) TestShouldValidateCompleteConfiguration() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 0) assert.Len(suite.T(), suite.validator.Errors(), 0)
@ -43,6 +50,107 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvi
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a `path` for the users database in `authentication_backend`") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a `path` for the users database in `authentication_backend`")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenMemoryNotMoreThanEightTimesParallelism() {
suite.configuration.File.PasswordHashing.Memory = 8
suite.configuration.File.PasswordHashing.Parallelism = 2
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Memory for argon2id must be 16 or more (parallelism * 8), you configured memory as 8 and parallelism as 2")
}
func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWhenBlank() {
suite.configuration.File.PasswordHashing = &schema.PasswordHashingConfiguration{}
assert.Equal(suite.T(), 0, suite.configuration.File.PasswordHashing.KeyLength)
assert.Equal(suite.T(), 0, suite.configuration.File.PasswordHashing.Iterations)
assert.Equal(suite.T(), 0, suite.configuration.File.PasswordHashing.SaltLength)
assert.Equal(suite.T(), "", suite.configuration.File.PasswordHashing.Algorithm)
assert.Equal(suite.T(), 0, suite.configuration.File.PasswordHashing.Memory)
assert.Equal(suite.T(), 0, suite.configuration.File.PasswordHashing.Parallelism)
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 0)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsConfiguration.KeyLength, suite.configuration.File.PasswordHashing.KeyLength)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsConfiguration.Iterations, suite.configuration.File.PasswordHashing.Iterations)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsConfiguration.SaltLength, suite.configuration.File.PasswordHashing.SaltLength)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsConfiguration.Algorithm, suite.configuration.File.PasswordHashing.Algorithm)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsConfiguration.Memory, suite.configuration.File.PasswordHashing.Memory)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsConfiguration.Parallelism, suite.configuration.File.PasswordHashing.Parallelism)
}
func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWhenOnlySHA512Set() {
suite.configuration.File.PasswordHashing = &schema.PasswordHashingConfiguration{}
assert.Equal(suite.T(), "", suite.configuration.File.PasswordHashing.Algorithm)
suite.configuration.File.PasswordHashing.Algorithm = "sha512"
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 0)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsSHA512Configuration.KeyLength, suite.configuration.File.PasswordHashing.KeyLength)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsSHA512Configuration.Iterations, suite.configuration.File.PasswordHashing.Iterations)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsSHA512Configuration.SaltLength, suite.configuration.File.PasswordHashing.SaltLength)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsSHA512Configuration.Algorithm, suite.configuration.File.PasswordHashing.Algorithm)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsSHA512Configuration.Memory, suite.configuration.File.PasswordHashing.Memory)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsSHA512Configuration.Parallelism, suite.configuration.File.PasswordHashing.Parallelism)
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenKeyLengthTooLow() {
suite.configuration.File.PasswordHashing.KeyLength = 1
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Key length for argon2id must be 16, you configured 1")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSaltLengthTooLow() {
suite.configuration.File.PasswordHashing.SaltLength = -1
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "The salt length must be 2 or more, you configured -1")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSaltLengthTooHigh() {
suite.configuration.File.PasswordHashing.SaltLength = 20
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "The salt length must be 16 or less, you configured 20")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBadAlgorithmDefined() {
suite.configuration.File.PasswordHashing.Algorithm = "bogus"
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Unknown hashing algorithm supplied, valid values are argon2id and sha512, you configured 'bogus'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenIterationsTooLow() {
suite.configuration.File.PasswordHashing.Iterations = -1
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "The number of iterations specified is invalid, must be 1 or more, you configured -1")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenParallelismTooLow() {
suite.configuration.File.PasswordHashing.Parallelism = -1
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Parallelism for argon2id must be 1 or more, you configured -1")
}
func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultValues() {
suite.configuration.File.PasswordHashing.Algorithm = ""
suite.configuration.File.PasswordHashing.Iterations = 0
suite.configuration.File.PasswordHashing.SaltLength = 0
suite.configuration.File.PasswordHashing.Memory = 0
suite.configuration.File.PasswordHashing.Parallelism = 0
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 0)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsConfiguration.Algorithm, suite.configuration.File.PasswordHashing.Algorithm)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsConfiguration.Iterations, suite.configuration.File.PasswordHashing.Iterations)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsConfiguration.SaltLength, suite.configuration.File.PasswordHashing.SaltLength)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsConfiguration.Memory, suite.configuration.File.PasswordHashing.Memory)
assert.Equal(suite.T(), schema.DefaultPasswordOptionsConfiguration.Parallelism, suite.configuration.File.PasswordHashing.Parallelism)
}
func TestFileBasedAuthenticationBackend(t *testing.T) { func TestFileBasedAuthenticationBackend(t *testing.T) {
suite.Run(t, new(FileBasedAuthenticationBackend)) suite.Run(t, new(FileBasedAuthenticationBackend))
} }

View File

@ -1,6 +1,12 @@
package utils package utils
func IsStringInSlice(a string, list []string) bool { import (
"math/rand"
"time"
)
// Checks if a single string is in an array of strings
func IsStringInSlice(a string, list []string) (inSlice bool) {
for _, b := range list { for _, b := range list {
if b == a { if b == a {
return true return true
@ -8,3 +14,28 @@ func IsStringInSlice(a string, list []string) bool {
} }
return false return false
} }
// Splits a string s into an array with each item being a max of int d
// d = denominator, n = numerator, q = quotient, r = remainder
func SliceString(s string, d int) (array []string) {
n := len(s)
q := n / d
r := n % d
for i := 0; i < q; i++ {
array = append(array, s[i*d:i*d+d])
if i+1 == q && r != 0 {
array = append(array, s[i*d+d:])
}
}
return
}
// RandomString generate a random string of n characters
func RandomString(n int, characters []rune) (randomString string) {
rand.Seed(time.Now().UnixNano())
b := make([]rune, n)
for i := range b {
b[i] = characters[rand.Intn(len(characters))]
}
return string(b)
}

View File

@ -0,0 +1,37 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestShouldSplitIntoEvenStringsOfFour(t *testing.T) {
input := "abcdefghijkl"
arrayOfStrings := SliceString(input, 4)
assert.Equal(t, len(arrayOfStrings), 3)
assert.Equal(t, "abcd", arrayOfStrings[0])
assert.Equal(t, "efgh", arrayOfStrings[1])
assert.Equal(t, "ijkl", arrayOfStrings[2])
}
func TestShouldSplitIntoEvenStringsOfOne(t *testing.T) {
input := "abcdefghijkl"
arrayOfStrings := SliceString(input, 1)
assert.Equal(t, 12, len(arrayOfStrings))
assert.Equal(t, "a", arrayOfStrings[0])
assert.Equal(t, "b", arrayOfStrings[1])
assert.Equal(t, "c", arrayOfStrings[2])
assert.Equal(t, "d", arrayOfStrings[3])
assert.Equal(t, "l", arrayOfStrings[11])
}
func TestShouldSplitIntoUnevenStringsOfFour(t *testing.T) {
input := "abcdefghijklm"
arrayOfStrings := SliceString(input, 4)
assert.Equal(t, len(arrayOfStrings), 4)
assert.Equal(t, "abcd", arrayOfStrings[0])
assert.Equal(t, "efgh", arrayOfStrings[1])
assert.Equal(t, "ijkl", arrayOfStrings[2])
assert.Equal(t, "m", arrayOfStrings[3])
}