Skip to content

feat: native SQLCipher database readers and native key store - #7398

Merged
diegolmello merged 21 commits into
feat/native-1272-sqlcipher-migrationfrom
feat/native-1276-native-readers
Jun 23, 2026
Merged

feat: native SQLCipher database readers and native key store#7398
diegolmello merged 21 commits into
feat/native-1272-sqlcipher-migrationfrom
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 using per-database encryption keys stored securely on iOS (Keychain) and Android (Keystore).
    • Added a native key/value module to supply encryption keys to the database.
  • Improvements
    • Updated notification-based room reading to open the encrypted database directly and apply keying and access timing safeguards.
  • Chores
    • Added/updated native wiring and build configuration for SQLCipher and the encryption key modules on iOS and Android.
  • Tests
    • Added Jest coverage for native key store shim behavior and missing-module error handling.

…ks (NATIVE-1274)

Delivers the driver adapter layer sitting between expo-sqlite and the rest of
the app, as the only permitted call site for expo-sqlite:

- connection.ts: open/close lifecycle with App Group path resolution (iOS),
  post-open PRAGMA key → busy_timeout=500 → WAL invariant, per-DB handle
  registry, drizzle() wrapping, deleteDb support.

- keyService.ts: getOrCreateDatabaseKey / deleteDatabaseKey backed by a
  keychainShim interface; shim defaults to an in-memory dev stand-in pending
  the native Keychain binding (NATIVE-1276). CSPRNG from @rocket.chat/mobile-crypto
  randomBytes (64 hex chars / 32 bytes).

- observe.ts: useTableQuery (V2 structural-sharing list hook, ~16ms debounce,
  table-filtered addDatabaseChangeListener) and useRowObserve (V3 per-rowid hook),
  both ported from the on-device validated PoC.

- ios/Podfile.properties.json: expo.sqlite.useSQLCipher = "true"
- android/gradle.properties: expo.sqlite.useSQLCipher=true
- expo-sqlite ~16.0.10 added via `expo install` (SDK-54 compatible)

L1 Jest tests cover: key creation/idempotence, no key material in errors,
shim replacement, DB name derivation, open-sequence ordering (PRAGMA key
first), busy_timeout, WAL, registry dedup, debounce coalescing, table
filtering, structural sharing (same ref/new ref), useRowObserve rowId
matching. 33 tests added; full suite 1572 tests green.
…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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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/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, android/app/build.gradle
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 to avoid sqlite3 header conflicts and fix library search paths, 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,keyService: Key shim 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.
  • RocketChat/Rocket.Chat.ReactNative#7418: Both PRs add/register the same DatabaseKeyStore* TurboModule stack, implementing fail-closed key-management behavior for encrypted database work.

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.
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.
Title check ✅ Passed The title 'feat: native SQLCipher database readers and native key store' clearly and accurately summarizes the main changes across all platform implementations (iOS, Android, and JS layer).

✏️ 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.

@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 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.
The SQLCipher-encrypted databases run in WAL mode inside the iOS App Group
container. iOS kills a suspended app that holds a file lock on a file in a
shared container unless it can recognise the WAL file as SQLite — but default
SQLCipher encrypts the file header, so iOS denies the idle-WAL background
exemption and the held shared lock trips RUNNINGBOARD 0xdead10cc on suspend.

Open every database with PRAGMA cipher_plaintext_header_size = 32 so iOS reads
the WAL magic and grants the exemption. With a plaintext header SQLCipher no
longer stores the salt in the file, so generate and persist a per-database salt
alongside the key and supply it via PRAGMA cipher_salt at open time. The 32
plaintext bytes are header metadata only (version/page size), so this is not a
security regression. Losing the salt makes the DB unreadable, same as losing
the key, so both are destroyed together.

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

Comment thread ios/Libraries/DatabaseKeyStore.swift Outdated

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

Comment thread android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java Outdated
Comment thread ios/Libraries/DatabaseKeyStore.mm
Comment thread android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java Outdated
Comment thread ios/Libraries/DatabaseKeyStore.swift
Comment thread app/lib/database/driver/keyStore.ts
…unused Insert types

- Extract shared messageColumns object spread into messagesTable, threadsTable,
  and threadMessagesTable; table-specific fields (tmid/tmsg/blocks/tshow/md/comment
  for messages; tmid/draft_message for threads; subscription_id for thread_messages)
  added per-table. messages.alias and messages.parse_urls override the shared
  nullable default to notNull.
- Add .notNull() to all columns whose WMDB counterpart lacks isOptional, across
  subscriptions, rooms, messages, threads, thread_messages, custom_emojis,
  frequently_used_emojis, uploads, permissions, users (app), and servers_history.
- Remove 16 TXxxInsert aliases (InferInsertModel-based) from index.ts; none are
  consumed in this or any stacked branch.
- Regenerate Drizzle migrations (drizzle-kit generate) for both app and servers
  schemas; new SQL files and snapshots committed.

Claude-Session: https://claude.ai/code/session_01TE9VsFTeXsXc8ssqeR8MJ7
…into 0000

