Skip to content

Commit b864480

Browse files
committed
feat(registry): store FEE params in their own table
Review of the FIL-480 draft rejected extending blob_locations with the per-blob FEE wrap material. blob_locations is a reconstructible cache of the indexing-service contract — every row can be re-derived from the indexer or the accept receipt, and the table disappears when the topology moves to a real indexer. A wrapped CEK is not reconstructible: lose the row and the ciphertext is unreadable forever. Key material therefore does not belong in the one table whose design allows it to be rebuilt, or truncated (the live test already does). blob_encryption_params holds it instead, with no foreign key to blob_locations: the two have independent lifecycles, and an FK would force location-before-params write ordering. Because nothing cascades, DeleteLocation no longer shreds — a caller removing a blob must delete from both tables. Two fixes to the material itself, both raised in review: - Add header_len. The stored blob is envelope||ciphertext and nothing recorded where the envelope ends, so a read could not locate byte 0 of the ciphertext without decoding the header, leaving the "no header round-trip" goal out of reach. - Store the whole COSE Enc_structure as aad instead of the protected header. The structure's context string differs between a COSE_Encrypt and a COSE_Encrypt0, which a bare row cannot record, so the protected header alone is not enough to rebuild the AAD. The header stays recoverable from the Enc_structure as element 1. Every column is NOT NULL, so the all-or-nothing invariant is structural rather than a nullable-column check: the existence of a row is what marks a blob as encrypted. BlobEncryptionParams.Validate rejects an incomplete set before SQL, replacing ValidateFEE/ErrPartialFEE. Migration 00013 is reshaped in place — it has not shipped anywhere. Assisted-by: Claude:claude-fable-5 Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
1 parent b3b1e3a commit b864480

8 files changed

Lines changed: 521 additions & 301 deletions

File tree

inmem/store.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,17 @@ type MemStore struct {
5959
blobRefs map[claimKey]registry.BlobClaim
6060
intents map[string]registry.UploadIntent // keyed by string(digest)
6161
locations map[locKey]registry.BlobLocation // keyed by (space, digest)
62+
encParams map[locKey]registry.BlobEncryptionParams // keyed by (space, digest)
6263
inclusions map[locKey]registry.BlobInclusion // keyed by (space, digest)
6364
parks map[string]registry.BlobPark // keyed by string(digest)
6465
sessions map[string]registry.MultipartSession // keyed by uploadID
6566
parts map[string]map[int]registry.MultipartPart // uploadID -> partNumber -> part
6667
gcCands map[string]struct{} // keyed by string(cid)
6768
}
6869

