2024-01-31 19:42:00 -05:00
|
|
|
package player
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"crypto/rand"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"log/slog"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"gitlab.com/zephyrtronium/sq"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// Argon2id parameters recommended in
|
|
|
|
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
|
2024-01-31 23:29:31 -05:00
|
|
|
timeCost = 3
|
|
|
|
memory = 12 << 10
|
|
|
|
threads = 1
|
|
|
|
hashLen = 48
|
2024-01-31 19:42:00 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
type (
|
|
|
|
Execer interface {
|
|
|
|
Exec(ctx context.Context, query string, args ...any) (sq.Result, error)
|
|
|
|
}
|
|
|
|
RowQuerier interface {
|
|
|
|
QueryRow(ctx context.Context, query string, args ...any) *sq.Row
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
// InitUsers initializes the table for permanent storage of user credentials.
|
|
|
|
func InitUsers(ctx context.Context, db Execer) error {
|
|
|
|
_, err := db.Exec(ctx, initUsers)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("couldn't init users table: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register adds a new user to a database initialized with [InitUsers].
|
|
|
|
func Register(ctx context.Context, db Execer, user, pass string) error {
|
|
|
|
user = strings.ToLower(user)
|
|
|
|
// TODO(zeph): validate length and content
|
|
|
|
slog.InfoContext(ctx, "register user", "user", user)
|
|
|
|
salt := make([]byte, 16)
|
|
|
|
if _, err := rand.Read(salt); err != nil {
|
|
|
|
slog.ErrorContext(ctx, "failed to get salt for new user", "user", user, "err", err.Error())
|
|
|
|
panic(err)
|
|
|
|
}
|
2024-01-31 23:29:31 -05:00
|
|
|
h := argon2.IDKey([]byte(pass), salt, timeCost, memory, threads, hashLen)
|
2024-01-31 19:42:00 -05:00
|
|
|
id := uuid.New()
|
|
|
|
_, err := db.Exec(ctx, `INSERT INTO shotgun_users(user, pass, salt, id) VALUES (?, ?, ?, ?)`, user, h, salt, id)
|
|
|
|
if err != nil {
|
|
|
|
slog.ErrorContext(ctx, "failed to register user", "user", user, "err", err.Error())
|
|
|
|
return fmt.Errorf("couldn't register new user: %w", err)
|
|
|
|
}
|
|
|
|
slog.InfoContext(ctx, "registered user", "user", user, "id", id)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Login gets the user ID associcated with a user if their saved credentials
|
|
|
|
// match those provided.
|
|
|
|
func Login(ctx context.Context, db RowQuerier, user, pass string) (ID, error) {
|
|
|
|
// TODO(zeph): we are making almost no attempt to distinguish
|
|
|
|
// "user does not exist" from "wrong password"
|
|
|
|
user = strings.ToLower(user)
|
|
|
|
slog.InfoContext(ctx, "login", "user", user)
|
|
|
|
var (
|
|
|
|
p, salt []byte
|
|
|
|
id uuid.UUID
|
|
|
|
)
|
|
|
|
if err := db.QueryRow(ctx, `SELECT pass, salt, id FROM shotgun_users WHERE user = ?`, user).Scan(&p, &salt, &id); err != nil {
|
|
|
|
slog.ErrorContext(ctx, "failed to get user creds", "user", user, "err", err.Error())
|
|
|
|
if errors.Is(err, sq.ErrNoRows) {
|
|
|
|
return ID{}, fmt.Errorf("invalid credentials")
|
|
|
|
}
|
|
|
|
return ID{}, fmt.Errorf("couldn't get user creds: %w", err)
|
|
|
|
}
|
2024-01-31 23:29:31 -05:00
|
|
|
h := argon2.IDKey([]byte(pass), salt, timeCost, memory, threads, hashLen)
|
2024-01-31 19:42:00 -05:00
|
|
|
if !bytes.Equal(p, h) {
|
|
|
|
slog.ErrorContext(ctx, "login failed", "user", user)
|
|
|
|
return ID{}, fmt.Errorf("invalid credentials")
|
|
|
|
}
|
2024-01-31 23:40:12 -05:00
|
|
|
return ID{id}, nil
|
2024-01-31 19:42:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
const initUsers = `CREATE TABLE shotgun_users (
|
|
|
|
user TEXT PRIMARY KEY NOT NULL,
|
|
|
|
pass BLOB NOT NULL,
|
|
|
|
salt BLOB NOT NULL,
|
2024-01-31 23:29:31 -05:00
|
|
|
id TEXT NOT NULL UNIQUE
|
|
|
|
) STRICT;`
|