package db import ( "context" "database/sql" "errors" "fmt" "time" "github.com/lib/pq" "sourcecraft.dev/bigbes/sr-ht-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") }