Replace the two-migration sequence (0000 nullable + 0001 ALTER) with a single
0000 baseline that already carries all NOT NULL columns. Nothing has shipped,
so there are no existing databases to migrate from the old 0000.

App:   0000_wise_mockingbird.sql
Servers: 0000_puzzling_colleen_wing.sql

Claude-Session: https://claude.ai/code/session_01TE9VsFTeXsXc8ssqeR8MJ7
- connection: coalesce concurrent opens for the same dbName via _inflight
  map so only one openDatabaseAsync call races; close raw handle on
  failed PRAGMA application (no fd leak)
- connection: replace slashes with '_' not '.' in deriveServerDbName
  so distinct paths can't collide with host dots
- connection: collapse 5 PRAGMA statements into one multi-statement
  execAsync call; keep the verify getFirstAsync as a separate call
- keyService: extract getOrCreate helper with per-storageKey inflight
  serialization, removing the key/salt duplication; re-validate stored
  values against the expected hex pattern before returning (corrupt
  entry throws a safe error with no key material)
- keyService: parallelize the two removeItem calls in deleteDatabaseKey
- observe: derive stable tableKey string from tables list and use it
  as the effect dep instead of spreading tables (removes eslint-disable)
- observe: remove dead mounted ref and its effect from useRowObserve
- rid lookup: Encryption.java queried by wrong column (id == ?) instead of
  WHERE rid = ? LIMIT 1; now matches iOS Database.swift and Drizzle schema
- PRAGMA parity: readRoom now applies cipher_plaintext_header_size = 32 and
  cipher_salt (from db_salt_v1: keychain entry) to match the JS driver's
  post-merge open sequence; without these the reader cannot open the DB
- static lib load: System.loadLibrary("sqlcipher") moved from readRoom into
  a static {} initializer so it runs once per process, not per push
- AndroidKeyStore alias leak: removeItemInternal now deletes the keystore
  alias after clearing the SharedPreferences blob; previously every logout
  orphaned an alias and a later setItem would reuse stale key material
- removeItem rejects on failure instead of silently resolving null
- hasConstants: DatabaseKeyStoreTurboPackage passes false — module has no
  getConstants() override
- dead @ReactMethod: removed three annotations and the ReactMethod import from
  NativeDatabaseKeyStoreSpec; TurboModule dispatch is codegen, not reflection
- spurious import: removed #import <SSLPinning/SSLPinning.h> from
  DatabaseKeyStore.mm (copy-paste; no SSL symbols used)
- access group: made DatabaseKeyStore.accessGroup internal (was private) so it
  is the single source of truth for both the keychain module and Database.swift;
  added team-id coupling comment
- idempotent shim: installNativeKeychainShim is now a no-op after the first
  install (via a module-level _installed flag in keyStore.ts); installKeychainShim
  in keyService stays unrestricted for test isolation

Claude-Session: https://claude.ai/code/session_01TE9VsFTeXsXc8ssqeR8MJ7
…S driver

- Android deriveDbName: interior '/' → '_' (was '.'); corrects storage-key
  derivation for subpath server URLs (db_key_v1:, db_salt_v1:)
- iOS deriveServerDbName: interior '/' → '_' (was '.'); same parity fix
  plus adjacent comment and init-docstring updated to match
- iOS open: add cipher_plaintext_header_size = 32 and cipher_salt from
  keychain key db_salt_v1:<dbName> after PRAGMA key, before busy_timeout;
  fail-closed if salt is missing — mirrors Android Encryption.java readRoom

Claude-Session: https://claude.ai/code/session_01TE9VsFTeXsXc8ssqeR8MJ7
diegolmello added a commit that referenced this pull request Jun 22, 2026
- Restore SQLite/ subdir in resolveDbDirectory() (lost in merge-forward from
  #7398 which took incoming and reverted this branch's isolation): new encrypted
  DBs must not land at the App Group root where legacy plaintext WatermelonDB
  files live — openServersDb() would open the legacy default.db, fail PRAGMA key,
  and crash every existing user on upgrade
- Drop dead _relationCache field from Model (never read; @relation caches under
  _rel_* keys, not here)
- Correct observeRow JSDoc: listener re-fetches on any table change, never
  consults event.rowId
@diegolmello diegolmello changed the title feat: native SQLCipher database readers and native key store (NATIVE-1276) feat: native SQLCipher database readers and native key store Jun 22, 2026
Base automatically changed from feat/native-1274-driver-adapter to feat/native-1272-sqlcipher-migration June 22, 2026 21:24
…tion' into feat/native-1276-native-readers

# Conflicts:
#	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/native/NativeDatabaseKeyStore.ts
#	ios/Libraries/DatabaseKeyStore.mm
#	ios/Libraries/DatabaseKeyStore.swift
@diegolmello
diegolmello had a problem deploying to approve_e2e_testing June 22, 2026 23:36 — with GitHub Actions Failure
@diegolmello
diegolmello merged commit bb40218 into feat/native-1272-sqlcipher-migration Jun 23, 2026
5 of 8 checks passed
@diegolmello
diegolmello deleted the feat/native-1276-native-readers branch June 23, 2026 13:08
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