Skip to content

Commit 46a967b

Browse files
committed
refactor: split body params from AAD
Keep header-only validation separate from AAD reconstruction so PlaintextSize avoids building Enc_structure bytes it does not use. Return validated body parameters and rebuilt AAD separately in the body-cipher paths, and split the cached descriptor accessors to match. Assisted-by: Copilot:gpt-5.4
1 parent eeff7ba commit 46a967b

3 files changed

Lines changed: 56 additions & 38 deletions

File tree

descriptor.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,16 @@ func plaintextSizeFrom(blobSize, headerLen int64, chunkSize int) (int64, error)
120120
return n, nil
121121
}
122122

123-
// body returns the envelope body parameters m describes, for the range wiring it
124-
// shares with the envelope-backed paths. HeaderLen is not among them: it says
125-
// where the ciphertext starts, not how to decrypt it.
126-
func (m BodyDescriptor) body() bodyParams {
127-
return bodyParams{baseNonce: m.BaseNonce, chunkSize: m.ChunkSize, aad: m.AAD}
123+
// bodyParams returns the envelope body parameters m describes, for the range
124+
// wiring it shares with the envelope-backed paths. HeaderLen is not among them:
125+
// it says where the ciphertext starts, not how to decrypt it.
126+
func (m BodyDescriptor) bodyParams() bodyParams {
127+
return bodyParams{baseNonce: m.BaseNonce, chunkSize: m.ChunkSize}
128+
}
129+
130+
// aad returns the cached Enc_structure bytes m carries for body decryption.
131+
func (m BodyDescriptor) aad() []byte {
132+
return m.AAD
128133
}
129134

130135
// clone returns a deep copy, so a BodyDescriptor handed to a caller shares no

fee.go

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -518,55 +518,56 @@ func DecryptWithCEK(src io.Reader, cek []byte) (io.Reader, error) {
518518
// lazily on later reads, which work from the internalized key, never the cek
519519
// slice.
520520
func openStream(env *cose.Envelope, ciphertext io.Reader, cek []byte) (io.Reader, error) {
521-
body, err := validateBody(env)
521+
body, aad, err := buildBodyParamsWithAAD(env)
522522
if err != nil {
523523
return nil, err
524524
}
525-
r, err := aesstream.NewReader(ciphertext, body.streamConfig(cek))
525+
r, err := aesstream.NewReader(ciphertext, body.streamConfig(cek, aad))
526526
if err != nil {
527527
return nil, fmt.Errorf("fee: initializing body cipher: %w", err)
528528
}
529529
return r, nil
530530
}
531531

532532
// bodyParams is the validated STREAM configuration a FEE envelope's body header
533-
// carries: everything fee/aesstream needs to decrypt the detached ciphertext
534-
// apart from the content-encryption key.
533+
// carries: everything fee/aesstream needs to size and locate the detached
534+
// ciphertext apart from the content-encryption key and Enc_structure AAD.
535535
//
536-
// [BodyDescriptor] is the same parameters plus the envelope's encoded length —
536+
// [buildBodyParamsWithAAD] rebuilds that AAD alongside the validated params, and
537+
// [BodyDescriptor] caches the same inputs plus the envelope's encoded length —
537538
// where the ciphertext starts within a stored blob. That is what a caller caches
538539
// and what a range read needs; a whole-object read has neither the number (the
539-
// streaming decoder does not report it) nor a use for it, so the two shapes stay
540-
// distinct and [BodyDescriptor.body] converts one way.
540+
// streaming decoder does not report it) nor a use for it, so the shapes stay
541+
// distinct and [BodyDescriptor.bodyParams] / [BodyDescriptor.aad] convert one
542+
// way.
541543
type bodyParams struct {
542544
baseNonce []byte
543545
chunkSize int
544-
aad []byte
545546
}
546547

547548
// streamConfig returns the fee/aesstream configuration for decrypting a body
548-
// with these parameters under cek. It is the only place FEE body parameters
549-
// become a stream configuration, so the whole-object reader ([openStream]) and
550-
// the range reader ([spanRangeReader]) cannot drift apart in how they configure
551-
// the cipher.
552-
func (b bodyParams) streamConfig(cek []byte) aesstream.Config {
549+
// with these parameters under cek, authenticating aad as the envelope
550+
// Enc_structure. It is the only place FEE body parameters become a stream
551+
// configuration, so the whole-object reader ([openStream]) and the range reader
552+
// ([spanRangeReader]) cannot drift apart in how they configure the cipher.
553+
func (b bodyParams) streamConfig(cek, aad []byte) aesstream.Config {
553554
return aesstream.Config{
554555
Key: cek,
555556
BaseNonce: b.baseNonce,
556-
AAD: b.aad,
557+
AAD: aad,
557558
ChunkSize: b.chunkSize,
558559
}
559560
}
560561

561-
// validateBody checks a decoded envelope's FEE body headers — the algorithm is
562-
// the chunked AES-256-GCM-STREAM cipher, the base nonce (iv) is present, and the
563-
// self-describing chunk size is in range — and rebuilds the Enc_structure AAD
564-
// that the encoder bound into every chunk.
562+
// validateBodyParams checks a decoded envelope's FEE body headers — the
563+
// algorithm is the chunked AES-256-GCM-STREAM cipher, the base nonce (iv) is
564+
// present, and the self-describing chunk size is in range.
565565
//
566-
// It is shared by the whole-object path ([openStream]) and the range path
567-
// ([newRangeReader]), so both accept exactly the same envelopes and report the
568-
// same errors for a body header they cannot honour.
569-
func validateBody(env *cose.Envelope) (bodyParams, error) {
566+
// It is shared by the whole-object path's AAD-building resolution
567+
// ([buildBodyParamsWithAAD]) and the header-only sizing path ([PlaintextSize]),
568+
// so both accept exactly the same envelopes and report the same errors for a
569+
// body header they cannot honour.
570+
func validateBodyParams(env *cose.Envelope) (bodyParams, error) {
570571
alg, ok := env.Headers.Protected.Int(cose.HeaderLabelAlg)
571572
if !ok {
572573
return bodyParams{}, fmt.Errorf("%w: body algorithm header missing or not an integer", ErrUnsupportedBodyAlg)
@@ -597,15 +598,26 @@ func validateBody(env *cose.Envelope) (bodyParams, error) {
597598
ErrMalformedEnvelope, chunkSize, aesstream.MinChunkSize, aesstream.MaxChunkSize)
598599
}
599600

601+
return bodyParams{baseNonce: baseNonce, chunkSize: int(chunkSize)}, nil
602+
}
603+
604+
// buildBodyParamsWithAAD validates the body headers and rebuilds the
605+
// Enc_structure AAD that the encoder bound into every chunk.
606+
func buildBodyParamsWithAAD(env *cose.Envelope) (bodyParams, []byte, error) {
607+
body, err := validateBodyParams(env)
608+
if err != nil {
609+
return bodyParams{}, nil, err
610+
}
611+
600612
// The decrypt-side AAD is rebuilt from the decoded envelope, using the
601613
// Enc_structure context that matches its tag — byte-identical to the value
602614
// the encoder bound into every chunk.
603615
aad, err := env.EncStructure(nil)
604616
if err != nil {
605-
return bodyParams{}, fmt.Errorf("fee: building envelope AAD: %w", err)
617+
return bodyParams{}, nil, fmt.Errorf("fee: building envelope AAD: %w", err)
606618
}
607619

608-
return bodyParams{baseNonce: baseNonce, chunkSize: int(chunkSize), aad: aad}, nil
620+
return body, aad, nil
609621
}
610622

611623
// checkCEK reports whether a caller-provided content-encryption key is the right

range.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ func DecryptRangeWithCEK(blob io.ReaderAt, blobSize int64, cek []byte, off, leng
201201
if err != nil {
202202
return nil, err
203203
}
204-
return spanRangeReader(blob, blobSize, d.HeaderLen, d.body(), plainSize, cek, off, length)
204+
return spanRangeReader(blob, blobSize, d.HeaderLen, d.bodyParams(), d.aad(), plainSize, cek, off, length)
205205
}
206206

207207
env, headerLen, err := decodeHeaderAt(blob, blobSize)
@@ -223,7 +223,7 @@ func PlaintextSize(blob io.ReaderAt, blobSize int64) (int64, error) {
223223
if err != nil {
224224
return 0, err
225225
}
226-
body, err := validateBody(env)
226+
body, err := validateBodyParams(env)
227227
if err != nil {
228228
return 0, err
229229
}
@@ -241,7 +241,7 @@ func PlaintextSize(blob io.ReaderAt, blobSize int64) (int64, error) {
241241
// as soon as this returns — even though the reader decrypts lazily on later
242242
// reads, which work from the internalized key, never the cek slice.
243243
func newRangeReader(env *cose.Envelope, blob io.ReaderAt, blobSize, headerLen int64, cek []byte, off, length int64) (*RangeReader, error) {
244-
body, err := validateBody(env)
244+
body, aad, err := buildBodyParamsWithAAD(env)
245245
if err != nil {
246246
return nil, err
247247
}
@@ -250,7 +250,7 @@ func newRangeReader(env *cose.Envelope, blob io.ReaderAt, blobSize, headerLen in
250250
if err != nil {
251251
return nil, err
252252
}
253-
return spanRangeReader(blob, blobSize, headerLen, body, plainSize, cek, off, length)
253+
return spanRangeReader(blob, blobSize, headerLen, body, aad, plainSize, cek, off, length)
254254
}
255255

256256
// spanRangeReader is the geometry-and-wiring tail shared by the envelope-backed
@@ -259,10 +259,11 @@ func newRangeReader(env *cose.Envelope, blob io.ReaderAt, blobSize, headerLen in
259259
// parameters and the object's plaintext size, it resolves the ciphertext span
260260
// the range overlaps and hands exactly that span to the body cipher.
261261
//
262-
// The two paths differ only in how they arrive at body and plainSize — decoded
263-
// from the envelope, or supplied from a caller's cache — so keeping the wiring in
264-
// one place is what makes them accept the same ranges and fail the same way.
265-
func spanRangeReader(blob io.ReaderAt, blobSize, headerLen int64, body bodyParams, plainSize int64, cek []byte, off, length int64) (*RangeReader, error) {
262+
// The two paths differ only in how they arrive at body, aad and plainSize —
263+
// decoded from the envelope, or supplied from a caller's cache — so keeping the
264+
// wiring in one place is what makes them accept the same ranges and fail the
265+
// same way.
266+
func spanRangeReader(blob io.ReaderAt, blobSize, headerLen int64, body bodyParams, aad []byte, plainSize int64, cek []byte, off, length int64) (*RangeReader, error) {
266267
ciphertextSize := blobSize - headerLen
267268

268269
start, n, plainLen, err := aesstream.CiphertextRange(ciphertextSize, body.chunkSize, off, length)
@@ -282,7 +283,7 @@ func spanRangeReader(blob io.ReaderAt, blobSize, headerLen int64, body bodyParam
282283
// aesstream exactly the bytes it will ask for and nothing else.
283284
sr, err := aesstream.NewSpanReader(
284285
io.NewSectionReader(blob, headerLen+start, n),
285-
body.streamConfig(cek),
286+
body.streamConfig(cek, aad),
286287
ciphertextSize, off, length)
287288
if err != nil {
288289
return nil, fmt.Errorf("fee: initializing body cipher: %w", err)

0 commit comments

Comments
 (0)