Skip to content

feat: native SQLCipher database readers and native key store (NATIVE-1276)#7398

Open
diegolmello wants to merge 5 commits into
feat/native-1274-driver-adapterfrom
feat/native-1276-native-readers
Open

feat: native SQLCipher database readers and native key store (NATIVE-1276)#7398
diegolmello wants to merge 5 commits into
feat/native-1274-driver-adapterfrom
feat/native-1276-native-readers

Conversation

@diegolmello

@diegolmello diegolmello commented Jun 15, 2026

Copy link
Copy Markdown
Member

Proposed changes

Rewrites the native (non-JS) database readers so they can open the SQLCipher-encrypted databases created by the new expo-sqlite/Drizzle driver, and adds a native key store that holds the per-database encryption keys. This is the native counterpart to the JS driver adapter — the iOS Notification Service Extension and the Android FCM notification path both read the local database directly (outside JS) to resolve E2E room keys, so they must be taught to open the encrypted files with the same key material and open sequence the JS side uses.

The key store is a TurboModule: the app runs the New Architecture (bridgeless), so native modules must expose a codegen-style spec rather than the legacy RCT_EXTERN_MODULE / NativeModules path.

iOS

  • ios/Libraries/DatabaseKeyStore.swift: Keychain helper class (kSecClassGenericPassword, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, kSecAttrSynchronizable = false, access group S6UPZG7ZR3.chat.rocket.reactnative). Static read/write/delete the NotificationService extension calls directly.
  • ios/Libraries/DatabaseKeyStore.mm: the TurboModule (DatabaseKeyStoreModule) implementing the spec and returning its JSI instance from getTurboModule. The JS registration name is deliberately distinct from the Swift helper class so it does not collide with an existing ObjC class — required by the bridgeless NSClassFromString fallback for app-target modules. getItem resolves explicit JS null on a miss (nil bridges to undefined, which breaks the Promise<string | null> contract and the !== null check in getOrCreateDatabaseKey).
  • ios/Shared/RocketChat/Database.swift: opens SQLCipher-encrypted databases (raw-key PRAGMA key = "x'…'"busy_timeout → open-verify), derives the clean .db filename the same way the JS driver does, and reads the key from the Keychain helper. Room-key/encrypted query semantics are unchanged.
  • ios/Podfile: adds pod 'SQLCipher', '~> 4.7.0' (locked to the version expo-sqlite vendors) plus the static-framework and xcconfig fix-ups required to link SQLCipher alongside the SDK's built-in SQLite.
  • ios/RocketChatRN.xcodeproj + bridging header: register the new files in the app and extension targets.

Android

  • …/storage/NativeDatabaseKeyStoreSpec.java: abstract TurboModule spec (getName()DatabaseKeyStoreModule, abstract getItem/setItem/removeItem).
  • …/storage/DatabaseKeyStoreModule.java: concrete module. Each key is wrapped with an AndroidKeyStore AES-GCM key; the wrapped blob lives in its own SharedPreferences file. Exposes the JS methods plus static helpers the notification path calls without a React context.
  • …/storage/DatabaseKeyStoreTurboPackage.java (+ MainApplication.kt registration): TurboReactPackage that maps the module name to a ReactModuleInfo with isTurboModule = true and returns the concrete instance.
  • …/notification/Encryption.java: opens the database with net.zetetic SQLCipher using the raw-key string form (never the byte[] overload, which silently PBKDF2-derives and fails), derives the clean filename to match the JS convention, reads the key from the key store, and applies the same busy_timeout invariant.
  • android/app/build.gradle: adds net.zetetic:sqlcipher-android:4.7.0.

JS

  • app/lib/native/NativeDatabaseKeyStore.ts: TurboModule spec resolved via TurboModuleRegistry.get('DatabaseKeyStoreModule').
  • app/lib/database/driver/keyStore.ts (+ test): thin shim wrapping the native module as the driver's IKeychainShim, with installNativeKeychainShim() to wire it into the key service at startup.

Issue(s)

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

How to test or reproduce

This PR is stacked on #7396 (driver adapter), which is stacked on #7395 (schema). Review/merge those first. The native open path is exercised end-to-end once the data layer (wipe-and-restore) lands; for this PR:

  • iOS: pnpm pod-install then build the app and the NotificationService extension — both targets must compile with the new SQLCipher pod and the registered TurboModule. Runtime: a Keychain round-trip through DatabaseKeyStoreModule returns null on a miss, the stored value after setItem, and null again after removeItem.
  • Android: build the app — net.zetetic:sqlcipher-android resolves and Encryption.java compiles against the concrete module's static helpers.
  • JS: TZ=UTC pnpm test --testPathPattern='driver/__tests__/keyStore' and pnpm lint.

