Skip to content

Commit c5148a4

Browse files
committed
refactor: share stream config and size math
BodyMaterial arrived after the range API was written, so it duplicated logic the envelope path already had. Two copies mattered, because both underpin the claim that the envelope path and the cached-material path accept the same inputs and fail the same way: - the aesstream.Config literal, built field by field in openStream and again in spanRangeReader, now bodyParams.streamConfig - the plaintext-size derivation, in both BodyMaterial.PlaintextSize and plaintextSizeFor, now plaintextSizeFrom bodyParams stays a separate type. Merging it into BodyMaterial would hand the whole-object path a material with no header length to put in it: cose.DecodeReader does not report how many bytes the envelope consumed, and Decrypt has no use for the number. Its doc comment now records what BodyMaterial adds and why the two shapes differ. Validate stays off the envelope paths. validateBody checks that the iv header is present but not that it is BaseNonceSize bytes, so a short iv keeps failing in the body cipher rather than as ErrIncompleteMaterial. Internal only: the exported surface is unchanged. Signed-off-by: Miroslav Bajtoš <oss@bajtos.net> Assisted-by: Claude:claude-opus-5
1 parent ce0ed5b commit c5148a4

3 files changed

Lines changed: 53 additions & 26 deletions

File tree

fee.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -534,12 +534,7 @@ func openStream(env *cose.Envelope, ciphertext io.Reader, cek []byte) (io.Reader
534534
if err != nil {
535535
return nil, err
536536
}
537-
r, err := aesstream.NewReader(ciphertext, aesstream.Config{
538-
Key: cek,
539-
BaseNonce: body.baseNonce,
540-
AAD: body.aad,
541-
ChunkSize: body.chunkSize,
542-
})
537+
r, err := aesstream.NewReader(ciphertext, body.streamConfig(cek))
543538
if err != nil {
544539
return nil, fmt.Errorf("fee: initializing body cipher: %w", err)
545540
}
@@ -549,12 +544,32 @@ func openStream(env *cose.Envelope, ciphertext io.Reader, cek []byte) (io.Reader
549544
// bodyParams is the validated STREAM configuration a FEE envelope's body header
550545
// carries: everything fee/aesstream needs to decrypt the detached ciphertext
551546
// apart from the content-encryption key.
547+
//
548+
// [BodyMaterial] is the same parameters plus the envelope's encoded length —
549+
// where the ciphertext starts within a stored blob. That is what a caller caches
550+
// and what a range read needs; a whole-object read has neither the number (the
551+
// streaming decoder does not report it) nor a use for it, so the two shapes stay
552+
// distinct and [BodyMaterial.body] converts one way.
552553
type bodyParams struct {
553554
baseNonce []byte
554555
chunkSize int
555556
aad []byte
556557
}
557558

559+
// streamConfig returns the fee/aesstream configuration for decrypting a body
560+
// with these parameters under cek. It is the only place FEE body parameters
561+
// become a stream configuration, so the whole-object reader ([openStream]) and
562+
// the range reader ([spanRangeReader]) cannot drift apart in how they configure
563+
// the cipher.
564+
func (b bodyParams) streamConfig(cek []byte) aesstream.Config {
565+
return aesstream.Config{
566+
Key: cek,
567+
BaseNonce: b.baseNonce,
568+
AAD: b.aad,
569+
ChunkSize: b.chunkSize,
570+
}
571+
}
572+
558573
// validateBody checks a decoded envelope's FEE body headers — the algorithm is
559574
// the chunked AES-256-GCM-STREAM cipher, the base nonce (iv) is present, and the
560575
// self-describing chunk size is in range — and rebuilds the Enc_structure AAD

material.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,36 @@ func (m BodyMaterial) PlaintextSize(blobSize int64) (int64, error) {
9696
if err := m.Validate(); err != nil {
9797
return 0, err
9898
}
99-
ciphertextSize := blobSize - m.HeaderLen
99+
return plaintextSizeFrom(blobSize, m.HeaderLen, m.ChunkSize)
100+
}
101+
102+
// plaintextSizeFrom derives the total plaintext size of a blobSize-byte blob
103+
// whose envelope occupies headerLen bytes and whose STREAM chunks carry chunkSize
104+
// plaintext bytes each.
105+
//
106+
// It is shared by [BodyMaterial.PlaintextSize] and the envelope-backed paths (via
107+
// plaintextSizeFor), so a blob size that cannot describe a FEE body is reported
108+
// the same way whether the parameters came from a cache or from the envelope.
109+
func plaintextSizeFrom(blobSize, headerLen int64, chunkSize int) (int64, error) {
110+
ciphertextSize := blobSize - headerLen
100111
if ciphertextSize < 0 {
101112
return 0, fmt.Errorf("fee: blob size %d is shorter than its %d-byte envelope: %w",
102-
blobSize, m.HeaderLen, aesstream.ErrCiphertextSize)
113+
blobSize, headerLen, aesstream.ErrCiphertextSize)
103114
}
104-
n, err := aesstream.DecryptedSize(ciphertextSize, m.ChunkSize)
115+
n, err := aesstream.DecryptedSize(ciphertextSize, chunkSize)
105116
if err != nil {
106117
return 0, fmt.Errorf("fee: blob of %d ciphertext bytes: %w", ciphertextSize, err)
107118
}
108119
return n, nil
109120
}
110121

122+
// body returns the envelope body parameters m describes, for the range wiring it
123+
// shares with the envelope-backed paths. HeaderLen is not among them: it says
124+
// where the ciphertext starts, not how to decrypt it.
125+
func (m BodyMaterial) body() bodyParams {
126+
return bodyParams{baseNonce: m.BaseNonce, chunkSize: m.ChunkSize, aad: m.AAD}
127+
}
128+
111129
// clone returns a deep copy, so a BodyMaterial handed to a caller shares no
112130
// backing array with the envelope it came from (and one handed back to us cannot
113131
// be mutated underneath a live reader).

range.go

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,7 @@ func DecryptRangeWithMaterial(blob io.ReaderAt, blobSize int64, m BodyMaterial,
202202
if err != nil {
203203
return nil, err
204204
}
205-
body := bodyParams{baseNonce: m.BaseNonce, chunkSize: m.ChunkSize, aad: m.AAD}
206-
return spanRangeReader(blob, blobSize, m.HeaderLen, body, plainSize, cek, off, length)
205+
return spanRangeReader(blob, blobSize, m.HeaderLen, m.body(), plainSize, cek, off, length)
207206
}
208207

209208
// PlaintextSize reports the total decrypted size of a FEE blob from its envelope
@@ -222,7 +221,7 @@ func PlaintextSize(blob io.ReaderAt, blobSize int64) (int64, error) {
222221
if err != nil {
223222
return 0, err
224223
}
225-
return plaintextSizeFor(env, blobSize-headerLen, body.chunkSize)
224+
return plaintextSizeFor(env, blobSize, headerLen, body.chunkSize)
226225
}
227226

228227
// newRangeReader is the shared core of DecryptRange and DecryptRangeWithCEK:
@@ -240,7 +239,7 @@ func newRangeReader(env *cose.Envelope, blob io.ReaderAt, blobSize, headerLen in
240239
return nil, err
241240
}
242241

243-
plainSize, err := plaintextSizeFor(env, blobSize-headerLen, body.chunkSize)
242+
plainSize, err := plaintextSizeFor(env, blobSize, headerLen, body.chunkSize)
244243
if err != nil {
245244
return nil, err
246245
}
@@ -268,12 +267,7 @@ func spanRangeReader(blob io.ReaderAt, blobSize, headerLen int64, body bodyParam
268267
// aesstream exactly the bytes it will ask for and nothing else.
269268
sr, err := aesstream.NewSpanReader(
270269
io.NewSectionReader(blob, headerLen+start, n),
271-
aesstream.Config{
272-
Key: cek,
273-
BaseNonce: body.baseNonce,
274-
AAD: body.aad,
275-
ChunkSize: body.chunkSize,
276-
},
270+
body.streamConfig(cek),
277271
ciphertextSize, off, length)
278272
if err != nil {
279273
return nil, fmt.Errorf("fee: initializing body cipher: %w", err)
@@ -282,14 +276,14 @@ func spanRangeReader(blob io.ReaderAt, blobSize, headerLen int64, body bodyParam
282276
return &RangeReader{sr: sr, size: plainSize, spanOff: headerLen + start, spanLen: n}, nil
283277
}
284278

285-
// plaintextSizeFor derives the object's total plaintext size from its ciphertext
286-
// length and chunk size, and cross-checks the envelope's declared chunk count
287-
// against it when one is present — catching a blob size that describes a
288-
// different object than the envelope does.
289-
func plaintextSizeFor(env *cose.Envelope, ciphertextSize int64, chunkSize int) (int64, error) {
290-
plainSize, err := aesstream.DecryptedSize(ciphertextSize, chunkSize)
279+
// plaintextSizeFor is [plaintextSizeFrom] for a blob whose parameters came from
280+
// its envelope rather than from a caller's cache: it adds the cross-check of the
281+
// envelope's declared chunk count against the derived size, when one is present,
282+
// catching a blob size that describes a different object than the envelope does.
283+
func plaintextSizeFor(env *cose.Envelope, blobSize, headerLen int64, chunkSize int) (int64, error) {
284+
plainSize, err := plaintextSizeFrom(blobSize, headerLen, chunkSize)
291285
if err != nil {
292-
return 0, fmt.Errorf("fee: blob of %d ciphertext bytes: %w", ciphertextSize, err)
286+
return 0, err
293287
}
294288
if !env.Headers.Unprotected.Has(labelChunkCount) {
295289
return plainSize, nil

0 commit comments

Comments
 (0)