Skip to content

feat: fail-closed DatabaseKeyStore TurboModule#7418

Merged
diegolmello merged 3 commits into
feat/native-1272-sqlcipher-migrationfrom
feat/native-1311-database-keystore
Jun 19, 2026
Merged

feat: fail-closed DatabaseKeyStore TurboModule#7418
diegolmello merged 3 commits into
feat/native-1272-sqlcipher-migrationfrom
feat/native-1311-database-keystore

Conversation

@diegolmello

@diegolmello diegolmello commented Jun 19, 2026

Copy link
Copy Markdown
Member

Proposed changes

Adds a native, fail-closed key store for the per-database SQLCipher keys the upcoming encrypted-database work depends on.

  • DatabaseKeyStore TurboModule — stores and retrieves a per-database key in the iOS Keychain and the Android Keystore. New-Architecture TurboModule with a codegen spec (app/lib/native/NativeDatabaseKeyStore.ts); replaces the earlier legacy-bridge variant.
  • getOrCreateDatabaseKey(dbName) (app/lib/encryption/keyStore.ts) — returns the stored 64-hex-char key for a database, minting a new 32-byte key only when none exists yet (storage-key format db_key_v1:<dbName>).
  • Fail-closed reads — if the Keychain/Keystore read fails, the call rejects instead of minting a replacement key. Minting on a read error would overwrite the key of an already-encrypted database and make it permanently unreadable; failing closed prevents that data-loss path.
  • iOS Keychain attributes: accessible after first unlock (this device only), non-synchronizable, dedicated service, team-prefixed access group so the NotificationService extension shares the same items. Android: AES-GCM under a non-exportable Keystore key, stored in a dedicated SharedPreferences file.

Additive and inert: nothing reads these keys yet. They are consumed once the databases are actually encrypted at the SQLCipher cutover.

Issue(s)

https://rocketchat.atlassian.net/browse/NATIVE-1311

The native readers work (https://rocketchat.atlassian.net/browse/NATIVE-1276) depends on this module and reads keys by the shared db_key_v1: format.

How to test or reproduce

  • Unit: TZ=UTC pnpm test --testPathPattern='keyStore' — 5 tests pass, including the fail-closed case (a read error rejects and does not write a new key).
  • Lint/typecheck: pnpm lint — 0 errors.
  • Native round-trip (device or simulator): build iOS and Android, then call getOrCreateDatabaseKey('test.db') twice — the same 64-hex key is returned both times and is persisted in the iOS Keychain (service chat.rocket.reactnative.dbkeys) / Android Keystore-backed preferences.

Screenshots

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

Targets the migration integration branch feat/native-1272-sqlcipher-migration, not develop. Native build + Keychain/Keystore round-trip verification is pending on a device/simulator.

https://claude.ai/code/session_01TE9VsFTeXsXc8ssqeR8MJ7

Summary by CodeRabbit

  • New Features
    • Added a cross-platform native secure key/value store backed by device encryption for sensitive data.
    • Exposed async operations to retrieve, persist, and delete stored values with safer handling of missing or inconsistent data.
  • Tests
    • Added comprehensive Jest coverage to validate key retrieval and generation behavior across success and failure scenarios.

…dule

- iOS: new `DatabaseKeyStore.swift` Keychain helper with `AfterFirstUnlockThisDeviceOnly`
  accessibility and dedicated `chat.rocket.reactnative.dbkeys` service; distinguishes
  a true not-found (errSecItemNotFound → null, no error) from any other Keychain failure
  (→ KEYCHAIN_READ_ERROR reject), closing the silent-overwrite hole.
- iOS: new `DatabaseKeyStore.mm` TurboModule (`RCT_EXPORT_MODULE(DatabaseKeyStoreModule)` +
  `getTurboModule:`) replacing the legacy `RCTBridgeModule`/`RCT_EXPORT_METHOD` module that
  did not vend under bridgeless New Architecture.
- Android: self-contained AES-GCM + AndroidKeyStore implementation in a dedicated
  `RCDatabaseKeyStore` SharedPreferences file (separate from MMKV); `getItem` rejects on
  decrypt failure instead of resolving null, closing the same silent-overwrite hole;
  adds package-visible `getItemInternal`/`setItemInternal`/`removeItemInternal` for
  Encryption.java native callers.
- Xcode: registers DatabaseKeyStore.swift (both main-app and NotificationService targets)
  and DatabaseKeyStore.mm (main-app); removes DatabaseKeyStoreModule.mm.
- Android: registers DatabaseKeyStoreTurboPackage in MainApplication.kt.
- TS codegen spec: registration name corrected to `DatabaseKeyStoreModule` (was
  `DatabaseKeyStore`, which could collide with the Swift helper class under bridgeless).

Claude-Session: https://claude.ai/code/session_01Syt2ncLcXKw1oL1tCeFmko
Thin wrapper over the DatabaseKeyStore TurboModule:
- Storage key format `db_key_v1:<dbName>`.
- Returns the stored 64-hex-char key if present.
- Mints a fresh `randomKey(32)` (64 hex chars) on a genuine not-found (null).
- Propagates KEYCHAIN_READ_ERROR / KEYSTORE_READ_ERROR rejections without minting
  — prevents a transient read failure from silently replacing an existing
  encrypted database's key (silent data loss).
- Treats `undefined` as a contract violation (throws) rather than falling through
  to the mint path.

Tests cover: existing key round-trip, fresh-mint path, read-error propagation,
null-as-not-found, and undefined-as-violation.

Claude-Session: https://claude.ai/code/session_01Syt2ncLcXKw1oL1tCeFmko
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9344f03b-68da-441e-a206-e94a2181519e

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb791f and f6ad1aa.

📒 Files selected for processing (5)
  • android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.java
  • app/lib/encryption/__tests__/keyStore.test.ts
  • app/lib/encryption/keyStore.ts
  • app/lib/native/NativeDatabaseKeyStore.ts
  • ios/Libraries/DatabaseKeyStore.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/lib/encryption/tests/keyStore.test.ts
📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/lib/native/NativeDatabaseKeyStore.ts
  • app/lib/encryption/keyStore.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript with strict mode enabled

Files:

  • app/lib/native/NativeDatabaseKeyStore.ts
  • app/lib/encryption/keyStore.ts
**/*.{js,jsx,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier formatting with tabs, single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses

Files:

  • app/lib/native/NativeDatabaseKeyStore.ts
  • app/lib/encryption/keyStore.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Enforce ESLint rules from @rocket.chat/eslint-config with React, React Native, TypeScript, and Jest plugins

Files:

  • app/lib/native/NativeDatabaseKeyStore.ts
  • app/lib/encryption/keyStore.ts
app/lib/encryption/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Place end-to-end encryption implementation in 'app/lib/encryption/' directory using @rocket.chat/mobile-crypto

Files:

  • app/lib/encryption/keyStore.ts
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/lib/native/NativeDatabaseKeyStore.ts
  • app/lib/encryption/keyStore.ts
🪛 ast-grep (0.43.0)
android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.java

[warning] 135-135: Triple DES (3DES or DESede) is considered deprecated. AES is the recommended cipher. Upgrade to use AES.
Context: KeyStore.getInstance(KEYSTORE_PROVIDER)
Note: [CWE-326]: Inadequate Encryption Strength [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(desede-is-deprecated-java)


[warning] 135-135: Use of AES with ECB mode detected. ECB doesn't provide message confidentiality and is not semantically secure so should not be used. Instead, use a strong, secure cipher: Cipher.getInstance("AES/CBC/PKCS7PADDING"). See https://owasp.org/www-community/Using_the_Java_Cryptographic_Extensions for more information.
Context: KeyStore.getInstance(KEYSTORE_PROVIDER)
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(use-of-aes-ecb-java)

🔇 Additional comments (11)
app/lib/encryption/keyStore.ts (2)

16-40: Race condition is now mitigated by native write-once semantics.

The past review flagged a race where concurrent first-calls could mint different keys. The native layer now enforces write-once: on errSecDuplicateItem, iOS compares values and returns false if they differ (similarly for Android per PR objectives). This means the second caller receives KEYCHAIN_WRITE_ERROR and must retry—no data is lost or overwritten.

This is an acceptable trade-off for a security-critical module, though callers should be aware they may need to handle/retry on write errors in edge cases. The JS-side single-flight guard suggested previously would eliminate the retry scenario but adds complexity.


6-6: LGTM!

Also applies to: 21-24, 35-37

app/lib/native/NativeDatabaseKeyStore.ts (1)

10-10: LGTM!

ios/Libraries/DatabaseKeyStore.swift (1)

65-92: LGTM!

android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.java (7)

73-82: removeItem still swallows exceptions and removeItemInternal uses non-durable writes.

The catch block at line 80 resolves with null instead of rejecting, hiding removal failures from JS callers. Additionally, removeItemInternal at line 175 uses apply() (async) rather than commit() (sync with result check). These issues were flagged in a previous review and remain unaddressed.

Also applies to: 172-176


35-46: LGTM!


52-71: LGTM!


94-121: LGTM!


123-134: Write-once enforcement looks correct.

The logic properly handles the idempotent case (existing value matches) as a no-op, and throws IllegalStateException when attempting to overwrite with a different value. This prevents stranding databases encrypted with a previously-minted key.


136-151: Ignore static analysis false positives on this block.

The ast-grep warnings about "Triple DES" and "AES with ECB mode" are false positives—they incorrectly flag KeyStore.getInstance(KEYSTORE_PROVIDER) which simply retrieves a KeyStore instance. The actual encryption uses TRANSFORMATION = "AES/GCM/NoPadding" (line 40), which is the recommended authenticated encryption mode.

Source: Linters/SAST tools


165-170: Durable write with failure check—addresses previous concern.

Using commit() instead of apply() ensures synchronous persistence before resolving success, and throwing on failure prevents the JS layer from believing a key was stored when it wasn't. This properly implements the fail-closed semantics for the critical key-minting path.


Walkthrough

Introduces a DatabaseKeyStore TurboModule on iOS (Keychain via Swift + ObjC++ bridge) and Android (AES-GCM via Android Keystore + SharedPreferences), along with a TypeScript getOrCreateDatabaseKey function that reads or mints a 64-char hex SQLCipher key using a db_key_v1:-namespaced storage key, failing closed on read errors.

Changes

Secure Database Key Store TurboModule

Layer / File(s) Summary
TypeScript TurboModule contract and key retrieval logic
app/lib/native/NativeDatabaseKeyStore.ts, app/lib/encryption/keyStore.ts, app/lib/encryption/__tests__/keyStore.test.ts
Defines the Spec TurboModule interface (getItem, setItem, removeItem) and implements getOrCreateDatabaseKey with fail-closed semantics: propagates read errors, returns existing string keys, mints a new 32-byte hex key via randomKey(32) only on null, and throws on unexpected non-string/non-null values. Tests cover all five behavioral branches.
iOS Keychain storage (Swift)
ios/Libraries/DatabaseKeyStore.swift
Implements DatabaseKeyStore with read, write, and delete static methods scoped by Keychain service and access group. read distinguishes not-found (nil with no error) from failure (nil with NSError). write adds and falls back to compare existing on duplicate.
iOS ObjC++ TurboModule bridge and Xcode wiring
ios/Libraries/DatabaseKeyStore.mm, ios/RocketChatRN.xcodeproj/project.pbxproj
Bridges DatabaseKeyStore to React Native Promises via DatabaseKeyStoreModule, mapping errors to KEYCHAIN_READ_ERROR/KEYCHAIN_WRITE_ERROR, resolving explicit JS null on miss using NSNull, and wiring the TurboModule JSI entrypoint. Xcode project adds both files to the Rocket.Chat and NotificationService build targets.
Android TurboModule spec and abstract base
android/app/src/main/java/.../storage/NativeDatabaseKeyStoreSpec.java
NativeDatabaseKeyStoreSpec is the abstract TurboModule base class with the fixed module name and three abstract @ReactMethod declarations for getItem, setItem, and removeItem.
Android Keystore AES-GCM implementation
android/app/src/main/java/.../storage/DatabaseKeyStoreModule.java
DatabaseKeyStoreModule stores AES-GCM encrypted blobs (IV prepended, Base64-encoded) in SharedPreferences, with per-key Android Keystore aliases. setItemInternal enforces write-once consistency by reading existing values and rejecting overwrites with different plaintext. removeItemInternal deletes only the SharedPreferences entry. JS-facing methods map exceptions to Promise error codes.
Android TurboPackage registration and app wiring
android/app/src/main/java/.../storage/DatabaseKeyStoreTurboPackage.java, .../MainApplication.kt
DatabaseKeyStoreTurboPackage implements getModule and getReactModuleInfoProvider to register the module with isTurboModule and hasConstants flags. MainApplication adds the package to getPackages().

Sequence Diagram(s)

sequenceDiagram
  rect rgba(70, 130, 180, 0.5)
    Note over App,NativeDatabaseKeyStore: Key retrieval or first-time minting
  end
  participant App
  participant getOrCreateDatabaseKey
  participant NativeDatabaseKeyStore
  participant PlatformStore

  App->>getOrCreateDatabaseKey: getOrCreateDatabaseKey("mydb")
  getOrCreateDatabaseKey->>NativeDatabaseKeyStore: getItem("db_key_v1:mydb")
  NativeDatabaseKeyStore->>PlatformStore: read key (Keychain / Keystore+SharedPrefs)
  PlatformStore-->>NativeDatabaseKeyStore: stored value or null
  NativeDatabaseKeyStore-->>getOrCreateDatabaseKey: string | null

  alt Key exists
    getOrCreateDatabaseKey-->>App: existing 64-char hex key
  else Not found (null)
    getOrCreateDatabaseKey->>getOrCreateDatabaseKey: randomKey(32) → new 64-char hex
    getOrCreateDatabaseKey->>NativeDatabaseKeyStore: setItem("db_key_v1:mydb", newKey)
    NativeDatabaseKeyStore->>PlatformStore: write encrypted key
    getOrCreateDatabaseKey-->>App: new 64-char hex key
  else Read error
    getOrCreateDatabaseKey-->>App: throws (fail closed)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • OtavioStasiak
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: fail-closed DatabaseKeyStore TurboModule' accurately summarizes the main change: introducing a new TurboModule for database key storage with fail-closed semantics, which is the primary objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (3)
  • NATIVE-1311: Request failed with status code 401
  • NATIVE-1276: Request failed with status code 401
  • NATIVE-1272: Request failed with status code 401

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 changed the title feat: fail-closed DatabaseKeyStore TurboModule (NATIVE-1311) feat: fail-closed DatabaseKeyStore TurboModule Jun 19, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

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

7-12: ⚡ Quick win

Annotate the native-module narrowing helper.

getNativeModule is the boundary that narrows Spec | null to Spec; make that return type explicit. As per coding guidelines, **/*.{ts,tsx} should “add explicit type annotations to function parameters and return types.”

Proposed fix
-import NativeDatabaseKeyStore from '../native/NativeDatabaseKeyStore';
+import NativeDatabaseKeyStore, { type Spec } from '../native/NativeDatabaseKeyStore';
 
 const KEY_PREFIX = 'db_key_v1:';
 
-function getNativeModule() {
+function getNativeModule(): Spec {
 	if (!NativeDatabaseKeyStore) {
 		throw new Error('DatabaseKeyStore native module not found — ensure the module is linked and the app is rebuilt');
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/encryption/keyStore.ts` around lines 7 - 12, Add an explicit return
type annotation to the getNativeModule function to make the type narrowing
boundary clear. The function currently lacks a return type annotation and should
be updated to explicitly declare that it returns the Spec type (the type of
NativeDatabaseKeyStore). This makes the type guard contract explicit and aligns
with the coding guideline requiring explicit type annotations on function return
types.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.java`:
- Around line 74-81: In the removeItem method, the catch block incorrectly calls
promise.resolve(null) when an exception occurs, which hides the failure from the
caller. Replace the promise.resolve(null) call in the exception handler with
promise.reject(e) to properly propagate the error. Apply the same fix to the
similar code pattern mentioned at lines 159-162 where removal operations fail
but incorrectly resolve with success.
- Around line 154-156: The setItem method in DatabaseKeyStoreModule uses apply()
for SharedPreferences writes, which is asynchronous and allows the method to
resolve before the data is actually persisted to disk. This creates a crash
window where the database key could be lost before being written, making
encrypted data unreadable. Replace the apply() call with commit() to ensure a
synchronous, durable write that blocks until the SharedPreferences data is
persisted before the method returns.

In `@app/lib/encryption/__tests__/keyStore.test.ts`:
- Around line 33-50: The test fixtures and assertions do not properly validate
the hexadecimal contract for SQLCipher keys. In the first test (the one checking
that existing keys are returned unchanged), replace the mock fixture string
'existingHexKey64chars0000000000000000000000000000000000000000000000' with a
valid 64-character hexadecimal string containing only 0-9 and a-f characters. In
the second test (the one checking that new keys are minted and stored), add an
additional assertion after the length check to verify that the result is a valid
hexadecimal string, such as by testing it against a hex pattern. This ensures
both tests will catch any malformed key generation or storage.

In `@app/lib/encryption/keyStore.ts`:
- Around line 28-39: The validation for the existing key in the
DatabaseKeyStore.getItem block only checks if the value is a string, but does
not validate that it conforms to the API contract of being a 32-byte key encoded
as exactly 64 hexadecimal characters. Add validation logic when checking typeof
existing === 'string' to ensure the string matches the expected format (64 hex
characters) and throw an error with a descriptive message if it does not.
Additionally, before persisting the newly generated key with native.setItem
after calling randomKey(32), validate that the generated key also matches the
expected 64-character hex format to ensure consistency.
- Around line 22-40: The getOrCreateDatabaseKey function has a race condition
where two concurrent calls with the same dbName can both read null, mint
different keys, and race on setItem, causing one key to overwrite the other and
making data unreadable. Add serialization using a per-dbName lock or similar
single-flight guard to ensure only one concurrent request actually creates and
stores the key for each database name. This prevents multiple keys from being
generated and ensures all callers for the same dbName receive the same key
value.

In `@ios/Libraries/DatabaseKeyStore.swift`:
- Around line 80-87: The current implementation overwrites an existing Keychain
item when SecItemAdd returns errSecDuplicateItem, which breaks the fail-closed
guarantee by replacing a key that may have been created by another caller.
Instead of updating the existing key with new data in the duplicate item case,
remove the SecItemUpdate call and its attrs manipulation, and simply return true
(indicating the key now exists and can be used) or return false to signal that a
duplicate was encountered, depending on your intended behavior for handling
pre-existing keys.

---

Nitpick comments:
In `@app/lib/encryption/keyStore.ts`:
- Around line 7-12: Add an explicit return type annotation to the
getNativeModule function to make the type narrowing boundary clear. The function
currently lacks a return type annotation and should be updated to explicitly
declare that it returns the Spec type (the type of NativeDatabaseKeyStore). This
makes the type guard contract explicit and aligns with the coding guideline
requiring explicit type annotations on function return types.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d91e1b50-aa08-46b7-bfa6-eee799c6a139

📥 Commits

Reviewing files that changed from the base of the PR and between bf1dbdb and 6eb791f.

📒 Files selected for processing (10)
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
  • android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.java
  • android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreTurboPackage.java
  • android/app/src/main/java/chat/rocket/reactnative/storage/NativeDatabaseKeyStoreSpec.java
  • app/lib/encryption/__tests__/keyStore.test.ts
  • app/lib/encryption/keyStore.ts
  • app/lib/native/NativeDatabaseKeyStore.ts
  • ios/Libraries/DatabaseKeyStore.mm
  • ios/Libraries/DatabaseKeyStore.swift
  • ios/RocketChatRN.xcodeproj/project.pbxproj
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/lib/encryption/keyStore.ts
  • app/lib/native/NativeDatabaseKeyStore.ts
  • app/lib/encryption/__tests__/keyStore.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript with strict mode enabled

Files:

  • app/lib/encryption/keyStore.ts
  • app/lib/native/NativeDatabaseKeyStore.ts
  • app/lib/encryption/__tests__/keyStore.test.ts
**/*.{js,jsx,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier formatting with tabs, single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses

Files:

  • app/lib/encryption/keyStore.ts
  • app/lib/native/NativeDatabaseKeyStore.ts
  • app/lib/encryption/__tests__/keyStore.test.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Enforce ESLint rules from @rocket.chat/eslint-config with React, React Native, TypeScript, and Jest plugins

Files:

  • app/lib/encryption/keyStore.ts
  • app/lib/native/NativeDatabaseKeyStore.ts
  • app/lib/encryption/__tests__/keyStore.test.ts
app/lib/encryption/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Place end-to-end encryption implementation in 'app/lib/encryption/' directory using @rocket.chat/mobile-crypto

Files:

  • app/lib/encryption/keyStore.ts
  • app/lib/encryption/__tests__/keyStore.test.ts
🧠 Learnings (3)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/lib/encryption/keyStore.ts
  • app/lib/native/NativeDatabaseKeyStore.ts
  • app/lib/encryption/__tests__/keyStore.test.ts
📚 Learning: 2026-03-05T06:06:12.277Z
Learnt from: divyanshu-patil
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6957
File: ios/RCTWatchModule.mm:19-24
Timestamp: 2026-03-05T06:06:12.277Z
Learning: Do not re-activate or reset the WCSession singleton in iOS Objective-C/Swift bridge modules. Ensure WCSession is activated and its delegate is set in a single, central place (e.g., ios/RocketChat Watch App/Loaders/WatchSession.swift) and avoid duplicating activation or delegate assignment in other iOS bridge files like ios/RCTWatchModule.mm. If WCSession is already activated via the central loader, relying on WCSession.defaultSession is sufficient and maintains a single session lifecycle.

Applied to files:

  • ios/Libraries/DatabaseKeyStore.mm
📚 Learning: 2026-05-12T21:58:10.053Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6991
File: ios/Libraries/Challenge.mm:29-37
Timestamp: 2026-05-12T21:58:10.053Z
Learning: For iOS code using react-native-mmkv, when calling `[MMKV initializeMMKV:nil groupDir:groupDir logLevel:...]` (and when passing `rootPath` to MMKVBridge), pass the raw App Group container URL path only (e.g., `[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroup] path]`). Do NOT append `/mmkv` yourself. The MMKV SDK already appends `/mmkv` internally (`...stringByAppendingPathComponent:@"mmkv"`), so passing `<AppGroup>/mmkv` will cause double-nesting (`<AppGroup>/mmkv/mmkv`) and can break shared storage/SSL cert lookups.

Applied to files:

  • ios/Libraries/DatabaseKeyStore.mm
🪛 ast-grep (0.43.0)
android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.java

[warning] 101-101: Triple DES (3DES or DESede) is considered deprecated. AES is the recommended cipher. Upgrade to use AES.
Context: KeyStore.getInstance(KEYSTORE_PROVIDER)
Note: [CWE-326]: Inadequate Encryption Strength [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(desede-is-deprecated-java)


[warning] 116-116: Triple DES (3DES or DESede) is considered deprecated. AES is the recommended cipher. Upgrade to use AES.
Context: Cipher.getInstance(TRANSFORMATION)
Note: [CWE-326]: Inadequate Encryption Strength [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(desede-is-deprecated-java)


[warning] 124-124: Triple DES (3DES or DESede) is considered deprecated. AES is the recommended cipher. Upgrade to use AES.
Context: KeyStore.getInstance(KEYSTORE_PROVIDER)
Note: [CWE-326]: Inadequate Encryption Strength [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(desede-is-deprecated-java)


[warning] 129-129: Triple DES (3DES or DESede) is considered deprecated. AES is the recommended cipher. Upgrade to use AES.
Context: KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER)
Note: [CWE-326]: Inadequate Encryption Strength [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(desede-is-deprecated-java)


[warning] 142-142: Triple DES (3DES or DESede) is considered deprecated. AES is the recommended cipher. Upgrade to use AES.
Context: Cipher.getInstance(TRANSFORMATION)
Note: [CWE-326]: Inadequate Encryption Strength [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(desede-is-deprecated-java)


[warning] 101-101: Use of AES with ECB mode detected. ECB doesn't provide message confidentiality and is not semantically secure so should not be used. Instead, use a strong, secure cipher: Cipher.getInstance("AES/CBC/PKCS7PADDING"). See https://owasp.org/www-community/Using_the_Java_Cryptographic_Extensions for more information.
Context: KeyStore.getInstance(KEYSTORE_PROVIDER)
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(use-of-aes-ecb-java)


[warning] 116-116: Use of AES with ECB mode detected. ECB doesn't provide message confidentiality and is not semantically secure so should not be used. Instead, use a strong, secure cipher: Cipher.getInstance("AES/CBC/PKCS7PADDING"). See https://owasp.org/www-community/Using_the_Java_Cryptographic_Extensions for more information.
Context: Cipher.getInstance(TRANSFORMATION)
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(use-of-aes-ecb-java)


[warning] 124-124: Use of AES with ECB mode detected. ECB doesn't provide message confidentiality and is not semantically secure so should not be used. Instead, use a strong, secure cipher: Cipher.getInstance("AES/CBC/PKCS7PADDING"). See https://owasp.org/www-community/Using_the_Java_Cryptographic_Extensions for more information.
Context: KeyStore.getInstance(KEYSTORE_PROVIDER)
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(use-of-aes-ecb-java)


[warning] 129-129: Use of AES with ECB mode detected. ECB doesn't provide message confidentiality and is not semantically secure so should not be used. Instead, use a strong, secure cipher: Cipher.getInstance("AES/CBC/PKCS7PADDING"). See https://owasp.org/www-community/Using_the_Java_Cryptographic_Extensions for more information.
Context: KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER)
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(use-of-aes-ecb-java)


[warning] 142-142: Use of AES with ECB mode detected. ECB doesn't provide message confidentiality and is not semantically secure so should not be used. Instead, use a strong, secure cipher: Cipher.getInstance("AES/CBC/PKCS7PADDING"). See https://owasp.org/www-community/Using_the_Java_Cryptographic_Extensions for more information.
Context: Cipher.getInstance(TRANSFORMATION)
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(use-of-aes-ecb-java)

🔇 Additional comments (9)
app/lib/native/NativeDatabaseKeyStore.ts (1)

1-10: LGTM!

app/lib/encryption/__tests__/keyStore.test.ts (1)

53-75: LGTM!

ios/Libraries/DatabaseKeyStore.swift (1)

39-63: LGTM!

Also applies to: 93-102

ios/Libraries/DatabaseKeyStore.mm (2)

25-57: LGTM!


1-3: Import the generated DatabaseKeyStore TurboModule spec or verify the spec header path.

This file uses <NativeDatabaseKeyStoreSpec> at line 14 and facebook::react::NativeDatabaseKeyStoreSpecJSI at line 63, but the corresponding generated spec header does not exist in the repository. The codegenConfig in package.json does not include DatabaseKeyStore (only SSLPinning is configured). While VoipModule.mm uses a similar pattern with <NativeVoipSpec>, the missing spec header declaration requires verification that this code actually compiles or confirmation of the correct import path through React Native's codegen setup.

ios/RocketChatRN.xcodeproj/project.pbxproj (1)

151-153: LGTM!

Also applies to: 438-439, 942-943, 1684-1684, 1735-1736

android/app/src/main/java/chat/rocket/reactnative/storage/NativeDatabaseKeyStoreSpec.java (1)

1-30: LGTM!

android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreTurboPackage.java (1)

1-43: LGTM!

android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (1)

22-22: LGTM!

Also applies to: 47-47

Comment thread app/lib/encryption/__tests__/keyStore.test.ts
Comment thread app/lib/encryption/keyStore.ts
Comment thread app/lib/encryption/keyStore.ts Outdated
Comment thread ios/Libraries/DatabaseKeyStore.swift Outdated
The DatabaseKeyStore mints and stores the per-database SQLCipher key; losing
or overwriting that key permanently bricks the encrypted database, so every
write path must be fail-closed.

- Android: persist the encrypted key blob with commit() instead of apply(),
  and throw if it returns false. apply() flushes asynchronously, so a crash
  between mint and flush left the just-encrypted database unrecoverable.
- Android & iOS: make writes write-once. Both layers previously overwrote an
  existing key (Android re-encrypted unconditionally; iOS ran SecItemUpdate on
  errSecDuplicateItem). Now a write with a differing value for an existing key
  is refused; an identical value is a no-op success.
- JS: validate the 64-hex-char key contract on both read and mint, rejecting a
  malformed value before it reaches SQLCipher's PRAGMA key.
- JS: register the TurboModule with getEnforcing (always-present cross-platform
  module), making the export non-nullable and removing the dead null guard.

Claude-Session: https://claude.ai/code/session_01TE9VsFTeXsXc8ssqeR8MJ7
@diegolmello diegolmello requested a deployment to approve_e2e_testing June 19, 2026 17:48 — with GitHub Actions Waiting
@diegolmello diegolmello merged commit 38ffab5 into feat/native-1272-sqlcipher-migration Jun 19, 2026
5 of 8 checks passed
@diegolmello diegolmello deleted the feat/native-1311-database-keystore branch June 19, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant