summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
authorJulien Dessaux2024-11-16 00:36:17 +0100
committerJulien Dessaux2024-11-16 00:36:17 +0100
commit5b6da560896970c610c691dff6ed052a57ed5a1d (patch)
tree7ec12f39943513230659d3068d59e8687770f053 /pkg
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
Diffstat (limited to 'pkg')
-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
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)
}