Types of changes

  • New feature (non-breaking change which adds functionality)

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)

Further comments

Native modules use the New Architecture TurboModule path because the app is bridgeless; the legacy RCT_EXTERN_MODULE / NativeModules registration does not vend modules under bridgeless. The crypto (iOS Keychain, Android AndroidKeyStore AES-GCM) is unchanged from the prior revision — only the module registration and class layout moved to the TurboModule shape.

Summary by CodeRabbit

  • New Features
    • Added SQLCipher-encrypted database support with per-database encryption keys securely stored on iOS (Keychain) and Android (Keystore).
    • Introduced a native-backed key/value module to provide encryption keys to the database.
  • Chores
    • Updated native database opening to apply encryption settings (including key loading and busy timeout) and verify successful access.
    • Added required iOS/Android build and framework configuration for SQLCipher.
  • Tests
    • Added Jest coverage for native key store shim integration and missing-module error handling.

…y store (NATIVE-1276)

- ios/Libraries/DatabaseKeyStore.swift + .m: new RCTBridgeModule backed by
  kSecClassGenericPassword with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
  kSecAttrSynchronizable=false, and the full team-prefixed access group
  S6UPZG7ZR3.chat.rocket.reactnative; exposes getItem/setItem/removeItem to JS
  and a static read() helper callable from Database.swift in the NSE

- ios/Shared/RocketChat/Database.swift: rewrites plain sqlite3_open to open
  SQLCipher-encrypted DBs; derives clean filenames via deriveServerDbName()
  (mirrors JS connection.ts); reads keys from DatabaseKeyStore using the JS
  KEY_PREFIX "db_key_v1:<dbName>"; applies PRAGMA key (raw x'...' form) then
  busy_timeout=500 then open-verify; ARC hazard fixed (handle private, never
  exposed to callers)

- ios/Podfile: adds pod 'SQLCipher' ~> 4.7.0 to abstract_target defaults,
  adds SQLCipher to $static_framework list, and adds the two post_install
  xcconfig fixups from the spike (strip SDK sqlite3 defines that break Swift
  module maps, re-inject LIBRARY_SEARCH_PATHS suppressed by static_framework
  override); Dir.glob rooted with __dir__ for worktree/CI safety

- android/app/src/.../storage/DatabaseKeyStore.java: new native module with
  AES-GCM AndroidKeyStore wrapping; SharedPreferences file "RCDatabaseKeyStore";
  static getItemInternal/setItemInternal callable from Encryption.java without
  ReactApplicationContext

- android/app/src/.../storage/DatabaseKeyStorePackage.java: ReactPackage wrapper

- android/app/src/.../MainApplication.kt: registers DatabaseKeyStorePackage

- android/app/src/.../notification/Encryption.java: replaces WMDatabase.getInstance
  with net.zetetic:sqlcipher-android SQLiteDatabase; uses raw-key string form
  "x'<64 hex>'" (never byte[] overload); derives clean .db filename via
  deriveDbName() matching JS convention; reads key from DatabaseKeyStore

- android/app/build.gradle: adds net.zetetic:sqlcipher-android:4.7.0@aar

- app/lib/database/driver/keyStore.ts: thin JS shim wrapping NativeModules.DatabaseKeyStore
  into an IKeychainShim; exports installNativeKeychainShim() to wire into keyService

- app/lib/database/driver/__tests__/keyStore.test.ts: Jest tests for the shim
  delegation and missing-module error path
- ios/RocketChatRN.xcodeproj: register DatabaseKeyStore.swift in both the
  Rocket.Chat app and NotificationService Sources phases (Database.swift in
  the NSE references it) and the .m bridge in the app target; without this the
  files never compile
- ios bridging header: import <React/RCTBridgeModule.h> explicitly rather than
  relying on RNCallKeep's transitive include
- Encryption.java: add PRAGMA busy_timeout = 500 after open to match the JS
  driver and iOS reader (avoids SQLITE_BUSY when the notification path reads
  while the app holds a WAL lock); drop two unused SQLCipher imports
@coderabbitai

coderabbitai Bot commented Jun 15, 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: 50d47b26-f35a-45bb-9537-f8b27a09f3be

📥 Commits

Reviewing files that changed from the base of the PR and between 72a719c and 5606e6f.

📒 Files selected for processing (5)
  • android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
  • android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.java
  • ios/Libraries/DatabaseKeyStore.mm
  • ios/Libraries/DatabaseKeyStore.swift
  • ios/Shared/RocketChat/Database.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
  • ios/Libraries/DatabaseKeyStore.mm
  • ios/Shared/RocketChat/Database.swift
📜 Recent review details
🧰 Additional context used
🪛 ast-grep (0.43.0)
android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.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] 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)

🔇 Additional comments (6)
android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.java (3)

52-60: LGTM!

Also applies to: 88-120


123-156: LGTM!


73-82: LGTM!

Also applies to: 158-162

ios/Libraries/DatabaseKeyStore.swift (3)

34-63: LGTM!


65-91: LGTM!


93-103: LGTM!


Walkthrough

Adds a cross-platform DatabaseKeyStore native TurboModule (iOS: Keychain; Android: AES-GCM/AndroidKeyStore) and a JS shim registering it in keyService. Updates Encryption.java and Database.swift to derive SQLCipher database filenames from server URLs and open encrypted databases using keys retrieved from the new store, replacing prior WatermelonDB and path-based approaches.

Changes

DatabaseKeyStore and SQLCipher Key-Based Database Access

Layer / File(s) Summary
JS/TS TurboModule spec, keyService shim, and tests
app/lib/native/NativeDatabaseKeyStore.ts, app/lib/database/driver/keyStore.ts, app/lib/database/driver/__tests__/keyStore.test.ts
Declares the Spec TurboModule interface for DatabaseKeyStoreModule, wraps it in a getNativeModule/makeNativeShim adapter, exports installNativeKeychainShim() to register it in keyService, and tests delegation and the missing-module error path.
Android DatabaseKeyStore module spec, implementation, and wiring
android/app/build.gradle, android/app/src/main/java/chat/rocket/reactnative/storage/NativeDatabaseKeyStoreSpec.java, 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/MainApplication.kt
Adds the abstract TurboModule spec, a concrete implementation using AES-GCM encryption backed by AndroidKeystore with ciphertext stored in SharedPreferences, a TurboReactPackage wrapping it, registration in MainApplication, and a locked net.zetetic:sqlcipher-android:4.7.0@aar dependency.
iOS DatabaseKeyStore Keychain helpers, RN bridge, and build wiring
ios/Libraries/DatabaseKeyStore.swift, ios/Libraries/DatabaseKeyStore.mm, ios/RocketChatRN-Bridging-Header.h, ios/Podfile, ios/RocketChatRN.xcodeproj/project.pbxproj
Adds DatabaseKeyStore.swift with idempotent Keychain read/write/delete using kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, bridges it to React Native via DatabaseKeyStore.mm as a NativeDatabaseKeyStoreSpec TurboModule, imports RCTBridgeModule in the bridging header, adds the SQLCipher ~> 4.7.0 pod with static-framework override and xcconfig post-install patches, and wires the new source files into both Xcode targets.
Android Encryption.java SQLCipher database access
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
Replaces the WatermelonDB WMDatabase-based readRoom path with direct SQLCipher access: adds deriveDbName(serverUrl) matching the JS naming scheme, loads the sqlcipher native library, fetches the raw hex key from DatabaseKeyStoreModule.getItemInternal, opens the database read-only, sets PRAGMA busy_timeout, queries subscriptions, and closes in finally.
iOS Database.swift SQLCipher open and query rewrite
ios/Shared/RocketChat/Database.swift
Rewrites Database.swift to add deriveServerDbName(from:) matching JS logic, open the database from the app-group container path, apply PRAGMA key = x'...' as the first statement using a key from DatabaseKeyStore, set busy_timeout = 500, verify with sqlite_master, and update query/decodeQueryResult/subscription accessors with defer-finalize and NSLog error handling.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(173, 216, 230, 0.5)
    Note over JS,DatabaseKeyStoreModule: Key registration (app startup)
    JS->>installNativeKeychainShim: installNativeKeychainShim()
    installNativeKeychainShim->>keyService: installKeychainShim(nativeShim)
  end

  rect rgba(144, 238, 144, 0.5)
    Note over Encryption,SQLCipher: Android FCM notification path
    Encryption->>Encryption: deriveDbName(serverUrl)
    Encryption->>DatabaseKeyStoreModule: getItemInternal(context, "db_key_v1:<dbName>")
    DatabaseKeyStoreModule->>AndroidKeyStore: load AES-GCM key
    DatabaseKeyStoreModule->>SharedPreferences: read IV+ciphertext
    DatabaseKeyStoreModule-->>Encryption: hex key string
    Encryption->>SQLCipher: openDatabase(path, rawKey)
    Encryption->>SQLCipher: PRAGMA busy_timeout
    Encryption->>SQLCipher: SELECT e2e_key, encrypted FROM subscriptions
    SQLCipher-->>Encryption: Room row
  end

  rect rgba(255, 218, 185, 0.5)
    Note over NotificationService,SQLite: iOS notification path
    NotificationService->>Database: init(serverUrl)
    Database->>Database: deriveServerDbName(serverUrl)
    Database->>SQLite: sqlite3_open_v2(dbPath)
    Database->>DatabaseKeyStore: read("db_key_v1:<dbName>")
    DatabaseKeyStore->>Keychain: SecItemCopyMatching
    Keychain-->>DatabaseKeyStore: stored key
    DatabaseKeyStore-->>Database: hex key string
    Database->>SQLite: PRAGMA key = x'...'
    Database->>SQLite: PRAGMA busy_timeout = 500
    Database->>SQLite: SELECT count(*) FROM sqlite_master
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • RocketChat/Rocket.Chat.ReactNative#7322: Both PRs modify Encryption.java to change the database naming logic, with #7322 removing the -experimental path suffix and this PR replacing the entire WatermelonDB-based read flow with direct SQLCipher access.

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.46% 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 pull request title clearly and specifically describes the main change: introducing native SQLCipher database readers and a native key store mechanism. This directly aligns with the core objective stated in the PR summary.
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 (1)
  • NATIVE-1276: 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.

