summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Dessaux2024-11-16 00:36:17 +0100
committerJulien Dessaux2024-11-16 00:36:17 +0100
commit5b6da560896970c610c691dff6ed052a57ed5a1d (patch)
tree7ec12f39943513230659d3068d59e8687770f053
parentfix(tfstated): return 403 Forbidden on non existent account (diff)
downloadtfstated-5b6da560896970c610c691dff6ed052a57ed5a1d.tar.gz
tfstated-5b6da560896970c610c691dff6ed052a57ed5a1d.tar.bz2
tfstated-5b6da560896970c610c691dff6ed052a57ed5a1d.zip
fix(tfstated): hash passwords instead of relying on the database encryption key
-rw-r--r--cmd/tfstated/main_test.go14
-rw-r--r--go.mod5
-rw-r--r--go.sum2
-rw-r--r--pkg/basic_auth/middleware.go2
-rw-r--r--pkg/database/accounts.go38
-rw-r--r--pkg/database/sql/000_init.sql3
-rw-r--r--pkg/model/account.go42
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)
}