Skip to content

feat(sdk): pure scale-limit sizing helpers and a segment-integrity ADR (DSPX-4650) - #1020

Open
dmihalcik-virtru wants to merge 1 commit into
DSPX-4651-manifest-fail-fastfrom
DSPX-4650-scale-limits
Open

feat(sdk): pure scale-limit sizing helpers and a segment-integrity ADR (DSPX-4650)#1020
dmihalcik-virtru wants to merge 1 commit into
DSPX-4651-manifest-fail-fastfrom
DSPX-4650-scale-limits

Conversation

@dmihalcik-virtru

@dmihalcik-virtru dmihalcik-virtru commented Sep 8, 2026

Copy link
Copy Markdown
Member

Stacked on #1019#1018#1017. Review the parents first.

Jira: DSPX-4650 (sub-task of DSPX-4648, under epic DSPX-4502).

What

Item 0 of the 50 TiB workstream. Two things, both prerequisites for everything after them, neither of which changes any behaviour:

  1. A decision, written down. spec/DSPX-4650-segment-integrity-algorithm.md records that per-segment integrity stays GMAC and the root signature stays HS256 — the current defaults — and why.
  2. The size arithmetic, extracted. lib/tdf3/src/utils/scale-limits.ts adds base64Length, perSegmentEntryBytes, finalSegmentEntryBytes, segmentCountFor, estimateSegmentsArrayBytes, estimateManifestBytes and minSegmentSizeFor. All pure, all functions of sizes only. Nothing calls them yet.

Also moves the plan document into spec/ as DSPX-4648-web-sdk-large-files.md, matching the existing spec/DSPX-*.md convention.

Why the ADR blocks the rest

The manifest carries one segments[] entry per segment, and the digest in that entry is the only part of the manifest that scales with payload size:

alg digest base64 entry ({"hash":"…"},)
GMAC 16 B 24 B 36 B
HS256 32 B 44 B 56 B

At 50 TiB with 16 MiB segments that is 118 MB of manifest versus 184 MB — a 1.56× factor on the single quantity the whole plan is trying to bound. Picking a segment size, a manifest cap, or a maximum encryptable payload before fixing this constant means deriving all three twice.

The decision: GMAC is free — getSignature() (lib/tdf3/src/tdf.ts:369-372) returns content.slice(-16), the AES-GCM auth tag the cipher already computed. HS256 is a separate HMAC-SHA256 pass over every segment's ciphertext: at 50 TiB, 50 TiB of extra hashing on encrypt and another 50 TiB on decrypt-verify, competing with the encrypt for the same WebCrypto queue. It buys an integrity value nominally independent of the cipher, but getSignature keys the HMAC from the same unwrapped key, so that independence is weaker in practice than in principle. Not worth it per-segment; kept for the root signature, where it is computed once over the concatenated segment hashes rather than over the payload.

But every limit is still derived for HS256. perSegmentEntryBytes(alg) takes the algorithm as a parameter, so the limits are a function of it rather than of the shipped default. The landing point in item 1 (16 MiB segments, 256 MiB cap) is chosen to fit 50 TiB at 56 B/entry; running at 36 B/entry just leaves headroom. If the default ever moves, nothing needs re-deriving and no limit silently loosens.

Why extract the arithmetic

Whether a manifest will fit, how big a payload may be, what segment size to pick and how much to prefetch are all pure functions of sizes — none of them need a byte of payload. Separating them means the behaviour at 50 TiB is reachable in a unit test instead of only by encrypting 50 TiB. That is the whole testing strategy for DSPX-4648, and it only works if the estimate provably matches what the serializer really emits.

How the constants are pinned

Not by asserting on a hand-built entry — that would just restate the bug. lib/tests/mocha/helpers/write-tdf.ts drives writeStream directly with mock keys, no KAS and no network, so a real container can be written with a 64-byte segment size. The specs then measure deltas against real emitted manifests, for both algorithms:

  • a 5-segment TDF's segments array minus a 4-segment one equals perSegmentEntryBytes(alg) exactly;
  • the trailing partial segment (which carries explicit segmentSize/encryptedSegmentSize) costs finalSegmentEntryBytes(...) exactly;
  • estimateSegmentsArrayBytes never under-estimates, swept over [0, 1, 63, 64, 65, 127, 128, 200, 640] bytes.

