Skip to content

feat(security): seal emergency payloads end-to-end before submission - #663

Merged
Mikey-222 merged 5 commits into
Hel-Phone:mainfrom
aliyuHabibu:feat/e2e-encrypted-payloads
Sep 30, 2026
Merged

Mikey-222 merged 5 commits into
Hel-Phone:mainfrom
aliyuHabibu:feat/e2e-encrypted-payloads

Conversation

@aliyuHabibu

Copy link
Copy Markdown
Contributor

Contact numbers and medical notes are now encrypted on the requester's device with ECDH P-256 + HKDF-SHA256 + AES-256-GCM before anything is submitted. The ledger, the relay and the map markers only ever carry ciphertext.

Crypto (src/lib/crypto.ts)

  • One random content key per request, sealed once and wrapped per recipient, so a 32-responder envelope still fits the contract budget.
  • Every AEAD tag is domain-separated: the payload is bound to bindingContext, each CEK wrap to bindingContext + recipient keyId.
  • bindingContext is a client-generated submission id carried in the envelope. It is public but tamper-evident, and a responder needs no out-of-band coordination to read it. The contract assigns the request id only after signing, so it cannot be part of a pre-signature AAD.
  • Per-request ephemeral keys, so a compromised responder key only exposes the envelopes addressed to it.

Contract (BREAKING)

  • HelpRequest.nickname/contact replaced by encrypted_payload: Bytes.
  • create_request drops the phantom priority argument, takes the sealed payload, and returns Result<u64, Error>.
  • New validation: PayloadEmpty=12, PayloadTooLarge=13, EmergencyTypeInvalid=14; MAX_ENCRYPTED_PAYLOAD_BYTES=12288.
  • This changes the stored XDR layout. Testnet and mainnet contracts must be redeployed and drain live requests first; there is no in-place upgrade path. encodeEncryptedPayload rejects anything that is not a valid v1 envelope, so plaintext cannot reach the new field.

Relay (server/routes/dispatch.ts)

  • Blind opaque store indexed by both submission id and on-chain request id, public-key registry, structural and size validation, rate limiting, Prometheus counters. Logs shape only, never the blob.
  • An accelerator, not a system of record: the authoritative copy is encrypted_payload on-chain, so a relay wipe costs latency only.
  • Left unmounted, matching every other factory in server/routes/.

Location and emergency_type stay readable on purpose: dispatch needs them, and the existing ZK proof plus client-side coarsening is what protects location.

Verification

  • cargo test -p helphone-contract: 29 passed, up from 24; the 4 pre-existing nonce:: failures are unchanged.
  • cargo build --target wasm32v1-none --release and cargo clippy clean.
  • test/e2e-encryption.test.js: 37 new tests covering the seal/open path, tamper and downgrade resistance, limits, plaintext-leak checks and the relay round trip through the real Express routers.
  • Full vitest: +40 passing, failures 33 -> 32 files and 245 -> 244 tests. tsc --noEmit is byte-identical to the pre-change baseline.

Responder keys live in sessionStorage, so they are lost when the tab closes; moving them into the encrypted SecureStorage is follow-up work.

Closes #504

Contact numbers and medical notes are now encrypted on the requester's
device with ECDH P-256 + HKDF-SHA256 + AES-256-GCM before anything is
submitted. The ledger, the relay and the map markers only ever carry
ciphertext.

Crypto (src/lib/crypto.ts)
- One random content key per request, sealed once and wrapped per
  recipient, so a 32-responder envelope still fits the contract budget.
- Every AEAD tag is domain-separated: the payload is bound to
  bindingContext, each CEK wrap to bindingContext + recipient keyId.
- bindingContext is a client-generated submission id carried in the
  envelope. It is public but tamper-evident, and a responder needs no
  out-of-band coordination to read it. The contract assigns the request
  id only after signing, so it cannot be part of a pre-signature AAD.
- Per-request ephemeral keys, so a compromised responder key only
  exposes the envelopes addressed to it.

Contract (BREAKING)
- HelpRequest.nickname/contact replaced by encrypted_payload: Bytes.
- create_request drops the phantom priority argument, takes the sealed
  payload, and returns Result<u64, Error>.
- New validation: PayloadEmpty=12, PayloadTooLarge=13,
  EmergencyTypeInvalid=14; MAX_ENCRYPTED_PAYLOAD_BYTES=12288.
- This changes the stored XDR layout. Testnet and mainnet contracts must
  be redeployed and drain live requests first; there is no in-place
  upgrade path. encodeEncryptedPayload rejects anything that is not a
  valid v1 envelope, so plaintext cannot reach the new field.

Relay (server/routes/dispatch.ts)
- Blind opaque store indexed by both submission id and on-chain request
  id, public-key registry, structural and size validation, rate
  limiting, Prometheus counters. Logs shape only, never the blob.
- An accelerator, not a system of record: the authoritative copy is
  encrypted_payload on-chain, so a relay wipe costs latency only.
- Left unmounted, matching every other factory in server/routes/.

Location and emergency_type stay readable on purpose: dispatch needs
them, and the existing ZK proof plus client-side coarsening is what
protects location.

Verification
- cargo test -p helphone-contract: 29 passed, up from 24; the 4
  pre-existing nonce:: failures are unchanged.
- cargo build --target wasm32v1-none --release and cargo clippy clean.
- test/e2e-encryption.test.js: 37 new tests covering the seal/open
  path, tamper and downgrade resistance, limits, plaintext-leak checks
  and the relay round trip through the real Express routers.
- Full vitest: +40 passing, failures 33 -> 32 files and 245 -> 244
  tests. tsc --noEmit is byte-identical to the pre-change baseline.

Responder keys live in sessionStorage, so they are lost when the tab
closes; moving them into the encrypted SecureStorage is follow-up work.

Signed-off-by: Aliyu Habibu <aliyuhabibu111@gmail.com>
@drips-wave

drips-wave Bot commented Sep 26, 2026

Copy link
Copy Markdown

@aliyuHabibu Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@aliyuHabibu

Copy link
Copy Markdown
Contributor Author

@ALL Please merge, Wave has ended

@aliyuHabibu

Copy link
Copy Markdown
Contributor Author

@ALL Please merge, wave has ended

@Mikey-222
Mikey-222 merged commit 2941a50 into Hel-Phone:main Sep 30, 2026
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.

End-to-End Encrypted Payload Delivery for Emergency Data

2 participants