69-
// claimKey / locKey are the composite map keys for the blob_refs and
70-
// blob_locations tables (digest bytes carried as a string for comparability).
70+
// claimKey / locKey are the composite map keys for the blob_refs and the
71+
// (space, digest)-keyed tables — blob_locations, blob_encryption_params and
72+
// shard_inclusions (digest bytes carried as a string for comparability).
7173
type claimKey struct {
7274
digest, bucket, objectKey, versionID string
7375
}
@@ -86,6 +88,7 @@ func NewMemStore() *MemStore {
8688
blobRefs: map[claimKey]registry.BlobClaim{},
8789
intents: map[string]registry.UploadIntent{},
8890
locations: map[locKey]registry.BlobLocation{},
91+
encParams: map[locKey]registry.BlobEncryptionParams{},
8992
inclusions: map[locKey]registry.BlobInclusion{},
9093
parks: map[string]registry.BlobPark{},
9194
sessions: map[string]registry.MultipartSession{},

inmem/stores.go

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,19 @@ import (
1111
)
1212

1313
// In-memory implementations of the architecture's relational stores
14-
// (registry.BlobRefStore / IntentStore / LocationStore / MultipartStore /
15-
// GCStore), mirroring the Postgres tables so the in-process suite and
16-
// standalone mode exercise the same write/read/delete code paths.
14+
// (registry.BlobRefStore / IntentStore / LocationStore / EncryptionParamsStore /
15+
// MultipartStore / GCStore), mirroring the Postgres tables so the in-process
16+
// suite and standalone mode exercise the same write/read/delete code paths.
1717

1818
// Compile-time assertions: *MemStore satisfies every store interface.
1919
var (
20-
_ registry.BlobRefStore = (*MemStore)(nil)
21-
_ registry.IntentStore = (*MemStore)(nil)
22-
_ registry.LocationStore = (*MemStore)(nil)
23-
_ registry.InclusionStore = (*MemStore)(nil)
24-
_ registry.MultipartStore = (*MemStore)(nil)
25-
_ registry.GCStore = (*MemStore)(nil)
20+
_ registry.BlobRefStore = (*MemStore)(nil)
21+
_ registry.IntentStore = (*MemStore)(nil)
22+
_ registry.LocationStore = (*MemStore)(nil)
23+
_ registry.EncryptionParamsStore = (*MemStore)(nil)
24+
_ registry.InclusionStore = (*MemStore)(nil)
25+
_ registry.MultipartStore = (*MemStore)(nil)
26+
_ registry.GCStore = (*MemStore)(nil)
2627
)
2728

2829
func cloneBytes(b []byte) []byte {
@@ -123,10 +124,6 @@ func (m *MemStore) DeleteIntent(_ context.Context, digest []byte) error {
123124
// LocationStore ==============================================================
124125

125126
func (m *MemStore) PutLocation(_ context.Context, loc registry.BlobLocation) error {
126-
// Match the Postgres store: reject a partial FEE set (see ValidateFEE).
127-
if err := loc.ValidateFEE(); err != nil {
128-
return err
129-
}
130127
m.mu.Lock()
131128
defer m.mu.Unlock()
132129
m.locations[locKey{loc.Space, string(loc.Digest)}] = cloneLocation(loc)
@@ -151,6 +148,37 @@ func (m *MemStore) DeleteLocation(_ context.Context, space did.DID, digest []byt
151148
return nil
152149
}
153150

151+
// EncryptionParamsStore ======================================================
152+
153+
func (m *MemStore) PutEncryptionParams(_ context.Context, params registry.BlobEncryptionParams) error {
154+
// Match the Postgres store, whose columns are all NOT NULL.
155+
if err := params.Validate(); err != nil {
156+
return err
157+
}
158+
m.mu.Lock()
159+
defer m.mu.Unlock()
160+
m.encParams[locKey{params.Space, string(params.Digest)}] = cloneEncryptionParams(params)
161+
return nil
162+
}
163+
164+
func (m *MemStore) GetEncryptionParams(_ context.Context, space did.DID, digest []byte) (*registry.BlobEncryptionParams, error) {
165+
m.mu.Lock()
166+
defer m.mu.Unlock()
167+
params, ok := m.encParams[locKey{space, string(digest)}]
168+
if !ok {
169+
return nil, registry.ErrNotFound
170+
}
171+
cp := cloneEncryptionParams(params)
172+
return &cp, nil
173+
}
174+
175+
func (m *MemStore) DeleteEncryptionParams(_ context.Context, space did.DID, digest []byte) error {
176+
m.mu.Lock()
177+
defer m.mu.Unlock()
178+
delete(m.encParams, locKey{space, string(digest)})
179+
return nil
180+
}
181+
154182
// ParkStore ==================================================================
155183

156184
func (m *MemStore) PutPark(_ context.Context, p registry.BlobPark) error {
@@ -387,17 +415,24 @@ func cloneSession(s registry.MultipartSession) registry.MultipartSession {
387415
return s
388416
}
389417

390-
// cloneLocation deep-copies a BlobLocation's byte-slice fields (the digest and
391-
// the FEE wrap material) so the stored copy and any returned copy never alias
392-
// the caller's slices. Nil slices stay nil, preserving "unencrypted blob".
418+
// cloneLocation deep-copies a BlobLocation's digest so the stored copy and any
419+
// returned copy never alias the caller's slice.
393420
func cloneLocation(loc registry.BlobLocation) registry.BlobLocation {
394421
loc.Digest = cloneBytes(loc.Digest)
395-
loc.RegionWrappedCEK = cloneBytes(loc.RegionWrappedCEK)
396-
loc.BaseNonce = cloneBytes(loc.BaseNonce)
397-
loc.ProtectedHeader = cloneBytes(loc.ProtectedHeader)
398422
return loc
399423
}
400424

425+
// cloneEncryptionParams deep-copies a BlobEncryptionParams' byte-slice fields —
426+
// the digest and the key material — so the stored copy and any returned copy
427+
// never alias the caller's slices.
428+
func cloneEncryptionParams(p registry.BlobEncryptionParams) registry.BlobEncryptionParams {
429+
p.Digest = cloneBytes(p.Digest)
430+
p.RegionWrappedCEK = cloneBytes(p.RegionWrappedCEK)
431+
p.BaseNonce = cloneBytes(p.BaseNonce)
432+
p.AAD = cloneBytes(p.AAD)
433+
return p
434+
}
435+
401436
func clonePart(p registry.MultipartPart) registry.MultipartPart {
402437
p.ETagMD5 = cloneBytes(p.ETagMD5)
403438
if p.BlobDigests != nil {

0 commit comments

Comments
 (0)