If the serializer ever changes shape — a renamed field, a new per-segment key — those fail rather than the estimate silently drifting. Verified red-first: perturbing SEGMENT_ENTRY_OVERHEAD_BYTES by one byte fails 13 of the 19 tests.

The estimate is deliberately an over-estimate: every entry is counted with its separating comma even though the last has none, which pays for the closing bracket with a byte to spare. Under-estimating would let a doomed encrypt start.

The numbers

A table-driven block asserts the plan's sizing directly, at 50 TiB, in milliseconds:

50TiB@1MiB/HS256  -> ~2936 MB
50TiB@4MiB/HS256  ->  ~734 MB
50TiB@16MiB/HS256 ->  ~184 MB   <- recommended landing point
50TiB@64MiB/HS256 ->   ~46 MB
50TiB@1MiB/GMAC   -> ~1887 MB
50TiB@16MiB/GMAC  ->  ~118 MB

minSegmentSizeFor(50 TiB) === 12,800 — the floor imposed by MAX_GCM_INVOCATIONS_PER_KEY from #1018. Every segment size under consideration is orders of magnitude above it, so the IV ceiling is not the binding constraint, but it is the floor any future default has to clear.

How to test

cd lib && npm run build && npx mocha 'dist/web/tests/mocha/unit/scale-limits.spec.js'

19 passing, ~30 ms. Full suite: 410 passing, 0 failing.

Risk

None to runtime behaviour — no existing code path calls any of this. The risk is that the estimator is wrong, which is what the serializer-agreement tests are for.

Item 0 of DSPX-4648: fix the per-segment integrity algorithm before any of
the 50 TiB limits are derived from it, and extract the size arithmetic those
limits need into pure functions.

Decision (spec/DSPX-4650-segment-integrity-algorithm.md): per-segment
integrity stays GMAC -- free, since getSignature just returns the AES-GCM auth
tag the cipher already computed -- while the root signature stays HS256. No
default changes. Every limit is nonetheless derived for HS256, the more
expensive case at 56 vs 36 bytes per manifest segment entry, so moving the
default later loosens nothing and needs no re-derivation.

Adds lib/tdf3/src/utils/scale-limits.ts: base64Length, perSegmentEntryBytes,
finalSegmentEntryBytes, segmentCountFor, estimateSegmentsArrayBytes,
estimateManifestBytes and minSegmentSizeFor. All pure, all functions of sizes
only, so 50 TiB behaviour is reachable in a unit test rather than only by
encrypting 50 TiB. Nothing calls them yet; this is behaviour-neutral.

The constants are pinned against the real serializer rather than a hand-built
entry: tests/mocha/helpers/write-tdf.ts drives writeStream with no KAS and no
network, and the specs measure what a 5th segment actually adds to an emitted
manifest for both GMAC and HS256. Perturbing any entry-cost constant by one
byte fails 13 of the 19 tests.

Also moves the plan document into spec/ as DSPX-4648-web-sdk-large-files.md.

DSPX-4650
@dmihalcik-virtru
dmihalcik-virtru requested a review from a team as a code owner September 8, 2026 22:19
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 32430362-99a6-45e0-8993-8f55c13eaa40

📥 Commits

Reviewing files that changed from the base of the PR and between d3028cf and 1d6a578.

📒 Files selected for processing (5)
  • lib/tdf3/src/utils/scale-limits.ts
  • lib/tests/mocha/helpers/write-tdf.ts
  • lib/tests/mocha/unit/scale-limits.spec.ts
  • spec/DSPX-4648-web-sdk-large-files.md
  • spec/DSPX-4650-segment-integrity-algorithm.md

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant