Skip to content

Conversation

@diegolmello
Copy link
Member

@diegolmello diegolmello commented Sep 19, 2025

Proposed changes

Issue(s)

Depends on:

https://rocketchat.atlassian.net/browse/ESH-23
https://rocketchat.atlassian.net/browse/ESH-24
https://rocketchat.atlassian.net/browse/ESH-30
https://rocketchat.atlassian.net/browse/ESH-26
https://rocketchat.atlassian.net/browse/ESH-28
https://rocketchat.atlassian.net/browse/ESH-44
https://rocketchat.atlassian.net/browse/ESH-47

How to test or reproduce

Screenshots

UI improvements on change e2ee password

image image image image

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

Summary by CodeRabbit

  • New Features

    • E2E v2 (AES‑GCM) support with versioned encrypted payloads and 12‑word passphrase generation.
  • Improvements

    • Version-aware encryption across app, push notifications and background processing; per-room handlers, safer key flows and improved logging while preserving compatibility.
  • UI/Style

    • Updated Save Your Password and Change Password screens, manual password entry, copy-to-clipboard and password policy UX.
  • Localization

    • Added “Enter manually” and “Generate new password” translations.
  • Tests

    • New consolidated Maestro E2E encryption suites added; legacy flows removed.
  • Chores

    • iOS project/pod reorganization and dependency update.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 19, 2025

Walkthrough

Implements server-versioned E2EE (v1/v2) across JS/TS and native layers, adds prefixed‑base64 payloads, BIP‑39 passphrase generation, per‑room refactor and async push decryption, updates message/content models and chat.update to accept content, plus UI/i18n and Maestro E2E test updates.

Changes