@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: 4

🤖 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/notification/Encryption.java`:
- Line 205: The Android and iOS implementations have a column name mismatch in
their subscription queries. In
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
at line 205, the rawQuery method uses WHERE id == ? but should use WHERE rid = ?
to match the actual database schema column name. The iOS implementation at
ios/Shared/RocketChat/Database.swift at lines 159-163 already correctly uses rid
as the column name, confirming this is the correct schema column. Update the
WHERE clause in the Android query to use rid instead of id to ensure consistency
with the actual database schema and match the iOS implementation.
- Around line 205-226: The cursor obtained from db.rawQuery() may leak if an
exception occurs during the column access operations (such as getColumnIndex or
getString calls). Wrap the entire cursor usage block in a try-finally statement,
ensuring that cursor.close() is called in the finally block so it executes
regardless of whether an exception is thrown. Move the cursor creation and all
subsequent operations (getCount, moveToFirst, getColumnIndex, getString, getInt)
into the try block, and place the cursor.close() call in the finally block to
guarantee resource cleanup.

In `@ios/Podfile`:
- Around line 106-110: The exact literal string matching for
LIBRARY_SEARCH_PATHS in the gsub call on line 108 is too rigid and fails
silently when xcconfigs have spacing variations or already contain additional
paths. Replace the exact string 'LIBRARY_SEARCH_PATHS = $(inherited)' with a
regex pattern that matches LIBRARY_SEARCH_PATHS with flexible whitespace around
the equals sign and captures any existing content after $(inherited), then
append the sqlcipher_lib_dir to the matched pattern. This ensures the SQLCipher
library path is correctly added regardless of spacing variations or pre-existing
paths in the xcconfig file.

In `@ios/Shared/RocketChat/Database.swift`:
- Around line 104-112: The verification query in the database open method only
calls sqlite3_prepare_v2 which validates SQL syntax but does not read encrypted
data, so a wrong encryption key won't be detected. After preparing the statement
with sqlite3_prepare_v2, add a call to sqlite3_step on the prepared statement to
actually execute the query and read from sqlite_master. The sqlite3_step call
should return SQLITE_ROW to indicate successful execution of the SELECT query,
and only then can you determine if the key is correct. Keep the sqlite3_finalize
call to properly clean up the statement pointer after the step is executed.
Update the verification logic to check the result of sqlite3_step rather than
just sqlite3_prepare_v2.
🪄 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: e1b44220-d55d-4afa-9b9f-42d6c66aa09b

📥 Commits

Reviewing files that changed from the base of the PR and between e5e3091 and 6aaba61.

📒 Files selected for processing (13)
  • android/app/build.gradle
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
  • android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStore.java
  • android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStorePackage.java
  • app/lib/database/driver/__tests__/keyStore.test.ts
  • app/lib/database/driver/keyStore.ts
  • ios/Libraries/DatabaseKeyStore.m
  • ios/Libraries/DatabaseKeyStore.swift
  • ios/Podfile
  • ios/RocketChatRN-Bridging-Header.h
  • ios/RocketChatRN.xcodeproj/project.pbxproj
  • ios/Shared/RocketChat/Database.swift
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/database/driver/__tests__/keyStore.test.ts
  • app/lib/database/driver/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/database/driver/__tests__/keyStore.test.ts
  • app/lib/database/driver/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/database/driver/__tests__/keyStore.test.ts
  • app/lib/database/driver/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/database/driver/__tests__/keyStore.test.ts
  • app/lib/database/driver/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/database/driver/__tests__/keyStore.test.ts
  • app/lib/database/driver/keyStore.ts
🪛 ast-grep (0.43.0)
android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStore.java

[warning] 106-106: 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] 120-120: 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] 132-132: 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] 137-137: 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] 150-150: 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] 106-106: 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] 120-120: 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] 132-132: 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] 137-137: 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] 150-150: 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 (13)
app/lib/database/driver/keyStore.ts (1)

15-45: LGTM!

app/lib/database/driver/__tests__/keyStore.test.ts (1)

41-71: LGTM!

android/app/build.gradle (1)

144-147: LGTM!

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

12-23: LGTM!

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

20-53: LGTM!

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

22-24: LGTM!

Also applies to: 299-300, 954-955, 1684-1684, 1735-1736

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

1-171: LGTM!

ios/Libraries/DatabaseKeyStore.swift (1)

1-121: LGTM!

ios/Libraries/DatabaseKeyStore.m (1)

1-20: LGTM!

ios/RocketChatRN-Bridging-Header.h (1)

6-6: LGTM!

android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (1)

239-252: LGTM!

ios/Shared/RocketChat/Database.swift (2)

119-170: LGTM!


66-73: Remove this verification request — the path difference between platforms is intentional and consistent.

The iOS path construction (groupRoot + "/" + dbName) and Android path construction (getFilesDir() + "/SQLite/" + dbName) are platform-specific by design. On Android, expo-sqlite automatically stores databases in the /SQLite/ subdirectory within the app's internal files directory. The JavaScript layer on Android passes the filename to expo-sqlite without an explicit path, allowing the library to manage the subdirectory. On iOS, the app group container directory is used instead of the default internal storage, so no subdirectory is needed. Both platforms correctly align their native and JavaScript implementations.

			> Likely an incorrect or invalid review comment.

Comment thread android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java Outdated
Comment thread ios/Podfile
Comment thread ios/Shared/RocketChat/Database.swift Outdated
The app runs the New Architecture (bridgeless), so the key store must be a
TurboModule with a codegen-style spec, not the legacy RCT_EXTERN_MODULE /
NativeModules path it used before.

iOS: replace the .m with a .mm module class (DatabaseKeyStoreModule) that
implements the spec and returns its JSI instance from getTurboModule. The
Swift Keychain helper stays a separate class (DatabaseKeyStore) so the JS
registration name does not collide with an existing ObjC class — required by
the bridgeless NSClassFromString fallback. getItem resolves explicit JS null
on a miss (nil bridges to undefined and breaks the Promise<string|null>
contract and the !== null check in getOrCreateDatabaseKey).

Android: add an abstract spec (NativeDatabaseKeyStoreSpec) plus a concrete
module and a TurboReactPackage that registers it with isTurboModule=true.
The AndroidKeyStore AES-GCM crypto is unchanged; only the registration name
and class layout move. Encryption.java (NSE reader) now calls the static
helpers on the concrete module class.

JS resolves the module via TurboModuleRegistry.get('DatabaseKeyStoreModule').

@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: 1

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

14-19: ⚡ Quick win

Add an explicit return type for getNativeModule.

Line 14 omits a return type annotation, which violates the TS guideline used in this repo.

Suggested change
-function getNativeModule() {
+function getNativeModule(): NonNullable<typeof NativeDatabaseKeyStore> {
 	if (!NativeDatabaseKeyStore) {
		throw new Error('DatabaseKeyStore native module not found — ensure the module is linked and the app is rebuilt');
	}
	return NativeDatabaseKeyStore;
}

As per coding guidelines, **/*.{ts,tsx} requires explicit type annotations for function return types.

🤖 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/database/driver/keyStore.ts` around lines 14 - 19, Add an explicit
return type annotation to the getNativeModule function. The function currently
has no return type specified on line 14. Add the appropriate return type
annotation after the function signature (between the closing parenthesis and
opening brace). The return type should match the type of NativeDatabaseKeyStore,
which is what the function returns. This ensures compliance with the
repository's TypeScript guidelines requiring explicit return type annotations
for all functions.

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 53-59: The getItem method in DatabaseKeyStoreModule.java (lines
53-59, the getItem function) currently resolves the promise with null on any
exception during key retrieval, which causes downstream logic to interpret the
failure as a missing key rather than an actual error. Reject the promise instead
of resolving it when an unexpected exception occurs in the try block—use
promise.reject() to propagate the error. Correspondingly, in the iOS
implementation at ios/Libraries/DatabaseKeyStore.swift (lines 47-50),
distinguish between true not-found errors and other failures: only return nil
when the key genuinely does not exist, and for unexpected failures, propagate or
log them distinctly rather than also returning nil.

