Skip to content

Replace the v1/v2 Security Envelope With Per-Session v3/v4 Protocols - #156

Merged
AndrewRedican merged 17 commits into
mainfrom
week-2026-08-31_3
Sep 7, 2026
Merged

AndrewRedican merged 17 commits into
mainfrom
week-2026-08-31_3

Conversation

@AndrewRedican

Copy link
Copy Markdown
Owner

Description

Replaces the v1/v2 security envelope with two per-session protocols, v3 and v4, across @hyperfrontend/cryptography, @hyperfrontend/network-protocol, @hyperfrontend/nexus, and @hyperfrontend/features.

The old envelope ran PBKDF2 (100,000 iterations, about 17 ms) on every message on both sides because each packet carried a fresh random salt, so concurrent channels shared one machine-wide derivation budget: a host with seven open features received about nine messages a second in total and silently dropped the rest (showcase finding F-011, cleared in this PR). v1 also keyed that work from a clock-derived password anyone can compute, v2 paid the derivation twice, the envelope sequence was never checked, and a secured channel still routed plaintext actions.

v3 and v4 agree AES-GCM-256 keys once per session from an ephemeral P-256 key agreement (v4 mixes in one PBKDF2 stretch of the pre-shared key at 600,000 iterations), expand them per direction with HKDF bound to the protocol and both endpoint ids, seal each packet under a counter nonce with the ten-byte frame header as authenticated data, and reject a replayed or foreign frame before decrypting it. nexus runs the hello exchange inside its secure transport, treats the counterpart's first authenticated frame as the session confirmation, closes a session nobody confirms, pins the negotiated protocol on both sides, and drops plaintext on a secured channel. features registers the envelope fail-closed, enforces the v4 key rules, forwards every dropped packet as an error, and ends a refused handshake at once.

Measured on the envelope alone (Node, in-process wiring, quiet machine): seven channels at 100 Hz deliver 100% at about 663 messages/s with the process 79% idle, against 9% delivered in the finding's browser measurement; session setup costs 5.8 ms (v3) or 192 ms (v4) once per session.

Type of Change

  • 💥 Breaking change
  • ✨ Feature
  • ⚡ Performance
  • 🔒 Security
  • 📝 Docs

Changes Made

  • cryptography (1.1.0, additive): createKeyAgreement (ECDH P-256, non-extractable private key, peer point validation), expandKey (HKDF-SHA256 into usage-restricted AES-GCM-256 keys), seal/open (AES-GCM with additional authenticated data), stretchPassword (PBKDF2 deriveBits), and a shared keyStretchingConfig that generateKey now reads.
  • network-protocol (2.0.0): v3 and v4 session protocols on one implementation (99-byte plaintext hello, 10-byte authenticated data-frame header, counter nonces, replay rejection, typed ProtocolError codes); one seal stage and one open stage with an array-backed FIFO; onDrop reporting; createChannel(label, { send, receive, protocolProvider, session, onDrop? }); the browser|node/v1|v2 entries, bundle/v1|v2 builds, password-keyed packet and data exports, six staged queues, time-window helpers, and the in-band dynamic key are removed; CDN default moves to bundle/v4.
  • nexus (3.0.0): the secure transport owns the session (hello posted after ACCEPT/OPEN and retried every requestRetryMs, sealed [nexus] security-confirmed, security-ready on the first authenticated inbound frame, security-unconfirmed close after connectTimeoutMs); responder attaches its transport only when composing ACCEPT; protocol pin on both sides; ACCEPT and OPEN must answer the pending process; plaintext gate for every non-handshake action; typed SecurityErrorCode; the protocol loader, broker-wide default key settings, per-channel sharedKey/refreshRate, the never-emitted security-negotiated event, and SecurityTransport.isReady are removed; peer dependency on network-protocol 2.0.0.
  • features (0.9.0): SecurityProtocol = 'none' | 'v3' | 'v4' across createShell, createFeature, feature.config, hf build --protocol, the build executor schema, generators, and the debug UI; registerSecurity returns fail-closed settings, requires a 16-character v4 key, and rejects a key given with another protocol; security-error forwarded as error { reason: 'security-error', code }; the host destroys the mount on deny or a counterpart cancel (handshake-cancelled) and the feature's ready() rejects at once.
  • Docs: package READMEs and architecture guides rewritten for the session envelope (wire format, key schedule, handshake sequence with hello and confirmation, event semantics, error codes); docs-site v1/v2 entry pages replaced by v3/v4, security concept page, quick start, guides, article snippet, and decision-framework notes and dataset updated; root ARCHITECTURE, MANIFESTO, README, LIBRARY_COMPATIBILITY, and the build-executor README follow.
  • Registry: showcase finding F-011 cleared (file and index row removed, cross-references updated); version bumps and changelogs emitted by the versioning harness.

