-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: migrate react-native-mmkv-storage to react-native-mmkv #6744
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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 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.
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_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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.