Skip to content

Conversation

@OtavioStasiak
Copy link
Contributor

@OtavioStasiak OtavioStasiak commented Oct 22, 2025

Proposed changes

This PR migrates our storage implementation from react-native-mmkv-storage to react-native-mmkv.
The new library is more actively maintained and provides improved stability and performance.

This migration may also resolve issues previously reported on iOS, such as unexpected user logouts.

Issue(s)

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

How to test or reproduce

New Installation

Default user

  • Log in to the app.
  • Close the app.
  • Open it again (the user must remain logged in).
  • Send a message, edit a message, and react to a message (just to ensure everything is working as expected).

Server with SSL Certificate

  • Log in using an SSL certificate.
  • Close the app.
  • Open it again (the user must remain logged in).
  • Try to open an attachment (image and PDF).

E2EE

  • Add an E2EE password.
  • Send a message in an encrypted room.
  • Close the app.
  • Open it again.
  • E2EE must be working.

Notifications (This case must be repeated in Default user, SSL Server and E2EE)

  • Open the app.
  • Receive a notification (it should appear as an in-app notification).
  • Close the app.
  • Receive a notification (it should appear as a push notification).

Upgrading the app

For the upgrade process, all tests performed in New Installation must be repeated, but under different scenarios:

  • Upgrading the app while logged in to a single server.
  • Upgrading the app while logged in to a server with E2EE enabled.
  • Upgrading the app while logged in to a server with SSL Pinning.
  • Upgrading the app while logged in to multiple servers.
  • Upgrading the app while logged in to multiple servers, where the unfocused server has E2EE enabled.
  • Upgrading the app while logged in to multiple servers, where the unfocused server has SSL Pinning.

Apple Watch

  • Test fresh install
  • Test upgrading the app

Screenshots

Types of changes

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

Checklist

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

Further comments

Summary by CodeRabbit

  • New Features

    • New secure storage system with MMKV-backed encryption on Android and iOS; exposes secure key access for app initialization.
    • WebView: ability to set a client certificate alias for certificate-based requests.
  • Bug Fixes

    • Fixed playback speed and media-download typing issues.
    • Improved SSL pinning hostname handling and robustness.
    • Prevented undefined accessibility alert type values.
  • Chores

    • Migrated storage stack to react-native-mmkv and removed legacy keychain/storage integrations.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 22, 2025

Walkthrough

This PR migrates storage from react-native-mmkv-storage to react-native-mmkv, adds platform-native SecureStorage/MMKVKeyManager modules, updates native bridges and initialization (iOS/Android), refactors TypeScript user-preferences to new MMKV API/hooks, and adjusts notification/encryption flows to use the new storage bridge.

Changes

Cohort / File(s) Summary
Mocks
__mocks__/react-native-mmkv-storage.js, __mocks__/react-native-mmkv.js
Removed react-native-mmkv-storage mock; added a comprehensive react-native-mmkv in-memory mock with per-id shared storage, CRUD ops, listener management, and React hooks (useMMKVString, useMMKVNumber, useMMKVBoolean, useMMKVObject).
Package & patches
package.json, patches/react-native-mmkv+3.3.3.patch, patches/react-native-mmkv-storage+12.0.0.patch, patches/react-native-webview+13.15.0.patch
Swapped dependencies to [email protected]; disabled older mocks; added MMKV patch metadata and react-native-webview setCertificateAlias changes.
Android native secure storage
android/app/src/main/java/.../storage/Constants.java, SecureKeystore.java, Storage.java, SecureStorage.java, SecureStoragePackage.java, MMKVKeyManager.java
Added SecureKeystore and SecureStorage implementations, Storage helper, constants, a SecureStoragePackage RN bridge, and MMKVKeyManager to manage/generate MMKV encryption keys.
Android integration & startup
android/app/build.gradle, android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
Added androidx.security:security-crypto:1.1.0 dependency; register SecureStoragePackage and initialize MMKVKeyManager during application onCreate.
Android notification & encryption flows
android/app/src/main/java/.../notification/Ejson.java, LoadNotification.java, CustomPushNotification.java, Encryption.java, ReplyBroadcast.java
Removed ReactApplicationContext-dependent MMKV initializations; switched to MMKVKeyManager-backed getMMKV(), changed several method signatures to accept Context, and simplified encryption/notification credential retrieval.
iOS native secure storage & key manager
ios/SecureStorage.h, ios/SecureStorage.m, ios/MMKVKeyManager.h, ios/MMKVKeyManager.mm
Added SecureStorage RN module for keychain operations and synchronous MMKV key retrieval, plus MMKVKeyManager to initialize MMKV and ensure/generate encryption keys.
iOS MMKV bridge & storage wiring
ios/Shared/RocketChat/MMKVBridge.h, ios/Shared/RocketChat/MMKVBridge.mm, ios/Shared/RocketChat/MMKV.swift, ios/Shared/RocketChat/Storage.swift, ios/Shared/RocketChat/ClientSSL.swift
Introduced MMKVBridge Obj‑C++ wrapper; migrated build()/extensions to return/use MMKVBridge, adjusted initialization to accept cryptKey and rootPath, and updated SSL/credential accessors to use the bridge.
iOS project & AppDelegate changes
ios/AppDelegate.swift, ios/RocketChatRN-Bridging-Header.h, ios/NotificationService/NotificationService-Bridging-Header.h, ios/RocketChatRN.xcodeproj/project.pbxproj, ios/Podfile
Call MMKVKeyManager.initialize() early in AppDelegate; replace external MMKV imports with local bridge headers; add react-native-mmkv pod and FORCE_POSIX preprocessor adjustments; update Xcode project file entries for MMKVBridge and SecureStorage.
iOS SSLPinning & Notification Service
ios/SSLPinning.mm, ios/SSLPinning/SSLPinning.swift, ios/NotificationService/NotificationService-Bridging-Header.h
Replaced direct MMKV usage with MMKVBridge calls; updated set/get certificate APIs to use bridge-backed storage.
TypeScript storage & preferences
app/lib/methods/userPreferences.ts
Replaced mmkv-storage with react-native-mmkv, added platform-aware MMKV_INSTANCE using native encryption key, refactored useUserPreferences into a generic hook, and expanded UserPreferences API (getNumber/setNumber, getAllKeys, contains, clearAll, removeItem). Exposed initializeStorage alias.
Auth/logout/login
app/lib/methods/logout.ts, app/sagas/login.js
Removed react-native-keychain usage and iOS Keychain credential persistence; converted some async removal to synchronous behavior.
TypeScript small fixes & UI types
app/containers/AudioPlayer/PlaybackSpeed.tsx, app/views/AccessibilityAndAppearanceView/index.tsx, app/views/MediaAutoDownloadView/index.tsx, app/lib/methods/helpers/sslPinning.ts
Minor type casts and guards: cast playbackSpeed to number, default alertDisplayType to 'TOAST', cast media download options, and guard hostname truthiness before mapping.
Metro config & minor cleanup
metro.config.js, app/sagas/init.js, __mocks__/react-native-mmkv-storage.js (deleted)
Changed E2E toggle to explicit string compare, removed a console.log, and deleted the old mmkv-storage mock.

Sequence Diagram(s)

