feat: migrate react-native-mmkv-storage to react-native-mmkv#6744
Conversation
WalkthroughThis 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
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas requiring extra attention:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro Disabled knowledge base sources:
📒 Files selected for processing (1)
🧰 Additional context used🧬 Code graph analysis (1)android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (1)
⏰ 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)
🔇 Additional comments (3)
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. Comment |
|
iOS Build Available Rocket.Chat 4.68.0.107777 |
|
Android Build Available Rocket.Chat Experimental 4.68.0.107775 |
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
ios/RocketChatRN.xcodeproj/project.pbxproj (1)
276-279: SecureStorage.m is added twice to both app targets’ Sources → will cause duplicate symbol linker errorsIn the current PBXSourcesBuildPhase wiring:
RocketChatRN Sources (target
13B07F861A680F5B00A75B9A) includes two entries forSecureStorage.m:
- Line 2138:
66C270212EBBCB780062725F /* SecureStorage.m in Sources */(fileRef66C2701F…)- Line 2140:
A2C6E2DD38F8BEE19BFB2E1D /* SecureStorage.m in Sources */(fileRef9B215A42…)Rocket.Chat Sources (target
7AAB3E0D257E6A6E00707CF6) also includes two entries forSecureStorage.m:
- Line 2413:
66C270202EBBCB780062725F /* SecureStorage.m in Sources */- Line 2415:
79D8C97F8CE2EC1B6882826B /* SecureStorage.m in Sources */Both PBXBuildFile groups ultimately point at the same path (
SecureStorage.m), so each app target will compile the same Objective‑C implementation twice, producing duplicateSecureStoragesymbols at link time.This is the same structural problem previously flagged in an earlier review; it appears still unresolved in the current project file.
Minimal, safe fix (deduplicate per target by keeping the newer 66C27020/21 entries and dropping the extra ones from the Sources phases). For RocketChatRN’s Sources block:
1E76CBDA25152C8E0067298C /* SendMessage.swift in Sources */, - 66C270212EBBCB780062725F /* SecureStorage.m in Sources */, - 4C4C8603EF082F0A33A95522 /* ExpoModulesProvider.swift in Sources */, - A2C6E2DD38F8BEE19BFB2E1D /* SecureStorage.m in Sources */, + 66C270212EBBCB780062725F /* SecureStorage.m in Sources */, + 4C4C8603EF082F0A33A95522 /* ExpoModulesProvider.swift in Sources */,For Rocket.Chat’s Sources block:
7AAB3E30257E6A6E00707CF6 /* NotificationType.swift in Sources */, 7AAB3E31257E6A6E00707CF6 /* SendMessage.swift in Sources */, - 66C270202EBBCB780062725F /* SecureStorage.m in Sources */, - BC404914E86821389EEB543D /* ExpoModulesProvider.swift in Sources */, - 79D8C97F8CE2EC1B6882826B /* SecureStorage.m in Sources */, + 66C270202EBBCB780062725F /* SecureStorage.m in Sources */, + BC404914E86821389EEB543D /* ExpoModulesProvider.swift in Sources */,This removes the duplicate compilation per target while still leaving one
SecureStorage.mper target. You can later clean up the now-unused PBXBuildFile / PBXFileReference entries for9B215A42…if desired, but functionally the above is sufficient to unblock linking.Also applies to: 290-295, 364-365, 2091-2141, 2360-2363, 2367-2416
🧹 Nitpick comments (2)
ios/RocketChatRN.xcodeproj/project.pbxproj (2)
1271-1287: Pods-defaults-RocketChatRN frameworks script runs twice in RocketChatRN targetThe RocketChatRN target’s
buildPhaseslist includes both:
- Line 1272:
A659038DEAED3B4C51B767B6 /* [CP] Check Pods Manifest.lock */(good)- Line 1284:
307228C120BB932837CF0491 /* [CP] Embed Pods Frameworks */→ runsPods-defaults-RocketChatRN-frameworks.sh(canonical CocoaPods phase)- Line 1283:
407D3EDE3DABEE15D27BD87D /* ShellScript */→ also runsPods-defaults-RocketChatRN-frameworks.shbut is mis‑named and uses an unrelated output path (Pods-defaults-NotificationService-checkManifestLockResult.txt)So the same frameworks embed script is executed twice for the RocketChatRN target. That’s redundant work, can lengthen build times, and the misleading output path on
407D3EDE…makes maintenance harder.Suggested cleanup (keep the standard
[CP] Embed Pods Frameworksphase and drop the ad‑hoc duplicate):
- Remove
407D3EDE3DABEE15D27BD87D /* ShellScript */from the RocketChatRN target’sbuildPhaseslist:buildPhases = ( A659038DEAED3B4C51B767B6 /* [CP] Check Pods Manifest.lock */, 7AA5C63E23E30D110005C4A7 /* Start Packager */, 589729E8381BA997CD19EF19 /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 1EC6ACF422CB9FC300A41C61 /* Embed App Extensions */, 1E1EA8082326CCE300E22452 /* ShellScript */, 1ED0389C2B507B4F00C007D4 /* Embed Watch Content */, 7AAE9EB32891A0D20024F559 /* Upload source maps to Bugsnag */, - 407D3EDE3DABEE15D27BD87D /* ShellScript */, - 9C104B12BEE385F7555E641F /* [Expo] Configure project */, + 9C104B12BEE385F7555E641F /* [Expo] Configure project */, 307228C120BB932837CF0491 /* [CP] Embed Pods Frameworks */, CE78D416C9C8B8C82E068650 /* [CP] Copy Pods Resources */, );
- Delete the
PBXShellScriptBuildPhasedefinition for407D3EDE3DABEE15D27BD87D /* ShellScript */at lines 1617–1634 entirely.This leaves the single, correctly named CocoaPods
[CP] Embed Pods Frameworksphase as the only frameworks embed step for RocketChatRN.Also applies to: 1599-1616, 1617-1634
1271-1287: RocketChatRN target is running the NotificationService Expo configure script unnecessarilyRocketChatRN’s
buildPhasescurrently include:
- Line 1274:
589729E8381BA997CD19EF19 /* [Expo] Configure project */→ runsPods-defaults-RocketChatRN/expo-configure-project.sh(correct for this target).- Line 1284:
9C104B12BEE385F7555E641F /* [Expo] Configure project */→ runsPods-defaults-NotificationService/expo-configure-project.sh(intended for the NotificationService pods target, not the main app).NotificationService itself already has its own dedicated Expo configure phase:
- Lines 1354–1359:
86A998705576AFA7CE938617 /* [Expo] Configure project */attached to theNotificationServicetarget, running the samePods-defaults-NotificationService/expo-configure-project.sh.So the NotificationService configure script is run twice: once when building
NotificationService(expected) and once again when buildingRocketChatRN(unnecessary and potentially confusing).Recommended change: detach the NotificationService Expo configure script from the RocketChatRN target and leave it only on the NotificationService target:
- Remove
9C104B12BEE385F7555E641F /* [Expo] Configure project */from the RocketChatRNbuildPhaseslist (combined with the previous diff if you also remove407D3EDE…):buildPhases = ( A659038DEAED3B4C51B767B6 /* [CP] Check Pods Manifest.lock */, 7AA5C63E23E30D110005C4A7 /* Start Packager */, 589729E8381BA997CD19EF19 /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 1EC6ACF422CB9FC300A41C61 /* Embed App Extensions */, 1E1EA8082326CCE300E22452 /* ShellScript */, 1ED0389C2B507B4F00C007D4 /* Embed Watch Content */, 7AAE9EB32891A0D20024F559 /* Upload source maps to Bugsnag */, - 9C104B12BEE385F7555E641F /* [Expo] Configure project */, 307228C120BB932837CF0491 /* [CP] Embed Pods Frameworks */, CE78D416C9C8B8C82E068650 /* [CP] Copy Pods Resources */, );
- Remove the
PBXShellScriptBuildPhasedefinition for9C104B12BEE385F7555E641F /* [Expo] Configure project */at lines 1867–1885.This keeps each target’s Expo configuration confined to its own Pods-defaults script and reduces redundant/possibly surprising work when building the main app.
Also applies to: 1867-1885, 1352-1360
📜 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.
⛔ Files ignored due to path filters (2)
ios/Podfile.lockis excluded by!**/*.lockyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (2)
ios/RocketChatRN.xcodeproj/project.pbxproj(43 hunks)package.json(1 hunks)
🔇 Additional comments (3)
package.json (1)
109-109: Dependency migration properly pinned.The exact version
"3.3.3"forreact-native-mmkvis now correctly specified without a caret, aligning with thereact-native-mmkv+3.3.3.patchfile and ensuring the postinstallpatch-packagestep (line 18) applies consistently. This addresses the past review concern about semver drift preventing patch application.The old
react-native-mmkv-storagedependency has been cleanly removed, completing the first step of the migration. The version pinning strategy is sound for this transition.ios/RocketChatRN.xcodeproj/project.pbxproj (2)
2475-2572: MMKV header search path and FORCE_POSIX define look consistent across app targetsFor both
RocketChatRNandRocket.Chatdebug/release configs you’ve:
- Pointed
baseConfigurationReferenceat the newPods-defaults-*xcconfigs.- Added
FORCE_POSIX=1toGCC_PREPROCESSOR_DEFINITIONS.- Updated
HEADER_SEARCH_PATHSto include$(SRCROOT)/../node_modules/react-native-mmkv/MMKV/Core/**.This wiring is consistent between the two app targets and aligns with the new
react-native-mmkvCore integration. No issues spotted here.Also applies to: 3059-3090
2956-3037: NotificationService configs: FORCE_POSIX and header search paths look reasonableFor the
NotificationServicetarget’s Debug/Release configs you’ve:
- Switched to
Pods-defaults-NotificationService.*.xcconfig.- Added
FORCE_POSIX=1toGCC_PREPROCESSOR_DEFINITIONS.- Set
HEADER_SEARCH_PATHSto$(inherited)plus$(SRCROOT), which should be enough for the newly added Objective‑C(++) sources (MMKVMigration.mm,SecureStorage.m,MMKVBridge.mm) given the Pods xcconfigs handle MMKV/Core headers.This looks coherent with the MMKV migration and the shared native storage stack. No further issues from the project-file perspective.
|
Android Build Available Rocket.Chat Experimental 4.68.0.107812 Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNTU3W2-3eWYWNtVyDp03ekug0i_M6x4MWP2U1Xbx2M9nPitlYNsfpYXUsSPl89wD_numqBdKCYa0bWPWw-8 |
|
iOS Build Available Rocket.Chat Experimental 4.68.0.107813 |
…dling - Removed unnecessary ReactApplicationContext parameters from LoadNotification and CustomPushNotification. - Updated Ejson class to use a simplified method for MMKV access without requiring ReactApplicationContext. - Adjusted Encryption class methods to accept context directly, improving clarity and reducing dependencies on ReactApplicationContext. - Cleaned up unused variables and comments for better code readability.
- Deleted react-native-keychain from package.json and yarn.lock. - Removed all references to Keychain in logout and login methods, simplifying credential management. - Updated Storage.swift to read credentials directly from MMKV, enhancing security and performance.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java (1)
61-136: MissingCallbackimport will break compilation
load(final Ejson ejson, Callback callback)usesCallbackbut this file no longer imports it, and there’s no localCallbacktype in this package. This will fail to compile.Add the missing import (or fully qualify the type) to fix it:
import android.os.Bundle; import android.util.Log; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; +import com.facebook.react.bridge.Callback;
♻️ Duplicate comments (1)
ios/RocketChatRN.xcodeproj/project.pbxproj (1)
276-279: DuplicateSecureStorage.mentries in RocketChatRN and Rocket.Chat targets will still cause linker errorsRocketChatRN (
13B07F871A680F5B00A75B9A) and Rocket.Chat (7AAB3E14257E6A6E00707CF6) each compile two differentSecureStorage.mfiles:
- RocketChatRN Sources:
66C270212EBBCB780062725F /* SecureStorage.m in Sources */(fileRef66C2701F…→RocketChatRN/SecureStorage.m) andA2C6E2DD38F8BEE19BFB2E1D /* SecureStorage.m in Sources */(fileRef9B215A42…→SecureStorage.mat project root).- Rocket.Chat Sources:
66C270202EBBCB780062725F /* SecureStorage.m in Sources */(fileRef66C2701F…) and79D8C97F8CE2EC1B6882826B /* SecureStorage.m in Sources */(fileRef9B215A42…).This is the same duplication previously flagged and will still produce duplicate symbol linker errors once both implementations define the same Objective‑C symbols.
Please pick a single canonical
SecureStorage.mfor each of these two targets (either the one underRocketChatRN/or the root-level one), and remove the extra PBXBuildFile entries from their Sources phases (and, ideally, the unused PBXFileReference + physical file) so that each target compiles exactly oneSecureStorage.m.Also applies to: 290-295, 364-365, 2091-2141, 2367-2416
🧹 Nitpick comments (6)
ios/Shared/RocketChat/Storage.swift (1)
9-9: MMKVBridge usage looks correct; consider reuse ifStorageis frequently createdUsing
MMKVBridge.build()here is consistent with the new storage stack and app-group MMKV setup. IfStorageis instantiated often, you might later consider injecting a sharedMMKVBridgeor making it static to avoid repeated initialization, but this is non-blocking.android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java (1)
30-170: MMKV-based auth lookup and helpers look solid; consider tightening one edge caseThe move to a shared
getMMKV()helper and direct MMKV access intoken(),userId(), andprivateKey()is consistent with the migration goals: no React context dependency, clear null/empty checks, and debug-only key listing that avoids leaking secret values. This should help with notification/E2E flows even when React isn’t fully initialized.One small optional tweak: in
userId()you currently special-case onlyserverURL == null. Treating an empty string the same way (log and return"") would avoid ever building a key with just theTOKEN_KEYprefix and no server component, which would make failure modes a bit clearer:- if (serverURL == null) { - Log.e(TAG, "userId() called but serverURL is null"); - return ""; - } + if (serverURL == null || serverURL.isEmpty()) { + Log.e(TAG, "userId() called but serverURL is null or empty"); + return ""; + }ios/RocketChatRN.xcodeproj/project.pbxproj (4)
615-616: MMKV migration / SecureStorage / MMKVBridge file references look coherent, but verify duplication of file locationsThe new PBXFileReference and PBXGroup wiring for:
MMKVMigration.h/mm(66C27019/1A…)SecureStorage.h/m(66C2701E/1F…underRocketChatRN,B179038F…/9B215A42…at project root)MMKVBridge.mm(9B215A44…underShared/RocketChat/MMKVBridge.mm)is structurally consistent and matches the intended new native stack.
However, note you now have two physical
SecureStorage.{h,m}locations:
RocketChatRN/SecureStorage.{h,m}(via66C2701E/1F…).ios/SecureStorage.{h,m}(viaB179038F…/9B215A42…under the root group).Given the duplicate build-phase entries in the main app targets, it would be safer to keep only one of these locations as the canonical implementation and remove the other reference (and file) to avoid confusion and accidental re‑introduction of duplicate compilation in future edits.
Also applies to: 618-618, 620-620, 625-628, 644-646, 649-655, 725-729, 747-748, 1181-1183, 1226-1231
1599-1616: Redundant / miswired frameworks script on RocketChatRN targetOn the
RocketChatRNtarget you now have:
[CP] Embed Pods Frameworks(307228C1…) correctly callingPods-defaults-RocketChatRN-frameworks.shwith Hermes as input/output.- An additional
ShellScriptphase (407D3EDE…) that also callsPods-defaults-RocketChatRN-frameworks.sh, but:
- Has outputPaths set to
"$(DERIVED_FILE_DIR)/Pods-defaults-NotificationService-checkManifestLockResult.txt", which belongs to the NotificationService Pods check phase.- Does not actually write that output file.
This second phase is redundant with the
[CP] Embed Pods Frameworksphase and the mismatched output path is confusing and can interfere with incremental builds (shared sentinel file for a different target).Suggestion: remove
407D3EDE…entirely, or, if you truly need a second frameworks embed, align its inputs/outputs and naming with its real purpose instead of reusing the NotificationService checkManifest output path.Also applies to: 1617-1634, 1978-1995
1715-1723: Extra[Expo] Configure projectphase on RocketChatRN target appears redundantYou now have multiple
[Expo] Configure projectshell phases:
589729E8…onRocketChatRN→ usesPods-defaults-RocketChatRN/expo-configure-project.sh(expected).84028E94…onRocket.Chat→ usesPods-defaults-Rocket.Chat/expo-configure-project.sh(expected).86A99870…onNotificationService→ usesPods-defaults-NotificationService/expo-configure-project.sh(expected).- Plus
9C104B12…also onRocketChatRN, but calling the NotificationService expo script (Pods-defaults-NotificationService/expo-configure-project.sh).Running the NotificationService expo configuration from the main app target seems unnecessary given
NotificationServicealready has its own Expo phase, and it couples the app build to the extension’s Pod target.Unless there’s a specific reason to generate NotificationService’s provider during the app build, consider removing
9C104B12…fromRocketChatRNto keep per-target Expo configuration isolated.Also applies to: 1839-1847, 1858-1866, 1867-1885
3250-3251: Minor: trailing spaces inOTHER_LDFLAGSare harmless but can be cleaned upBoth project-level Debug/Release configs now have
OTHER_LDFLAGS = "$(inherited) ";(two trailing spaces). This shouldn’t affect linking, but if you touch these settings again it’s worth normalizing to$(inherited)for cleanliness.Also applies to: 3314-3315
📜 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.
⛔ Files ignored due to path filters (2)
ios/Podfile.lockis excluded by!**/*.lockyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (10)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java(1 hunks)android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java(6 hunks)android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java(2 hunks)android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java(1 hunks)android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java(1 hunks)app/lib/methods/logout.ts(3 hunks)app/sagas/login.js(0 hunks)ios/RocketChatRN.xcodeproj/project.pbxproj(49 hunks)ios/Shared/RocketChat/Storage.swift(1 hunks)package.json(1 hunks)
💤 Files with no reviewable changes (1)
- app/sagas/login.js
🧰 Additional context used
🧬 Code graph analysis (2)
android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java (1)
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (1)
Encryption(105-436)
ios/Shared/RocketChat/Storage.swift (1)
ios/Shared/RocketChat/MMKV.swift (3)
build(4-19)userId(28-33)userToken(21-26)
🔇 Additional comments (11)
package.json (1)
108-108: Version pinning concern resolved.The exact version pinning (
3.3.3without caret) aligns the react-native-mmkv dependency with the applied patch filereact-native-mmkv+3.3.3.patchand prevents silent bypass of the patch on future semver-compatible upgrades. This addresses the previous review concern.app/lib/methods/logout.ts (2)
4-6: ImportingisSslfrom helpers is consistent and clearUsing the shared
isSslhelper here keeps SSL detection centralized and matches howuseSslis passed toRocketchatClientelsewhere. No issues from this change.
16-25: No action required —UserPreferences.removeItemis synchronousThe
removeItemmethod returnsvoidand directly callsthis.mmkv.delete(key), which is a synchronous MMKV operation. ConvertingremoveServerKeysto a synchronous helper and removingawaitat call sites is correct and safe. There is no fire-and-forget risk.android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)
653-656: UpdatedLoadNotification.loadcall is consistent with new APIThe invocation now matches the new
load(Ejson, Callback)signature and keeps the control flow unchanged around message-id-only notifications. No issues here.android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java (1)
117-151: Context-aware encryption call is wired correctlyPassing
mContextintoEncryption.shared.encryptMessageContent(...)aligns with the new signature and ensures encryption has access to DB/config viaContext. SincemContextis assigned inonReceivebeforereplyToMessage/buildMessagerun, this should safely satisfy the new null-check inencryptMessageContent.android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (1)
312-418: ExplicitContextusage in decrypt/encrypt improves robustnessRequiring a non-null
ContextindecryptMessageandencryptMessageContent, and routing DB access throughreadRoom(ejson, context), removes hidden dependencies on stored React context and makes failure modes explicit (null context, missing room/e2eKey). Given the updated call sites pass appropriate contexts, this looks correct and aligns well with the storage/encryption migration.ios/RocketChatRN.xcodeproj/project.pbxproj (5)
281-282: Pods-defaults frameworks wiring looks correct across targetsThe new
Pods_defaults_*framework references and their inclusion in theFrameworksbuild phases for:
Pods_defaults_RocketChatRN.frameworkPods_defaults_Rocket_Chat.frameworkPods_defaults_NotificationService.frameworkare consistent with the Pods-defaults xcconfig/baseConfiguration wiring and per-target Pod integration. No issues spotted here.
Also applies to: 675-676, 697-698, 718-719, 1250-1252
1271-1287: Pods manifest lock checks restored and per-target [CP] phases look goodFor
RocketChatRN,Rocket.Chat, andNotificationServiceyou now have proper[CP] Check Pods Manifest.lock,[CP] Embed Pods Frameworks, and[CP] Copy Pods Resourcesphases, each pointing at the correspondingPods-defaults-*scripts with correct input/output paths.This resolves the earlier issue where Rocket.Chat’s Pods manifest check was effectively disabled and generally aligns with CocoaPods’ expected setup.
Also applies to: 1353-1360, 1374-1387, 1546-1580, 1886-1907, 2066-2086
2477-2506: MMKV Core header search paths and FORCE_POSIX define look consistent; verify against installed MMKV versionFor
RocketChatRNandRocket.ChatDebug/Release configs you’ve:
- Added
FORCE_POSIX=1toGCC_PREPROCESSOR_DEFINITIONS.- Added
$(SRCROOT)/../node_modules/react-native-mmkv/MMKV/Core/**toHEADER_SEARCH_PATHS.This is consistent across both app targets and aligns with the MMKV migration intent.
Please double-check that:
- The actual node_modules layout for your chosen
react-native-mmkvversion exposes headers underMMKV/Core/**(and not a slightly different path).FORCE_POSIXis only consumed where intended (e.g., in your new MMKV/SecureStorage native code) and doesn’t accidentally affect other third‑party code.Also applies to: 2563-2572, 3060-3090, 3145-3153
2957-2983: NotificationService build settings for MMKV / SecureStorage look reasonableFor
NotificationServiceDebug/Release configs you’ve:
- Switched to
Pods-defaults-NotificationService.*.xcconfigas base configs.- Added
FORCE_POSIX=1toGCC_PREPROCESSOR_DEFINITIONS.- Simplified
HEADER_SEARCH_PATHSto$(inherited)plus$(SRCROOT).Given that:
SecureStorage.h/mused by NotificationService live at the iOS project root (via9B215A42…/B179038F…), having$(SRCROOT)in header search paths is sufficient for includes like"SecureStorage.h".- MMKV itself is provided via Pods for this target.
This wiring looks sound. Just confirm that your NotificationService Swift/ObjC files include
SecureStorage.hvia a path that resolves with$(SRCROOT)(e.g., no remaining assumptions about the file living underRocketChatRN/).Also applies to: 3009-3036
2646-2650: Share extension no longer referencesreact-native-mmkv-storageheadersThe ShareRocketChatRN Debug/Release configs’
HEADER_SEARCH_PATHSnow only include:
rn-extensions-sharereact-native-firebase$PODS_CONFIGURATION_BUILD_DIR/Firebasewith no remaining
react-native-mmkv-storagepath. This removes the earlier dead dependency on the old storage library and is compatible with the migration toreact-native-mmkv.Also applies to: 2722-2726
| // Read credentials from MMKV (shared via app group) | ||
| // Credentials are stored during login in React Native | ||
| guard let userId = mmkv.userId(for: server), | ||
| let userToken = mmkv.userToken(for: userId) else { | ||
| return nil | ||
| } | ||
|
|
||
| return .init(userId: account, userToken: password) | ||
| return Credentials(userId: userId, userToken: userToken) |
There was a problem hiding this comment.
Server → userId → token lookup likely broken by MMKV key-prefix mismatch
The high-level flow in getCredentials is sound, but given the current MMKV.swift helpers it looks like this will almost never return credentials:
userId(for server:)anduserToken(for userId:)both appear to use the"reactnativemeteor_usertoken-"prefix (seeios/Shared/RocketChat/MMKV.swift), just with different dynamic parts.- That means:
mmkv.userId(for: server)is actually reading from a “token” slot keyed byserver, not a dedicated userId slot.- The resulting value is then passed into
mmkv.userToken(for:), which looks up another"reactnativemeteor_usertoken-\(value)"entry.
- In practice this mapping is inconsistent and will typically cause the
guardto fail, sogetCredentialsreturnsniland login won’t be persisted across restarts—exactly the kind of regression this migration is trying to avoid.
Please double‑check the underlying MMKV key scheme and adjust accordingly (e.g., userId(for:) using a distinct userId prefix like "reactnativemeteor_userid-\(server)", or otherwise aligning with how React Native writes these keys) so the server→userId→token chain actually works.
🤖 Prompt for AI Agents
In ios/Shared/RocketChat/Storage.swift around lines 12–18, the current
server→userId→token lookup uses MMKV helpers that share the same
"reactnativemeteor_usertoken-" prefix, causing the guard to fail and credentials
to be nil; fix this by aligning the MMKV key scheme: update the MMKV helper(s)
in ios/Shared/RocketChat/MMKV.swift so userId(for:) reads from a distinct
"reactnativemeteor_userid-\(server)" key (or alternatively change
userToken(for:) to accept the server key flow), then update
Storage.getCredentials to call the corrected helpers so it performs server ->
userId -> token lookup correctly and returns Credentials when present.
|
Android Build Available Rocket.Chat Experimental 4.68.0.107814 Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNSSu574J6Nq4pU6JA9FuGkNFq9QW1BwqOMrJPhnwZYhn484I0ppllkRIdJb5M38o4XCXirsd6WlzRG95fbU |
|
Android Build Available Rocket.Chat Experimental 4.68.0.107834 Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNSV5_g8UPny2XRjXO2KUC8wCJHkCOLJeHsSBmZyBs_3UmE0An-XgiuSseKOyqyGkkW5vjUD-UmaLlgxnYC3 |
|
iOS Build Available Rocket.Chat Experimental 4.68.0.107837 |
|
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.
There was a problem hiding this comment.
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_PATHSentry$(SRCROOT)/../node_modules/react-native-mmkv/MMKV/Core/**should be removed from app targets. Sincereact-native-mmkvis configured in the Podfile withmodular_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'spost_installhook (as already done forGCC_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 forMode.MULTI_PROCESS.The
Modeenum exportsMULTI_PROCESSdirectly. 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 existingappGroupPathhelper.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).
getStringreturnsnullfor empty strings (""). Use nullish coalescing instead.- return this.mmkv.getString(key) || null; + return this.mmkv.getString(key) ?? null;
111-117: Critical:||breaksfalsehandling (still unresolved).
getBoolreturnsnullwhen the stored value isfalse, breaking boolean logic entirely.- return this.mmkv.getBoolean(key) || null; + return this.mmkv.getBoolean(key) ?? null;
140-146: Critical:||breaks0handling (still unresolved).
getNumberreturnsnullwhen the stored value is0, 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 compilesSecureStorage.mtwice → duplicate symbols
PBXSourcesBuildPhaselists both66C270212EBBCB780062725F /* SecureStorage.m in Sources */(Line 2138) andA2C6E2DD38F8BEE19BFB2E1D /* SecureStorage.m in Sources */(Line 2140). Keep only one.
2366-2417: Rocket.Chat target compilesSecureStorage.mtwice → duplicate symbols
Same issue: both66C270202EBBCB780062725F /* SecureStorage.m in Sources */(Line 2413) and79D8C97F8CE2EC1B6882826B /* 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:anddataForKey:currently treat empty values as missing.Also applies to: 65-76
ios/SecureStorage.m (3)
5-7: Fix invalid Objective‑C syntax:@implementationmust not include superclass
@implementation SecureStorage : NSObjectwon’t compile.-@implementation SecureStorage : NSObject +@implementation SecureStorage
83-103: Fix Keychain read: wrong cast + missing status handling + leak
kSecReturnDatareturns CFData/NSData, but this casts toNSDictionaryand never releasesresult.
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 !== nullis redundant—the truthy checkkey &&already excludesnull. 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 intoHex()
IftoHex()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 forgetSecureKeyvssetSecureKey
getSecureKeyresolvesnullon exception (Line 48-51) whilesetSecureKeyrejects (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 ininitializeMMKV
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 repeatedMMKV::initializeMMKV()across instances
If multipleMMKVBridgeobjects are created, you’ll re-run initialization. Considerdispatch_once(or a static guard) around initialization.ios/SecureStorage.m (2)
20-81: MakeserviceNameastaticand initialize once
A file-global mutableNSString *serviceNamecan be raced and repeatedly reassigned. Prefer astatic NSString *anddispatch_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.
📒 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).
| 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; | ||
| } |
There was a problem hiding this comment.
🧩 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
- Stop shipping further releases that change MMKV config (instance name, encryption key, or storage path).
- Backup the device MMKV files (app data directory) immediately from an affected device before attempting fixes.
- 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:
- UUID bytes (16 bytes):
ByteBuffer.allocate(16).putLong(uuid.getMostSignificantBits()).putLong(uuid.getLeastSignificantBits()).array()then convert to a string using ISO_8859_1 encoding. - 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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)
| @interface MMKVKeyManager : NSObject | ||
|
|
||
| + (void)initialize; | ||
|
|
||
| @end |
There was a problem hiding this comment.
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.
| + (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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| + (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.
| // Verify MMKV can be opened with this key | ||
| NSData *cryptKey = [password dataUsingEncoding:NSUTF8StringEncoding]; | ||
| MMKVBridge *mmkv = [[MMKVBridge alloc] initWithID:@"default" | ||
| cryptKey:cryptKey | ||
| rootPath:mmkvPath]; | ||
|
|
There was a problem hiding this comment.
🧩 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.mmRepository: 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 30Repository: 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 -100Repository: 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/nullRepository: 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.mmRepository: 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 70Repository: 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.mmRepository: 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 -5Repository: 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 -50Repository: 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.javaRepository: 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 -50Repository: 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 -100Repository: 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 -20Repository: 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 -30Repository: 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 2Repository: 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/nullRepository: RocketChat/Rocket.Chat.ReactNative
Length of output: 152
🏁 Script executed:
# Examine MMKVBridge header
cat -n ./ios/Shared/RocketChat/MMKVBridge.hRepository: RocketChat/Rocket.Chat.ReactNative
Length of output: 972
🏁 Script executed:
# Examine MMKVBridge implementation
cat -n ./ios/Shared/RocketChat/MMKVBridge.mm | head -150Repository: 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 3Repository: 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:
- input = uuidString.toByteArray(UTF_8)
- salt = app-specific random or constant (prefer random and stored)
- okm = HKDF-Extract-and-Expand(HMAC-SHA256, salt, input, info="mmkv-cryptKey", L=16)
- use okm (16 bytes) as MMKV cryptKey
Kotlin (fallback: SHA-256 truncate)
- Pseudocode:
- hash = MessageDigest.getInstance("SHA-256").digest(uuidString.toByteArray(UTF_8))
- cryptKey = hash.copyOfRange(0, 16)
- 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 -30Repository: 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 jsRepository: 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.
| - (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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| - (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); | ||
| } |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 1Repository: 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 cppRepository: 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.hRepository: 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.
| - (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.
|
Android Build Available Rocket.Chat Experimental 4.68.0.107842 Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNRMqKpViorHLk_khPSJMTe94zn4sMU1A8U1zXJwT9Pbg2HEA9LqPmb-DTUxhhb3OdsTe1rYOQfXUPA6n99o |
|
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.
|
iOS Build Available Rocket.Chat Experimental 4.68.0.107847 |
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
Server with SSL Certificate
E2EE
Notifications (This case must be repeated in Default user, SSL Server and E2EE)
Upgrading the app
For the upgrade process, all tests performed in New Installation must be repeated, but under different scenarios:
Apple Watch
Screenshots
Types of changes
Checklist
Further comments
Summary by CodeRabbit
New Features
Bug Fixes
Chores
✏️ Tip: You can customize this high-level summary in your review settings.