Testing

  • Unit and integration suites rewritten for all four packages on the node test runner; coverage against thresholds: cryptography 100 / 100 / 100, network-protocol 99.77 / 99.10 / 98.31 (99 / 98 / 98), nexus 99.91 / 99.55 / 99.68 (99 / 98 / 99), features 99.96 / 99.43 / 100 (99 / 99 / 100).
  • New coverage includes: every protocol error code, counter monotonicity and cross-direction rejection, session binding, hello accept/duplicate/reject, the transport's confirmation and deadline, the plaintext gate, ACCEPT/OPEN process checks, forged frames and hellos against real brokers, wrong-key v4 pairs, fail-closed mismatches between host and feature, and the deny/cancel teardown on both sides.
  • Package e2e: network-protocol ESM/CJS/IIFE/UMD suites exercise the v3/v4 entries, bundle globals, hello exchange, seal/open across two channels, replay and wrong-key rejection, and the shared-key gate against the built package; the features CLI check builds with --protocol v4.
  • Benchmark (f011-envelope-bench.ts, not committed): v3 7 × 100 Hz 100% at 663/s (79% idle), v4 7 × 100 Hz 100% at 667/s, v4 7 × 1000 Hz 100% at 4,255/s with a 0.46 s drain.
  • Lint, typecheck, and format clean for the four packages; docs-site typecheck, link validation (0 broken of 2,009), sitemap validation, and the decision-framework drift guard pass.

Checklist

  • I have performed a self-review of my code
  • I have added/updated tests as needed
  • I have updated relevant documentation
  • I have used npm run commit for conventional commit messages

AI Assistance

GitHub Copilot was used to generate this PR description.

Additional Notes

  • Breaking, no wire compatibility. v1 and v2 are deleted, not deprecated. An old build and a new build advertise disjoint identifiers, negotiate none, and a fail-closed channel is denied with security-unavailable; both sides of a channel move together. Migration is a mode mapping: v1 (keyless) becomes v3, v2 with a sharedKey becomes v4 with a sharedKey of at least 16 characters (a generated key of 128 bits or more is the guarantee).
  • Security claims are stated per protocol. v3 defeats scripts that can only listen; any script that can post to a peer's window with a genuine source can complete a v3 session as that peer, so v3 does not authenticate the counterpart. v4 is the control against the documented co-resident-script adversary. The handshake and the hello frames remain plaintext by design.
  • Behaviour changes reviewers may notice: security-ready now carries { protocol } and fires after the counterpart's first sealed frame authenticates; security-error.code is the protocol's kebab-case verdict; CloseReason gains 'security-unconfirmed'; Data no longer carries key; the features SDK is fail-closed by default (nexus keeps fail-open for direct users).
  • Measurement scope. The throughput figures are the envelope alone in Node; the browser end-to-end number for the koi pond is not yet taken because the demos are frozen on features 0.5.1 with protocol: 'v1' and keep working on that release until they are explicitly re-vendored.
  • Publish order: cryptography, network-protocol, nexus, features.