mermaid
sequenceDiagram
autonumber
participant JS as JavaScript
participant RN as React Native Bridge
participant Native as Native Module (SecureStorage/MMKVKeyManager)
participant MMKV as MMKV (native storage)
JS->>RN: app start -> require storage
RN->>Native: call getMMKVEncryptionKey()
Native->>Native: read/generate key (Keychain/Keystore)
Native->>MMKV: initialize MMKV with cryptKey & rootPath
MMKV-->>Native: return instance
Native-->>RN: return encryption key / confirm init
RN-->>JS: MMKV_INSTANCE constructed with cryptKey
JS->>MMKV: read/write via bridge (get/set/clear/listeners)
MMKV-->>JS: notify listeners on change

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • Android SecureKeystore, SecureStorage, and MMKVKeyManager (cryptography, keystore usage).
  • iOS SecureStorage, MMKVKeyManager, MMKVBridge, and Pod/xcodeproj changes (bridging, header/search paths, FORCE_POSIX).
  • userPreferences.ts migration (API changes, types, initialization timing).
  • Notification/encryption call-site signature changes (ensure callers updated and Contexts valid).
  • react-native-webview setCertificateAlias integrations and cross-target build implications.

Possibly related PRs

Poem

🐰 I nibbled code and found a key,

MMKV now hops with me,
Bridges built and secrets kept,
No lost keys while I slept,
A tiny rabbit cheers, secure and free! 🥕🔐

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: migrating from react-native-mmkv-storage to react-native-mmkv across the codebase.
Linked Issues check ✅ Passed The PR comprehensively addresses NATIVE-1064 objectives: replaces react-native-mmkv-storage with react-native-mmkv, implements platform-specific encryption key management (MMKVKeyManager), adds secure storage layers (SecureStorage/SecureKeystore), maintains feature parity with updated hooks/APIs, and includes extensive testing across new install, upgrade, E2EE, SSL pinning, and Watch scenarios.
Out of Scope Changes check ✅ Passed All changes are scope-aligned to the migration objectives. Updates to TypeScript casting (AudioPlayer, MediaAutoDownloadView), SSL pinning guards, and minor logging cleanup in init.js/logout.ts are supporting changes required for the MMKV migration and do not introduce unrelated functionality.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat.migrate-react-native-mmkv-storage

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e42e32b and f1b9081.