---

Nitpick comments:
In `@app/lib/database/driver/keyStore.ts`:
- Around line 14-19: Add an explicit return type annotation to the
getNativeModule function. The function currently has no return type specified on
line 14. Add the appropriate return type annotation after the function signature
(between the closing parenthesis and opening brace). The return type should
match the type of NativeDatabaseKeyStore, which is what the function returns.
This ensures compliance with the repository's TypeScript guidelines requiring
explicit return type annotations for all functions.
🪄 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: 5e95918b-e410-45db-a4e6-30a2f1f17d1a

📥 Commits

Reviewing files that changed from the base of the PR and between 6aaba61 and a3b082e.

⛔ Files ignored due to path filters (1)
  • ios/Podfile.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
  • 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/database/driver/__tests__/keyStore.test.ts
  • app/lib/database/driver/keyStore.ts
  • app/lib/native/NativeDatabaseKeyStore.ts
  • ios/Libraries/DatabaseKeyStore.mm
  • ios/Libraries/DatabaseKeyStore.swift
  • ios/RocketChatRN.xcodeproj/project.pbxproj
🚧 Files skipped from review as they are similar to previous changes (3)
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
  • ios/RocketChatRN.xcodeproj/project.pbxproj
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/database/driver/keyStore.ts
  • app/lib/database/driver/__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/native/NativeDatabaseKeyStore.ts
  • app/lib/database/driver/keyStore.ts
  • app/lib/database/driver/__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/native/NativeDatabaseKeyStore.ts
  • app/lib/database/driver/keyStore.ts
  • app/lib/database/driver/__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/native/NativeDatabaseKeyStore.ts
  • app/lib/database/driver/keyStore.ts
  • app/lib/database/driver/__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/native/NativeDatabaseKeyStore.ts
  • app/lib/database/driver/keyStore.ts
  • app/lib/database/driver/__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