v1 keyed every packet from a password anyone can compute from the clock and v2 paid the same per-packet PBKDF2 twice, so concurrent channels shared one machine-wide derivation budget and a receiver whose queue aged past the password window discarded frames without reporting them. v3 and v4 agree AES-GCM-256 keys once per session from an ephemeral P-256 agreement (v4 mixes in a PBKDF2 stretch of the pre-shared key), seal each packet under a counter nonce with the ten-byte header as authenticated data, and reject a replayed or foreign frame before decrypting it. One seal stage and one open stage replace the encrypt, serialize, and obfuscate queues; every rejected packet reaches the channel's onDrop with a protocol error code.

BREAKING CHANGE: the browser/v1, browser/v2, node/v1, and node/v2 entries, the bundle/v1 and bundle/v2 builds, the password-keyed packet and data exports, and the six staged queues are removed; createChannel takes an options object with a session, ProtocolProvider receives the session, and Data no longer carries a key.
…fore security-ready

The transport owns the session: it posts the protocol's hello after ACCEPT or OPEN and again every requestRetryMs, seals a confirmation once the peer's hello keys the session, and treats the first inbound frame that authenticates as proof that the peer derived the same keys. Nothing authenticated within connectTimeoutMs ends the session silently with reason security-unconfirmed. The responder attaches its transport only when it composes ACCEPT, both sides pin the protocol the channel asked for, ACCEPT and OPEN must answer the pending process, and a plaintext action other than the six handshake actions is dropped on a secured channel.

BREAKING CHANGE: SecurityProtocolVersion is 'none' | 'v3' | 'v4'; the protocol loader, the default protocol, shared key, and refresh-rate settings, the per-channel sharedKey and refreshRate, the security-negotiated event, SecurityTransport.isReady, and the securityReady channel state are removed; security-ready carries { protocol } and fires only after the counterpart's first sealed frame authenticates; security-error codes are the protocol's kebab-case verdicts; the peer dependency is @hyperfrontend/network-protocol 2.0.0.
…hared-key rules

A selected envelope now opens on that protocol or not at all: registerSecurity returns fail-closed channel settings, so a counterpart that cannot run the pinned protocol is denied instead of silently downgraded to plaintext. v4 requires a shared key of at least 16 characters, a key given with v3 or none throws, and both checks run in the caller's frame. A refused handshake ends at once: the host destroys the mount on a deny or a counterpart's cancel (error reason handshake-cancelled) and the feature's ready() rejects, instead of both waiting out the open deadline. A packet the envelope drops reaches the shell and the feature as error { reason: 'security-error' } with the protocol's code.

BREAKING CHANGE: SecurityProtocol is 'none' | 'v3' | 'v4' across createShell, createFeature, feature.config, hf build --protocol, and the build executor schema; v1 and v2 no longer build, negotiation is fail-closed, and v4 rejects a shared key shorter than 16 characters.
@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
docs-site Ready Ready Preview Sep 7, 2026 8:17am UTC

Request Review

@nx-cloud

nx-cloud Bot commented Sep 7, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit d5de79d

Command Status Duration Result
nx run-many -t=e2e --projects=@hyperfrontend/wo... ✅ Succeeded 2m 11s View ↗
nx run-many -t=build --projects=@hyperfrontend/... ✅ Succeeded 1h 40m 21s View ↗
nx run-many -t=test --projects=@hyperfrontend/w... ✅ Succeeded 4m 53s View ↗
nx run-many -t=typecheck --projects=@hyperfront... ✅ Succeeded 2m 27s View ↗
nx run-many -t=lint --projects=@hyperfrontend/w... ✅ Succeeded 2m 6s View ↗
nx version-check lib-function-utils ✅ Succeeded 1s View ↗
nx version-check lib-questions ✅ Succeeded 1s View ↗
nx version-check lib-time-utils ✅ Succeeded 1s View ↗
Additional runs (16) ✅ Succeeded ... View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-09-07 10:06:49 UTC

@AndrewRedican
AndrewRedican merged commit e25d9f8 into main Sep 7, 2026
15 checks passed
@AndrewRedican
AndrewRedican deleted the week-2026-08-31_3 branch September 7, 2026 10:22

This branch was successfully deployed

1 active deployment
Preview d5de79d0 Deployed Sep 7, 2026 by vercel[bot]
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