Implement the storage path: for each accepted report, scrub PII (#8), encrypt with AES-256-GCM (ADR #5), and write only ciphertext to R2, wired in as the real ingest Sink replacing NopSink. - internal/crypto: AES-256-GCM in the exact ADR #5 wire format (magic "LMB1" || version || key_id BE16 || nonce(12) || ct || tag(16); the 7-byte header is the GCM AAD). Provider-independent framing shared by a host crypto/aes+crypto/cipher impl (tests, devserver) and a Wasm SubtleCrypto impl (syscall/js, //go:build js && wasm) per the TinyGo constraint; both produce byte-identical frames. Versioned keyring with key_id rotation; ParseKeyring reads the Secrets Store JSON secret. - internal/storage: ObjectStore interface with an in-memory fake (tests, devserver) and a Wasm R2Store (syumai/workers R2 binding). Sink ties scrub -> Seal -> Put under a unique reports/<ts>-<rand> key. WorkerSink loads the keyring from Secrets Store (BUGREPORT_ENC_KEYRING), cached for the isolate lifetime. - handler.New now takes an injectable ingest.Sink; the Worker uses the real R2/Secrets-Store sink, the devserver a memory + throwaway-key sink. - wrangler.jsonc: add REPORTS_BUCKET (R2) and BUGREPORT_ENC_KEYRING (Secrets Store) bindings. Tests (host, no TinyGo): encrypt/decrypt roundtrip; ciphertext != plaintext; wrong key + tamper (ct/tag/nonce/header-AAD) fail; exact wire layout plus a known-answer vector; key_id rotation with retained keys; full sink path (PII scrubbed then encrypted, readback requires the key and yields the scrubbed content). Existing ingest/handler behavior preserved (202 on valid POST). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
62 lines
1.4 KiB
Go
62 lines
1.4 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"sync"
|
|
)
|
|
|
|
// MemoryStore is an in-memory ObjectStore for tests, cmd/devserver, and local
|
|
// experimentation. It retains every object in a map and is not a production
|
|
// backend (it grows without bound). It is safe for concurrent use.
|
|
type MemoryStore struct {
|
|
mu sync.Mutex
|
|
objects map[string][]byte
|
|
}
|
|
|
|
// NewMemoryStore returns an empty MemoryStore.
|
|
func NewMemoryStore() *MemoryStore {
|
|
return &MemoryStore{objects: make(map[string][]byte)}
|
|
}
|
|
|
|
// Put stores a copy of data at key.
|
|
func (m *MemoryStore) Put(_ context.Context, key string, data []byte) error {
|
|
cp := append([]byte(nil), data...)
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.objects[key] = cp
|
|
return nil
|
|
}
|
|
|
|
// Get returns a copy of the object at key, or ErrNotFound.
|
|
func (m *MemoryStore) Get(_ context.Context, key string) ([]byte, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
data, ok := m.objects[key]
|
|
if !ok {
|
|
return nil, ErrNotFound
|
|
}
|
|
return append([]byte(nil), data...), nil
|
|
}
|
|
|
|
// Keys returns the stored object keys in sorted order (test/ops helper).
|
|
func (m *MemoryStore) Keys() []string {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
keys := make([]string, 0, len(m.objects))
|
|
for k := range m.objects {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
// Len reports how many objects are stored.
|
|
func (m *MemoryStore) Len() int {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return len(m.objects)
|
|
}
|
|
|
|
var _ ObjectStore = (*MemoryStore)(nil)
|