🔇 Additional comments (5)
app/lib/native/NativeDatabaseKeyStore.ts (1)

4-10: LGTM!

app/lib/database/driver/__tests__/keyStore.test.ts (1)

17-23: LGTM!

Also applies to: 46-49, 63-71

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

9-30: LGTM!

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

16-43: LGTM!

ios/Libraries/DatabaseKeyStore.mm (1)

25-33: LGTM!

Also applies to: 35-45, 56-59

The iOS open-verify only prepared the probe statement; sqlite3_prepare_v2
parses SQL without decrypting a page, so a wrong key surfaced later at
first real use instead of at open. Step the statement (SELECT count(*)
FROM sqlite_master) so SQLCipher decrypts and the wrong-key case is caught
at open time.

On Android, the NSE subscription-read cursor leaked if getColumnIndex or
getString threw between open and close. Wrap the cursor body in try/finally
so it always closes.
getOrCreateDatabaseKey treats a null getItem result as "no key exists"
and generates a replacement. Both platforms previously collapsed every
read/decrypt failure into that same null, so a Keychain access error
(iOS) or an invalidated Keystore master key / corrupt blob (Android)
would silently mint a new key over an existing encrypted database and
orphan its data permanently.

Distinguish a genuine not-found from a read failure on the JS-facing
path: getItem now resolves null only for a true miss and rejects on any
other failure, so the caller aborts the open instead of regenerating.

iOS: read gains an NSError out-param (errSecItemNotFound -> nil/no error;
any other status -> nil + error). The .mm rejects when the error is set;
the NSE reader in Database.swift fails the open on error without
regenerating.

Android: getItemInternal returns null only when the SharedPreferences
blob is absent and throws otherwise (missing alias for an existing blob,
decrypt failure). getItem rejects on those throws; the NSE reader in
Encryption.java catches and skips the room rather than regenerating.

@diegolmello diegolmello left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Review: native SQLCipher readers + native key store

The security design is largely right: SQLCipher raw-key string form (x'…'), a dedicated Keychain service namespace, ThisDeviceOnly + non-synchronizable attributes, and AES-GCM on Android. Module naming is consistent across the iOS RCT_EXPORT_MODULE, the Android spec NAME, and the TS TurboModuleRegistry.get (all DatabaseKeyStoreModule) — I verified this against the PR branch, ignoring the stale DatabaseKeyStoreModule.mm / mismatched spec left uncommitted in the working tree.

