From de79f03aa3dbe5bb1e154a7e8d3dccbd229f3ea3 Mon Sep 17 00:00:00 2001 From: Amir Raminfar Date: Thu, 26 Sep 2024 16:40:47 -0700 Subject: [PATCH] feat: uses bcrypt hash instead (#3293) --- docs/guide/authentication.md | 22 ++++++++++------------ go.mod | 2 +- go.sum | 2 -- internal/auth/users.go | 30 ++++++++++++++++++++++++++---- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md index 598d4dfa..940c3dde 100644 --- a/docs/guide/authentication.md +++ b/docs/guide/authentication.md @@ -18,16 +18,16 @@ The content of the file looks like: users: # "admin" here is username admin: - name: "Admin" - # Just sha-256 which can be computed with "echo -n password | shasum -a 256" - password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" email: me@email.net + name: Admin + # Generate with docker run amir20/dozzle generate --name Admin --email me@email.net --password secret admin + password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK ``` -> [!TIP] -> This file can be generated with `docker run amir20/dozzle generate` with v6.6.x. See [below](#generating-users-yml) for more details. +Dozzle uses `email` to generate avatars using [Gravatar](https://gravatar.com/). It is optional. The password is hashed using `bcrypt` which can be generated using `docker run amir20/dozzle generate`. -Dozzle uses `email` to generate avatars using [Gravatar](https://gravatar.com/). It is optional. The password is hashed using `sha256` which can be generated with `echo -n 'secret-password' | shasum -a 256` or `echo -n 'secret-password' | sha256sum` on linux. +> [!WARNING] +> In previous versions of Dozzle, SHA-256 was used to hash passwords. Bcrypt is now more secure and is recommended for future use. Dozzle will revert to SHA-256 if it does not find a bcrypt hash. It is advisable to update the password hash to bcrypt using `docker run amir20/dozzle generate`. For more details, see [this issue](https://github.com/amir20/dozzle/security/advisories/GHSA-w7qr-q9fh-fj35). You will need to mount this file for Dozzle to find it. Here is an example: @@ -52,21 +52,19 @@ services: ```yaml [users.yml] users: - # "admin" here is username admin: - name: "Admin" - # Just sha-256 which can be computed with "echo -n password | shasum -a 256" - password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" email: me@email.net + name: Admin + password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK ``` ::: Dozzle uses [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token) to generate tokens for authentication. This token is saved in a cookie. -## Generating users.yml +## Generating users.yml -Starting with version `v6.6.x`, Dozzle has a builtin `generate` command to generate `users.yml`. Here is an example: +Dozzle has a builtin `generate` command to generate `users.yml`. Here is an example: ```sh docker run amir20/dozzle generate admin --password password --email test@email.net --name "John Doe" > users.yml diff --git a/go.mod b/go.mod index 1505e99f..002a7757 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/samber/lo v1.47.0 github.com/wk8/go-ordered-map/v2 v2.1.8 github.com/yuin/goldmark v1.7.4 + golang.org/x/crypto v0.27.0 golang.org/x/sync v0.8.0 google.golang.org/grpc v1.67.0 google.golang.org/protobuf v1.34.2 @@ -71,7 +72,6 @@ require ( go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/otel/sdk v1.22.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect - golang.org/x/crypto v0.27.0 // indirect golang.org/x/text v0.18.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect gotest.tools/v3 v3.0.3 // indirect diff --git a/go.sum b/go.sum index 346da706..04762bf9 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnN github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v27.3.0+incompatible h1:BNb1QY6o4JdKpqwi9IB+HUYcRRrVN4aGFUTvDmWYK1A= -github.com/docker/docker v27.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= diff --git a/internal/auth/users.go b/internal/auth/users.go index e8437e2e..5826e3d3 100644 --- a/internal/auth/users.go +++ b/internal/auth/users.go @@ -13,6 +13,7 @@ import ( "github.com/go-chi/jwtauth/v5" "github.com/rs/zerolog/log" + "golang.org/x/crypto/bcrypt" "gopkg.in/yaml.v3" ) @@ -61,7 +62,11 @@ func GenerateUsers(user User, hashPassword bool) *bytes.Buffer { buffer := &bytes.Buffer{} if hashPassword { - user.Password = sha256sum(user.Password) + hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 11) + if err != nil { + log.Fatal().Err(err).Msg("Failed to hash password") + } + user.Password = string(hash) } users := UserDatabase{ @@ -93,8 +98,8 @@ func decodeUsersFromFile(path string) (UserDatabase, error) { log.Fatal().Msgf("User %s has an empty password", username) } - if len(user.Password) != 64 { - log.Fatal().Str("password", user.Password).Msgf("User %s has an invalid password hash", username) + if !(len(user.Password) == 64 || len(user.Password) == 60) { + log.Fatal().Str("password", user.Password).Str("user", username).Msg("Invalid password for user") } if user.Name == "" { @@ -146,9 +151,10 @@ func (u *UserDatabase) FindByPassword(username, password string) *User { return nil } - if user.Password != sha256sum(password) { + if !CompareHashAndPassword(user.Password, password) { return nil } + return user } @@ -157,6 +163,22 @@ func sha256sum(s string) string { return hex.EncodeToString(bytes[:]) } +func CompareHashAndPassword(hash, password string) bool { + if len(hash) == 64 { + log.Warn().Msg("Using sha256sum for password comparison. Consider using a more secure hash algorithm to protected against brute-force attacks. See https://github.com/amir20/dozzle/security/advisories/GHSA-w7qr-q9fh-fj35 for more details.") + return hash == sha256sum(password) + } + + if len(hash) == 60 { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil + } + + log.Error().Str("hash", hash).Msg("Invalid hash length. Expecting 64 or 60 characters.") + + return false +} + func UserFromContext(ctx context.Context) *User { if user, ok := ctx.Value(remoteUser).(User); ok { return &user