qsl designer
This commit is contained in:
@@ -1,20 +1,120 @@
|
||||
// Package settings is a tiny key/value store backed by the SQLite settings table.
|
||||
//
|
||||
// Sensitive keys (passwords) are encrypted transparently when a passphrase
|
||||
// cipher is installed: Set encrypts them, Get/GetMany/All decrypt them. When a
|
||||
// passphrase is configured but not yet unlocked (cipher nil) an encrypted value
|
||||
// reads back as "" rather than leaking ciphertext — callers treat that as
|
||||
// "not configured" and degrade gracefully.
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"hamlog/internal/secret"
|
||||
)
|
||||
|
||||
// Cipher is the subset of *secret.Cipher the store needs.
|
||||
type Cipher interface {
|
||||
Encrypt(plain string) string
|
||||
Decrypt(stored string) (string, error)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
db *sql.DB
|
||||
mu sync.RWMutex
|
||||
cipher Cipher // non-nil when secrets are unlocked
|
||||
sensitive func(key string) bool // which keys are encrypted at rest
|
||||
}
|
||||
|
||||
func NewStore(db *sql.DB) *Store { return &Store{db: db} }
|
||||
|
||||
// SetSensitivePredicate registers which keys hold secrets. Set once at startup.
|
||||
func (s *Store) SetSensitivePredicate(fn func(key string) bool) {
|
||||
s.mu.Lock()
|
||||
s.sensitive = fn
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Unlock installs the cipher derived from the user's passphrase so sensitive
|
||||
// values encrypt on write and decrypt on read.
|
||||
func (s *Store) Unlock(c Cipher) {
|
||||
s.mu.Lock()
|
||||
s.cipher = c
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Lock drops the cipher (secrets become unreadable until the next Unlock).
|
||||
func (s *Store) Lock() {
|
||||
s.mu.Lock()
|
||||
s.cipher = nil
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Unlocked reports whether a cipher is currently installed.
|
||||
func (s *Store) Unlocked() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.cipher != nil
|
||||
}
|
||||
|
||||
func (s *Store) isSensitive(key string) bool {
|
||||
if s.sensitive == nil {
|
||||
return false
|
||||
}
|
||||
return s.sensitive(key)
|
||||
}
|
||||
|
||||
// decodeRead decrypts a sensitive value for reading (plaintext passes through;
|
||||
// locked → "").
|
||||
func (s *Store) decodeRead(key, val string) string {
|
||||
if val == "" || !s.isSensitive(key) || !secret.IsEncrypted(val) {
|
||||
return val
|
||||
}
|
||||
s.mu.RLock()
|
||||
c := s.cipher
|
||||
s.mu.RUnlock()
|
||||
if c == nil {
|
||||
return "" // locked: don't leak ciphertext to callers
|
||||
}
|
||||
if pt, err := c.Decrypt(val); err == nil {
|
||||
return pt
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// encodeWrite encrypts a sensitive value for storage when unlocked.
|
||||
func (s *Store) encodeWrite(key, val string) string {
|
||||
if val == "" || !s.isSensitive(key) {
|
||||
return val
|
||||
}
|
||||
s.mu.RLock()
|
||||
c := s.cipher
|
||||
s.mu.RUnlock()
|
||||
if c == nil {
|
||||
return val // no passphrase / locked → store as-is (legacy behaviour)
|
||||
}
|
||||
return c.Encrypt(val)
|
||||
}
|
||||
|
||||
// Get returns the value for key, or "" if not set.
|
||||
func (s *Store) Get(ctx context.Context, key string) (string, error) {
|
||||
var v string
|
||||
err := s.db.QueryRowContext(ctx, `SELECT value FROM settings WHERE key = ?`, key).Scan(&v)
|
||||
if err == sql.ErrNoRows {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s.decodeRead(key, v), nil
|
||||
}
|
||||
|
||||
// GetRaw returns the stored value WITHOUT decryption — used by the passphrase
|
||||
// migration which must read/re-write the raw ciphertext or plaintext.
|
||||
func (s *Store) GetRaw(ctx context.Context, key string) (string, error) {
|
||||
var v string
|
||||
err := s.db.QueryRowContext(ctx, `SELECT value FROM settings WHERE key = ?`, key).Scan(&v)
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -23,8 +123,8 @@ func (s *Store) Get(ctx context.Context, key string) (string, error) {
|
||||
return v, err
|
||||
}
|
||||
|
||||
// Set upserts a key/value pair.
|
||||
func (s *Store) Set(ctx context.Context, key, value string) error {
|
||||
// SetRaw stores a value verbatim (no encryption) — used by the migration.
|
||||
func (s *Store) SetRaw(ctx context.Context, key, value string) error {
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO settings(key, value) VALUES(?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
@@ -37,7 +137,12 @@ func (s *Store) Set(ctx context.Context, key, value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// All returns every stored setting. Used by the UI to populate the prefs panel.
|
||||
// Set upserts a key/value pair (encrypting sensitive keys when unlocked).
|
||||
func (s *Store) Set(ctx context.Context, key, value string) error {
|
||||
return s.SetRaw(ctx, key, s.encodeWrite(key, value))
|
||||
}
|
||||
|
||||
// All returns every stored setting (sensitive values decrypted when unlocked).
|
||||
func (s *Store) All(ctx context.Context) (map[string]string, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT key, value FROM settings`)
|
||||
if err != nil {
|
||||
@@ -50,7 +155,7 @@ func (s *Store) All(ctx context.Context) (map[string]string, error) {
|
||||
if err := rows.Scan(&k, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[k] = v
|
||||
out[k] = s.decodeRead(k, v)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user