Cohort / File(s) Summary of changes
Core encryption (TS)
app/lib/encryption/encryption.ts, app/lib/encryption/room.ts, app/lib/encryption/utils.ts, app/lib/encryption/wordList.ts
Add versioned private‑key encoding/decoding (v1/v2), AES‑GCM flows, prefixed‑base64 helpers, parsePrivateKey, generatePassphrase (BIP‑39), adjust generateMasterKey(password,salt,iterations), remove randomPassword, refactor per‑room instance model and delegate encryption/decryption to room instances.
Type & models (TS / iOS)
app/definitions/IMessage.ts, app/lib/encryption/definitions.ts, app/definitions/IAttachment.ts, ios/Shared/Models/EncryptedContent.swift, ios/Shared/Models/Message.swift
Introduce EncryptedContent union (v1/v2), add optional kid/iv, widen algorithm union to include rc.v2.aes-sha2, make msg/content optional, add FallbackMessage and e2eMentions fields.
Android native: encryption & notifications
android/.../notification/Encryption.java, ReplyBroadcast.java, Ejson.java, CustomPushNotification.java, E2ENotificationProcessor.java, MainApplication.kt
Introduce PrefixedData/ParsedMessage/RoomKeyResult/EncryptionContent, change decryptRoomKey to return RoomKeyResult, add dual v1/v2 AES‑CBC/AES‑GCM handling, prefixed‑base64 parsing, async E2E notification processor with ReactContext polling and lazy MMKV init, add kid/iv/messageType fields.
iOS native: encryption & messaging
ios/Shared/RocketChat/Encryption.swift, RocketChat.swift, API/Requests/SendMessage.swift, NotificationService.swift, ReplyNotification.swift
Add prefixed‑base64 helpers and ParsedMessage/RoomKeyResult types, change decryptRoomKey to return RoomKeyResult, support AES‑CBC/AES‑GCM, make SendMessage accept optional msg and content, simplify notification decryption and reply send background handling.
E2E security UI & helpers
app/views/E2ESaveYourPasswordView.tsx, app/views/E2EEncryptionSecurityView/*
UI restyling, manual vs generated passphrase flow, integrate generatePassphrase (BIP‑39), clipboard copy action with new log event, password policy validator, and updated styles/components.
Subscriptions / decryption triggers
app/lib/methods/subscriptions/rooms.ts, related app/lib/methods/*
Stop in‑method DB mutations; filter already‑decrypted subscriptions/messages; trigger per‑room decryption when message.msg or message.content present; remove broad readiness waits and delegate to room instances.
REST / send & edit message
app/lib/services/restApi.ts, ios/Shared/RocketChat/RocketChat.swift, app/lib/methods/sendMessage.ts
editMessage now accepts optional text and content; sending branches to include content payload (or fallback msg), with safer optional chaining and explicit MessageType typing.
Maestro E2E tests & helpers
.maestro/tests/e2e/**, .maestro/tests/assorted/**, .maestro/helpers/**, .maestro/scripts/data.js
Remove old assorted suite; add new .maestro/tests/e2e flows and helpers for E2E encryption (create room, enter key, send/verify, reset, edit, quote), update deeplink/login helpers and add e2eePassword test data.
i18n & UI text
app/i18n/locales/*.json (many)
Add Enter_manually and Generate_new_password translations across locales.
Project config & deps
ios/RocketChatRN.xcodeproj/project.pbxproj, package.json
Rework CocoaPods xcconfig/framework refs and RN bundling scripts; update @rocket.chat/mobile-crypto dependency to tag fix.gcm.
Misc & tests
app/lib/constants/keys.ts, app/lib/methods/getThreadName.ts, app/lib/methods/updateMessages.ts, app/reducers/encryption.ts, app/reducers/encryption.test.ts, others
Make E2E_STATUS readonly const, add E2E_SEC_COPY_PASSWORD log event, remove encryptionInit handling, add explicit decrypted merges/casts and small safety/typing refactors.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as RN UI
  participant Room as EncryptionRoom (TS)
  participant Native as Native Crypto
  participant Server as Server

  UI->>Room: encryptText(plaintext)
  Room->>Room: determine algorithm (server/version)
  alt v2 (rc.v2.aes-sha2)
    Room->>Native: aesGcmEncrypt(key, iv, plaintext)
    Native-->>Room: {ciphertext, iv}
    Room-->>UI: content {algorithm, ciphertext, kid, iv}
  else v1 (rc.v1.aes-sha2)
    Room->>Native: legacy AES‑CBC-like encrypt
    Native-->>Room: ciphertext
    Room-->>UI: content {algorithm, ciphertext}
  end
  UI->>Server: sendMessage({ content, msg? })
Loading
sequenceDiagram
  autonumber
  participant Push as Push Receiver
  participant Proc as E2ENotificationProcessor (Android)
  participant Ctx as React Context Provider
  participant Enc as Native Encryption
  participant Sys as System Notification

  Push->>Proc: processAsync(bundle, ejson)
  loop poll React context until available or timeout
    Proc->>Ctx: getReactContext()
  end
  Proc->>Enc: decryptRoomKey(e2eKey)
  Enc-->>Proc: RoomKeyResult { decryptedKey, algorithm }
  Proc->>Enc: decryptContent(content/msg, decryptedKey, algorithm)
  Enc-->>Proc: plaintext
  Proc->>Sys: showNotification(plaintext)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Areas to focus review on:

  • Cross-language encryption compatibility and payload formats (prefixed‑base64, IV and iterations handling) across TS, Java, and Swift.
  • Signature change: generateMasterKey(password, salt, iterations) — ensure all call sites updated.
  • Room key/session export formats, keyID migration, and algorithm selection in room.ts and native counterparts.
  • Push notification async decryption: ReactContext polling/timeouts and MMKV lazy init (Android).
  • API change for send/edit message (content field) and server compatibility (chat.update payload).
  • Maestro E2E test additions/removals and updated deeplink/login helpers.

Possibly related PRs

Poem

I nibble keys beneath the digital moon,
Two ciphers hum — the old and new in tune.
Twelve words I gather, warm and bright,
Prefixed bytes tucked safe at night.
Hop, whisker, encrypt — snug till morning light. 🐰🔐

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: Encryption security hardening' accurately summarizes the main objective of implementing E2EE v2 encryption with enhanced security hardening measures.
Linked Issues check ✅ Passed The changeset implements all major coding requirements from linked issues: BIP39 wordlist adoption (ESH-23), longer passphrase enforcement with policies (ESH-24), v2 write path as default (ESH-26), v1/v2 auto-detection on read (ESH-28), password generation mechanism with manual/auto modes (ESH-44), and chat.update API content parameter support (ESH-47).
Out of Scope Changes check ✅ Passed All changes are within scope: encryption core refactoring, v1/v2 versioning support, password management UI updates, test suite organization, and localization entries directly support the encryption security hardening objectives.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat.e2ee-v2

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8d17033 and 412d689.

📒 Files selected for processing (2)
  • app/lib/encryption/encryption.ts (10 hunks)
  • app/lib/methods/subscriptions/rooms.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/lib/methods/subscriptions/rooms.ts
🧰 Additional context used
🧬 Code graph analysis (1)
app/lib/encryption/encryption.ts (4)
app/lib/encryption/room.ts (1)
  • EncryptionRoom (71-822)
app/lib/store/auxStore.ts (1)
  • store (6-6)
app/lib/methods/helpers/compareServerVersion.ts (1)
  • compareServerVersion (10-15)
app/lib/encryption/utils.ts (8)
  • b64ToBuffer (15-15)
  • bufferToHex (12-12)
  • bufferToB64 (17-17)
  • utf8ToBuffer (42-59)
  • joinVectorData (73-78)
  • parsePrivateKey (239-276)
  • bufferToUtf8 (61-65)
  • generatePassphrase (300-329)
🔇 Additional comments (7)
app/lib/encryption/encryption.ts (7)

13-15: LGTM: Import additions support v2 encryption architecture.

The new imports for GCM encryption primitives, file checksum validation, private key parsing, and BIP-39 passphrase generation align with the v2 encryption requirements and ESH-23/ESH-44 objectives.

Also applies to: 51-51, 61-63


73-73: LGTM: Per-room instance architecture simplified.

The flat Record<string, EncryptionRoom> structure is cleaner than a nested per-RID architecture and aligns with the delegation pattern used throughout the file.


227-242: LGTM: Version-aware private key decoding implemented correctly.

The method properly uses parsePrivateKey to detect v1/v2 format, derives the master key with the appropriate salt and iterations, and dispatches to the correct decryption primitive (GCM for v2, CBC for v1). This aligns with ESH-28 (auto-detect v1/v2 on read).


244-254: LGTM: Master key generation now supports configurable iterations and salt.

The updated signature generateMasterKey(password: string, salt: string, iterations: number) correctly accommodates v2's higher iteration count (100000) and prefixed salt format, strengthening key derivation per ESH-24 requirements.


257-261: LGTM: BIP-39 passphrase generation implemented.

Replacing randomPassword with generatePassphrase adopts the BIP-39 2048-word list for high-entropy, human-memorable passphrases, fulfilling ESH-23 and ESH-44 objectives.


310-312: LGTM: Clean helper for room instance lifecycle management.

This method provides a clear API for removing room instances when they need to be recreated (e.g., after key exchange completion).


382-421: LGTM: Efficient filtering avoids redundant decryption work.

The filtering logic at lines 394-396 correctly identifies subscriptions whose lastMessage requires decryption (type is E2E_MESSAGE_TYPE and status is not DONE), avoiding unnecessary work on already-decrypted messages. The conditional lastMessage update (lines 404-406) is also safe.


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 and usage tips.

@diegolmello diegolmello temporarily deployed to experimental_android_build November 7, 2025 13:44 — with GitHub Actions Inactive
@diegolmello diegolmello temporarily deployed to experimental_ios_build November 7, 2025 13:44 — with GitHub Actions Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b5dff4a and 8a3f7e5.

📒 Files selected for processing (1)
  • app/lib/encryption/encryption.ts (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/lib/encryption/encryption.ts (7)
app/lib/encryption/room.ts (1)
  • EncryptionRoom (71-823)
app/lib/encryption/index.ts (1)
  • EncryptionRoom (6-6)
app/lib/store/auxStore.ts (1)
  • store (6-6)
app/lib/methods/helpers/compareServerVersion.ts (1)
  • compareServerVersion (10-15)
app/sagas/login.js (6)
  • userId (125-125)
  • userId (338-338)
  • server (89-89)
  • server (221-221)
  • server (296-296)
  • server (372-372)
app/lib/encryption/utils.ts (8)
  • b64ToBuffer (15-15)
  • bufferToHex (12-12)
  • bufferToB64 (17-17)
  • utf8ToBuffer (42-59)
  • joinVectorData (73-78)
  • parsePrivateKey (239-276)
  • bufferToUtf8 (61-65)
  • generatePassphrase (300-329)
app/sagas/encryption.js (3)
  • privateKey (102-102)
  • server (20-20)
  • server (93-93)
🔇 Additional comments (7)
app/lib/encryption/encryption.ts (7)

228-243: LGTM: Clean v1/v2 decode path.

The decode logic correctly delegates version detection to parsePrivateKey, then routes to the appropriate decryption algorithm. The flow is clear and handles both legacy v1 and new v2 formats transparently.


258-262: LGTM: BIP-39 passphrase generation.

Switching to generatePassphrase aligns with ESH-23 and ESH-44 objectives to use BIP-39 wordlist for stronger, human-memorable passphrases.


400-439: LGTM: Efficient subscription decryption with filtering.

The filtering logic (lines 412-414) correctly skips already-decrypted messages, reducing redundant work. Delegation to per-room decryptSubscription aligns with the new architecture.


536-540: LGTM: Clean delegation to per-room instance.

The refactored method correctly delegates subscription decryption to the per-room instance, simplifying the flow and aligning with the new architecture.


573-587: LGTM: Simplified message decryption.

The refactored method efficiently short-circuits non-E2E or already-decrypted messages (line 577), then delegates to the per-room instance. Clean and maintainable.


364-375: LGTM: Improved type annotations.

The explicit type annotation at line 364 and the cast at line 375 improve type safety and clarity.


245-255: Remove this review comment—generateMasterKey is a private class method with no external callers.

The search confirms that generateMasterKey is called only within app/lib/encryption/encryption.ts (lines 211 and 233), and both internal calls have been properly updated with the new iterations parameter. The method is not exported individually; only the Encryption class is exported as default. Therefore, there are no external callers that could break, and this concern does not apply.

Likely an incorrect or invalid review comment.

Comment on lines 207 to 225
// TODO: get the appropriate server version
const { version } = store.getState().server;
const isV2 = compareServerVersion(version, 'greaterThanOrEqualTo', '7.13.0');

const ivArrayBuffer = b64ToBuffer(await randomBytes(16));
const keyBase64 = await this.generateMasterKey(password, isV2 ? `v2:${userId}:mobile` : userId, isV2 ? 100000 : 1000);
const ivB64 = isV2 ? await randomBytes(12) : await randomBytes(16);
const ivArrayBuffer = b64ToBuffer(ivB64);
const keyHex = bufferToHex(b64ToBuffer(keyBase64));
const ivHex = bufferToHex(ivArrayBuffer);

if (isV2) {
const ciphertextB64 = await aesGcmEncrypt(bufferToB64(utf8ToBuffer(privateKey)), keyHex, ivHex);
return EJSON.stringify({ iv: ivB64, ciphertext: ciphertextB64, salt: userId, iterations: 100000 });
}

// v1
const data = b64ToBuffer(await aesEncrypt(bufferToB64(utf8ToBuffer(privateKey)), keyHex, ivHex));
return EJSON.stringify(new Uint8Array(joinVectorData(ivArrayBuffer, data)));
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Enforce server.version presence and prefer JSON.stringify for v2 encoding.

This addresses an unresolved past review comment and a maintainer's feedback:

  1. Critical: The TODO at line 207 and past review remain unaddressed. If store.getState().server.version is undefined, compareServerVersion returns false and silently falls back to v1 (weaker encryption). Replace the TODO with an explicit null check that throws or defers if version is missing:

    const { version } = store.getState().server;
    if (!version) {
      throw new Error('server.version not set: encryption requires server version');
    }
    const isV2 = compareServerVersion(version, 'greaterThanOrEqualTo', '7.13.0');
  2. Recommended refactor: At line 219, use JSON.stringify instead of EJSON.stringify for the v2 payload, as the object contains only plain properties (base64 strings and numbers). EJSON is intended for MongoDB-specific types and adds unnecessary overhead here.

Based on learnings from past reviews and maintainer feedback.

🤖 Prompt for AI Agents
In app/lib/encryption/encryption.ts around lines 207 to 225, add an explicit
check that store.getState().server.version is present and throw a clear Error if
missing before calling compareServerVersion so we don't silently fall back to
v1; then, for the v2 branch return a JSON.stringify of the plain object ({ iv,
ciphertext, salt, iterations }) instead of EJSON.stringify to avoid unnecessary
EJSON overhead. Ensure the thrown error message clearly states "server.version
not set: encryption requires server version" (or similar) and that
JSON.stringify is used only in the v2 path.

@cardoso cardoso dismissed their stale review November 7, 2025 16:33

Changes were made

@diegolmello diegolmello had a problem deploying to upload_experimental_android November 7, 2025 17:26 — with GitHub Actions Error
@github-actions
Copy link

github-actions bot commented Nov 7, 2025

Android Build Available

Rocket.Chat Experimental 4.67.0.107684

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNTLWD301NKEfS7cYerwXTIsDsXorWuDVlowRNPzCK4WLWbfnDy-9_EEbioZP2S8PyV2c16yHrZhBNmNMy7w

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

iOS Build Available

Rocket.Chat Experimental 4.67.0.107685

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
app/lib/encryption/room.ts (2)

196-222: Unresolved: Back-compat risk with fixed RSA ciphertext length.

Line 203's decodePrefixedBase64 call enforces a fixed 256-byte ciphertext length (2048-bit RSA). Different key sizes will throw RangeError and break room key imports.

Add fallback handling for legacy key formats:

 ): Promise<{ sessionKeyExportedString: string; roomKey: ArrayBuffer; keyID: string; algorithm: TAlgorithm }> => {
   try {
-    // Parse the encrypted key using prefixed base64
-    const [kid, encryptedData] = decodePrefixedBase64(E2EKey);
+    // Try strict prefixed-base64 (v2/modern)
+    let kid: string;
+    let encryptedData: Uint8Array;
+    try {
+      [kid, encryptedData] = decodePrefixedBase64(E2EKey);
+    } catch (err) {
+      // Fallback: legacy v1 uses 12-char keyID prefix + base64 ciphertext
+      kid = E2EKey.slice(0, 12);
+      encryptedData = new Uint8Array(b64ToBuffer(E2EKey.slice(12)));
+    }
 
     // Decrypt the session key
     const decryptedKey = await rsaDecrypt(bufferToB64(encryptedData.buffer), privateKey);

Based on learnings from past reviews.


352-361: Unresolved: Handle non-2048-bit RSA keys when sharing room keys.

Line 357's encodePrefixedBase64 requires exactly 256-byte ciphertext (2048-bit RSA). Users with 4096-bit or other key sizes will fail to receive room keys.

Add fallback handling:

     const encryptedUserKey = await rsaEncrypt(this.sessionKeyExportedString as string, userKey);
     const encryptedBuffer = b64ToBuffer(encryptedUserKey as string);
-    return encodePrefixedBase64(this.keyID, encryptedBuffer);
+    try {
+      return encodePrefixedBase64(this.keyID, encryptedBuffer);
+    } catch (error) {
+      if (error instanceof RangeError) {
+        // Fallback for non-2048-bit RSA keys
+        return `${this.keyID}${encryptedUserKey}`;
+      }
+      throw error;
+    }
   } catch (e) {

Based on learnings from past reviews.

app/lib/encryption/encryption.ts (1)

207-225: Address unresolved past review concerns: version validation and v2 serialization.

Two issues from previous reviews remain unaddressed:

  1. Critical: Line 207 TODO and missing validation — if store.getState().server.version is undefined, compareServerVersion returns false and silently falls back to v1 (weaker encryption). Add an explicit null check before line 209:

    const { version } = store.getState().server;
    if (!version) {
      throw new Error('server.version not set: encryption requires server version');
    }
    const isV2 = compareServerVersion(version, 'greaterThanOrEqualTo', '7.13.0');
  2. Recommended refactor: Line 219 uses EJSON.stringify for the v2 payload. Since the object contains only plain properties (base64 strings and numbers), use JSON.stringify instead to avoid unnecessary EJSON overhead.

Based on learnings from past reviews.

🧹 Nitpick comments (1)
app/lib/encryption/room.ts (1)

447-474: Consider explicit algorithm validation to prevent silent v1 fallback.

When this.algorithm is uninitialized (empty string) or has an unexpected value, execution silently falls through to the v1 encryption path (line 467). While createNewRoomKey now properly initializes this.algorithm, explicit validation would prevent subtle edge cases:

 	): Promise<
 		| { algorithm: 'rc.v2.aes-sha2'; kid: string; iv: string; ciphertext: string }
 		| { algorithm: 'rc.v1.aes-sha2'; ciphertext: string }
 	> => {
 		const textBuffer = utf8ToBuffer(text);
+		if (!this.algorithm || (this.algorithm !== 'A256GCM' && this.algorithm !== 'A128CBC')) {
+			throw new Error(`Invalid encryption algorithm: ${this.algorithm}. Room encryption not properly initialized.`);
+		}
 		if (this.algorithm === 'A256GCM') {

Based on learnings from past reviews.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8a3f7e5 and b019fec.

📒 Files selected for processing (3)
  • app/lib/encryption/encryption.ts (10 hunks)
  • app/lib/encryption/room.ts (15 hunks)
  • app/lib/methods/subscriptions/rooms.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/lib/encryption/room.ts (5)
app/lib/encryption/helpers/deferred.ts (1)
  • Deferred (1-39)
app/lib/services/restApi.ts (2)
  • e2eAcceptSuggestedGroupKey (84-86)
  • e2eRejectSuggestedGroupKey (88-90)
app/lib/encryption/utils.ts (9)
  • decodePrefixedBase64 (200-219)
  • bufferToB64 (17-17)
  • b64ToBuffer (15-15)
  • bufferToB64URI (21-40)
  • encodePrefixedBase64 (221-237)
  • utf8ToBuffer (42-59)
  • joinVectorData (73-78)
  • splitVectorData (67-71)
  • bufferToUtf8 (61-65)
app/definitions/IMessage.ts (1)
  • IMessage (147-178)
app/definitions/ISubscription.ts (1)
  • ISubscription (40-119)
app/lib/encryption/encryption.ts (4)
app/lib/encryption/room.ts (1)
  • EncryptionRoom (71-822)
app/lib/store/auxStore.ts (1)
  • store (6-6)
app/lib/methods/helpers/compareServerVersion.ts (1)
  • compareServerVersion (10-15)
app/lib/encryption/utils.ts (8)
  • b64ToBuffer (15-15)
  • bufferToHex (12-12)
  • bufferToB64 (17-17)
  • utf8ToBuffer (42-59)
  • joinVectorData (73-78)
  • parsePrivateKey (239-276)
  • bufferToUtf8 (61-65)
  • generatePassphrase (300-329)
🔇 Additional comments (13)
app/lib/methods/subscriptions/rooms.ts (2)

223-225: LGTM: Decryption deferred to after persistence.

Moving decryption out of the database transaction to explicit pending calls is a sound architectural improvement. This aligns with the per-room decryption pipeline introduced in this PR.


389-389: LGTM: Supports v2 encrypted message notifications.

Adding message.content to the condition correctly handles v2 encrypted messages that use the content field instead of msg.

app/lib/encryption/encryption.ts (4)

228-243: LGTM: Version-aware private key decoding.

The refactored decode path correctly delegates parsing to parsePrivateKey and selects the appropriate decryption algorithm based on version. Clean separation of concerns.


246-255: LGTM: Iteration count parameterized for v2 hardening.

Adding the iterations parameter enables v2's higher iteration count (100000 vs v1's 1000), strengthening key derivation as required by the security hardening objectives.


258-262: LGTM: BIP-39 passphrase generation implemented.

Switching to generatePassphrase correctly implements ESH-23 (BIP-39 wordlist) and ESH-24 (longer passphrase enforcement).


391-422: LGTM: Optimized subscription decryption with filtering.

The new filtering logic (lines 391-397) prevents redundant decryption of already-decrypted lastMessages, improving performance. The simplified delegation to per-room instances (line 401) aligns well with the refactored per-room architecture.

app/lib/encryption/room.ts (7)

122-133: LGTM: Race condition guard and private key validation.

The redundant establishing check (lines 122-125) prevents race conditions during async subscription fetch, and the private key check (lines 127-132) correctly guards against proceeding before the E2EE password is entered.


226-272: LGTM: Algorithm properly initialized for v2 and v1.

Both encryption paths now correctly set this.algorithm ('A256GCM' for v2 at line 245, 'A128CBC' for v1 at line 265), resolving the previously flagged initialization issue.


477-498: LGTM: Message encryption updated for v2 content structure.

The refactored encrypt method correctly uses the new structured content format from encryptText and assigns it to message.content, aligning with the v2 encryption architecture.


646-663: LGTM: Clean abstraction for dual-format payload parsing.

The new parse helper cleanly abstracts v2 JSON format (lines 654-656) and v1 kid+base64 format (lines 658-663), providing good separation of concerns.


665-678: LGTM: Algorithm-aware decryption helper.

The new doDecrypt helper cleanly selects the appropriate decryption function based on algorithm (A256GCM vs A128CBC) and handles parsing. Good separation of concerns.


680-702: LGTM: Refactored content decryption with key rotation support.

The refactored decryptContent cleanly delegates to the new parse and doDecrypt helpers, and properly handles old room keys (lines 688-695) for key rotation scenarios.


776-821: LGTM: New subscription decryption with DB optimization.

The new decryptSubscription method supports the per-room architecture with appropriate guard clauses and includes a smart optimization (lines 803-814) to reuse already-decrypted messages from the DB, avoiding redundant work.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/lib/encryption/room.ts (2)

477-498: Encryption failure silently returns unencrypted message.

If encryptText throws (line 483), the catch block logs the error but returns the original unencrypted message (line 497). This fail-open behavior could allow sensitive content to be sent unencrypted. Consider either throwing the error to prevent sending or marking the message with an error state.

Apply this diff to prevent unencrypted sends:

 		} catch (e) {
-			// Do nothing
 			console.error(e);
+			throw new Error('Failed to encrypt message: ' + (e instanceof Error ? e.message : String(e)));
 		}

501-528: Same fail-open behavior in upload encryption.

Like the encrypt method, encryptUpload returns the original unencrypted upload if encryption fails (line 527). This creates the same security risk of unencrypted content being sent.

Consider applying the same fix as suggested for the encrypt method.

♻️ Duplicate comments (2)
app/lib/encryption/room.ts (2)

196-222: Back-compat risk: decodePrefixedBase64 assumes fixed RSA ciphertext length.

decodePrefixedBase64 at line 203 enforces a fixed 256-byte Base64 length (2048-bit RSA). Older v1 keys using 12-char prefix + variable-length base64, or users with 4096-bit RSA keys, will throw RangeError and break key imports.

Wrap the call in a try-catch with fallback to legacy parsing:

 	): Promise<{ sessionKeyExportedString: string; roomKey: ArrayBuffer; keyID: string; algorithm: TAlgorithm }> => {
 		try {
-			// Parse the encrypted key using prefixed base64
-			const [kid, encryptedData] = decodePrefixedBase64(E2EKey);
+			let kid: string;
+			let encryptedData: ArrayBuffer;
+			try {
+				[kid, encryptedData] = decodePrefixedBase64(E2EKey);
+			} catch (err) {
+				// Fallback: legacy v1 uses 12-char keyID prefix + base64 ciphertext of variable length
+				if (err instanceof RangeError) {
+					kid = E2EKey.slice(0, 12);
+					encryptedData = b64ToBuffer(E2EKey.slice(12));
+				} else {
+					throw err;
+				}
+			}
 
 			// Decrypt the session key
 			const decryptedKey = await rsaDecrypt(bufferToB64(encryptedData), privateKey);

352-361: Handle >2048-bit RSA ciphertext when exporting room keys.

encodePrefixedBase64 at line 357 requires exactly 256 bytes. If a recipient has a 4096-bit RSA key (or any non-2048-bit key), rsaEncrypt produces a different-sized ciphertext, encodePrefixedBase64 throws, and you cannot share the room key with that user.

Add fallback to legacy format on size mismatch:

 		const encryptedUserKey = await rsaEncrypt(this.sessionKeyExportedString as string, userKey);
 		const encryptedBuffer = b64ToBuffer(encryptedUserKey as string);
-		return encodePrefixedBase64(this.keyID, encryptedBuffer);
+		try {
+			return encodePrefixedBase64(this.keyID, encryptedBuffer);
+		} catch (error) {
+			if (error instanceof RangeError) {
+				// Fallback to legacy format for non-2048-bit keys
+				return `${this.keyID}${encryptedUserKey}`;
+			}
+			throw error;
+		}
🧹 Nitpick comments (2)
app/lib/encryption/room.ts (2)

447-474: Consider explicit algorithm validation to prevent silent v1 fallback.

When this.algorithm === '' (uninitialized) or has an unexpected value, execution silently falls through to the v1 encryption path at line 467. While handshake() should set this.algorithm via createNewRoomKey or importRoomKey, explicit validation would catch edge cases where the room is not properly initialized.

Add validation before branching:

 	): Promise<
 		| { algorithm: 'rc.v2.aes-sha2'; kid: string; iv: string; ciphertext: string }
 		| { algorithm: 'rc.v1.aes-sha2'; ciphertext: string }
 	> => {
 		const textBuffer = utf8ToBuffer(text);
+		if (!this.algorithm) {
+			throw new Error('Encryption algorithm not initialized. Room handshake may have failed.');
+		}
 		if (this.algorithm === 'A256GCM') {

776-821: Well-structured subscription decryption with smart caching.

The method includes appropriate guard clauses for ready state, message type, and encryption status. The optimization at lines 803-814 avoids re-decrypting identical messages by comparing _updatedAt timestamps, which is a good performance enhancement for frequently accessed subscriptions.

Consider adding a type guard for lastMessage to improve type safety:

 	decryptSubscription = async (subscription: Partial<ISubscription>) => {
 		if (!this.ready) {
 			return subscription;
 		}
 
 		// If the subscription doesn't have a lastMessage just return
 		const { rid, lastMessage } = subscription;
-		if (!lastMessage) {
+		if (!lastMessage || typeof lastMessage !== 'object') {
 			return subscription;
 		}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b019fec and 8d17033.

📒 Files selected for processing (2)
  • app/lib/encryption/room.ts (15 hunks)
  • app/lib/methods/subscriptions/rooms.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/lib/methods/subscriptions/rooms.ts
🧰 Additional context used
🧬 Code graph analysis (1)
app/lib/encryption/room.ts (4)
app/lib/encryption/helpers/deferred.ts (1)
  • Deferred (1-39)
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (1)
  • Encryption (108-454)
app/lib/services/restApi.ts (2)
  • e2eAcceptSuggestedGroupKey (84-86)
  • e2eRejectSuggestedGroupKey (88-90)
app/lib/encryption/utils.ts (9)
  • decodePrefixedBase64 (200-219)
  • bufferToB64 (17-17)
  • b64ToBuffer (15-15)
  • bufferToB64URI (21-40)
  • encodePrefixedBase64 (221-237)
  • utf8ToBuffer (42-59)
  • joinVectorData (73-78)
  • splitVectorData (67-71)
  • bufferToUtf8 (61-65)
🔇 Additional comments (7)
app/lib/encryption/room.ts (7)

226-272: Version-aware key creation implemented correctly.

The method properly branches by server version (>= 7.13.0 for v2) and sets this.algorithm to 'A256GCM' (line 245) for v2 or 'A128CBC' (line 265) for v1. Key sizes, key ID generation, and session key formats are appropriate for each version.


646-663: Parse method correctly handles v1/v2 payload formats.

The method properly distinguishes v2 structured payloads (with algorithm: 'rc.v2.aes-sha2') from v1 string-based payloads. The v1 path correctly extracts the 12-character key ID and splits the vector from ciphertext. Defensive handling with payload?.ciphertext || '' prevents null reference errors.


665-678: Clean algorithm-aware decryption dispatcher.

The method correctly routes decryption to aesGcmDecrypt for v2 (A256GCM) or aesDecrypt for v1 (A128CBC), gracefully handles null decryption results, and parses the final EJSON payload. Accepting algorithm as a parameter rather than using this.algorithm provides flexibility for decrypting old keys.


680-702: Robust v1/v2 decryption with key rotation support.

The method correctly handles both encryption versions via the parse helper, supports key rotation by searching oldRoomKeys when the key ID doesn't match, and uses the appropriate algorithm for each key. Error handling returns null rather than throwing, which aligns with the graceful degradation pattern used elsewhere.


705-741: Message decryption properly handles v1/v2 formats and nested content.

The flow correctly decrypts message content (with v1 fallback to msg field), spreads the decrypted structured data (attachments, files, text) into the message, and processes quote attachments. Marking attachments as e2e: 'pending' appropriately signals that file content requires separate decryption.


530-636: File encryption consistently uses structured encryption results.

The encryptFile method properly returns structured encryption results from encryptText for both the getContent callback (line 607-608) and fileContent (line 624). This maintains consistency with the versioned encryption approach across message and file flows.


638-644: LGTM!

File content decryption correctly checks for v1/v2 algorithm markers and delegates to the versioned decryptContent method.

Comment on lines +135 to 174
if (E2ESuggestedKey) {
try {
this.establishing = true;
const { keyID, roomKey, sessionKeyExportedString, algorithm } = await this.importRoomKey(
E2ESuggestedKey,
Encryption.privateKey
);
this.keyID = keyID;
this.roomKey = roomKey;
this.sessionKeyExportedString = sessionKeyExportedString;
this.algorithm = algorithm;
try {
this.establishing = true;
const { keyID, roomKey, sessionKeyExportedString } = await this.importRoomKey(E2ESuggestedKey, Encryption.privateKey);
this.keyID = keyID;
this.roomKey = roomKey;
this.sessionKeyExportedString = sessionKeyExportedString;
await e2eAcceptSuggestedGroupKey(this.roomId);
Encryption.deleteRoomInstance(this.roomId);
return;
} catch (error) {
await e2eRejectSuggestedGroupKey(this.roomId);
}
await e2eAcceptSuggestedGroupKey(this.roomId);
this.readyPromise.resolve();
return;
} catch (e) {
log(e);
}
}

// If this room has a E2EKey, we import it
if (E2EKey && Encryption.privateKey) {
this.establishing = true;
const { keyID, roomKey, sessionKeyExportedString } = await this.importRoomKey(E2EKey, Encryption.privateKey);
this.keyID = keyID;
this.roomKey = roomKey;
this.sessionKeyExportedString = sessionKeyExportedString;
this.readyPromise.resolve();
return;
if (E2EKey) {
try {
this.establishing = true;
const { keyID, roomKey, sessionKeyExportedString, algorithm } = await this.importRoomKey(E2EKey, Encryption.privateKey);
this.keyID = keyID;
this.roomKey = roomKey;
this.sessionKeyExportedString = sessionKeyExportedString;
this.algorithm = algorithm;
this.readyPromise.resolve();
return;
} catch (error) {
this.establishing = false;
log(error);
// Fall through to try other options
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Ready promise not rejected on key import failure, can leave room stuck.

When importRoomKey fails for E2ESuggestedKey (lines 153-155) or E2EKey (lines 169-173), the code sets establishing = false and logs the error but never rejects this.readyPromise. Callers awaiting handshake() will hang indefinitely. The room remains in a not-ready state with no path to recovery until the subscription updates externally.

Consider rejecting the promise to signal failure:

 		} catch (error) {
 			await e2eRejectSuggestedGroupKey(this.roomId);
+			this.establishing = false;
+			this.readyPromise.reject(error);
+			return;
 		}
 		} catch (error) {
 			this.establishing = false;
+			this.readyPromise.reject(error);
 			log(error);
-			// Fall through to try other options
+			return;
 		}

@diegolmello diegolmello temporarily deployed to approve_e2e_testing November 7, 2025 18:41 — with GitHub Actions Inactive
@diegolmello diegolmello temporarily deployed to experimental_ios_build November 7, 2025 18:53 — with GitHub Actions Inactive
@diegolmello diegolmello temporarily deployed to experimental_android_build November 7, 2025 18:53 — with GitHub Actions Inactive
@diegolmello diegolmello had a problem deploying to upload_experimental_android November 7, 2025 19:33 — with GitHub Actions Error
@github-actions
Copy link

github-actions bot commented Nov 7, 2025

Android Build Available

Rocket.Chat Experimental 4.67.0.107687

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNTUEipTZV9RaVOUtE0NSFeLqHqbTyUPzqNTigUDW2gt8bFHzkXPaE7_bzXE8x90LmybFQI5C99zM-BjerKo

@diegolmello diegolmello requested a deployment to official_android_build November 7, 2025 20:35 — with GitHub Actions Waiting
@diegolmello diegolmello requested a deployment to upload_experimental_android November 7, 2025 20:35 — with GitHub Actions Waiting
@github-actions
Copy link

github-actions bot commented Nov 7, 2025

iOS Build Available

Rocket.Chat Experimental 4.67.0.107690

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