The removeItemInternal alias leak and the hasConstants=true flag are the two worth prioritising.

Open questions worth confirming outside the diff: (1) the NSE .entitlements must list the keychain-access-group for the extension to read the key; (2) KeyGenParameterSpec does not set setIsStrongBoxBacked(true), so the claim of hardware-backing falls back to the TEE rather than StrongBox where present — fine if deliberate, worth a comment.

Reviewed by an automated pass; treat inline comments as suggestions, not blockers.

}

/** Removes the SharedPreferences entry. Does not delete the AndroidKeyStore key. */
public static void removeItemInternal(Context context, String key) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🔴 [high] removeItemInternal leaks the AndroidKeyStore AES alias

Only the SharedPreferences blob is removed; ks.deleteEntry(key) is never called. After removal, a subsequent setItem for the same key finds containsAlias(key) == true and silently reuses the old AES key material instead of generating a fresh one. This breaks key rotation.

Fix:

public static void removeItemInternal(Context context, String key) {
    SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
    prefs.edit().remove(key).apply();
    try {
        KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
        ks.load(null);
        if (ks.containsAlias(key)) ks.deleteEntry(key);
    } catch (Exception e) {
        Log.e(TAG, "removeItemInternal: failed to delete KeyStore alias for " + key, e);
    }
}


// -------------------------------------------------------------------------
// Native-side helpers — callable from Encryption.java (same process, plain Context)
// -------------------------------------------------------------------------

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🟡 [medium] removeItem catch resolves instead of rejects

The catch block on line 86 calls promise.resolve(null), so the JS caller receives a successful result even when the delete failed. The TS contract (Promise<void>) implies rejection on error; callers cannot distinguish success from failure and will proceed believing the key was removed.

Fix: replace promise.resolve(null) in the catch with promise.reject:

} catch (Exception e) {
    Log.e(TAG, "removeItem failed for key: " + key, e);
    promise.reject("KEYSTORE_REMOVE_ERROR", e);
}

NativeDatabaseKeyStoreSpec.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🔴 [high] hasConstants=true with no getConstants() triggers unnecessary eager sync call

When hasConstants is true, the RN TurboModule infrastructure makes a synchronous JS→native call at startup to fetch constants. DatabaseKeyStoreModule implements no getConstants(), so this call returns nothing and adds startup latency with zero benefit. On older devices this is a measurable main-thread stall.

Fix:

new ReactModuleInfo(
    NativeDatabaseKeyStoreSpec.NAME,
    NativeDatabaseKeyStoreSpec.NAME,
    false, // canOverrideExistingModule
    false, // needsEagerInit
    false, // hasConstants — module exposes no constants
    false, // isCxxModule
    true   // isTurboModule
)


Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
byte[] plain = cipher.doFinal(encrypted);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🟢 [low] Missing length guard before new byte[combined.length - GCM_IV_LENGTH]

If combined is shorter than GCM_IV_LENGTH bytes (e.g. a truncated SharedPreferences blob), combined.length - GCM_IV_LENGTH is negative, throwing NegativeArraySizeException with no indication of what went wrong.

Fix: add an explicit guard before the array allocation:

