fix(tfstated): hash passwords instead of relying on the database encryption key
This commit is contained in:
parent
7c96e1b780
commit
5b6da56089
7 changed files with 66 additions and 40 deletions
|
@ -19,7 +19,7 @@ var baseURI = url.URL{
|
||||||
Scheme: "http",
|
Scheme: "http",
|
||||||
}
|
}
|
||||||
var db *database.DB
|
var db *database.DB
|
||||||
var password string
|
var adminPassword string
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
@ -46,6 +46,9 @@ func TestMain(m *testing.M) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
database.AdvertiseAdminPassword = func(password string) {
|
||||||
|
adminPassword = password
|
||||||
|
}
|
||||||
go run(
|
go run(
|
||||||
ctx,
|
ctx,
|
||||||
&config,
|
&config,
|
||||||
|
@ -59,13 +62,6 @@ func TestMain(m *testing.M) {
|
||||||
os.Exit(1)
|
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()
|
ret := m.Run()
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
@ -88,7 +84,7 @@ func runHTTPRequest(method string, auth bool, uriRef *url.URL, body io.Reader, t
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if auth {
|
if auth {
|
||||||
req.SetBasicAuth("admin", password)
|
req.SetBasicAuth("admin", adminPassword)
|
||||||
}
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
5
go.mod
5
go.mod
|
@ -4,4 +4,7 @@ go 1.23.3
|
||||||
|
|
||||||
require github.com/mattn/go-sqlite3 v1.14.24
|
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
|
||||||
|
)
|
||||||
|
|
2
go.sum
2
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=
|
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 h1:QYbHaaFqx6hMor1L6iMSmyhMFvXQXhKaNk9nefug07M=
|
||||||
go.n16f.net/uuid v0.0.0-20240707135755-e4fd26b968ad/go.mod h1:hvPEWZmyP50in1DH72o5vUvoXFFyfRU6oL+p2tAcbgU=
|
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=
|
||||||
|
|
|
@ -27,7 +27,7 @@ func Middleware(db *database.DB) func(http.Handler) http.Handler {
|
||||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if password != account.Password {
|
if !account.CheckPassword(password) {
|
||||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,22 +11,27 @@ import (
|
||||||
"go.n16f.net/uuid"
|
"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) {
|
func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) {
|
||||||
account := model.Account{
|
account := model.Account{
|
||||||
Username: username,
|
Username: username,
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
encryptedPassword []byte
|
|
||||||
created int64
|
created int64
|
||||||
lastLogin int64
|
lastLogin int64
|
||||||
)
|
)
|
||||||
err := db.QueryRow(
|
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
|
FROM accounts
|
||||||
WHERE username = ?;`,
|
WHERE username = ?;`,
|
||||||
username,
|
username,
|
||||||
).Scan(&account.Id,
|
).Scan(&account.Id,
|
||||||
&encryptedPassword,
|
&account.Salt,
|
||||||
|
&account.PasswordHash,
|
||||||
&account.IsAdmin,
|
&account.IsAdmin,
|
||||||
&created,
|
&created,
|
||||||
&lastLogin,
|
&lastLogin,
|
||||||
|
@ -38,11 +43,6 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) {
|
||||||
}
|
}
|
||||||
return nil, err
|
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.Created = time.Unix(created, 0)
|
||||||
account.LastLogin = time.Unix(lastLogin, 0)
|
account.LastLogin = time.Unix(lastLogin, 0)
|
||||||
return &account, nil
|
return &account, nil
|
||||||
|
@ -69,23 +69,21 @@ func (db *DB) InitAdminAccount() error {
|
||||||
if err = password.Generate(uuid.V4); err != nil {
|
if err = password.Generate(uuid.V4); err != nil {
|
||||||
return fmt.Errorf("failed to generate initial admin password: %w", err)
|
return fmt.Errorf("failed to generate initial admin password: %w", err)
|
||||||
}
|
}
|
||||||
var encryptedPassword []byte
|
salt := model.GenerateSalt()
|
||||||
encryptedPassword, err = db.dataEncryptionKey.EncryptAES256([]byte(password.String()))
|
hash := model.HashPassword(password.String(), salt)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to encrypt initial admin password: %w", err)
|
|
||||||
}
|
|
||||||
if _, err = tx.ExecContext(db.ctx,
|
if _, err = tx.ExecContext(db.ctx,
|
||||||
`INSERT INTO accounts(username, password, is_admin)
|
`INSERT INTO accounts(username, salt, password_hash, is_admin)
|
||||||
VALUES ("admin", :password, TRUE)
|
VALUES ("admin", :salt, :hash, TRUE)
|
||||||
ON CONFLICT DO UPDATE SET password = :password
|
ON CONFLICT DO UPDATE SET password_hash = :hash
|
||||||
WHERE username = "admin";`,
|
WHERE username = "admin";`,
|
||||||
sql.Named("password", encryptedPassword),
|
sql.Named("salt", salt),
|
||||||
|
sql.Named("hash", hash),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("failed to set initial admin password: %w", err)
|
return fmt.Errorf("failed to set initial admin password: %w", err)
|
||||||
}
|
}
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
if err == nil {
|
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
|
return err
|
||||||
|
|
|
@ -5,7 +5,8 @@ CREATE TABLE schema_version (
|
||||||
CREATE TABLE accounts (
|
CREATE TABLE accounts (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
username TEXT NOT NULL,
|
username TEXT NOT NULL,
|
||||||
password BLOB NOT NULL,
|
salt BLOB NOT NULL,
|
||||||
|
password_hash BLOB NOT NULL,
|
||||||
is_admin INTEGER NOT NULL DEFAULT FALSE,
|
is_admin INTEGER NOT NULL DEFAULT FALSE,
|
||||||
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||||
last_login INTEGER NOT NULL DEFAULT (unixepoch()),
|
last_login INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||||
|
|
|
@ -1,15 +1,41 @@
|
||||||
package model
|
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 AccountContextKey struct{}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Id int
|
Id int
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Salt []byte
|
||||||
|
PasswordHash []byte
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
Created time.Time
|
Created time.Time
|
||||||
LastLogin time.Time
|
LastLogin time.Time
|
||||||
Settings any
|
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)
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue