diff options
author | Julien Dessaux | 2024-11-16 00:36:17 +0100 |
---|---|---|
committer | Julien Dessaux | 2024-11-16 00:36:17 +0100 |
commit | 5b6da560896970c610c691dff6ed052a57ed5a1d (patch) | |
tree | 7ec12f39943513230659d3068d59e8687770f053 /pkg | |
parent | fix(tfstated): return 403 Forbidden on non existent account (diff) | |
download | tfstated-5b6da560896970c610c691dff6ed052a57ed5a1d.tar.gz tfstated-5b6da560896970c610c691dff6ed052a57ed5a1d.tar.bz2 tfstated-5b6da560896970c610c691dff6ed052a57ed5a1d.zip |
fix(tfstated): hash passwords instead of relying on the database encryption key
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/basic_auth/middleware.go | 2 | ||||
-rw-r--r-- | pkg/database/accounts.go | 38 | ||||
-rw-r--r-- | pkg/database/sql/000_init.sql | 3 | ||||
-rw-r--r-- | pkg/model/account.go | 42 |
4 files changed, 55 insertions, 30 deletions
diff --git a/pkg/basic_auth/middleware.go b/pkg/basic_auth/middleware.go index 1b51c8a..7f8fb4a 100644 --- a/pkg/basic_auth/middleware.go +++ b/pkg/basic_auth/middleware.go @@ -27,7 +27,7 @@ func Middleware(db *database.DB) func(http.Handler) http.Handler { http.Error(w, "Forbidden", http.StatusForbidden) return } - if password != account.Password { + if !account.CheckPassword(password) { http.Error(w, "Forbidden", http.StatusForbidden) return } diff --git a/pkg/database/accounts.go b/pkg/database/accounts.go index 3919709..6400d5a 100644 --- a/pkg/database/accounts.go +++ b/pkg/database/accounts.go @@ -11,22 +11,27 @@ import ( "go.n16f.net/uuid" ) +// Overriden by tests +var AdvertiseAdminPassword = func(password string) { + slog.Info("Generated an initial admin password, please change it or delete the admin account after your first login", "password", password) +} + func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) { account := model.Account{ Username: username, } var ( - encryptedPassword []byte - created int64 - lastLogin int64 + created int64 + lastLogin int64 ) err := db.QueryRow( - `SELECT id, password, is_admin, created, last_login, settings + `SELECT id, salt, password_hash, is_admin, created, last_login, settings FROM accounts WHERE username = ?;`, username, ).Scan(&account.Id, - &encryptedPassword, + &account.Salt, + &account.PasswordHash, &account.IsAdmin, &created, &lastLogin, @@ -38,11 +43,6 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) { } return nil, err } - password, err := db.dataEncryptionKey.DecryptAES256(encryptedPassword) - if err != nil { - return nil, err - } - account.Password = string(password) account.Created = time.Unix(created, 0) account.LastLogin = time.Unix(lastLogin, 0) return &account, nil @@ -69,23 +69,21 @@ func (db *DB) InitAdminAccount() error { if err = password.Generate(uuid.V4); err != nil { return fmt.Errorf("failed to generate initial admin password: %w", err) } - var encryptedPassword []byte - encryptedPassword, err = db.dataEncryptionKey.EncryptAES256([]byte(password.String())) - if err != nil { - return fmt.Errorf("failed to encrypt initial admin password: %w", err) - } + salt := model.GenerateSalt() + hash := model.HashPassword(password.String(), salt) if _, err = tx.ExecContext(db.ctx, - `INSERT INTO accounts(username, password, is_admin) - VALUES ("admin", :password, TRUE) - ON CONFLICT DO UPDATE SET password = :password + `INSERT INTO accounts(username, salt, password_hash, is_admin) + VALUES ("admin", :salt, :hash, TRUE) + ON CONFLICT DO UPDATE SET password_hash = :hash WHERE username = "admin";`, - sql.Named("password", encryptedPassword), + sql.Named("salt", salt), + sql.Named("hash", hash), ); err != nil { return fmt.Errorf("failed to set initial admin password: %w", err) } err = tx.Commit() if err == nil { - slog.Info("Generated an initial admin password, please change it or delete the admin account after your first login", "password", password.String()) + AdvertiseAdminPassword(password.String()) } } return err diff --git a/pkg/database/sql/000_init.sql b/pkg/database/sql/000_init.sql index c56473f..b635442 100644 --- a/pkg/database/sql/000_init.sql +++ b/pkg/database/sql/000_init.sql @@ -5,7 +5,8 @@ CREATE TABLE schema_version ( CREATE TABLE accounts ( id INTEGER PRIMARY KEY, username TEXT NOT NULL, - password BLOB NOT NULL, + salt BLOB NOT NULL, + password_hash BLOB NOT NULL, is_admin INTEGER NOT NULL DEFAULT FALSE, created INTEGER NOT NULL DEFAULT (unixepoch()), last_login INTEGER NOT NULL DEFAULT (unixepoch()), diff --git a/pkg/model/account.go b/pkg/model/account.go index 86032b8..4336dfa 100644 --- a/pkg/model/account.go +++ b/pkg/model/account.go @@ -1,15 +1,41 @@ package model -import "time" +import ( + "crypto/sha256" + "crypto/subtle" + "time" + + "git.adyxax.org/adyxax/tfstated/pkg/scrypto" + "golang.org/x/crypto/pbkdf2" +) + +const ( + PBKDF2Iterations = 600000 + SaltSize = 32 +) type AccountContextKey struct{} type Account struct { - Id int - Username string - Password string - IsAdmin bool - Created time.Time - LastLogin time.Time - Settings any + Id int + Username string + Salt []byte + PasswordHash []byte + IsAdmin bool + Created time.Time + LastLogin time.Time + Settings any +} + +func (account *Account) CheckPassword(password string) bool { + hash := HashPassword(password, account.Salt) + return subtle.ConstantTimeCompare(hash, account.PasswordHash) == 1 +} + +func GenerateSalt() []byte { + return scrypto.RandomBytes(SaltSize) +} + +func HashPassword(password string, salt []byte) []byte { + return pbkdf2.Key([]byte(password), salt, PBKDF2Iterations, 32, sha256.New) } |