if (combined.length <= GCM_IV_LENGTH) {
    throw new IllegalStateException(
        "stored blob for key '" + key + "' is too short to contain a GCM IV (" + combined.length + " bytes)");
}

}
if addStatus == errSecDuplicateItem {
attrs.removeValue(forKey: kSecValueData as String)
let updateStatus = SecItemUpdate(attrs as CFDictionary, [kSecValueData as String: data] as CFDictionary)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🟢 [low] SecItemUpdate dict missing kSecAttrAccessible

When SecItemAdd returns errSecDuplicateItem, the update dict passed to SecItemUpdate contains only kSecValueData. An item previously stored with a weaker accessibility (e.g. kSecAttrAccessibleAlways) will not be tightened to kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly.

Fix:

let updateStatus = SecItemUpdate(
    attrs as CFDictionary,
    [
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String
    ] as CFDictionary
)

@diegolmello diegolmello left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Structural review (NATIVE-1276). Verified clean: SQLCipher raw-key x'<hex>' form on both platforms (not the byte[] overload), iOS Keychain attributes (AfterFirstUnlockThisDeviceOnly, non-synchronizable, NSE access group), TurboModule wiring (getTurboModule: on iOS, TurboReactPackage on Android — New Architecture path), busy_timeout PRAGMA, cursor/DB close in finally, and no key material in logs. Findings inline.

Note on URL→filename derivation: Android (Encryption.java replaceFirst("^(\\w+:)?//")) and iOS (Database.swift range(of: "://")) are two separate copies that agree on https:// today but diverge on edge cases (scheme-less //, trailing slashes), and neither is tested against the canonical JS deriveServerDbName. They decide which DB file gets opened — worth a shared fixture per platform.

NativeDatabaseKeyStoreSpec.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

bug: med — hasConstants = true but the module has no getConstants(). DatabaseKeyStoreModule never overrides getConstants(), so the base returns null; declaring hasConstants true asks the bridge to serialize a null constants map. Other modules here use false when they have no constants (PushNotification, VideoConf) and implement getConstants() when they set true (SSLPinning). Set this to false.

promise.resolve(null);
} catch (Exception e) {
Log.e(TAG, "removeItem failed for key: " + key, e);
promise.resolve(null);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

bug: med — removeItem swallows failures. The catch logs and then calls promise.resolve(null), so any exception from removeItemInternal surfaces to JS as a successful void return. Reject with a distinct error code instead.

// busy timeout that contention surfaces as an immediate SQLITE_BUSY.
db.execSQL("PRAGMA busy_timeout = 500;");

Cursor cursor = db.rawQuery("SELECT * FROM subscriptions WHERE id == ? LIMIT 1", new String[]{ejson.rid});

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

bug: low — wrong lookup column. SELECT * FROM subscriptions WHERE id == ? is queried with ejson.rid (a room id). The iOS reader correctly uses WHERE rid = ?. This worked under WMDB only because subscription.id == rid there; in the new Drizzle schema id may be a generated value, so this returns no rows and decryption silently fails. Query WHERE rid = ?.

byte[] combined = Base64.decode(encoded, Base64.DEFAULT);

byte[] iv = new byte[GCM_IV_LENGTH];
byte[] encrypted = new byte[combined.length - GCM_IV_LENGTH];

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

bug: low — no length check before splitting the GCM blob. A corrupt blob that Base64-decodes to fewer than GCM_IV_LENGTH bytes makes combined.length - GCM_IV_LENGTH negative → NegativeArraySizeException. Guard with an explicit "ciphertext too short" check that throws a clear error.

@@ -0,0 +1,66 @@
#import <React/RCTBridgeModule.h>

#import <SSLPinning/SSLPinning.h>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

slop: heavy — spurious #import <SSLPinning/SSLPinning.h>. Copy-paste from another module; this file has no SSL-pinning dependency. Remove.

String dbPath = context.getFilesDir().getAbsolutePath() + "/SQLite/" + dbName;

// Load the SQLCipher native library. Safe to call multiple times per process.
System.loadLibrary("sqlcipher");

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

slop: heavy — System.loadLibrary("sqlcipher") runs inside readRoom(). Called on every incoming push. Move it to a static { } initializer so it loads once.

}

/** Removes the SharedPreferences entry. Does not delete the AndroidKeyStore key. */
public static void removeItemInternal(Context context, String key) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

slop: heavy — AndroidKeyStore alias never deleted on remove. removeItemInternal clears the SharedPreferences blob but leaves the AES-GCM wrapping key in the Keystore, so every server logout orphans one alias permanently. Call ks.deleteEntry(key) after removing the prefs entry.

static let service = "chat.rocket.reactnative.dbkeys"

// Full team-prefixed access group — bare suffix fails with errSecMissingEntitlement
private static let accessGroup = "S6UPZG7ZR3.chat.rocket.reactnative"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

slop: heavy — hardcoded team-prefixed Keychain access group. "S6UPZG7ZR3.chat.rocket.reactnative" is baked in, while the entitlements use $(AppIdentifierPrefix)chat.rocket.reactnative resolved at build time. If the signing team changes (enterprise build, fork, profile rotation) main-app writes succeed but NSE reads fail with errSecItemNotFound and notifications silently stop decrypting. Derive it from the bundle entitlement / a build-time constant.

* Must be called once before any database is opened.
* Safe to call multiple times (subsequent calls are no-ops in keyService).
*/
export function installNativeKeychainShim(): void {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

slop: moderate — installNativeKeychainShim() has no call site in this PR. The TurboModule key store is unreachable until this is invoked once at startup (the doc comment says "before any database is opened"). Wire it in the same PR.

return NAME;
}

@ReactMethod

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

slop: nit — @ReactMethod on the TurboModule spec methods. TurboModule registration is via JSI/codegen, not @ReactMethod reflection. Harmless but bridge-era noise (mirrors the existing SSLPinning spec); drop it.

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