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
|
@ -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
Add a link
Reference in a new issue