From 5b6da560896970c610c691dff6ed052a57ed5a1d Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Sat, 16 Nov 2024 00:36:17 +0100 Subject: fix(tfstated): hash passwords instead of relying on the database encryption key --- cmd/tfstated/main_test.go | 14 +++++--------- go.mod | 5 ++++- go.sum | 2 ++ pkg/basic_auth/middleware.go | 2 +- pkg/database/accounts.go | 38 ++++++++++++++++++-------------------- pkg/database/sql/000_init.sql | 3 ++- pkg/model/account.go | 42 ++++++++++++++++++++++++++++++++++-------- 7 files changed, 66 insertions(+), 40 deletions(-) diff --git a/cmd/tfstated/main_test.go b/cmd/tfstated/main_test.go index 3ad1d7e..82b7736 100644 --- a/cmd/tfstated/main_test.go +++ b/cmd/tfstated/main_test.go @@ -19,7 +19,7 @@ var baseURI = url.URL{ Scheme: "http", } var db *database.DB -var password string +var adminPassword string func TestMain(m *testing.M) { ctx := context.Background() @@ -46,6 +46,9 @@ func TestMain(m *testing.M) { } } + database.AdvertiseAdminPassword = func(password string) { + adminPassword = password + } go run( ctx, &config, @@ -59,13 +62,6 @@ func TestMain(m *testing.M) { os.Exit(1) } - admin, err := db.LoadAccountByUsername("admin") - if err != nil { - fmt.Fprintf(os.Stderr, "%+v\n", err) - os.Exit(1) - } - password = admin.Password - ret := m.Run() cancel() @@ -88,7 +84,7 @@ func runHTTPRequest(method string, auth bool, uriRef *url.URL, body io.Reader, t return } if auth { - req.SetBasicAuth("admin", password) + req.SetBasicAuth("admin", adminPassword) } resp, err := client.Do(req) if err != nil { diff --git a/go.mod b/go.mod index 0455251..2733025 100644 --- a/go.mod +++ b/go.mod @@ -4,4 +4,7 @@ go 1.23.3 require github.com/mattn/go-sqlite3 v1.14.24 -require go.n16f.net/uuid v0.0.0-20240707135755-e4fd26b968ad // indirect +require ( + go.n16f.net/uuid v0.0.0-20240707135755-e4fd26b968ad // indirect + golang.org/x/crypto v0.29.0 // indirect +) diff --git a/go.sum b/go.sum index 1d07d2a..2044398 100644 --- a/go.sum +++ b/go.sum @@ -2,3 +2,5 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= go.n16f.net/uuid v0.0.0-20240707135755-e4fd26b968ad h1:QYbHaaFqx6hMor1L6iMSmyhMFvXQXhKaNk9nefug07M= go.n16f.net/uuid v0.0.0-20240707135755-e4fd26b968ad/go.mod h1:hvPEWZmyP50in1DH72o5vUvoXFFyfRU6oL+p2tAcbgU= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= 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) } -- cgit v1.2.3