Repository navigation
fix(sdk): MAC the legacy 4.2.2 segment as it stands, not as UTF-8 (DSPX-4703) - #1037
dmihalcik-virtru wants to merge 1 commit into
Conversation
…PX-4703) TDF spec 4.2.2 differs from the current spec in encoding only: where a modern manifest stores base64(mac), a 4.2.2 manifest stores base64(hex(mac)). The MAC covers the same bytes either way -- Go's `calculateSignature` is one `if isLegacyTDF` around the encoding with identical input on both sides. `segmentIntegrityVersion422` disagreed. Before HMACing a segment it ran the ciphertext through `buffToString(..., 'utf-8')` and re-encoded the result, which replaces every byte sequence that is not valid UTF-8 with U+FFFD. AES-GCM output is indistinguishable from random, so nearly every segment was mangled and the writer signed a digest nothing could reproduce -- not Go, not Java, and not `decryptChunk`, which HMACs the ciphertext as it stands. A 4.2.2 file this SDK wrote with HS256 segments could not be opened by this SDK. `GMAC` hid it since 4.2.2 support landed: that branch reads the AEAD tag back out of the bytes without decoding anything, and `GMAC` is the default segment algorithm, so the broken combination was never the one anyone exercised. The reader was right and the writer was wrong, so the fix is in the writer. Making the reader mangle its input to match would have been symmetric and still wrong -- self-consistent, and alone. `rootIntegrityVersion422` had the same decode. It was inert there, since a legacy aggregate hash is a run of hex digits and the round trip is the identity, but it is gone too so the two legacy writers cannot drift apart. No compatibility cost: the files this produced were already unreadable everywhere, including here. `lib/tests/mocha/legacy-422-integrity.spec.ts` is new -- 5 cases. Two round-trip a 4.2.2 file under each segment algorithm; three recompute the manifest's integrity values from the DEK and the payload bytes and compare, pinning the encoding to the cross-SDK definition rather than to whatever the writer emits, so the symmetric-but-wrong fix does not pass them. Against this branch's base, 2 of the 5 fail: the HS256 round trip (`IntegrityError: Failed integrity check on segment hash`) and the HS256 segment hash comparison. With the fix, 432 passing / 6 pending under mocha, 432 under karma, 253 under web-test-runner; `tsc --noEmit` and lint clean. DSPX-4703 Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
|
Warning Review limit reachedNext included review available in 59 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
I'm pretty sure this is not needed since this is supporting an de-launched experimental feature |



The bug
Raised by CodeRabbit on #1031. The finding is real; the diagnosis and the
proposed fix were both pointed the wrong way, so this lands separately rather
than as a patch to that PR.
TDF spec 4.2.2 differs from the current spec in encoding only. Where a
modern manifest stores
base64(mac), a 4.2.2 manifest storesbase64(hex(mac)). The MAC itself covers the same bytes either way — the GoSDK's
calculateSignatureis oneif isLegacyTDFwrapped around the encoding,with identical input on both sides of it:
segmentIntegrityVersion422disagreed. Before HMACing a segment it ran theciphertext through a UTF-8 decode and re-encoded the result:
That decode replaces every byte sequence that is not valid UTF-8 with U+FFFD,
and re-encoding writes back three bytes (
EF BF BD) where one used to be.AES-GCM output is indistinguishable from random, so nearly every segment was
mangled and the writer signed a digest that nothing could reproduce — not Go,
not Java, and not this SDK's own
decryptChunk, which HMACs the ciphertext asit stands. A 4.2.2 file this SDK wrote with
HS256segments could not beopened by this SDK.
GMAChid it for the whole life of the 4.2.2 path: that branch reads the AEADtag back out of the trailing bytes and never touches the decode, and
GMACisthe default segment algorithm. The broken combination was simply never the one
anyone ran.
This is not a regression from #1030 or #1031. Pre-#1030
decryptChunkcalledgetSignature— raw HMAC, same hex-then-base64 encoding — while the writercalled
getSignatureVersion422. #1030 renamed the pair and #1031 split it fourways; the asymmetry is older than both and untouched by either.
The fix
The reader was right and the writer was wrong, so the writer moves. CodeRabbit
proposed the opposite — teach
decryptChunkto call the 422 variant — whichwould have been symmetric and still wrong: it makes us self-consistent, and
alone. Go and Java would keep producing legacy HS256 files we reject, and we
would keep producing ones they reject.
rootIntegrityVersion422carried the same decode. It was inert there, notwrong: a legacy aggregate hash is a run of hex digits, so the round trip is the
identity. It goes too, so the two legacy writers cannot drift apart again
without someone noticing.
No compatibility cost. The files the old writer produced were already
unreadable everywhere, this SDK included, so nothing that worked stops working.
How to test
lib/tests/mocha/legacy-422-integrity.spec.tsis new — 5 cases:GMACand underHS256segments.from the DEK and the payload bytes read back out of the zip, and compare
against the manifest. These pin the encoding to the cross-SDK definition
rather than to whatever the writer happens to emit, which is the point: a
round-trip assertion alone would be satisfied by the symmetric-but-wrong fix.
Baseline
Against this branch's base, 2 of the 5 fail — the
HS256round trip(
IntegrityError: Failed integrity check on segment hash) and theHS256segment hash comparison. The three
GMACand root cases stay green, which isthe same reason nobody caught this: they were never affected.
With the fix the whole suite is green: 432 passing / 6 pending under mocha, 432
under karma, 253 under web-test-runner, and
tsc --noEmitandnpm run lintclean.
Risk
Write path, legacy spec version only. Changes the bytes we emit for
tdfSpecVersion: '4.2.2'+segmentIntegrityAlgorithm: 'HS256', a combinationthat produced unreadable files before this. Every other combination — including
the 4.2.2 default,
GMAC— is byte-for-byte unchanged, as the encoding pinsabove assert.
Interop note
Worth an xtest case: 4.2.2 + HS256 segments, written by each SDK and read by
each of the others. Nothing exercises that cell of the matrix today, which is
how this survived.
DSPX-4703