📒 Files selected for processing (1)
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (1)
android/app/src/main/java/chat/rocket/reactnative/storage/SecureStoragePackage.java (1)
  • SecureStoragePackage (12-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🔇 Additional comments (3)
android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (3)

29-30: Secure storage imports are correctly wired

MMKVKeyManager and SecureStoragePackage are imported from the expected chat.rocket.reactnative.storage namespace and fit with the existing import style in this file.


37-42: SecureStoragePackage registration in getPackages() looks correct

Adding SecureStoragePackage() to the PackageList(this).packages.apply { ... } block will properly register the secure storage native module with React Native alongside your other custom packages.


59-63: MMKVKeyManager initialization order is good; verify idempotence and process-safety

Initializing MMKVKeyManager in onCreate() before calling load() is the right place to avoid MMKV/RN race conditions, in line with the comment. Please just confirm that MMKVKeyManager.initialize(this):

  • Does not depend on a ReactContext (only on the Application context, as used here), and
  • Is safe to call more than once or from multiple processes if that ever happens (idempotent and/or guarded internally).

This will ensure startup remains robust in edge cases without surprising crashes tied to key initialization.


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.

@github-actions
Copy link

iOS Build Available

Rocket.Chat Experimental 4.68.0.107837

@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.68.0.107834

…ment

- Introduced MMKVKeyManager to handle encryption key initialization and management for MMKV storage.
- Replaced MMKVMigration with MMKVKeyManager to streamline the process of reading or generating encryption keys at app startup.
- Updated MainApplication and SecureStorage to utilize the new key manager for encryption key retrieval.
- Removed the obsolete MMKVMigration class and its references across the codebase.
- Enhanced Ejson class to support MMKV access with encryption, ensuring backward compatibility for existing users.
@diegolmello diegolmello temporarily deployed to approve_e2e_testing December 12, 2025 20:50 — with GitHub Actions Inactive
@diegolmello diegolmello temporarily deployed to experimental_ios_build December 12, 2025 20:53 — with GitHub Actions Inactive
@diegolmello diegolmello temporarily deployed to experimental_android_build December 12, 2025 20:53 — with GitHub Actions Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
ios/RocketChatRN.xcodeproj/project.pbxproj (1)

2475-2507: Remove manual MMKV header path from Xcode settings; rely on CocoaPods integration.

The HEADER_SEARCH_PATHS entry $(SRCROOT)/../node_modules/react-native-mmkv/MMKV/Core/** should be removed from app targets. Since react-native-mmkv is configured in the Podfile with modular_headers: true, CocoaPods automatically exposes MMKV headers. Manual path entries in Xcode target settings bypass CocoaPods' build configuration and create brittle path coupling. If header resolution issues occur, use the Podfile's post_install hook (as already done for GCC_PREPROCESSOR_DEFINITIONS) rather than manually editing Xcode project settings.

Also applies to: 2540-2572, 3058-3090, 3123-3153

♻️ Duplicate comments (11)
app/lib/methods/userPreferences.ts (5)

29-32: Remove unnecessary type cast for Mode.MULTI_PROCESS.

The Mode enum exports MULTI_PROCESS directly. The defensive cast is unnecessary and obscures the code.

-	const multiProcessMode = (Mode as { MULTI_PROCESS?: Mode })?.MULTI_PROCESS;
-	if (multiProcessMode) {
-		config.mode = multiProcessMode;
-	}
+	config.mode = Mode.MULTI_PROCESS;

48-59: Duplicate: Consider using existing appGroupPath helper.

This function duplicates logic from app/lib/methods/appGroup.ts. Consider importing and reusing that helper to reduce maintenance overhead.


99-105: Critical: || breaks falsy value handling (still unresolved).

getString returns null for empty strings (""). Use nullish coalescing instead.

-		return this.mmkv.getString(key) || null;
+		return this.mmkv.getString(key) ?? null;

111-117: Critical: || breaks false handling (still unresolved).

getBool returns null when the stored value is false, breaking boolean logic entirely.

-		return this.mmkv.getBoolean(key) || null;
+		return this.mmkv.getBoolean(key) ?? null;

140-146: Critical: || breaks 0 handling (still unresolved).

getNumber returns null when the stored value is 0, corrupting numeric data.

-		return this.mmkv.getNumber(key) || null;
+		return this.mmkv.getNumber(key) ?? null;
ios/RocketChatRN.xcodeproj/project.pbxproj (2)

2091-2141: RocketChatRN target compiles SecureStorage.m twice → duplicate symbols
PBXSourcesBuildPhase lists both 66C270212EBBCB780062725F /* SecureStorage.m in Sources */ (Line 2138) and A2C6E2DD38F8BEE19BFB2E1D /* SecureStorage.m in Sources */ (Line 2140). Keep only one.


2366-2417: Rocket.Chat target compiles SecureStorage.m twice → duplicate symbols
Same issue: both 66C270202EBBCB780062725F /* SecureStorage.m in Sources */ (Line 2413) and 79D8C97F8CE2EC1B6882826B /* SecureStorage.m in Sources */ (Line 2415). Keep only one.

ios/Shared/RocketChat/MMKVBridge.mm (1)

42-54: Empty value semantics: don’t collapse “present but empty” into nil
This repeats an earlier concern: stringForKey: and dataForKey: currently treat empty values as missing.

Also applies to: 65-76

ios/SecureStorage.m (3)

5-7: Fix invalid Objective‑C syntax: @implementation must not include superclass
@implementation SecureStorage : NSObject won’t compile.

-@implementation SecureStorage : NSObject
+@implementation SecureStorage

83-103: Fix Keychain read: wrong cast + missing status handling + leak
kSecReturnData returns CFData/NSData, but this casts to NSDictionary and never releases result.


105-135: Forward-declare _accessibleValue (used before definition)
This is prone to “implicit declaration” build failures depending on compiler flags.

Also applies to: 151-170

🧹 Nitpick comments (7)
app/lib/methods/userPreferences.ts (1)

13-22: Minor: Redundant null check.

Line 17's key && key !== null is redundant—the truthy check key && already excludes null. Consider simplifying to:

-		return key && key !== null ? key : undefined;
+		return key || undefined;
android/app/src/main/java/chat/rocket/reactnative/storage/MMKVKeyManager.java (1)

57-109: Avoid silent empty-alias fallback in toHex()
If toHex() fails and returns "" (Line 105-108), you’ll read/write a secure key under an empty alias (Line 60/66), which is a dangerous silent fallback.

Suggested change (fail fast + use StandardCharsets.UTF_8):

-    private static String toHex(String arg) {
-        try {
-            byte[] bytes = arg.getBytes("UTF-8");
+    private static String toHex(String arg) {
+        if (arg == null || arg.isEmpty()) {
+            throw new IllegalArgumentException("arg must be non-empty");
+        }
+        try {
+            byte[] bytes = arg.getBytes(java.nio.charset.StandardCharsets.UTF_8);
             StringBuilder sb = new StringBuilder();
             for (byte b : bytes) {
                 sb.append(String.format("%02x", b & 0xff));
             }
             return sb.toString();
         } catch (Exception e) {
-            Log.e(TAG, "Error converting string to hex", e);
-            return "";
+            Log.e(TAG, "Error converting string to hex", e);
+            throw new RuntimeException("toHex failed", e);
         }
     }
android/app/src/main/java/chat/rocket/reactnative/storage/SecureStorage.java (1)

43-63: Align RN bridge error semantics for getSecureKey vs setSecureKey
getSecureKey resolves null on exception (Line 48-51) while setSecureKey rejects (Line 60-62). Pick one consistent contract (prefer reject on exception), otherwise JS can’t distinguish “missing key” vs “crypto/keystore failure”.

ios/MMKVKeyManager.mm (1)

75-88: Handle directory creation failure in initializeMMKV
createDirectoryAtPath:... error:nil (Line 83-87) drops the error. If the directory can’t be created (entitlements/path issues), you’ll return a path that can’t be used without any diagnostic.

     NSString *mmkvPath = [[groupURL path] stringByAppendingPathComponent:@"mmkv"];
-    [[NSFileManager defaultManager] createDirectoryAtPath:mmkvPath
+    NSError *err = nil;
+    [[NSFileManager defaultManager] createDirectoryAtPath:mmkvPath
                               withIntermediateDirectories:YES
                                                attributes:nil
-                                                    error:nil];
+                                                    error:&err];
+    if (err) {
+        Logger(@"Failed to create MMKV directory: %@", err);
+        return nil;
+    }
     return mmkvPath;
 }
ios/Shared/RocketChat/MMKVBridge.mm (1)

24-28: Avoid repeated MMKV::initializeMMKV() across instances
If multiple MMKVBridge objects are created, you’ll re-run initialization. Consider dispatch_once (or a static guard) around initialization.

ios/SecureStorage.m (2)

20-81: Make serviceName a static and initialize once
A file-global mutable NSString *serviceName can be raced and repeatedly reassigned. Prefer a static NSString * and dispatch_once.


189-207: Blocking sync Keychain calls: consider caching the MMKV key in-memory
This runs synchronously and can be a perf footgun if called frequently from JS. A process-lifetime cache (still sourced from Keychain on cold start) would reduce latency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a2eecbc and e42e32b.

📒 Files selected for processing (14)
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (3 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java (7 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/storage/MMKVKeyManager.java (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/storage/SecureStorage.java (1 hunks)
  • app/lib/methods/userPreferences.ts (1 hunks)
  • ios/AppDelegate.swift (1 hunks)
  • ios/MMKVKeyManager.h (1 hunks)
  • ios/MMKVKeyManager.mm (1 hunks)
  • ios/RocketChatRN-Bridging-Header.h (1 hunks)
  • ios/RocketChatRN.xcodeproj/project.pbxproj (53 hunks)
  • ios/SecureStorage.h (1 hunks)
  • ios/SecureStorage.m (1 hunks)
  • ios/Shared/RocketChat/MMKVBridge.h (1 hunks)
  • ios/Shared/RocketChat/MMKVBridge.mm (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • ios/SecureStorage.h
  • ios/Shared/RocketChat/MMKVBridge.h
  • ios/AppDelegate.swift
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
🧰 Additional context used
🧬 Code graph analysis (1)
android/app/src/main/java/chat/rocket/reactnative/storage/SecureStorage.java (1)
android/app/src/main/java/chat/rocket/reactnative/storage/MMKVKeyManager.java (1)
  • MMKVKeyManager (23-110)
🪛 ast-grep (0.40.0)
android/app/src/main/java/chat/rocket/reactnative/storage/SecureStorage.java

[warning] 75-75: 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 [REFERENCES]
- https://find-sec-bugs.github.io/bugs.htm#TDES_USAGE
- https://csrc.nist.gov/News/2017/Update-to-Current-Use-and-Deprecation-of-TDEA

(desede-is-deprecated-java)


[warning] 83-83: 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 [REFERENCES]
- https://find-sec-bugs.github.io/bugs.htm#TDES_USAGE
- https://csrc.nist.gov/News/2017/Update-to-Current-Use-and-Deprecation-of-TDEA

(desede-is-deprecated-java)


[warning] 104-104: 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 [REFERENCES]
- https://find-sec-bugs.github.io/bugs.htm#TDES_USAGE
- https://csrc.nist.gov/News/2017/Update-to-Current-Use-and-Deprecation-of-TDEA

(desede-is-deprecated-java)


[warning] 109-112: 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 [REFERENCES]
- https://find-sec-bugs.github.io/bugs.htm#TDES_USAGE
- https://csrc.nist.gov/News/2017/Update-to-Current-Use-and-Deprecation-of-TDEA

(desede-is-deprecated-java)


[warning] 128-128: 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 [REFERENCES]
- https://find-sec-bugs.github.io/bugs.htm#TDES_USAGE
- https://csrc.nist.gov/News/2017/Update-to-Current-Use-and-Deprecation-of-TDEA

(desede-is-deprecated-java)


[warning] 75-75: 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 [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures
- https://googleprojectzero.blogspot.com/2022/10/rc4-is-still-considered-harmful.html

(use-of-aes-ecb-java)


[warning] 83-83: 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 [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures
- https://googleprojectzero.blogspot.com/2022/10/rc4-is-still-considered-harmful.html

(use-of-aes-ecb-java)


[warning] 104-104: 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 [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures
- https://googleprojectzero.blogspot.com/2022/10/rc4-is-still-considered-harmful.html

(use-of-aes-ecb-java)


[warning] 109-112: 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 [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures
- https://googleprojectzero.blogspot.com/2022/10/rc4-is-still-considered-harmful.html

(use-of-aes-ecb-java)


[warning] 128-128: 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 [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures
- https://googleprojectzero.blogspot.com/2022/10/rc4-is-still-considered-harmful.html

(use-of-aes-ecb-java)

🔇 Additional comments (7)
app/lib/methods/userPreferences.ts (6)

1-5: LGTM!

Imports are correctly structured for the migration to react-native-mmkv.


61-61: LGTM!

Module-level MMKV initialization is appropriate for this storage utility.


63-90: LGTM - type safety fix implemented.

The deserialization logic now correctly handles string vs. non-string types with proper JSON parsing and error handling.


119-134: LGTM!

Setter methods and map operations are correctly implemented with proper JSON serialization and error handling.


136-162: LGTM!

The new utility methods (removeItem, getAllKeys, contains, clearAll) correctly delegate to the MMKV API.


165-167: LGTM!

Export structure is appropriate—default export for the class instance and named export for the MMKV instance alias.

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

5-7: Imports look fine; please verify header visibility in the target build settings
Since this swaps module imports for project headers, make sure the headers are reachable for the Swift target (Header Search Paths / target membership).

Comment on lines +48 to +88
public static void initialize(Context context) {
appContext = context.getApplicationContext();

try {
Log.i(TAG, "Initializing MMKV encryption...");

// Initialize MMKV
MMKV.initialize(context);

// Get or create the encryption key
SecureKeystore secureKeystore = new SecureKeystore(context);
String alias = toHex("com.MMKV." + DEFAULT_INSTANCE_ID);
String password = secureKeystore.getSecureKey(alias);

if (password == null || password.isEmpty()) {
// Fresh install - generate a new encryption key
Log.i(TAG, "No existing encryption key found, generating new one...");
password = UUID.randomUUID().toString();
secureKeystore.setSecureKey(alias, password);
Log.i(TAG, "New encryption key generated and stored");
} else {
Log.i(TAG, "Existing encryption key found");
}

// Cache the encryption key for other native code
encryptionKey = password;

// Verify MMKV can be opened with this key
MMKV mmkv = MMKV.mmkvWithID(DEFAULT_INSTANCE_ID, MMKV.SINGLE_PROCESS_MODE, password);
if (mmkv != null) {
long keyCount = mmkv.count();
Log.i(TAG, "MMKV initialized with encryption, " + keyCount + " keys found");
} else {
Log.w(TAG, "MMKV instance is null after initialization");
}

} catch (Exception e) {
Log.e(TAG, "MMKV encryption initialization failed", e);
// Clear the key on failure to avoid partial state
encryptionKey = null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

react-native-mmkv Android cryptKey format requirements length encoding

💡 Result:

Short answer

  • cryptKey must be ≤ 16 bytes (native MMKV requirement). [1]
  • In practice you pass a JavaScript string (react-native-mmkv option is encryptionKey) and MMKV will use the string's bytes — so multi‑byte Unicode characters count toward the 16‑byte limit. Use ASCII (one byte per char) to avoid surprises. [1][2]

Recommendations

  • Use a 16‑character ASCII key (e.g. 16 printable ASCII chars) or supply a 16‑byte value encoded as hex/Base64 and decode to bytes before passing if you need arbitrary bytes.
  • If you need to change/remove encryption, use the provided reKey/recrypt APIs (react-native-mmkv). [1][2]

Sources
[1] MMKV JavaDoc — mmkvWithID / cryptKey / reKey (cryptKey: no more than 16 bytes).
[2] react-native-mmkv README (encryptionKey / recrypt usage).


🌐 Web query:

react-native-mmkv MMKV wrong key behavior error reset data loss

💡 Result:

Short answer — common causes and fixes

Causes

  • Encryption key mismatch or wrong/changed secret when opening the same MMKV file (decryption fails / effectively looks like a reset). [1][2]
  • Library/native-version or ABI mismatch after upgrading React Native / MMKV (native format incompatible → storage appears empty or is reset). [2]
  • Known bugs / platform-specific issues (several reported issues where storage becomes empty after background/upgrade). [3]

Immediate actions to avoid data loss

  1. Stop shipping further releases that change MMKV config (instance name, encryption key, or storage path).
  2. Backup the device MMKV files (app data directory) immediately from an affected device before attempting fixes.
  3. Check logs (adb logcat / iOS console) for decryption/initialization errors to confirm "wrong key" or native-errors. [2]

How to fix

  • Ensure the exact same MMKV instance name/path and the exact same encryption key (if used) across app builds and upgrades. A changed key = unrecoverable decryption for that file. [1][2]
  • Upgrade react-native-mmkv to a current stable release (resolve known bugs and security logging issue — CVE fixed in 2.11.0+; newer major releases also changed native architecture and bugfixes). Test upgrades carefully on-device before rollout. [3]
  • If you must change encryption, migrate by reading the old MMKV (with old key) and writing to a new MMKV store (new key) — but only after you confirm decryption works locally.
  • If storage is empty after an RN/native upgrade, check that the native MMKV version and RN bridge version are compatible (see issues where new-arch or RN SDK upgrades caused errors). Revert or align versions if needed. [2]

References

  • Reports of storage emptying / background/upgrade issues (react-native-mmkv GitHub issues). [1]
  • Errors after RN/new-architecture upgrades and compatibility notes (react-native-mmkv GitHub). [2]
  • CVE / insecure logging of encryption key and recommended upgrade (react-native-mmkv 2.11.0+). [3]

Sources
[1] react-native-mmkv — “Storage Empties Itself When App is Backgrounded” (GitHub issue).
[2] react-native-mmkv — “Error after upgrading to new architecture and SDK 52” (GitHub issue).
[3] CVE and advisories re: react-native-mmkv insecure logging / Snyk (upgrade to 2.11.0+).


🌐 Web query:

MMKV Android cryptKey string UUID format compatibility

💡 Result:

Short answer: MMKV's cryptKey must be at most 16 bytes, so a standard UUID string (36 chars with dashes, or 32 hex chars without dashes) is too long. Use a 16-byte value (exactly 16 bytes recommended) — e.g. take the first 16 bytes of a UUID's bytes, use the UUID's two 64-bit parts, or derive a 16-byte key by hashing (MD5 or SHA-256 then truncating to 16 bytes). Failure to supply an appropriate-length key will violate MMKV's limit. [1][2]

Examples (Android/Java):

  • Use UUID bytes (16 bytes):
    byte[] key = ByteBuffer.allocate(16).putLong(uuid.getMostSignificantBits()).putLong(uuid.getLeastSignificantBits()).array();
    String cryptKey = new String(key, StandardCharsets.ISO_8859_1); // pass as 16-byte string

  • Derive via MD5:
    byte[] md5 = MessageDigest.getInstance("MD5").digest(uuid.toString().getBytes(StandardCharsets.UTF_8));
    String cryptKey = new String(md5, StandardCharsets.ISO_8859_1);

References: MMKV Java API (cryptKey <= 16 bytes) and MMKV Android docs. [1][2]

Sources
[1] MMKV JavaDoc — cryptKey (no more than 16 bytes).
[2] MMKV Android wiki — encryption usage and examples.


UUID.randomUUID().toString() exceeds MMKV's 16-byte cryptKey limit; will cause decryption failure and data loss

The cryptKey passed to MMKV must be ≤16 bytes (native MMKV hard requirement). UUID.randomUUID().toString() produces 36 characters (with dashes) or 32 hex characters (without dashes), which far exceeds this limit. When MMKV receives a key longer than 16 bytes, decryption fails and the storage appears empty or is reset — effectively causing data loss that cannot be recovered by changing the key again.

Fix this by deriving a 16-byte key from the UUID. Two recommended approaches:

  1. UUID bytes (16 bytes): ByteBuffer.allocate(16).putLong(uuid.getMostSignificantBits()).putLong(uuid.getLeastSignificantBits()).array() then convert to a string using ISO_8859_1 encoding.
  2. MD5 hash (16 bytes): hash the UUID string with MD5 and pass the 16-byte result.

Also, if mmkv == null after initialization (line 80–82), clear encryptionKey to avoid propagating a partial/invalid state to callers.

🤖 Prompt for AI Agents
In android/app/src/main/java/chat/rocket/reactnative/storage/MMKVKeyManager.java
around lines 48 to 88, the code uses UUID.randomUUID().toString() which produces
>16 bytes and violates MMKV's 16-byte cryptKey requirement; derive a proper
16-byte key instead (either take the UUID's raw 16 bytes via its most/least
significant longs and encode to a String using ISO_8859_1, or compute an MD5
hash of the UUID string and use the 16-byte MD5 result encoded with ISO_8859_1)
and store that as the password passed to MMKV; additionally, if
MMKV.mmkvWithID(...) returns null, clear encryptionKey (set to null) to avoid
exposing a partial/invalid state to callers.

Comment on lines +83 to +101
SecretKey secretKey = (SecretKey) keyStore.getKey(alias, null);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);

// Split IV and encrypted data
byte[] combined = Base64.decode(encryptedValue, Base64.DEFAULT);
byte[] iv = new byte[12];
byte[] encrypted = new byte[combined.length - 12];
System.arraycopy(combined, 0, iv, 0, 12);
System.arraycopy(combined, 12, encrypted, 0, encrypted.length);

GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);

byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
Log.e(TAG, "Error retrieving secure key", e);
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix IV handling: decrypt hardcodes 12 bytes and doesn’t validate payload length
Encrypt stores iv.length bytes (Line 132-138), but decrypt always assumes 12-byte IV (Line 88-91). If IV length ever differs (provider behavior change) or the stored value is truncated/corrupt, you’ll get incorrect splits (or negative sizes) and silently return null.

Safer approach: prefix IV length (or store IV separately), and validate lengths before copying.

-            byte[] combined = Base64.decode(encryptedValue, Base64.DEFAULT);
-            byte[] iv = new byte[12];
-            byte[] encrypted = new byte[combined.length - 12];
-            System.arraycopy(combined, 0, iv, 0, 12);
-            System.arraycopy(combined, 12, encrypted, 0, encrypted.length);
+            byte[] combined = Base64.decode(encryptedValue, Base64.NO_WRAP);
+            if (combined.length < 1) return null;
+            int ivLen = combined[0] & 0xff;
+            if (ivLen < 12 || combined.length < 1 + ivLen) return null;
+            int ctLen = combined.length - 1 - ivLen;
+            if (ctLen <= 0) return null;
+            byte[] iv = new byte[ivLen];
+            byte[] encrypted = new byte[ctLen];
+            System.arraycopy(combined, 1, iv, 0, ivLen);
+            System.arraycopy(combined, 1 + ivLen, encrypted, 0, ctLen);
-        byte[] combined = new byte[iv.length + encrypted.length];
-        System.arraycopy(iv, 0, combined, 0, iv.length);
-        System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);
-        
-        String encryptedValue = Base64.encodeToString(combined, Base64.DEFAULT);
+        byte[] combined = new byte[1 + iv.length + encrypted.length];
+        combined[0] = (byte) iv.length;
+        System.arraycopy(iv, 0, combined, 1, iv.length);
+        System.arraycopy(encrypted, 0, combined, 1 + iv.length, encrypted.length);
+
+        String encryptedValue = Base64.encodeToString(combined, Base64.NO_WRAP);

Also applies to: 127-145

🧰 Tools
🪛 ast-grep (0.40.0)

[warning] 83-83: 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 [REFERENCES]
- https://find-sec-bugs.github.io/bugs.htm#TDES_USAGE
- https://csrc.nist.gov/News/2017/Update-to-Current-Use-and-Deprecation-of-TDEA

(desede-is-deprecated-java)


[warning] 83-83: 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 [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures
- https://googleprojectzero.blogspot.com/2022/10/rc4-is-still-considered-harmful.html

(use-of-aes-ecb-java)

Comment on lines +12 to +16
@interface MMKVKeyManager : NSObject

+ (void)initialize;

@end
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Do not expose/call +initialize as an app initializer API (ObjC runtime special method)
+initialize is automatically invoked by the Objective-C runtime and can run at unpredictable times/threads; using it for keychain/disk work risks double init and subtle startup bugs. Rename to something like + (void)setup; and call that from AppDelegate.

🤖 Prompt for AI Agents
In ios/MMKVKeyManager.h around lines 12 to 16, the class exposes +initialize
which is a special Objective-C runtime method and must not be used as an app
initializer; rename the method to a non-runtime name such as + (void)setup or +
(void)initializeManager in both header and implementation, update all call sites
to call the new method from AppDelegate (or your explicit startup path), and
remove or leave an empty +initialize implementation to avoid runtime conflicts.

Comment on lines +37 to +73
+ (void)initialize {
@try {
NSString *mmkvPath = [self initializeMMKV];
if (!mmkvPath) {
Logger(@"Failed to initialize MMKV path");
return;
}

SecureStorage *secureStorage = [[SecureStorage alloc] init];
NSString *alias = toHex(@"com.MMKV.default");
NSString *password = [secureStorage getSecureKey:alias];

if (!password || password.length == 0) {
// Fresh install - generate a new key
password = [[NSUUID UUID] UUIDString];
[secureStorage setSecureKey:alias value:password options:nil];
Logger(@"Generated new MMKV encryption key");
} else {
Logger(@"Existing MMKV encryption key found");
}

// Verify MMKV can be opened with this key
NSData *cryptKey = [password dataUsingEncoding:NSUTF8StringEncoding];
MMKVBridge *mmkv = [[MMKVBridge alloc] initWithID:@"default"
cryptKey:cryptKey
rootPath:mmkvPath];

if (mmkv) {
NSUInteger keyCount = [mmkv count];
Logger(@"MMKV initialized with encryption, %lu keys found", (unsigned long)keyCount);
} else {
Logger(@"MMKV instance is nil after initialization");
}
} @catch (NSException *exception) {
Logger(@"MMKV initialization error: %@ - %@", exception.name, exception.reason);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: +initialize misuse + make init idempotent (dispatch_once)
This method does keychain + filesystem work inside +initialize (Line 37+), which the runtime may call implicitly (and not necessarily on the main thread). Rename it (e.g., +setup) and wrap the body with dispatch_once to guarantee single execution when called from AppDelegate.

-+ (void)initialize {
-    @try {
++ (void)setup {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+    @try {
         NSString *mmkvPath = [self initializeMMKV];
         if (!mmkvPath) {
             Logger(@"Failed to initialize MMKV path");
             return;
         }
@@
         if (mmkv) {
             NSUInteger keyCount = [mmkv count];
             Logger(@"MMKV initialized with encryption, %lu keys found", (unsigned long)keyCount);
         } else {
             Logger(@"MMKV instance is nil after initialization");
         }
     } @catch (NSException *exception) {
         Logger(@"MMKV initialization error: %@ - %@", exception.name, exception.reason);
     }
+    });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
+ (void)initialize {
@try {
NSString *mmkvPath = [self initializeMMKV];
if (!mmkvPath) {
Logger(@"Failed to initialize MMKV path");
return;
}
SecureStorage *secureStorage = [[SecureStorage alloc] init];
NSString *alias = toHex(@"com.MMKV.default");
NSString *password = [secureStorage getSecureKey:alias];
if (!password || password.length == 0) {
// Fresh install - generate a new key
password = [[NSUUID UUID] UUIDString];
[secureStorage setSecureKey:alias value:password options:nil];
Logger(@"Generated new MMKV encryption key");
} else {
Logger(@"Existing MMKV encryption key found");
}
// Verify MMKV can be opened with this key
NSData *cryptKey = [password dataUsingEncoding:NSUTF8StringEncoding];
MMKVBridge *mmkv = [[MMKVBridge alloc] initWithID:@"default"
cryptKey:cryptKey
rootPath:mmkvPath];
if (mmkv) {
NSUInteger keyCount = [mmkv count];
Logger(@"MMKV initialized with encryption, %lu keys found", (unsigned long)keyCount);
} else {
Logger(@"MMKV instance is nil after initialization");
}
} @catch (NSException *exception) {
Logger(@"MMKV initialization error: %@ - %@", exception.name, exception.reason);
}
}
(void)setup {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@try {
NSString *mmkvPath = [self initializeMMKV];
if (!mmkvPath) {
Logger(@"Failed to initialize MMKV path");
return;
}
SecureStorage *secureStorage = [[SecureStorage alloc] init];
NSString *alias = toHex(@"com.MMKV.default");
NSString *password = [secureStorage getSecureKey:alias];
if (!password || password.length == 0) {
// Fresh install - generate a new key
password = [[NSUUID UUID] UUIDString];
[secureStorage setSecureKey:alias value:password options:nil];
Logger(@"Generated new MMKV encryption key");
} else {
Logger(@"Existing MMKV encryption key found");
}
// Verify MMKV can be opened with this key
NSData *cryptKey = [password dataUsingEncoding:NSUTF8StringEncoding];
MMKVBridge *mmkv = [[MMKVBridge alloc] initWithID:@"default"
cryptKey:cryptKey
rootPath:mmkvPath];
if (mmkv) {
NSUInteger keyCount = [mmkv count];
Logger(@"MMKV initialized with encryption, %lu keys found", (unsigned long)keyCount);
} else {
Logger(@"MMKV instance is nil after initialization");
}
} @catch (NSException *exception) {
Logger(@"MMKV initialization error: %@ - %@", exception.name, exception.reason);
}
});
}
🤖 Prompt for AI Agents
In ios/MMKVKeyManager.mm around lines 37 to 73, the class method +initialize is
performing keychain and filesystem initialization which the ObjC runtime may
call implicitly and off the main thread; rename this method to +setup (or
similar) and make the implementation idempotent by wrapping the initialization
body in a dispatch_once token so it only runs once when explicitly invoked from
AppDelegate (or other startup point). Update any call sites to call +setup
instead of relying on +initialize, and keep the existing exception handling and
logging inside the dispatch_once block.

Comment on lines +58 to +63
// Verify MMKV can be opened with this key
NSData *cryptKey = [password dataUsingEncoding:NSUTF8StringEncoding];
MMKVBridge *mmkv = [[MMKVBridge alloc] initWithID:@"default"
cryptKey:cryptKey
rootPath:mmkvPath];

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

MMKV cryptKey requirements length encoding iOS

💡 Result:

Short answer

  • Historically MMKV expects an encryption key of up to 16 bytes (AES‑128) — the public APIs document "no more than 16 bytes". [1][2]
  • MMKV uses the raw bytes you give it (i.e., pass NSData/byte[]). If you pass an NSString the library will use that string's bytes (commonly UTF‑8) to construct the key. The native layer constructs an AESCrypt with the cryptKey bytes/length. [2][3]

Notes

  • Newer MMKV releases (Flutter plugin changelog) added optional AES‑256 support (migration via reKey). If you need AES‑256, follow that release's reKey upgrade flow and check that your MMKV build/version supports aes256. [4]

Sources
[1] MMKV Java API docs — "cryptKey ... (no more than 16 bytes)".
[2] MMKV iOS/CPP source analysis — mmkvWithID:cryptKey: shows the private API accepting NSData/bytes and the C++ constructor builds AESCrypt from cryptKey.data()/length.
[3] MMKV C++/implementation notes — AESCrypt created from provided bytes; IV handling is 16 bytes (CFB‑128 historically).
[4] mmkv Flutter changelog (2025‑12‑03) — mentions adding AES‑256 support and reKey upgrade procedure.

If you want, tell me which MMKV version and language (iOS Obj‑C/Swift, Android, Flutter) you use and I’ll give exact code examples for generating/passing a correct key.


🏁 Script executed:

# First, let's examine the file in question to understand context
wc -l ios/MMKVKeyManager.mm

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 102


🏁 Script executed:

# Look at the broader context of lines 58-63
head -n 80 ios/MMKVKeyManager.mm | tail -n 30

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1376


🏁 Script executed:

# Search for MMKV-related documentation or comments in the codebase
rg -i "mmkv|cryptkey" --type mm --type h -B 2 -A 2 | head -100

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 104


🏁 Script executed:

# Check if there are any MMKV configuration files or documentation
fd -e md -e txt | xargs grep -l -i "mmkv" 2>/dev/null

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 62


🏁 Script executed:

# Search for any hash-based key derivation
rg -i "hash|sha|md5|kdf|derive" ios/MMKVKeyManager.mm

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 118


🏁 Script executed:

# Look at the full context of the password handling
cat -n ios/MMKVKeyManager.mm | head -n 70

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2867


🏁 Script executed:

# Check if there's any key shortening or hashing before cryptKey assignment
rg -B 10 "cryptKey.*dataUsingEncoding" ios/MMKVKeyManager.mm

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 576


🏁 Script executed:

# Search for Android equivalent to see if there's a pattern we're missing
fd -e java -e kt | xargs grep -l -i "mmkv\|cryptkey" 2>/dev/null | head -5

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 462


🏁 Script executed:

# Check MMKVBridge definition to understand how it processes cryptKey
rg -i "class MMKVBridge\|initWithID.*cryptKey" --type mm --type h -A 5 | head -50

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 104


🏁 Script executed:

# Examine the Android MMKVKeyManager to see how it handles the password
cat -n android/app/src/main/java/chat/rocket/reactnative/storage/MMKVKeyManager.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 4778


🏁 Script executed:

# Search for any key hashing or derivation utilities in the codebase
rg -i "messagedigest|sha256|md5" --type java --type mm --type h -B 2 -A 2 | head -50

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 104


🏁 Script executed:

# Search for any comments or issues about MMKV key length
rg -i "mmkv.*key|cryptkey|16.*byte" --type mm --type java -B 2 -A 2 | head -100

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 104


🏁 Script executed:

# Check git history for recent changes to MMKVKeyManager files
git log --oneline -- ios/MMKVKeyManager.mm android/app/src/main/java/chat/rocket/reactnative/storage/MMKVKeyManager.java | head -20

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 160


🌐 Web query:

MMKV key length exceeded 16 bytes truncation behavior what happens

💡 Result:

Short answer: MMKV does NOT truncate your record keys to 16 bytes. The “16 bytes” limit you’re thinking of is the encryption/cryptKey size (MMKV’s cryptKey must be ≤16 bytes for its AES-based scheme), not the user-key (the string you use to encode/decode values). MMKV stores arbitrary-length key strings in its protobuf-based encoding; only the cryptKey/encryption key is documented with a 16‑byte maximum. [1][2]

Sources:

  • MMKV Java API docs (notes cryptKey “no more than 16 bytes”). [1]
  • Tencent/MMKV README/wiki (encryption and usage; keys are regular strings; cryptKey discussed separately). [2]

🏁 Script executed:

# Find the MMKV version being used
rg -i "mmkv" package.json Podfile build.gradle 2>/dev/null | grep -i "version\|@"

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 60


🏁 Script executed:

# Search for any tests or error handling related to MMKV keys
rg -i "mmkv.*test\|mmkv.*key" --type js --type ts --type java | head -30

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 60


🏁 Script executed:

# Check if there are any issue reports or TODOs about key length
rg -i "todo|fixme|hack|bug.*mmkv|mmkv.*bug" --type mm --type java -B 2 -A 2

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 104


🏁 Script executed:

# Look for MMKVBridge implementation to see how it handles oversized keys
find . -name "MMKVBridge*" -type f 2>/dev/null

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 152


🏁 Script executed:

# Examine MMKVBridge header
cat -n ./ios/Shared/RocketChat/MMKVBridge.h

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 972


🏁 Script executed:

# Examine MMKVBridge implementation
cat -n ./ios/Shared/RocketChat/MMKVBridge.mm | head -150

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3844


🏁 Script executed:

# Search for any key processing in MMKVBridge
rg -i "cryptkey\|hash\|sha" ./ios/Shared/RocketChat/ -A 3 -B 3

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 60


🌐 Web query:

MMKV cryptKey derive from UUID SHA256 16 bytes proper way

💡 Result:

Short answer

  • MMKV requires a cryptKey ≤16 bytes. The simplest safe approach is to run HKDF-SHA256 on your UUID (as raw bytes or UTF-8) and extract 16 bytes (AES-128) as the cryptKey. If you can't use HKDF, SHA-256(UUID) and take the first 16 bytes is acceptable but less flexible; prefer HKDF with a salt and purpose/info. [1][2]

Why

  • MMKV expects an encryption key no more than 16 bytes. Using a proper KDF (HKDF) avoids subtle issues and gives you salt/label separation. Truncating a cryptographic hash is common and usually safe for symmetric keys, but HKDF is best practice. [1][2]

Minimal examples

Kotlin (recommended: HKDF)

  • Pseudocode steps:
    1. input = uuidString.toByteArray(UTF_8)
    2. salt = app-specific random or constant (prefer random and stored)
    3. okm = HKDF-Extract-and-Expand(HMAC-SHA256, salt, input, info="mmkv-cryptKey", L=16)
    4. use okm (16 bytes) as MMKV cryptKey

Kotlin (fallback: SHA-256 truncate)

  • Pseudocode:
    1. hash = MessageDigest.getInstance("SHA-256").digest(uuidString.toByteArray(UTF_8))
    2. cryptKey = hash.copyOfRange(0, 16)
    3. pass cryptKey to MMKV (as byte[] or hex string)

Notes

  • Use raw bytes for KDF inputs (not UUID string with formatting changes). Store salt/parameters so you can recreate the key or rotate it via MMKV.reKey(). For portability, supply the 16 bytes as a byte[] or NSData per MMKV API. [1][3]

References

  • MMKV API / encryption key length and usage. [1]
  • Recommendation to use KDF (HKDF) vs direct hash truncation. [2]
  • MMKV wiki examples showing cryptKey usage. [3]

If you want, I can paste a ready-to-run Kotlin implementation of HKDF-SHA256 and the exact MMKV call.


🏁 Script executed:

# Search for any existing key derivation or hashing functions in the codebase
rg -i "crypto|hash|digest|derive" --type mm --type java --type ts --type js | grep -v node_modules | head -30

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 104


🏁 Script executed:

# Check if there are any comments or issues about this specific problem
rg -i "uuid.*key|key.*uuid|36.*byte|cryptkey.*uuid" --type mm --type java --type ts --type js

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 104


Derive a 16-byte cryptKey from the UUID; passing the full 36-byte UTF-8 string violates MMKV's documented limit

MMKV requires cryptKey ≤16 bytes for AES-128 encryption. The current code passes the full UUID string (~36 bytes when UTF-8 encoded) directly to MMKVBridge.initWithID:cryptKey:, which violates MMKV's API contract. This risks divergent behavior between iOS and Android, potential decryption failures on key upgrades, or silent failures if MMKV silently truncates keys in unpredictable ways.

Use HKDF-SHA256 (preferred) or SHA-256 with truncation to derive exactly 16 bytes from the UUID. For example:

NSData *uuidData = [password dataUsingEncoding:NSUTF8StringEncoding];
unsigned char hash[32];
CC_SHA256([uuidData bytes], (CC_LONG)[uuidData length], hash);
NSData *cryptKey = [NSData dataWithBytes:hash length:16];  // Truncate to 16 bytes

Apply the same key derivation on both iOS and Android to ensure consistency.

🤖 Prompt for AI Agents
In ios/MMKVKeyManager.mm around lines 58 to 63, the code passes the full UUID
string (≈36 bytes) as the MMKV cryptKey which violates MMKV's ≤16-byte
requirement; derive exactly 16 bytes from the UUID instead (use HKDF-SHA256 for
best practice, falling back to SHA-256+truncation if HKDF is unavailable).
Convert the UUID string to NSData, run HKDF-SHA256 (or compute SHA-256) over
that data, and take the first 16 bytes of the resulting digest as the cryptKey
NSData passed into MMKVBridge initWithID:cryptKey:rootPath:. Ensure the same
derivation is applied on Android so keys are consistent across platforms.

Comment on lines +19 to +38
- (instancetype)initWithID:(NSString *)mmapID
cryptKey:(nullable NSData *)cryptKey
rootPath:(nullable NSString *)rootPath {
self = [super init];
if (self) {
// Initialize MMKV if needed
if (rootPath) {
std::string rootPathStr = [rootPath UTF8String];
MMKV::initializeMMKV(rootPathStr);
}

std::string mmapIDStr = [mmapID UTF8String];

if (cryptKey && [cryptKey length] > 0) {
std::string cryptKeyStr((const char *)[cryptKey bytes], [cryptKey length]);
_mmkvInstance = MMKV::mmkvWithID(mmapIDStr, MMKV_MULTI_PROCESS, &cryptKeyStr);
} else {
_mmkvInstance = MMKV::mmkvWithID(mmapIDStr, MMKV_MULTI_PROCESS);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard against nil/empty mmapID and nil key to avoid UB/crashes
[nil UTF8String] yields NULL, and std::string(NULL) is undefined. Add input validation early in init/getters/setters.

 - (instancetype)initWithID:(NSString *)mmapID
                  cryptKey:(nullable NSData *)cryptKey
                  rootPath:(nullable NSString *)rootPath {
     self = [super init];
     if (self) {
+        if (mmapID.length == 0) {
+            return nil;
+        }
         // Initialize MMKV if needed
         if (rootPath) {
             std::string rootPathStr = [rootPath UTF8String];
             MMKV::initializeMMKV(rootPathStr);
         }
@@
 - (nullable NSString *)stringForKey:(NSString *)key {
     if (!_mmkvInstance) return nil;
+    if (key.length == 0) return nil;
@@
 - (nullable NSData *)dataForKey:(NSString *)key {
     if (!_mmkvInstance) return nil;
+    if (key.length == 0) return nil;
@@
 - (BOOL)setString:(NSString *)value forKey:(NSString *)key {
     if (!_mmkvInstance) return NO;
+    if (key.length == 0 || !value) return NO;
@@
 - (BOOL)setData:(NSData *)value forKey:(NSString *)key {
     if (!_mmkvInstance) return NO;
+    if (key.length == 0 || !value) return NO;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
ios/Shared/RocketChat/MMKVBridge.mm around lines 19 to 38: the initializer
currently calls [mmapID UTF8String] and constructs std::string from it which is
undefined if mmapID is nil (and similarly using cryptKey bytes can be unsafe);
add early input validation and guard clauses: if mmapID is nil or empty, fail
the initializer (return nil) or log and noop; for cryptKey, check for nil and
length>0 before accessing bytes; avoid constructing std::string from NULL by
converting Objective-C strings safely (e.g., obtain UTF8String only after
confirming non-nil) or use an explicit empty std::string fallback; apply the
same nil/empty checks in any getters/setters that accept mmapID or key.

Comment on lines +78 to +85
- (BOOL)setData:(NSData *)value forKey:(NSString *)key {
if (!_mmkvInstance) return NO;

std::string keyStr = [key UTF8String];
mmkv::MMBuffer buffer((void *)[value bytes], (size_t)[value length], mmkv::MMBufferNoCopy);

return _mmkvInstance->set(buffer, keyStr);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Tencent MMKV C++ MMBuffer MMBufferNoCopy ownership semantics set method copy

💡 Result:

Short answer — be careful: MMKV's C++ API accepts MMBuffer (and MMBufferNoCopy) by value/move for set, but MMBufferNoCopy does NOT copy or own the memory. set/move will move the MMBuffer object into MMKV internals (or use it immediately to write), but if the MMBuffer was created with MMBufferNoCopy it only wraps your memory pointer — MMKV will not magically take ownership of or deep-copy that memory for you in all cases. That means you must keep the underlying memory valid until set completes (and until MMKV finishes any async write/append it may perform). See MMKV code/use in the docs and multiple source analyses showing MMBuffer creation with MMBufferNoCopy and setDataForKey(std::move(data)). [1][2][3]

Sources

  • MMKV repo & wiki (C++ usage + set -> setDataForKey(std::move(data))). [1]
  • Analysis excerpts showing MMBuffer creation with MMBufferNoCopy and setDataForKey usage. [2]
  • Community writeups explaining MMBuffer / NoCopy wrapping of mmap or external memory and how data is written/appended. [3]

🏁 Script executed:

# Search for MMKV-related documentation or comments in the codebase
find . -type f \( -name "*.mm" -o -name "*.h" -o -name "*.cpp" \) | head -20

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1067


🏁 Script executed:

# Look for other MMKV usage patterns in the codebase
rg "MMBuffer|MMBufferNoCopy|MMBufferCopy" --type objcpp --type cpp -A 3 -B 1

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 450


🏁 Script executed:

# Search for MMKV include statements to understand which version/variant is being used
rg "include.*mmkv|#include.*MMKV" --type objcpp --type cpp

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 60


🏁 Script executed:

# Verify the NSData lifetime in the method context and check for any memory management comments
cat -n ios/Shared/RocketChat/MMKVBridge.mm | sed -n '70,90p'

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 783


🏁 Script executed:

# Check the header file for any documentation or method signatures
cat -n ios/Shared/RocketChat/MMKVBridge.h

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 972


Use MMBufferCopy instead of MMBufferNoCopy to prevent use-after-free

MMBufferNoCopy wraps a pointer without taking ownership; the underlying NSData can be deallocated by ARC immediately after this method returns, causing MMKV to access freed memory during subsequent write operations. Use a copying buffer to ensure the data is owned and protected by MMKV.

-    mmkv::MMBuffer buffer((void *)[value bytes], (size_t)[value length], mmkv::MMBufferNoCopy);
+    mmkv::MMBuffer buffer((void *)[value bytes], (size_t)[value length], mmkv::MMBufferCopy);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- (BOOL)setData:(NSData *)value forKey:(NSString *)key {
if (!_mmkvInstance) return NO;
std::string keyStr = [key UTF8String];
mmkv::MMBuffer buffer((void *)[value bytes], (size_t)[value length], mmkv::MMBufferNoCopy);
return _mmkvInstance->set(buffer, keyStr);
}
- (BOOL)setData:(NSData *)value forKey:(NSString *)key {
if (!_mmkvInstance) return NO;
std::string keyStr = [key UTF8String];
mmkv::MMBuffer buffer((void *)[value bytes], (size_t)[value length], mmkv::MMBufferCopy);
return _mmkvInstance->set(buffer, keyStr);
}
🤖 Prompt for AI Agents
In ios/Shared/RocketChat/MMKVBridge.mm around lines 78 to 85, the code currently
constructs an mmkv::MMBuffer using MMBufferNoCopy which can lead to
use-after-free because NSData may be deallocated after return; change the
MMBuffer construction to use MMBufferCopy so MMKV takes ownership of a copied
buffer (pass the NSData bytes and length with the MMBufferCopy flag) before
calling _mmkvInstance->set, ensuring the data remains valid after this method
returns.

@diegolmello diegolmello had a problem deploying to upload_experimental_android December 12, 2025 21:36 — with GitHub Actions Error
@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.68.0.107842

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNRMqKpViorHLk_khPSJMTe94zn4sMU1A8U1zXJwT9Pbg2HEA9LqPmb-DTUxhhb3OdsTe1rYOQfXUPA6n99o

@github-actions
Copy link

iOS Build Available

Rocket.Chat Experimental 4.68.0.107843

…ation

- Eliminated the direct initialization of MMKV in MainApplication to prevent race conditions.
- Retained MMKVKeyManager for managing encryption key initialization prior to React Native startup, ensuring proper key management.
@github-actions
Copy link

iOS Build Available

Rocket.Chat Experimental 4.68.0.107847

@diegolmello diegolmello merged commit 7215cda into develop Dec 15, 2025
29 of 66 checks passed
@diegolmello diegolmello deleted the feat.migrate-react-native-mmkv-storage branch December 15, 2025 16:47
@coderabbitai coderabbitai bot mentioned this pull request Dec 15, 2025
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants