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",
|
||||
}
|
||||
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 {
|
||||
|
|
5
go.mod
5
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
|
||||
)
|
||||
|
|
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=
|
||||
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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue