package db
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"github.com/lib/pq"
"go.bigb.es/sourcehut-dolt/core"
)
// DoltKey is a registered Ed25519 credential (dolt creds / dolt login). PubKey
// is the raw 32-byte public key; Comment and LastUsed are nullable in the
// schema and default to "" / nil.
type DoltKey struct {
ID int
UserID int
KID string
PubKey []byte
Comment string
Created time.Time
LastUsed *time.Time
}
// KeyAuth is everything the Bearer-JWT verifier needs to authenticate a dolt
// keypair request: the stored public key to verify the JWS signature, plus the
// owning user's identity to build a core.Caller. Suspended is derived by the
// caller from UserType == core.UserTypeSuspended.
type KeyAuth struct {
KeyID int
UserID int
Username string
UserType core.UserType
PubKey []byte
}
// InsertKey registers a dolt key for a user. kid is base32(SHA-512/224(pubkey))
// in dolt's alphabet; pubkey is the raw 32-byte Ed25519 public key. A duplicate
// kid (dolt_key.kid UNIQUE) is mapped to ErrKeyExists. Returns the created row.
func (s *Store) InsertKey(ctx context.Context, userID int, kid string, pubkey []byte, comment string) (*DoltKey, error) {
now := time.Now().UTC()
const q = `
INSERT INTO dolt_key (created, user_id, kid, pubkey, comment)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`
var cmt any
if comment != "" {
cmt = comment
}
var id int
err := s.q.QueryRowContext(ctx, q, now, userID, kid, pubkey, cmt).Scan(&id)
if err != nil {
var pqErr *pq.Error
if errors.As(err, &pqErr) && pqErr.Code == "23505" {
return nil, ErrKeyExists
}
return nil, fmt.Errorf("insert dolt key: %w", err)
}
return &DoltKey{
ID: id,
UserID: userID,
KID: kid,
PubKey: pubkey,
Comment: comment,
Created: now,
}, nil
}
// KeyByKID resolves a key by its kid and returns the public key together with
// the owning user's identity, for authentication. Returns ErrNotFound if no key
// with that kid is registered.
func (s *Store) KeyByKID(ctx context.Context, kid string) (*KeyAuth, error) {
const q = `
SELECT k.id, k.pubkey, u.id, COALESCE(u.username, ''), u.user_type
FROM dolt_key k
JOIN "user" u ON u.id = k.user_id
WHERE k.kid = $1`
var (
ka KeyAuth
userType string
)
err := s.q.QueryRowContext(ctx, q, kid).Scan(
&ka.KeyID, &ka.PubKey, &ka.UserID, &ka.Username, &userType)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("key by kid %s: %w", kid, err)
}
ka.UserType = core.UserType(userType)
return &ka, nil
}
// ListKeysByUser returns all of a user's registered dolt keys, newest first.
func (s *Store) ListKeysByUser(ctx context.Context, userID int) ([]*DoltKey, error) {
const q = `
SELECT id, user_id, kid, pubkey, COALESCE(comment, ''), created, last_used
FROM dolt_key
WHERE user_id = $1
ORDER BY created DESC, id DESC`
rows, err := s.q.QueryContext(ctx, q, userID)
if err != nil {
return nil, fmt.Errorf("list keys user=%d: %w", userID, err)
}
defer rows.Close()
var keys []*DoltKey
for rows.Next() {
var (
k DoltKey
lastUsed sql.NullTime
)
if err := rows.Scan(&k.ID, &k.UserID, &k.KID, &k.PubKey,
&k.Comment, &k.Created, &lastUsed); err != nil {
return nil, fmt.Errorf("scan key: %w", err)
}
if lastUsed.Valid {
t := lastUsed.Time
k.LastUsed = &t
}
keys = append(keys, &k)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate keys: %w", err)
}
return keys, nil
}
// DeleteKey removes one of a user's keys. It is scoped by userID so a user can
// only delete keys they own; a mismatch (or missing id) yields ErrNotFound.
func (s *Store) DeleteKey(ctx context.Context, id, userID int) error {
res, err := s.q.ExecContext(ctx,
`DELETE FROM dolt_key WHERE id = $1 AND user_id = $2`, id, userID)
if err != nil {
return fmt.Errorf("delete key %d: %w", id, err)
}
return requireOne(res, "delete key")
}
// TouchKeyLastUsed stamps a key's last_used with the current time, called after
// a successful keypair authentication. Returns ErrNotFound if the kid vanished
// (e.g. the key was deleted concurrently).
func (s *Store) TouchKeyLastUsed(ctx context.Context, kid string) error {
res, err := s.q.ExecContext(ctx,
`UPDATE dolt_key SET last_used = $2 WHERE kid = $1`, kid, time.Now().UTC())
if err != nil {
return fmt.Errorf("touch key %s: %w", kid, err)
}
return requireOne(res, "touch key")
}