// Package barrier provides an encrypted storage barrier backed by SQLite. package barrier import ( "context" "database/sql" "errors" "fmt" "strings" "sync" "git.wntrmute.dev/kyle/metacrypt/internal/crypto" ) var ( ErrSealed = errors.New("barrier: sealed") ErrNotFound = errors.New("barrier: entry not found") ) // Barrier is the encrypted storage barrier interface. type Barrier interface { // Unseal opens the barrier with the given master encryption key. Unseal(mek []byte) error // Seal closes the barrier and zeroizes the key material. Seal() error // IsSealed returns true if the barrier is sealed. IsSealed() bool // Get retrieves and decrypts a value by path. Get(ctx context.Context, path string) ([]byte, error) // Put encrypts and stores a value at the given path. Put(ctx context.Context, path string, value []byte) error // Delete removes an entry by path. Delete(ctx context.Context, path string) error // List returns paths with the given prefix. List(ctx context.Context, prefix string) ([]string, error) } // AESGCMBarrier implements Barrier using AES-256-GCM encryption. type AESGCMBarrier struct { db *sql.DB mu sync.RWMutex mek []byte // nil when sealed } // NewAESGCMBarrier creates a new AES-GCM barrier backed by the given database. func NewAESGCMBarrier(db *sql.DB) *AESGCMBarrier { return &AESGCMBarrier{db: db} } func (b *AESGCMBarrier) Unseal(mek []byte) error { b.mu.Lock() defer b.mu.Unlock() k := make([]byte, len(mek)) copy(k, mek) b.mek = k return nil } func (b *AESGCMBarrier) Seal() error { b.mu.Lock() defer b.mu.Unlock() if b.mek != nil { crypto.Zeroize(b.mek) b.mek = nil } return nil } func (b *AESGCMBarrier) IsSealed() bool { b.mu.RLock() defer b.mu.RUnlock() return b.mek == nil } func (b *AESGCMBarrier) Get(ctx context.Context, path string) ([]byte, error) { b.mu.RLock() mek := b.mek b.mu.RUnlock() if mek == nil { return nil, ErrSealed } var encrypted []byte err := b.db.QueryRowContext(ctx, "SELECT value FROM barrier_entries WHERE path = ?", path).Scan(&encrypted) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, fmt.Errorf("barrier: get %q: %w", path, err) } plaintext, err := crypto.Decrypt(mek, encrypted) if err != nil { return nil, fmt.Errorf("barrier: decrypt %q: %w", path, err) } return plaintext, nil } func (b *AESGCMBarrier) Put(ctx context.Context, path string, value []byte) error { b.mu.RLock() mek := b.mek b.mu.RUnlock() if mek == nil { return ErrSealed } encrypted, err := crypto.Encrypt(mek, value) if err != nil { return fmt.Errorf("barrier: encrypt %q: %w", path, err) } _, err = b.db.ExecContext(ctx, ` INSERT INTO barrier_entries (path, value) VALUES (?, ?) ON CONFLICT(path) DO UPDATE SET value = excluded.value, updated_at = datetime('now')`, path, encrypted) if err != nil { return fmt.Errorf("barrier: put %q: %w", path, err) } return nil } func (b *AESGCMBarrier) Delete(ctx context.Context, path string) error { b.mu.RLock() mek := b.mek b.mu.RUnlock() if mek == nil { return ErrSealed } _, err := b.db.ExecContext(ctx, "DELETE FROM barrier_entries WHERE path = ?", path) if err != nil { return fmt.Errorf("barrier: delete %q: %w", path, err) } return nil } func (b *AESGCMBarrier) List(ctx context.Context, prefix string) ([]string, error) { b.mu.RLock() mek := b.mek b.mu.RUnlock() if mek == nil { return nil, ErrSealed } rows, err := b.db.QueryContext(ctx, "SELECT path FROM barrier_entries WHERE path LIKE ?", prefix+"%") if err != nil { return nil, fmt.Errorf("barrier: list %q: %w", prefix, err) } defer rows.Close() var paths []string for rows.Next() { var p string if err := rows.Scan(&p); err != nil { return nil, fmt.Errorf("barrier: list scan: %w", err) } // Strip the prefix and return just the next segment. remainder := strings.TrimPrefix(p, prefix) paths = append(paths, remainder) } return paths, rows.Err() }