Skip to content

feat(e2ee): implement Double Ratchet for Forward Secrecy in end-to-en… - #266

Open
priyanshuvishwakarma273403 wants to merge 2 commits into
Vault-Web:mainfrom
priyanshuvishwakarma273403:feat/e2ee-ratchet
Open

feat(e2ee): implement Double Ratchet for Forward Secrecy in end-to-en…#266
priyanshuvishwakarma273403 wants to merge 2 commits into
Vault-Web:mainfrom
priyanshuvishwakarma273403:feat/e2ee-ratchet

Conversation

@priyanshuvishwakarma273403

Copy link
Copy Markdown
Contributor

Description

This Pull Request implements Forward Secrecy in our End-to-End Encryption (E2EE) pipeline by introducing a Double Ratchet algorithm (specifically the symmetric KDF chain ratchet). This ensures that compromised device key material cannot expose past messages.

Key Changes

1. Symmetric KDF Chain Ratchet (e2ee.service.ts)

  • Key Derivation (KDF): Added a KDF step using crypto.subtle.deriveBits with HKDF (SHA-256) to evolve the chain key and derive a message key for each message.
  • State Management (RatchetState): Defined a new state interface containing sending/receiving chain keys, counter states, and skipped message keys (for out-of-order messages).
  • Symmetric Initialization: Initialized the sending and receiving chain keys deterministically from the ECDH shared secret based on device ID ordering (comparators) without needing external negotiation.
  • Payload Upgrade (v2): Bumped the payload version to 2 to indicate ratcheted encryption. E2eeRecipientPayload now includes the counter (sequence number) of the message.
  • Backward Compatibility: Preserved support for v1 payload decryption. When a payload with version 1 (or undefined) is received, it gracefully falls back to the original ECDH shared secret decryption.
  • Replay Protection: Deletes the skipped message keys immediately after use and rejects duplicate or expired keys.

2. Unit Testing (e2ee.service.spec.ts)

  • Added a unit test validating backward compatibility by manually encrypting a v1 payload and ensuring the service decrypts it correctly.
  • Added a unit test validating out-of-order message decryption and key skipping (e.g. decrypting message 2, skipping 0 and 1, then decrypting 0 and 1 from skipped keys).
  • Added a unit test validating replay protection by ensuring that trying to decrypt an already-decrypted skipped key fails and returns null.

Verification & QA

  • Unit tests: Executed all frontend tests with ng test --watch=false --browsers=ChromeHeadless and confirmed they all passed successfully.
  • Prettier: Formatted modified source files.
  • Production Build: Verified that npm run build compiles with code 0.

Files Modified

  • frontend/src/app/services/e2ee.service.ts
  • frontend/src/app/services/e2ee.service.spec.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@DenizAltunkapan DenizAltunkapan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the work here. The direction is useful, but I would not merge this version yet because the ratchet state can be advanced in unsafe cases and a malicious or corrupted payload can force expensive local state progression. Please address the two inline points before merging.

One broader note: this is a symmetric-chain ratchet, not a full Double Ratchet with a DH ratchet step. That is fine as an incremental improvement, but the PR title/description should avoid overstating the security model unless the DH ratchet is actually implemented.

return plaintext;
}

if (messageCounter > state.receivingCounter) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a maximum skip window before entering this loop, for example something like MAX_SKIP = 1000. Right now a payload with a very large counter can force the client to run many KDF steps and store many skipped keys, which is an easy local DoS against the browser/localStorage.


state.receivingChainKey = this.arrayBufferToBase64(nextChainKey);
state.receivingCounter++;
this.saveRatchetState(ourDeviceId, remoteDeviceId, state);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not persist the advanced receiving state before decryption has succeeded. If the ciphertext is corrupted or malicious, this saves the new chain state anyway and the real message for this counter becomes undecryptable later. A safer flow is: derive the message key, try decryptWithKey, then update and save the state only after the decrypt call returns successfully.

Add MAX_SKIP to prevent DoS from large counters. Defer state save until decryption succeeds. Note: This implements a symmetric-chain ratchet, not a full DH Double Ratchet.

@GideonBa GideonBa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the previous two issues; the skip window and delayed state save look much better now. I still cannot approve this while the PR presents the change as Forward Secrecy / Double Ratchet. Please either add a real DH ratchet or update the title/description and in-code wording to describe this as a symmetric-chain ratchet only.

ourDeviceId: string,
remoteDeviceId: string,
): Promise<RatchetState> {
const sharedSecret = await crypto.subtle.deriveBits(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still initializes every chain from the static ECDH secret. If a device private key is compromised, an attacker can recompute the initial chain and derive message keys from the public counters, so this should not be described as Forward Secrecy or a full Double Ratchet unless a DH ratchet is added.

@DenizAltunkapan

Copy link
Copy Markdown
Member

@priyanshuvishwakarma273403 please apply the requested changes from @GideonBa

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.

4 participants