diff --git a/__mocks__/react-native-mmkv-storage.js b/__mocks__/react-native-mmkv-storage.js deleted file mode 100644 index 29b28d2892d..00000000000 --- a/__mocks__/react-native-mmkv-storage.js +++ /dev/null @@ -1,25 +0,0 @@ -export class MMKVLoader { - // eslint-disable-next-line no-useless-constructor - constructor() { - // console.log('MMKVLoader constructor mock'); - } - - setProcessingMode = jest.fn().mockImplementation(() => ({ - setAccessibleIOS: jest.fn().mockImplementation(() => ({ - withEncryption: jest.fn().mockImplementation(() => ({ - initialize: jest.fn().mockImplementation(() => {}) - })) - })) - })); -} - -export const ProcessingModes = { - MULTI_PROCESS: '' -}; - -export const IOSAccessibleStates = { - AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: '' -}; - -// fix the useUserPreference hook -export const create = jest.fn().mockImplementation(() => jest.fn().mockImplementation(() => [0, jest.fn()])); diff --git a/__mocks__/react-native-mmkv.js b/__mocks__/react-native-mmkv.js new file mode 100644 index 00000000000..3288748482c --- /dev/null +++ b/__mocks__/react-native-mmkv.js @@ -0,0 +1,199 @@ +// Mock for react-native-mmkv +const { useState, useEffect } = require('react'); + +// Shared storage between instances with the same id +const storageInstances = new Map(); + +export const Mode = { + SINGLE_PROCESS: 1, + MULTI_PROCESS: 2 +}; + +export class MMKV { + constructor(config = {}) { + const { id = 'default', mode, path } = config; + this.id = id; + this.mode = mode; + this.path = path; + + // Share storage between instances with the same id + if (!storageInstances.has(this.id)) { + storageInstances.set(this.id, { + storage: new Map(), + listeners: [] + }); + } + + const instance = storageInstances.get(this.id); + this.storage = instance.storage; + this.listeners = instance.listeners; + } + + set(key, value) { + this.storage.set(key, value); + this.notifyListeners(key); + } + + getString(key) { + const value = this.storage.get(key); + return typeof value === 'string' ? value : undefined; + } + + getNumber(key) { + const value = this.storage.get(key); + return typeof value === 'number' ? value : undefined; + } + + getBoolean(key) { + const value = this.storage.get(key); + return typeof value === 'boolean' ? value : undefined; + } + + contains(key) { + return this.storage.has(key); + } + + delete(key) { + const deleted = this.storage.delete(key); + if (deleted) { + this.notifyListeners(key); + } + return deleted; + } + + getAllKeys() { + return Array.from(this.storage.keys()); + } + + clearAll() { + this.storage.clear(); + // Notify about clear (pass undefined to indicate clear all) + this.notifyListeners(undefined); + } + + addOnValueChangedListener(callback) { + this.listeners.push(callback); + return { + remove: () => { + const index = this.listeners.indexOf(callback); + if (index > -1) { + this.listeners.splice(index, 1); + } + } + }; + } + + notifyListeners(key) { + this.listeners.forEach((listener) => { + try { + listener(key); + } catch (error) { + console.error('Error in MMKV listener:', error); + } + }); + } +} + +// Export Configuration type for TypeScript +export const Configuration = {}; + +// React hooks for MMKV +export function useMMKVString(key, mmkvInstance) { + const [value, setValue] = useState(() => mmkvInstance.getString(key)); + + useEffect(() => { + const listener = mmkvInstance.addOnValueChangedListener((changedKey) => { + if (changedKey === key || changedKey === undefined) { + setValue(mmkvInstance.getString(key)); + } + }); + return () => listener.remove(); + }, [key, mmkvInstance]); + + const setStoredValue = (newValue) => { + if (newValue === undefined) { + mmkvInstance.delete(key); + } else { + mmkvInstance.set(key, newValue); + } + setValue(newValue); + }; + + return [value, setStoredValue]; +} + +export function useMMKVNumber(key, mmkvInstance) { + const [value, setValue] = useState(() => mmkvInstance.getNumber(key)); + + useEffect(() => { + const listener = mmkvInstance.addOnValueChangedListener((changedKey) => { + if (changedKey === key || changedKey === undefined) { + setValue(mmkvInstance.getNumber(key)); + } + }); + return () => listener.remove(); + }, [key, mmkvInstance]); + + const setStoredValue = (newValue) => { + if (newValue === undefined) { + mmkvInstance.delete(key); + } else { + mmkvInstance.set(key, newValue); + } + setValue(newValue); + }; + + return [value, setStoredValue]; +} + +export function useMMKVBoolean(key, mmkvInstance) { + const [value, setValue] = useState(() => mmkvInstance.getBoolean(key)); + + useEffect(() => { + const listener = mmkvInstance.addOnValueChangedListener((changedKey) => { + if (changedKey === key || changedKey === undefined) { + setValue(mmkvInstance.getBoolean(key)); + } + }); + return () => listener.remove(); + }, [key, mmkvInstance]); + + const setStoredValue = (newValue) => { + if (newValue === undefined) { + mmkvInstance.delete(key); + } else { + mmkvInstance.set(key, newValue); + } + setValue(newValue); + }; + + return [value, setStoredValue]; +} + +export function useMMKVObject(key, mmkvInstance) { + const [value, setValue] = useState(() => { + const stored = mmkvInstance.getString(key); + return stored ? JSON.parse(stored) : undefined; + }); + + useEffect(() => { + const listener = mmkvInstance.addOnValueChangedListener((changedKey) => { + if (changedKey === key || changedKey === undefined) { + const stored = mmkvInstance.getString(key); + setValue(stored ? JSON.parse(stored) : undefined); + } + }); + return () => listener.remove(); + }, [key, mmkvInstance]); + + const setStoredValue = (newValue) => { + if (newValue === undefined) { + mmkvInstance.delete(key); + } else { + mmkvInstance.set(key, JSON.stringify(newValue)); + } + setValue(newValue); + }; + + return [value, setStoredValue]; +} diff --git a/android/app/build.gradle b/android/app/build.gradle index 9ec188c7d98..4b4fad1912e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -151,6 +151,9 @@ dependencies { implementation "com.google.code.gson:gson:2.8.9" implementation "com.tencent:mmkv-static:1.2.10" implementation 'com.facebook.soloader:soloader:0.10.4' + + // For SecureKeystore (EncryptedSharedPreferences) + implementation 'androidx.security:security-crypto:1.1.0' } apply plugin: 'com.google.gms.google-services' diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt index 0d07364246b..28958176b81 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt @@ -26,6 +26,8 @@ import com.wix.reactnativenotifications.core.notification.IPushNotification import com.bugsnag.android.Bugsnag import expo.modules.ApplicationLifecycleDispatcher import chat.rocket.reactnative.networking.SSLPinningTurboPackage; +import chat.rocket.reactnative.storage.MMKVKeyManager; +import chat.rocket.reactnative.storage.SecureStoragePackage; import chat.rocket.reactnative.notification.CustomPushNotification; open class MainApplication : Application(), ReactApplication, INotificationsApplication { @@ -36,6 +38,7 @@ open class MainApplication : Application(), ReactApplication, INotificationsAppl PackageList(this).packages.apply { add(SSLPinningTurboPackage()) add(WatermelonDBJSIPackage()) + add(SecureStoragePackage()) } override fun getJSMainModuleName(): String = "index" @@ -53,6 +56,10 @@ open class MainApplication : Application(), ReactApplication, INotificationsAppl super.onCreate() SoLoader.init(this, OpenSourceMergedSoMapping) Bugsnag.start(this) + + // Initialize MMKV encryption - reads existing key or generates new one + // Must run before React Native starts to avoid race conditions + MMKVKeyManager.initialize(this) // Load the native entry point for the New Architecture load() diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java index 18f89fb0ca4..caf90dc7ca1 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java @@ -652,7 +652,7 @@ private void notificationDismiss(Notification.Builder notification, int notifica private void notificationLoad(Ejson ejson, Callback callback) { LoadNotification loadNotification = new LoadNotification(); - loadNotification.load(reactApplicationContext, ejson, callback); + loadNotification.load(ejson, callback); } /** diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java b/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java index c12017dd0bc..d0b0e541c05 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java @@ -2,17 +2,13 @@ import android.util.Log; -import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.Callback; - -import com.ammarahmed.mmkv.SecureKeystore; import com.tencent.mmkv.MMKV; -import com.wix.reactnativenotifications.core.AppLifecycleFacade; -import com.wix.reactnativenotifications.core.AppLifecycleFacadeHolder; import java.math.BigInteger; import chat.rocket.reactnative.BuildConfig; +import chat.rocket.reactnative.storage.MMKVKeyManager; class RNCallback implements Callback { public void invoke(Object... args) { @@ -32,6 +28,7 @@ static public String toHex(String arg) { public class Ejson { private static final String TAG = "RocketChat.Ejson"; + private static final String TOKEN_KEY = "reactnativemeteor_usertoken-"; String host; String rid; @@ -42,68 +39,24 @@ public class Ejson { String messageType; String senderName; String msg; - String tmid; - Content content; - private ReactApplicationContext reactContext; - - private MMKV mmkv; - - private boolean initializationAttempted = false; - - private String TOKEN_KEY = "reactnativemeteor_usertoken-"; - - public Ejson() { - // Don't initialize MMKV in constructor - use lazy initialization instead - } - /** - * Lazily initialize MMKV when first needed. - * - * NOTE: MMKV requires ReactApplicationContext (not regular Context) because SecureKeystore - * needs access to React-specific keystore resources. This means MMKV cannot be initialized - * before React Native starts. + * Get MMKV instance with encryption. + * MMKV is already initialized in MainApplication.onCreate(). + * Uses the encryption key from MMKVKeyManager which was set at app startup. + * MMKV internally caches instances, so calling this multiple times is efficient. */ - private synchronized void ensureMMKVInitialized() { - if (initializationAttempted) { - return; - } - - initializationAttempted = true; - - // Try to get ReactApplicationContext from available sources - if (this.reactContext == null) { - AppLifecycleFacade facade = AppLifecycleFacadeHolder.get(); - if (facade != null) { - Object runningContext = facade.getRunningReactContext(); - if (runningContext instanceof ReactApplicationContext) { - this.reactContext = (ReactApplicationContext) runningContext; - } - } - - if (this.reactContext == null) { - this.reactContext = CustomPushNotification.reactApplicationContext; - } - } - - // Initialize MMKV if context is available - if (this.reactContext != null && mmkv == null) { - try { - MMKV.initialize(this.reactContext); - SecureKeystore secureKeystore = new SecureKeystore(this.reactContext); - // Alias format from react-native-mmkv-storage - String alias = Utils.toHex("com.MMKV.default"); - String password = secureKeystore.getSecureKey(alias); - mmkv = MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE, password); - } catch (Exception e) { - Log.e(TAG, "Failed to initialize MMKV", e); - mmkv = null; - } - } else if (this.reactContext == null) { - Log.w(TAG, "Cannot initialize MMKV: ReactApplicationContext not available"); - } + private MMKV getMMKV() { + String encryptionKey = MMKVKeyManager.getEncryptionKey(); + if (encryptionKey != null && !encryptionKey.isEmpty()) { + return MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE, encryptionKey); + } + // Fallback to no encryption if key is not available + // This can happen if Keystore is unavailable (e.g., device locked/Direct Boot) + Log.w(TAG, "MMKV encryption key not available, opening without encryption"); + return MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE); } public String getAvatarUri() { @@ -136,8 +89,8 @@ public String getAvatarUri() { } public String token() { - ensureMMKVInitialized(); String userId = userId(); + MMKV mmkv = getMMKV(); if (mmkv == null) { Log.e(TAG, "token() called but MMKV is null"); @@ -166,20 +119,22 @@ public String token() { } public String userId() { - ensureMMKVInitialized(); String serverURL = serverURL(); - String key = TOKEN_KEY.concat(serverURL); - if (mmkv == null) { - Log.e(TAG, "userId() called but MMKV is null"); + if (serverURL == null) { + Log.e(TAG, "userId() called but serverURL is null"); return ""; } - if (serverURL == null) { - Log.e(TAG, "userId() called but serverURL is null"); + MMKV mmkv = getMMKV(); + + if (mmkv == null) { + Log.e(TAG, "userId() called but MMKV is null"); return ""; } + String key = TOKEN_KEY.concat(serverURL); + if (BuildConfig.DEBUG) { Log.d(TAG, "Looking up userId with key: " + key); } @@ -216,8 +171,8 @@ public String userId() { } public String privateKey() { - ensureMMKVInitialized(); String serverURL = serverURL(); + MMKV mmkv = getMMKV(); if (mmkv != null && serverURL != null) { return mmkv.decodeString(serverURL.concat("-RC_E2E_PRIVATE_KEY")); } @@ -244,4 +199,4 @@ static class Content { String kid; String iv; } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java b/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java index b9fffc4bba7..8eafd8d1fff 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java @@ -6,10 +6,7 @@ import android.util.Log; import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.WritableMap; -import com.wix.reactnativenotifications.core.AppLifecycleFacade; -import com.wix.reactnativenotifications.core.AppLifecycleFacadeHolder; import com.google.gson.Gson; import com.google.gson.JsonObject; import chat.rocket.mobilecrypto.algorithms.AESCrypto; @@ -134,7 +131,6 @@ static class EncryptionContent { private static final String TAG = "RocketChat.E2E"; public static Encryption shared = new Encryption(); - private ReactApplicationContext reactContext; private PrefixedData decodePrefixedBase64(String input) { // A 256-byte array always encodes to 344 characters in Base64. @@ -315,22 +311,12 @@ private String decryptContent(Ejson.Content content, String e2eKey) throws Excep public String decryptMessage(final Ejson ejson, final Context context) { try { - // Get ReactApplicationContext for MMKV access - if (context instanceof ReactApplicationContext) { - this.reactContext = (ReactApplicationContext) context; - } else { - AppLifecycleFacade facade = AppLifecycleFacadeHolder.get(); - if (facade != null && facade.getRunningReactContext() instanceof ReactApplicationContext) { - this.reactContext = (ReactApplicationContext) facade.getRunningReactContext(); - } - } - - if (this.reactContext == null) { - Log.e(TAG, "Cannot decrypt: ReactApplicationContext not available"); + if (context == null) { + Log.e(TAG, "Cannot decrypt: context is null"); return null; } - Room room = readRoom(ejson, this.reactContext); + Room room = readRoom(ejson, context); if (room == null || room.e2eKey == null) { Log.w(TAG, "Cannot decrypt: room or e2eKey not found"); return null; @@ -368,18 +354,14 @@ public String decryptMessage(final Ejson ejson, final Context context) { } } - public EncryptionContent encryptMessageContent(final String message, final String id, final Ejson ejson) { + public EncryptionContent encryptMessageContent(final String message, final String id, final Ejson ejson, final Context context) { try { - AppLifecycleFacade facade = AppLifecycleFacadeHolder.get(); - if (facade != null && facade.getRunningReactContext() instanceof ReactApplicationContext) { - this.reactContext = (ReactApplicationContext) facade.getRunningReactContext(); - } - - // Use reactContext for database access - if (this.reactContext == null) { + if (context == null) { + Log.e(TAG, "Cannot encrypt: context is null"); return null; } - Room room = readRoom(ejson, this.reactContext); + + Room room = readRoom(ejson, context); if (room == null || !room.encrypted || room.e2eKey == null) { return null; } diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java b/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java index 6b6158393fc..1478a39c3ae 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java @@ -3,7 +3,6 @@ import android.os.Bundle; import android.util.Log; -import com.facebook.react.bridge.ReactApplicationContext; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; @@ -65,7 +64,7 @@ public class LoadNotification { private int[] TIMEOUT = new int[]{0, 1, 3, 5, 10}; private String TOKEN_KEY = "reactnativemeteor_usertoken-"; - public void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) { + public void load(final Ejson ejson, Callback callback) { Log.i(TAG, "Starting notification load for message-id-only notification"); // Validate ejson object diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java b/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java index 428332c3bf3..8ee23b64db6 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java @@ -120,7 +120,7 @@ protected String buildMessage(String rid, String message, Ejson ejson) { String id = getMessageId(); // Use the new content structure approach - Encryption.EncryptionContent content = Encryption.shared.encryptMessageContent(message, id, ejson); + Encryption.EncryptionContent content = Encryption.shared.encryptMessageContent(message, id, ejson, mContext); Map msgMap = new HashMap(); msgMap.put("_id", id); diff --git a/android/app/src/main/java/chat/rocket/reactnative/storage/Constants.java b/android/app/src/main/java/chat/rocket/reactnative/storage/Constants.java new file mode 100644 index 00000000000..6ae7d75f341 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/storage/Constants.java @@ -0,0 +1,30 @@ +package chat.rocket.reactnative.storage; + +/** + * Constants for SecureKeystore + * Copied from react-native-mmkv-storage to avoid dependency + * Original source: https://github.com/ammarahm-ed/react-native-mmkv-storage/blob/master/android/src/main/java/com/ammarahmed/mmkv/Constants.java + */ +public class Constants { + + // Key Store + public static final String KEYSTORE_PROVIDER_1 = "AndroidKeyStore"; + public static final String KEYSTORE_PROVIDER_2 = "AndroidKeyStoreBCWorkaround"; + public static final String KEYSTORE_PROVIDER_3 = "AndroidOpenSSL"; + + public static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding"; + public static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding"; + + public static final String TAG = "SecureKeystore"; + + // Internal storage file + public static final String SKS_KEY_FILENAME = "SKS_KEY_FILE"; + public static final String SKS_DATA_FILENAME = "SKS_DATA_FILE"; + + public static final int DATA_TYPE_STRING = 1; + public static final int DATA_TYPE_INT = 2; + public static final int DATA_TYPE_BOOL = 3; + public static final int DATA_TYPE_MAP = 4; + public static final int DATA_TYPE_ARRAY = 5; +} + diff --git a/android/app/src/main/java/chat/rocket/reactnative/storage/MMKVKeyManager.java b/android/app/src/main/java/chat/rocket/reactnative/storage/MMKVKeyManager.java new file mode 100644 index 00000000000..8113c245758 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/storage/MMKVKeyManager.java @@ -0,0 +1,111 @@ +package chat.rocket.reactnative.storage; + +import android.content.Context; +import android.util.Log; + +import com.tencent.mmkv.MMKV; + +import java.util.UUID; + +/** + * MMKV Key Manager - Ensures encryption key exists for MMKV storage + * + * The old library (react-native-mmkv-storage) used encryption by default. + * The new library (react-native-mmkv) requires manually passing the encryption key. + * + * This class: + * - Reads existing encryption keys from SecureKeystore (for existing users) + * - Generates new encryption keys for fresh installs + * - Provides the encryption key to other native code via static getter + * + * Runs synchronously at app startup before React Native initializes. + */ +public class MMKVKeyManager { + private static final String TAG = "MMKVKeyManager"; + private static final String DEFAULT_INSTANCE_ID = "default"; + + // Static field to hold the encryption key for other native code + private static String encryptionKey = null; + private static Context appContext = null; + + /** + * Get the MMKV encryption key. + * Returns null if initialize() hasn't been called yet. + * + * @return The encryption key or null + */ + public static String getEncryptionKey() { + return encryptionKey; + } + + /** + * Ensures MMKV encryption key exists and caches it for other native code. + * - For existing users: reads the key from SecureKeystore + * - For fresh installs: generates a new key and stores it in SecureKeystore + * + * @param context Application context + */ + 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; + } + } + + /** + * Convert string to hexadecimal (same as react-native-mmkv-storage) + * + * @param arg String to convert + * @return Hexadecimal representation + */ + private static String toHex(String arg) { + try { + byte[] bytes = arg.getBytes("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 ""; + } + } +} + diff --git a/android/app/src/main/java/chat/rocket/reactnative/storage/SecureKeystore.java b/android/app/src/main/java/chat/rocket/reactnative/storage/SecureKeystore.java new file mode 100644 index 00000000000..5c861b305e0 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/storage/SecureKeystore.java @@ -0,0 +1,311 @@ +package chat.rocket.reactnative.storage; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import android.security.KeyPairGeneratorSpec; +import android.util.Log; + +import androidx.security.crypto.EncryptedSharedPreferences; +import androidx.security.crypto.MasterKey; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.IllegalViewOperationException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Locale; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.security.auth.x500.X500Principal; + +/** + * SecureKeystore implementation + * Copied from react-native-mmkv-storage to avoid dependency + * Original source: https://github.com/ammarahm-ed/react-native-mmkv-storage/blob/master/android/src/main/java/com/ammarahmed/mmkv/SecureKeystore.java + * This manages encryption keys using Android Keystore System + */ +public class SecureKeystore { + + private SharedPreferences prefs; + private Context context; + private final String SharedPrefFileName = "rnmmkv.shareprefs"; + + private boolean useKeystore() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + } + + public SecureKeystore(ReactApplicationContext reactApplicationContext) { + context = reactApplicationContext; + if (!useKeystore()) { + try { + MasterKey key = new MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build(); + + // Filename for EncryptedSharedPreferences - must match react-native-mmkv-storage + // to maintain compatibility with existing encrypted preference files + // This hash was used by the original library and cannot be changed + prefs = EncryptedSharedPreferences.create( + context, + "e4b001df9a082298dd090bb7455c45d92fbd5dda.xml", + key, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM); + } catch (GeneralSecurityException | IOException | RuntimeException e) { + Log.e("SecureKeystore", "Failed to create encrypted shared preferences! Falling back to standard SharedPreferences", e); + prefs = context.getSharedPreferences(SharedPrefFileName, Context.MODE_PRIVATE); + } + } + } + + public SecureKeystore(Context context) { + this.context = context; + if (!useKeystore()) { + try { + MasterKey key = new MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build(); + + // Filename for EncryptedSharedPreferences - must match react-native-mmkv-storage + // to maintain compatibility with existing encrypted preference files + // This hash was used by the original library and cannot be changed + prefs = EncryptedSharedPreferences.create( + context, + "e4b001df9a082298dd090bb7455c45d92fbd5dda.xml", + key, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM); + } catch (GeneralSecurityException | IOException | RuntimeException e) { + Log.e("SecureKeystore", "Failed to create encrypted shared preferences! Falling back to standard SharedPreferences", e); + prefs = context.getSharedPreferences(SharedPrefFileName, Context.MODE_PRIVATE); + } + } + } + + public static boolean isRTL(Locale locale) { + final int directionality = Character.getDirectionality(locale.getDisplayName().charAt(0)); + return directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT || + directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC; + } + + public void setSecureKey(String key, String value) { + if (useKeystore()) { + try { + Locale initialLocale = Locale.getDefault(); + if (isRTL(initialLocale)) { + Locale.setDefault(Locale.ENGLISH); + setCipherText(context, key, value); + Locale.setDefault(initialLocale); + } else { + setCipherText(context, key, value); + } + } catch (Exception e) { + Log.e(Constants.TAG, "Error setting secure key", e); + } + } else { + try { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(key, value); + editor.apply(); + } catch (Exception e) { + Log.e(Constants.TAG, "Error setting key in prefs", e); + } + } + } + + public String getSecureKey(String key) { + if (useKeystore()) { + try { + String value = getPlainText(context, key); + return value; + } catch (FileNotFoundException fnfe) { + return null; + } catch (Exception e) { + Log.e(Constants.TAG, "Error getting secure key", e); + return null; + } + } else { + try { + String value = prefs.getString(key, null); + return value; + } catch (IllegalViewOperationException e) { + return null; + } + } + } + + public boolean removeSecureKey(String key) { + if (useKeystore()) { + try { + // Delete the cipher text file + String filename = Constants.SKS_DATA_FILENAME + key; + boolean deleted = context.deleteFile(filename); + Log.i(Constants.TAG, "Removed secure key file: " + deleted); + return deleted; + } catch (Exception e) { + Log.e(Constants.TAG, "Error removing secure key", e); + return false; + } + } else { + try { + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(key); + editor.apply(); + Log.i(Constants.TAG, "Removed secure key from prefs"); + return true; + } catch (Exception e) { + Log.e(Constants.TAG, "Error removing key from prefs", e); + return false; + } + } + } + + private PublicKey getOrCreatePublicKey(Context context, String alias) throws GeneralSecurityException, IOException { + KeyStore keyStore = KeyStore.getInstance(getKeyStore()); + keyStore.load(null); + + if (!keyStore.containsAlias(alias) || keyStore.getCertificate(alias) == null) { + Log.i(Constants.TAG, "no existing asymmetric keys for alias"); + + Calendar start = Calendar.getInstance(); + Calendar end = Calendar.getInstance(); + end.add(Calendar.YEAR, 50); + KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context) + .setAlias(alias) + .setSubject(new X500Principal("CN=" + alias)) + .setSerialNumber(BigInteger.ONE) + .setStartDate(start.getTime()) + .setEndDate(end.getTime()) + .build(); + + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", getKeyStore()); + generator.initialize(spec); + generator.generateKeyPair(); + + Log.i(Constants.TAG, "created new asymmetric keys for alias"); + } + + return keyStore.getCertificate(alias).getPublicKey(); + } + + private byte[] encryptRsaPlainText(PublicKey publicKey, byte[] plainTextBytes) throws GeneralSecurityException, IOException { + Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return encryptCipherText(cipher, plainTextBytes); + } + + private byte[] encryptAesPlainText(SecretKey secretKey, String plainText) throws GeneralSecurityException, IOException { + Cipher cipher = Cipher.getInstance(Constants.AES_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + return encryptCipherText(cipher, plainText); + } + + private byte[] encryptCipherText(Cipher cipher, String plainText) throws GeneralSecurityException, IOException { + return encryptCipherText(cipher, plainText.getBytes("UTF-8")); + } + + private byte[] encryptCipherText(Cipher cipher, byte[] plainTextBytes) throws GeneralSecurityException, IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); + cipherOutputStream.write(plainTextBytes); + cipherOutputStream.close(); + return outputStream.toByteArray(); + } + + private SecretKey getOrCreateSecretKey(Context context, String alias) throws GeneralSecurityException, IOException { + try { + return getSymmetricKey(context, alias); + } catch (FileNotFoundException fnfe) { + Log.i(Constants.TAG, "no existing symmetric key for alias"); + + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + //32bytes / 256bits AES key + keyGenerator.init(256); + SecretKey secretKey = keyGenerator.generateKey(); + PublicKey publicKey = getOrCreatePublicKey(context, alias); + Storage.writeValues(context, Constants.SKS_KEY_FILENAME + alias, + encryptRsaPlainText(publicKey, secretKey.getEncoded())); + + Log.i(Constants.TAG, "created new symmetric keys for alias"); + return secretKey; + } + } + + public void setCipherText(Context context, String alias, String input) throws GeneralSecurityException, IOException { + Storage.writeValues(context, Constants.SKS_DATA_FILENAME + alias, + encryptAesPlainText(getOrCreateSecretKey(context, alias), input)); + } + + private PrivateKey getPrivateKey(String alias) throws GeneralSecurityException, IOException { + KeyStore keyStore = KeyStore.getInstance(getKeyStore()); + keyStore.load(null); + return (PrivateKey) keyStore.getKey(alias, null); + } + + private byte[] decryptRsaCipherText(PrivateKey privateKey, byte[] cipherTextBytes) throws GeneralSecurityException, IOException { + Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + return decryptCipherText(cipher, cipherTextBytes); + } + + private byte[] decryptAesCipherText(SecretKey secretKey, byte[] cipherTextBytes) throws GeneralSecurityException, IOException { + Cipher cipher = Cipher.getInstance(Constants.AES_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + return decryptCipherText(cipher, cipherTextBytes); + } + + private byte[] decryptCipherText(Cipher cipher, byte[] cipherTextBytes) throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream(cipherTextBytes); + CipherInputStream cipherInputStream = new CipherInputStream(bais, cipher); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[256]; + int bytesRead = cipherInputStream.read(buffer); + while (bytesRead != -1) { + baos.write(buffer, 0, bytesRead); + bytesRead = cipherInputStream.read(buffer); + } + return baos.toByteArray(); + } + + private SecretKey getSymmetricKey(Context context, String alias) throws GeneralSecurityException, IOException { + byte[] cipherTextBytes = Storage.readValues(context, Constants.SKS_KEY_FILENAME + alias); + return new SecretKeySpec(decryptRsaCipherText(getPrivateKey(alias), cipherTextBytes), Constants.AES_ALGORITHM); + } + + public String getPlainText(Context context, String alias) throws GeneralSecurityException, IOException { + SecretKey secretKey = getSymmetricKey(context, alias); + byte[] cipherTextBytes = Storage.readValues(context, Constants.SKS_DATA_FILENAME + alias); + return new String(decryptAesCipherText(secretKey, cipherTextBytes), "UTF-8"); + } + + private String getKeyStore() { + try { + KeyStore.getInstance(Constants.KEYSTORE_PROVIDER_1); + return Constants.KEYSTORE_PROVIDER_1; + } catch (Exception err) { + try { + KeyStore.getInstance(Constants.KEYSTORE_PROVIDER_2); + return Constants.KEYSTORE_PROVIDER_2; + } catch (Exception e) { + return Constants.KEYSTORE_PROVIDER_3; + } + } + } +} + diff --git a/android/app/src/main/java/chat/rocket/reactnative/storage/SecureStorage.java b/android/app/src/main/java/chat/rocket/reactnative/storage/SecureStorage.java new file mode 100644 index 00000000000..ed48822a579 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/storage/SecureStorage.java @@ -0,0 +1,177 @@ +package chat.rocket.reactnative.storage; + +import android.content.Context; +import android.content.SharedPreferences; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.util.Base64; +import android.util.Log; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.util.UUID; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +public class SecureStorage extends ReactContextBaseJavaModule { + private static final String TAG = "SecureStorage"; + private static final String KEYSTORE_PROVIDER = "AndroidKeyStore"; + private static final String PREFS_NAME = "SecureStoragePrefs"; + private static final String TRANSFORMATION = "AES/GCM/NoPadding"; + private static final int GCM_TAG_LENGTH = 128; + + private final ReactApplicationContext reactContext; + + public SecureStorage(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; + } + + @Override + public String getName() { + return "SecureStorage"; + } + + @ReactMethod + public void getSecureKey(String alias, Promise promise) { + try { + String value = getSecureKeyInternal(alias); + promise.resolve(value); + } catch (Exception e) { + Log.e(TAG, "Error getting secure key: " + alias, e); + promise.resolve(null); + } + } + + @ReactMethod + public void setSecureKey(String alias, String value, Promise promise) { + try { + setSecureKeyInternal(alias, value); + promise.resolve(true); + } catch (Exception e) { + Log.e(TAG, "Error setting secure key: " + alias, e); + promise.reject("SET_SECURE_KEY_ERROR", e); + } + } + + // Internal methods (can be called from Java) + public String getSecureKeyInternal(String alias) { + try { + SharedPreferences prefs = reactContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + String encryptedValue = prefs.getString(alias, null); + + if (encryptedValue == null) { + return null; + } + + // Decrypt the value + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER); + keyStore.load(null); + + if (!keyStore.containsAlias(alias)) { + return null; + } + + 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; + } + } + + public void setSecureKeyInternal(String alias, String value) throws Exception { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER); + keyStore.load(null); + + // Create key if it doesn't exist + if (!keyStore.containsAlias(alias)) { + KeyGenerator keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + KEYSTORE_PROVIDER + ); + keyGenerator.init( + new KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setRandomizedEncryptionRequired(true) + .build() + ); + keyGenerator.generateKey(); + } + + // Encrypt the value + SecretKey secretKey = (SecretKey) keyStore.getKey(alias, null); + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + + byte[] iv = cipher.getIV(); + byte[] encrypted = cipher.doFinal(value.getBytes(StandardCharsets.UTF_8)); + + // Combine IV and encrypted data + 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); + + // Store in SharedPreferences + SharedPreferences prefs = reactContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + prefs.edit().putString(alias, encryptedValue).apply(); + } + + // Generate a secure key if it doesn't exist + public String getOrCreateSecureKey(String alias) { + String key = getSecureKeyInternal(alias); + if (key == null) { + // Generate a new random key + key = UUID.randomUUID().toString(); + try { + setSecureKeyInternal(alias, key); + } catch (Exception e) { + Log.e(TAG, "Error creating secure key", e); + return null; + } + } + return key; + } + + /** + * Synchronous method to get the MMKV encryption key. + * Returns the key cached by MMKVKeyManager at app startup. + * Used by JavaScript to initialize MMKV with the same encryption key as native code. + */ + @ReactMethod(isBlockingSynchronousMethod = true) + public String getMMKVEncryptionKey() { + String key = MMKVKeyManager.getEncryptionKey(); + if (key == null) { + Log.w(TAG, "getMMKVEncryptionKey called but encryption key is null"); + } + return key; + } +} + diff --git a/android/app/src/main/java/chat/rocket/reactnative/storage/SecureStoragePackage.java b/android/app/src/main/java/chat/rocket/reactnative/storage/SecureStoragePackage.java new file mode 100644 index 00000000000..57f468e6929 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/storage/SecureStoragePackage.java @@ -0,0 +1,25 @@ +package chat.rocket.reactnative.storage; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SecureStoragePackage implements ReactPackage { + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new SecureStorage(reactContext)); + return modules; + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} + diff --git a/android/app/src/main/java/chat/rocket/reactnative/storage/Storage.java b/android/app/src/main/java/chat/rocket/reactnative/storage/Storage.java new file mode 100644 index 00000000000..b086242b65b --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/storage/Storage.java @@ -0,0 +1,36 @@ +package chat.rocket.reactnative.storage; + +import android.content.Context; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Storage helper for SecureKeystore + * Copied from react-native-mmkv-storage to avoid dependency + * Original source: https://github.com/ammarahm-ed/react-native-mmkv-storage/blob/master/android/src/main/java/com/ammarahmed/mmkv/Storage.java + */ +public final class Storage { + + public static void writeValues(Context context, String filename, byte[] bytes) throws IOException { + FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); + fos.write(bytes); + fos.close(); + } + + public static byte[] readValues(Context context, String filename) throws IOException { + FileInputStream fis = context.openFileInput(filename); + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + byte[] buffer = new byte[1024]; + int bytesRead = fis.read(buffer); + while(bytesRead != -1) { + baos.write(buffer, 0, bytesRead); + bytesRead = fis.read(buffer); + } + return baos.toByteArray(); + } + +} + diff --git a/app/containers/AudioPlayer/PlaybackSpeed.tsx b/app/containers/AudioPlayer/PlaybackSpeed.tsx index 779d53db72a..68fcc886bcb 100644 --- a/app/containers/AudioPlayer/PlaybackSpeed.tsx +++ b/app/containers/AudioPlayer/PlaybackSpeed.tsx @@ -13,7 +13,7 @@ const PlaybackSpeed = () => { const { colors } = useTheme(); const onPress = () => { - const speedIndex = AVAILABLE_SPEEDS.indexOf(playbackSpeed); + const speedIndex = AVAILABLE_SPEEDS.indexOf(playbackSpeed as number); const nextSpeedIndex = speedIndex + 1 >= AVAILABLE_SPEEDS.length ? 0 : speedIndex + 1; setPlaybackSpeed(AVAILABLE_SPEEDS[nextSpeedIndex]); }; diff --git a/app/lib/methods/helpers/sslPinning.ts b/app/lib/methods/helpers/sslPinning.ts index cc0b42e4ade..d6ab84cb022 100644 --- a/app/lib/methods/helpers/sslPinning.ts +++ b/app/lib/methods/helpers/sslPinning.ts @@ -26,7 +26,10 @@ const persistCertificate = (server: string, name: string, password?: string) => password }; UserPreferences.setMap(name, certificate); - UserPreferences.setMap(extractHostname(server), certificate); + const hostname = extractHostname(server); + if (hostname) { + UserPreferences.setMap(hostname, certificate); + } UserPreferences.setString(`${CERTIFICATE_KEY}-${server}`, name); return certificate; }; diff --git a/app/lib/methods/logout.ts b/app/lib/methods/logout.ts index 9fe8b1bd9a5..69a9643ec2d 100644 --- a/app/lib/methods/logout.ts +++ b/app/lib/methods/logout.ts @@ -1,9 +1,8 @@ import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; import type Model from '@nozbe/watermelondb/Model'; -import * as Keychain from 'react-native-keychain'; import { getDeviceToken } from '../notifications'; -import { isIOS, isSsl } from './helpers'; +import { isSsl } from './helpers'; import { BASIC_AUTH_KEY } from './helpers/fetch'; import database, { getDatabase } from '../database'; import log from './helpers/log'; @@ -14,7 +13,7 @@ import { removePushToken } from '../services/restApi'; import { roomsSubscription } from './subscriptions/rooms'; import { _activeUsersSubTimeout } from './getUsersPresence'; -async function removeServerKeys({ server, userId }: { server: string; userId?: string | null }) { +function removeServerKeys({ server, userId }: { server: string; userId?: string | null }) { UserPreferences.removeItem(`${TOKEN_KEY}-${server}`); if (userId) { UserPreferences.removeItem(`${TOKEN_KEY}-${userId}`); @@ -23,9 +22,6 @@ async function removeServerKeys({ server, userId }: { server: string; userId?: s UserPreferences.removeItem(`${server}-${E2E_PUBLIC_KEY}`); UserPreferences.removeItem(`${server}-${E2E_PRIVATE_KEY}`); UserPreferences.removeItem(`${server}-${E2E_RANDOM_PASSWORD_KEY}`); - if (isIOS) { - await Keychain.resetInternetCredentials(server); - } } export async function removeServerData({ server }: { server: string }): Promise { @@ -44,7 +40,7 @@ export async function removeServerData({ server }: { server: string }): Promise< batch.push(serverRecord.prepareDestroyPermanently()); await serversDB.write(() => serversDB.batch(batch)); - await removeServerKeys({ server, userId }); + removeServerKeys({ server, userId }); } catch (e) { log(e); } diff --git a/app/lib/methods/userPreferences.ts b/app/lib/methods/userPreferences.ts index f0552fdf403..8e9a605ee3c 100644 --- a/app/lib/methods/userPreferences.ts +++ b/app/lib/methods/userPreferences.ts @@ -1,63 +1,167 @@ -import { create, MMKVLoader, type MMKVInstance, ProcessingModes, IOSAccessibleStates } from 'react-native-mmkv-storage'; +import { MMKV, Mode, useMMKVString } from 'react-native-mmkv'; +import type { Configuration } from 'react-native-mmkv'; +import { NativeModules } from 'react-native'; -const MMKV = new MMKVLoader() - // MODES.MULTI_PROCESS = ACCESSIBLE BY APP GROUP (iOS) - .setProcessingMode(ProcessingModes.MULTI_PROCESS) - .setAccessibleIOS(IOSAccessibleStates.AFTER_FIRST_UNLOCK) - .withEncryption() - .initialize(); +import { isAndroid } from './helpers'; -export const useUserPreferences = create(MMKV); +/** + * Get the MMKV encryption key from native secure storage. + * This key is managed by: + * - Android: MMKVKeyManager.java (reads from SecureKeystore or generates new) + * - iOS: SecureStorage.m (reads from Keychain or generates new) + */ +const getEncryptionKey = (): string | undefined => { + try { + const { SecureStorage } = NativeModules; + const key = SecureStorage?.getMMKVEncryptionKey?.(); + return key && key !== null ? key : undefined; + } catch (error) { + console.warn('[UserPreferences] Failed to get MMKV encryption key:', error); + return undefined; + } +}; + +const buildConfiguration = (): Configuration => { + const config: Configuration = { + id: 'default' + }; + + const multiProcessMode = (Mode as { MULTI_PROCESS?: Mode })?.MULTI_PROCESS; + if (multiProcessMode) { + config.mode = multiProcessMode; + } + + const appGroupPath = getAppGroupPath(); + if (!isAndroid && appGroupPath) { + config.path = `${appGroupPath}mmkv`; + } + + // Get encryption key from native secure storage + const encryptionKey = getEncryptionKey(); + if (encryptionKey) { + config.encryptionKey = encryptionKey; + } + + return config; +}; + +const getAppGroupPath = (): string => { + if (isAndroid) { + return ''; + } + + try { + const { AppGroup } = NativeModules; + return AppGroup?.path || ''; + } catch { + return ''; + } +}; + +const MMKV_INSTANCE = new MMKV(buildConfiguration()); + +export const useUserPreferences = (key: string, defaultValue?: T): [T | undefined, (value: T | undefined) => void] => { + const [storedValue, setStoredValue] = useMMKVString(key, MMKV_INSTANCE); + + let value: T | undefined = defaultValue; + if (storedValue !== undefined) { + if (typeof defaultValue === 'string' || defaultValue === undefined) { + value = storedValue as T; + } else { + try { + value = JSON.parse(storedValue) as T; + } catch { + value = defaultValue; + } + } + } + + const setValue = (newValue: T | undefined) => { + if (newValue === undefined) { + setStoredValue(undefined); + } else if (typeof newValue === 'string') { + setStoredValue(newValue); + } else { + setStoredValue(JSON.stringify(newValue)); + } + }; + + return [value, setValue]; +}; class UserPreferences { - private mmkv: MMKVInstance; + private mmkv: MMKV; + constructor() { - this.mmkv = MMKV; + this.mmkv = MMKV_INSTANCE; } getString(key: string): string | null { try { - return this.mmkv.getString(key) ?? null; + return this.mmkv.getString(key) || null; } catch { return null; } } - setString(key: string, value: string): boolean | undefined { - return this.mmkv.setString(key, value) ?? undefined; + setString(key: string, value: string): void { + this.mmkv.set(key, value); } getBool(key: string): boolean | null { try { - return this.mmkv.getBool(key) ?? null; + return this.mmkv.getBoolean(key) || null; } catch { return null; } } - setBool(key: string, value: boolean): boolean | undefined { - return this.mmkv.setBool(key, value) ?? undefined; + setBool(key: string, value: boolean): void { + this.mmkv.set(key, value); } getMap(key: string): object | null { try { - return this.mmkv.getMap(key) ?? null; + const jsonString = this.mmkv.getString(key); + return jsonString ? JSON.parse(jsonString) : null; } catch { return null; } } - setMap(key: string, value: object): boolean | undefined { - return this.mmkv.setMap(key, value) ?? undefined; + setMap(key: string, value: object): void { + this.mmkv.set(key, JSON.stringify(value)); } - removeItem(key: string): boolean | undefined { - if (this.getString(key) !== null) { - return this.mmkv.removeItem(key) ?? undefined; + removeItem(key: string): void { + this.mmkv.delete(key); + } + + getNumber(key: string): number | null { + try { + return this.mmkv.getNumber(key) || null; + } catch { + return null; } - return false; + } + + setNumber(key: string, value: number): void { + this.mmkv.set(key, value); + } + + getAllKeys(): string[] { + return this.mmkv.getAllKeys(); + } + + contains(key: string): boolean { + return this.mmkv.contains(key); + } + + clearAll(): void { + this.mmkv.clearAll(); } } const userPreferences = new UserPreferences(); export default userPreferences; +export { MMKV_INSTANCE as initializeStorage }; diff --git a/app/sagas/init.js b/app/sagas/init.js index 9e0a88c4ad3..d9d6024abe8 100644 --- a/app/sagas/init.js +++ b/app/sagas/init.js @@ -22,7 +22,6 @@ export const initLocalSettings = function* initLocalSettings() { }; const restore = function* restore() { - console.log('RESTORE'); try { const server = UserPreferences.getString(CURRENT_SERVER); let userId = UserPreferences.getString(`${TOKEN_KEY}-${server}`); diff --git a/app/sagas/login.js b/app/sagas/login.js index aa81e5d964c..00b79a37c3d 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -2,8 +2,6 @@ import React from 'react'; import { call, cancel, delay, fork, put, race, select, take, takeLatest } from 'redux-saga/effects'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { Q } from '@nozbe/watermelondb'; -import * as Keychain from 'react-native-keychain'; - import dayjs from '../lib/dayjs'; import * as types from '../actions/actionsTypes'; import { appStart } from '../actions/app'; @@ -37,7 +35,6 @@ import { connect, loginWithPassword, login } from '../lib/services/connect'; import { saveUserProfile, registerPushToken, getUsersRoles } from '../lib/services/restApi'; import { setUsersRoles } from '../actions/usersRoles'; import { getServerById } from '../lib/database/services/Server'; -import { appGroupSuiteName } from '../lib/methods/appGroup'; import appNavigation from '../lib/navigation/appNavigation'; import { showActionSheetRef } from '../containers/ActionSheet'; import { SupportedVersionsWarning } from '../containers/SupportedVersions'; @@ -279,12 +276,6 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) { UserPreferences.setString(`${TOKEN_KEY}-${server}`, user.id); UserPreferences.setString(`${TOKEN_KEY}-${user.id}`, user.token); UserPreferences.setString(CURRENT_SERVER, server); - if (isIOS) { - yield Keychain.setInternetCredentials(server, user.id, user.token, { - accessGroup: appGroupSuiteName, - securityLevel: Keychain.SECURITY_LEVEL.SECURE_SOFTWARE - }); - } yield put(setUser(user)); EventEmitter.emit('connected'); const currentRoot = yield select(state => state.app.root); diff --git a/app/views/AccessibilityAndAppearanceView/index.tsx b/app/views/AccessibilityAndAppearanceView/index.tsx index b5d5b39a7cc..de018f4975c 100644 --- a/app/views/AccessibilityAndAppearanceView/index.tsx +++ b/app/views/AccessibilityAndAppearanceView/index.tsx @@ -112,7 +112,7 @@ const AccessibilityAndAppearanceView = () => { setAlertDisplayType(value); }} title={I18n.t('A11y_appearance_show_alerts_as')} - value={alertDisplayType} + value={alertDisplayType ?? 'TOAST'} /> diff --git a/app/views/MediaAutoDownloadView/index.tsx b/app/views/MediaAutoDownloadView/index.tsx index d622f0db6b2..59b1269ce52 100644 --- a/app/views/MediaAutoDownloadView/index.tsx +++ b/app/views/MediaAutoDownloadView/index.tsx @@ -35,11 +35,11 @@ const MediaAutoDownload = () => { - + - + - + diff --git a/ios/AppDelegate.swift b/ios/AppDelegate.swift index 386a6feffd6..4303a0d39bb 100644 --- a/ios/AppDelegate.swift +++ b/ios/AppDelegate.swift @@ -3,7 +3,6 @@ import React import ReactAppDependencyProvider import Firebase import Bugsnag -import MMKV import WatchConnectivity @UIApplicationMain @@ -18,15 +17,14 @@ public class AppDelegate: ExpoAppDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { + // IMPORTANT: Initialize MMKV encryption FIRST, before any other initialization + // This reads existing encryption key or generates a new one for fresh installs + // Must run before Firebase, Bugsnag, and React Native start + MMKVKeyManager.initialize() + FirebaseApp.configure() Bugsnag.start() - // Initialize MMKV with app group - if let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String, - let groupDir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)?.path { - MMKV.initialize(rootDir: nil, groupDir: groupDir, logLevel: .debug) - } - // Initialize notifications RNNotifications.startMonitorNotifications() ReplyNotification.configure() diff --git a/ios/MMKVKeyManager.h b/ios/MMKVKeyManager.h new file mode 100644 index 00000000000..b5e2058cc34 --- /dev/null +++ b/ios/MMKVKeyManager.h @@ -0,0 +1,19 @@ +// +// MMKVKeyManager.h +// RocketChatRN +// +// MMKV Key Manager - Ensures encryption key exists for MMKV storage +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MMKVKeyManager : NSObject + ++ (void)initialize; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/ios/MMKVKeyManager.mm b/ios/MMKVKeyManager.mm new file mode 100644 index 00000000000..5a26b2f13f0 --- /dev/null +++ b/ios/MMKVKeyManager.mm @@ -0,0 +1,91 @@ +// +// MMKVKeyManager.mm +// RocketChatRN +// +// MMKV Key Manager - Ensures encryption key exists for MMKV storage +// For existing users: reads the key from Keychain +// For fresh installs: generates a new key and stores it in Keychain +// + +#import "MMKVKeyManager.h" +#import "SecureStorage.h" +#import "Shared/RocketChat/MMKVBridge.h" + +static NSString *toHex(NSString *str) { + if (!str) return @""; + + const char *utf8 = [str UTF8String]; + NSMutableString *hex = [NSMutableString string]; + + while (*utf8) { + [hex appendFormat:@"%02X", (unsigned char)*utf8++]; + } + + return [hex lowercaseString]; +} + +static void Logger(NSString *format, ...) { + va_list args; + va_start(args, format); + NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + fprintf(stderr, "[MMKVKeyManager] %s\n", [message UTF8String]); +} + +@implementation MMKVKeyManager + ++ (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); + } +} + ++ (NSString *)initializeMMKV { + NSString *appGroup = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"AppGroup"]; + if (!appGroup) return nil; + + NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroup]; + if (!groupURL) return nil; + + NSString *mmkvPath = [[groupURL path] stringByAppendingPathComponent:@"mmkv"]; + [[NSFileManager defaultManager] createDirectoryAtPath:mmkvPath + withIntermediateDirectories:YES + attributes:nil + error:nil]; + return mmkvPath; +} + +@end + diff --git a/ios/NotificationService/NotificationService-Bridging-Header.h b/ios/NotificationService/NotificationService-Bridging-Header.h index 0c2809ee172..1d63c024204 100644 --- a/ios/NotificationService/NotificationService-Bridging-Header.h +++ b/ios/NotificationService/NotificationService-Bridging-Header.h @@ -2,8 +2,8 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // -#import -#import +#import "SecureStorage.h" +#import "../Shared/RocketChat/MMKVBridge.h" #import #import #import diff --git a/ios/Podfile b/ios/Podfile index 693b2016291..128259fad07 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -8,6 +8,7 @@ prepare_react_native_project! def all_pods pod 'simdjson', path: '../node_modules/@nozbe/simdjson', modular_headers: true + pod 'react-native-mmkv', path: '../node_modules/react-native-mmkv', modular_headers: true $RNFirebaseAnalyticsWithoutAdIdSupport = true use_expo_modules! @@ -36,7 +37,6 @@ end $static_framework = [ 'WatermelonDB', 'simdjson', - 'react-native-mmkv-storage', 'react-native-notifications' ] pre_install do |installer| @@ -66,6 +66,30 @@ post_install do |installer| config.build_settings['CODE_SIGNING_REQUIRED'] = "NO" config.build_settings['CODE_SIGNING_ALLOWED'] = "NO" config.build_settings['ENABLE_BITCODE'] = "NO" + + # Add SecureStorage headers to search paths for react-native-webview + if target.name == 'react-native-webview' + config.build_settings['HEADER_SEARCH_PATHS'] ||= ['$(inherited)'] + config.build_settings['HEADER_SEARCH_PATHS'] << '$(SRCROOT)/..' + end + + # Add FORCE_POSIX for MMKV to expose C++ API properly + if target.name == 'react-native-mmkv' + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)'] + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'FORCE_POSIX=1' + end + end + end + + # Add FORCE_POSIX to main project targets that use MMKV C++ API + installer.aggregate_targets.each do |aggregate_target| + aggregate_target.user_project.targets.each do |target| + if ['NotificationService', 'RocketChatRN', 'Rocket.Chat'].include?(target.name) + target.build_configurations.each do |config| + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)'] + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'FORCE_POSIX=1' unless config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'].include?('FORCE_POSIX=1') + end + end end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b443f1d65a8..60d4d99c035 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -226,9 +226,6 @@ PODS: - libwebp/sharpyuv (1.5.0) - libwebp/webp (1.5.0): - libwebp/sharpyuv - - MMKV (1.3.14): - - MMKVCore (~> 1.3.14) - - MMKVCore (1.3.14) - MobileCrypto (0.2.0): - DoubleConversion - glog @@ -1686,11 +1683,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-mmkv-storage (12.0.0): + - react-native-mmkv (3.3.3): - DoubleConversion - glog - hermes-engine - - MMKV (~> 1.3.14) - RCT-Folly (= 2024.11.18.00) - RCTRequired - RCTTypeSafety @@ -1820,7 +1816,6 @@ PODS: - DoubleConversion - glog - hermes-engine - - MMKV (~> 1.3.9) - RCT-Folly (= 2024.11.18.00) - RCTRequired - RCTTypeSafety @@ -2381,8 +2376,6 @@ PODS: - ReactCommon/turbomodule/core - TOCropViewController (~> 2.7.4) - Yoga - - RNKeychain (8.2.0): - - React-Core - RNLocalize (2.1.1): - React-Core - RNNotifee (7.8.2): @@ -2704,7 +2697,7 @@ DEPENDENCIES: - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)" - "react-native-cookies (from `../node_modules/@react-native-cookies/cookies`)" - react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`) - - react-native-mmkv-storage (from `../node_modules/react-native-mmkv-storage`) + - react-native-mmkv (from `../node_modules/react-native-mmkv`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-notifications (from `../node_modules/react-native-notifications`) - react-native-restart (from `../node_modules/react-native-restart`) @@ -2756,7 +2749,6 @@ DEPENDENCIES: - RNFileViewer (from `../node_modules/react-native-file-viewer`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - - RNKeychain (from `../node_modules/react-native-keychain`) - RNLocalize (from `../node_modules/react-native-localize`) - "RNNotifee (from `../node_modules/@notifee/react-native`)" - RNReanimated (from `../node_modules/react-native-reanimated`) @@ -2783,8 +2775,6 @@ SPEC REPOS: - libavif - libdav1d - libwebp - - MMKV - - MMKVCore - nanopb - PromisesObjC - PromisesSwift @@ -2924,8 +2914,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-cookies/cookies" react-native-keyboard-controller: :path: "../node_modules/react-native-keyboard-controller" - react-native-mmkv-storage: - :path: "../node_modules/react-native-mmkv-storage" + react-native-mmkv: + :path: "../node_modules/react-native-mmkv" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" react-native-notifications: @@ -3028,8 +3018,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-gesture-handler" RNImageCropPicker: :path: "../node_modules/react-native-image-crop-picker" - RNKeychain: - :path: "../node_modules/react-native-keychain" RNLocalize: :path: "../node_modules/react-native-localize" RNNotifee: @@ -3089,8 +3077,6 @@ SPEC CHECKSUMS: libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - MMKV: 7b5df6a8bf785c6705cc490c541b9d8a957c4a64 - MMKVCore: 3f40b896e9ab522452df9df3ce983471aa2449ba MobileCrypto: 60a1e43e26a9d6851ae2aa7294b8041c9e9220b7 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 @@ -3131,13 +3117,13 @@ SPEC CHECKSUMS: react-native-cameraroll: 23d28040c32ca8b20661e0c41b56ab041779244b react-native-cookies: d648ab7025833b977c0b19e142503034f5f29411 react-native-keyboard-controller: 9ec7ee23328c30251a399cffd8b54324a00343bf - react-native-mmkv-storage: 9afc38c25213482f668c80bf2f0a50f75dda1777 + react-native-mmkv: ec96a16cd90e0d994d486c3993abf712186f7262 react-native-netinfo: 2e3c27627db7d49ba412bfab25834e679db41e21 react-native-notifications: 3bafa1237ae8a47569a84801f17d80242fe9f6a5 react-native-restart: f6f591aeb40194c41b9b5013901f00e6cf7d0f29 react-native-safe-area-context: 5928d84c879db2f9eb6969ca70e68f58623dbf25 react-native-slider: 605e731593322c4bb2eb48d7d64e2e4dbf7cbd77 - react-native-webview: e28f476ea60826ef0b1d7297244db1dfbec74acd + react-native-webview: 69c118d283fccfbc4fca0cd680e036ff3bf188fa React-NativeModulesApple: 5b234860053d0dd11f3442f38b99688ff1c9733b React-oscompat: 472a446c740e39ee39cd57cd7bfd32177c763a2b React-perflogger: bbca3688c62f4f39e972d6e21969c95fe441fb6c @@ -3183,7 +3169,6 @@ SPEC CHECKSUMS: RNFileViewer: f9424017fa643c115c1444e11292e84fb16ddd68 RNGestureHandler: 8ff2b1434b0ff8bab28c8242a656fb842990bbc8 RNImageCropPicker: b219389d3a300679b396e81d501e8c8169ffa3c0 - RNKeychain: bbe2f6d5cc008920324acb49ef86ccc03d3b38e4 RNLocalize: ca86348d88b9a89da0e700af58d428ab3f343c4e RNNotifee: 8768d065bf1e2f9f8f347b4bd79147431c7eacd6 RNReanimated: f52ccd5ceea2bae48d7421eec89b3f0c10d7b642 @@ -3200,6 +3185,6 @@ SPEC CHECKSUMS: Yoga: dfabf1234ccd5ac41d1b1d43179f024366ae9831 ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 -PODFILE CHECKSUM: 4c73563b34520b90c036817cdb9ccf65fea5f5c5 +PODFILE CHECKSUM: 757f1420bea093d5dadd6c82f2653ddd1ad8eb9b COCOAPODS: 1.15.2 diff --git a/ios/RocketChatRN-Bridging-Header.h b/ios/RocketChatRN-Bridging-Header.h index 84cdd29326b..32825b2b635 100644 --- a/ios/RocketChatRN-Bridging-Header.h +++ b/ios/RocketChatRN-Bridging-Header.h @@ -2,8 +2,9 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // -#import -#import +#import "SecureStorage.h" +#import "MMKVKeyManager.h" +#import "Shared/RocketChat/MMKVBridge.h" #import #import #import diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index 7f679cbc227..e510680542b 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -273,7 +273,13 @@ 1EFEB5982493B6640072EDC0 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFEB5972493B6640072EDC0 /* NotificationService.swift */; }; 1EFEB59C2493B6640072EDC0 /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 1EFEB5952493B6640072EDC0 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 24A2AEF2383D44B586D31C01 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 06BB44DD4855498082A744AD /* libz.tbd */; }; + 3F56D232A9EBA1C9C749F15D /* SecureStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B215A42CFB843397273C7EA /* SecureStorage.m */; }; + 3F56D232A9EBA1C9C749F15E /* MMKVBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9B215A44CFB843397273C7EC /* MMKVBridge.mm */; }; + 3F56D232A9EBA1C9C749F15F /* MMKVBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9B215A44CFB843397273C7EC /* MMKVBridge.mm */; }; + 3F56D232A9EBA1C9C749F160 /* MMKVBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9B215A44CFB843397273C7EC /* MMKVBridge.mm */; }; 4C4C8603EF082F0A33A95522 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D5C142B655F8EFD006792C /* ExpoModulesProvider.swift */; }; + 542B26E977F9D0D645359412 /* Pods_defaults_RocketChatRN.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E468595F0CACBBB30640F7D /* Pods_defaults_RocketChatRN.framework */; }; + 600F30C06FDB7188A8FCA452 /* Pods_defaults_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2BF76DA1450703CDCF8A4C42 /* Pods_defaults_NotificationService.framework */; }; 65AD38372BFBDF4A00271B39 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 65AD38362BFBDF4A00271B39 /* PrivacyInfo.xcprivacy */; }; 65AD38392BFBDF4A00271B39 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 65AD38362BFBDF4A00271B39 /* PrivacyInfo.xcprivacy */; }; 65AD383A2BFBDF4A00271B39 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 65AD38362BFBDF4A00271B39 /* PrivacyInfo.xcprivacy */; }; @@ -281,6 +287,12 @@ 65AD383C2BFBDF4A00271B39 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 65AD38362BFBDF4A00271B39 /* PrivacyInfo.xcprivacy */; }; 65B9A71A2AFC24190088956F /* ringtone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 65B9A7192AFC24190088956F /* ringtone.mp3 */; }; 65B9A71B2AFC24190088956F /* ringtone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 65B9A7192AFC24190088956F /* ringtone.mp3 */; }; + 66C2701B2EBBCB570062725F /* MMKVKeyManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 66C2701A2EBBCB570062725F /* MMKVKeyManager.mm */; }; + 66C2701C2EBBCB570062725F /* MMKVKeyManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 66C2701A2EBBCB570062725F /* MMKVKeyManager.mm */; }; + 66C2701D2EBBCB570062725F /* MMKVKeyManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 66C2701A2EBBCB570062725F /* MMKVKeyManager.mm */; }; + 66C270202EBBCB780062725F /* SecureStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 66C2701F2EBBCB780062725F /* SecureStorage.m */; }; + 66C270212EBBCB780062725F /* SecureStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 66C2701F2EBBCB780062725F /* SecureStorage.m */; }; + 79D8C97F8CE2EC1B6882826B /* SecureStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B215A42CFB843397273C7EA /* SecureStorage.m */; }; 7A006F14229C83B600803143 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7A006F13229C83B600803143 /* GoogleService-Info.plist */; }; 7A0129D42C6E8EC800F84A97 /* ShareRocketChatRN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0129D22C6E8B5900F84A97 /* ShareRocketChatRN.swift */; }; 7A0129D62C6E8F0700F84A97 /* ShareRocketChatRN.entitlements in Resources */ = {isa = PBXBuildFile; fileRef = 1EC6AD6022CBA20C00A41C61 /* ShareRocketChatRN.entitlements */; }; @@ -349,13 +361,12 @@ 7AE10C0628A59530003593CB /* Inter.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7AE10C0528A59530003593CB /* Inter.ttf */; }; 7AE10C0828A59530003593CB /* Inter.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7AE10C0528A59530003593CB /* Inter.ttf */; }; 85160EB6C143E0493FE5F014 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194D9A8897F4A486C2C6F89A /* ExpoModulesProvider.swift */; }; - 89240CF9E2591164EDB5E5D5 /* Pods_defaults_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 11BBDDD4BBAEEA60A9ABB37A /* Pods_defaults_NotificationService.framework */; }; + A2C6E2DD38F8BEE19BFB2E1D /* SecureStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B215A42CFB843397273C7EA /* SecureStorage.m */; }; A48B46D92D3FFBD200945489 /* A11yFlowModule.m in Sources */ = {isa = PBXBuildFile; fileRef = A48B46D82D3FFBD200945489 /* A11yFlowModule.m */; }; A48B46DA2D3FFBD200945489 /* A11yFlowModule.m in Sources */ = {isa = PBXBuildFile; fileRef = A48B46D82D3FFBD200945489 /* A11yFlowModule.m */; }; BC404914E86821389EEB543D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391C4F7AA7023CD41EEBD106 /* ExpoModulesProvider.swift */; }; - BE6FD5939EC2EE9166A93755 /* Pods_defaults_Rocket_Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13A33C9AA4B7B99630814AB5 /* Pods_defaults_Rocket_Chat.framework */; }; DD2BA30A89E64F189C2C24AC /* libWatermelonDB.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BA7E862283664608B3894E34 /* libWatermelonDB.a */; }; - FCC51A05F6D4B29D88600862 /* Pods_defaults_RocketChatRN.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 275D1FF97F64974B4E72549D /* Pods_defaults_RocketChatRN.framework */; }; + EE08C45FED0B9C53C0951564 /* Pods_defaults_Rocket_Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB48D08AB9265FA119D88B83 /* Pods_defaults_Rocket_Chat.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -455,13 +466,10 @@ /* Begin PBXFileReference section */ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; 06BB44DD4855498082A744AD /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; - 11BBDDD4BBAEEA60A9ABB37A /* Pods_defaults_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 13A33C9AA4B7B99630814AB5 /* Pods_defaults_Rocket_Chat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_Rocket_Chat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07F961A680F5B00A75B9A /* Rocket.Chat Experimental.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Rocket.Chat Experimental.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RocketChatRN/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = RocketChatRN/Info.plist; sourceTree = ""; }; 194D9A8897F4A486C2C6F89A /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-defaults-NotificationService/ExpoModulesProvider.swift"; sourceTree = ""; }; - 1B8E154274568748124AD479 /* Pods-defaults-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.debug.xcconfig"; sourceTree = ""; }; 1E01C81B2511208400FEF824 /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; 1E01C8202511301400FEF824 /* PushResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushResponse.swift; sourceTree = ""; }; 1E01C8242511303100FEF824 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; @@ -604,13 +612,21 @@ 1EFEB5972493B6640072EDC0 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 1EFEB5992493B6640072EDC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1EFEB5A12493B67D0072EDC0 /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; - 23CCE114AF2A329EBFA536D1 /* Pods-defaults-RocketChatRN.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.debug.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.debug.xcconfig"; sourceTree = ""; }; - 275D1FF97F64974B4E72549D /* Pods_defaults_RocketChatRN.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_RocketChatRN.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 265A77689414BCF7977F3E07 /* Pods-defaults-RocketChatRN.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.debug.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.debug.xcconfig"; sourceTree = ""; }; + 2BF76DA1450703CDCF8A4C42 /* Pods_defaults_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 338E74D5495AE57AB52BC764 /* Pods-defaults-Rocket.Chat.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.debug.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.debug.xcconfig"; sourceTree = ""; }; 391C4F7AA7023CD41EEBD106 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-defaults-Rocket.Chat/ExpoModulesProvider.swift"; sourceTree = ""; }; 45D5C142B655F8EFD006792C /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-defaults-RocketChatRN/ExpoModulesProvider.swift"; sourceTree = ""; }; 60B2A6A31FC4588700BD58E5 /* RocketChatRN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = RocketChatRN.entitlements; path = RocketChatRN/RocketChatRN.entitlements; sourceTree = ""; }; 65AD38362BFBDF4A00271B39 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 65B9A7192AFC24190088956F /* ringtone.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ringtone.mp3; sourceTree = ""; }; + 66C270192EBBCB570062725F /* MMKVKeyManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMKVKeyManager.h; sourceTree = ""; }; + 66C2701A2EBBCB570062725F /* MMKVKeyManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MMKVKeyManager.mm; sourceTree = ""; }; + 66C2701E2EBBCB780062725F /* SecureStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SecureStorage.h; sourceTree = ""; }; + 66C2701F2EBBCB780062725F /* SecureStorage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SecureStorage.m; sourceTree = ""; }; + 6E468595F0CACBBB30640F7D /* Pods_defaults_RocketChatRN.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_RocketChatRN.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6F50F2B44849C3F5AF549047 /* Pods-defaults-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.debug.xcconfig"; sourceTree = ""; }; + 72BE2DC82482F10567C83FD7 /* Pods-defaults-Rocket.Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.release.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.release.xcconfig"; sourceTree = ""; }; 7A006F13229C83B600803143 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 7A0129D22C6E8B5900F84A97 /* ShareRocketChatRN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareRocketChatRN.swift; sourceTree = ""; }; 7A0D62D1242AB187006D5C06 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; @@ -626,14 +642,16 @@ 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; 7ACFE7D82DDE48760090D9BC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AE10C0528A59530003593CB /* Inter.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Inter.ttf; sourceTree = ""; }; - 7B573F5E14B1C36153C67BB7 /* Pods-defaults-Rocket.Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.release.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.release.xcconfig"; sourceTree = ""; }; - 8596BEE43152ACCF33F41AB6 /* Pods-defaults-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.release.xcconfig"; sourceTree = ""; }; - 8B02168DA5A28406D0331D30 /* Pods-defaults-RocketChatRN.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.release.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.release.xcconfig"; sourceTree = ""; }; + 9B215A42CFB843397273C7EA /* SecureStorage.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SecureStorage.m; sourceTree = ""; }; + 9B215A44CFB843397273C7EC /* MMKVBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = MMKVBridge.mm; path = Shared/RocketChat/MMKVBridge.mm; sourceTree = ""; }; A48B46D72D3FFBD200945489 /* A11yFlowModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = A11yFlowModule.h; sourceTree = ""; }; A48B46D82D3FFBD200945489 /* A11yFlowModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = A11yFlowModule.m; sourceTree = ""; }; + B179038FDD7AAF285047814B /* SecureStorage.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SecureStorage.h; sourceTree = ""; }; B37C79D9BD0742CE936B6982 /* libc++.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; BA7E862283664608B3894E34 /* libWatermelonDB.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libWatermelonDB.a; sourceTree = ""; }; - EB54E3717238BA2E5971176E /* Pods-defaults-Rocket.Chat.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.debug.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.debug.xcconfig"; sourceTree = ""; }; + E508F5F0D3A212E54C74BC1D /* Pods-defaults-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.release.xcconfig"; sourceTree = ""; }; + EB48D08AB9265FA119D88B83 /* Pods_defaults_Rocket_Chat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_Rocket_Chat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EC3A37AC948D1CBA680F8A21 /* Pods-defaults-RocketChatRN.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.release.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -654,7 +672,7 @@ 7ACD4897222860DE00442C55 /* JavaScriptCore.framework in Frameworks */, 24A2AEF2383D44B586D31C01 /* libz.tbd in Frameworks */, DD2BA30A89E64F189C2C24AC /* libWatermelonDB.a in Frameworks */, - FCC51A05F6D4B29D88600862 /* Pods_defaults_RocketChatRN.framework in Frameworks */, + 542B26E977F9D0D645359412 /* Pods_defaults_RocketChatRN.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -676,7 +694,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 89240CF9E2591164EDB5E5D5 /* Pods_defaults_NotificationService.framework in Frameworks */, + 600F30C06FDB7188A8FCA452 /* Pods_defaults_NotificationService.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -697,7 +715,7 @@ 7AAB3E3D257E6A6E00707CF6 /* JavaScriptCore.framework in Frameworks */, 7AAB3E3E257E6A6E00707CF6 /* libz.tbd in Frameworks */, 7AAB3E3F257E6A6E00707CF6 /* libWatermelonDB.a in Frameworks */, - BE6FD5939EC2EE9166A93755 /* Pods_defaults_Rocket_Chat.framework in Frameworks */, + EE08C45FED0B9C53C0951564 /* Pods_defaults_Rocket_Chat.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -707,6 +725,8 @@ 13B07FAE1A68108700A75B9A /* RocketChatRN */ = { isa = PBXGroup; children = ( + 66C2701E2EBBCB780062725F /* SecureStorage.h */, + 66C2701F2EBBCB780062725F /* SecureStorage.m */, 7ACFE7D82DDE48760090D9BC /* AppDelegate.swift */, A48B46D72D3FFBD200945489 /* A11yFlowModule.h */, A48B46D82D3FFBD200945489 /* A11yFlowModule.m */, @@ -724,6 +744,8 @@ 7A0D62D1242AB187006D5C06 /* LaunchScreen.storyboard */, 1ED00BB02513E04400A1331F /* ReplyNotification.swift */, 65AD38362BFBDF4A00271B39 /* PrivacyInfo.xcprivacy */, + 66C270192EBBCB570062725F /* MMKVKeyManager.h */, + 66C2701A2EBBCB570062725F /* MMKVKeyManager.mm */, ); name = RocketChatRN; sourceTree = ""; @@ -1121,12 +1143,12 @@ 7AC2B09613AA7C3FEBAC9F57 /* Pods */ = { isa = PBXGroup; children = ( - 1B8E154274568748124AD479 /* Pods-defaults-NotificationService.debug.xcconfig */, - 8596BEE43152ACCF33F41AB6 /* Pods-defaults-NotificationService.release.xcconfig */, - EB54E3717238BA2E5971176E /* Pods-defaults-Rocket.Chat.debug.xcconfig */, - 7B573F5E14B1C36153C67BB7 /* Pods-defaults-Rocket.Chat.release.xcconfig */, - 23CCE114AF2A329EBFA536D1 /* Pods-defaults-RocketChatRN.debug.xcconfig */, - 8B02168DA5A28406D0331D30 /* Pods-defaults-RocketChatRN.release.xcconfig */, + 6F50F2B44849C3F5AF549047 /* Pods-defaults-NotificationService.debug.xcconfig */, + E508F5F0D3A212E54C74BC1D /* Pods-defaults-NotificationService.release.xcconfig */, + 338E74D5495AE57AB52BC764 /* Pods-defaults-Rocket.Chat.debug.xcconfig */, + 72BE2DC82482F10567C83FD7 /* Pods-defaults-Rocket.Chat.release.xcconfig */, + 265A77689414BCF7977F3E07 /* Pods-defaults-RocketChatRN.debug.xcconfig */, + EC3A37AC948D1CBA680F8A21 /* Pods-defaults-RocketChatRN.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -1156,6 +1178,8 @@ B8E79A681F3CCC69005B464F /* Recovered References */, 7AC2B09613AA7C3FEBAC9F57 /* Pods */, 7890E71355E6C0A3288089E7 /* ExpoModulesProviders */, + B179038FDD7AAF285047814B /* SecureStorage.h */, + 9B215A42CFB843397273C7EA /* SecureStorage.m */, ); indentWidth = 4; sourceTree = ""; @@ -1203,6 +1227,7 @@ isa = PBXGroup; children = ( BA7E862283664608B3894E34 /* libWatermelonDB.a */, + 9B215A44CFB843397273C7EC /* MMKVBridge.mm */, ); name = "Recovered References"; sourceTree = ""; @@ -1222,9 +1247,9 @@ 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */, B37C79D9BD0742CE936B6982 /* libc++.tbd */, 06BB44DD4855498082A744AD /* libz.tbd */, - 11BBDDD4BBAEEA60A9ABB37A /* Pods_defaults_NotificationService.framework */, - 13A33C9AA4B7B99630814AB5 /* Pods_defaults_Rocket_Chat.framework */, - 275D1FF97F64974B4E72549D /* Pods_defaults_RocketChatRN.framework */, + 2BF76DA1450703CDCF8A4C42 /* Pods_defaults_NotificationService.framework */, + EB48D08AB9265FA119D88B83 /* Pods_defaults_Rocket_Chat.framework */, + 6E468595F0CACBBB30640F7D /* Pods_defaults_RocketChatRN.framework */, ); name = Frameworks; sourceTree = ""; @@ -1244,7 +1269,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "RocketChatRN" */; buildPhases = ( - E162B0C32CD33B9B9A1F4361 /* [CP] Check Pods Manifest.lock */, + 601E2DCB3D2AE923C1934115 /* [CP] Check Pods Manifest.lock */, 7AA5C63E23E30D110005C4A7 /* Start Packager */, 589729E8381BA997CD19EF19 /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, @@ -1257,8 +1282,8 @@ 7AAE9EB32891A0D20024F559 /* Upload source maps to Bugsnag */, 407D3EDE3DABEE15D27BD87D /* ShellScript */, 9C104B12BEE385F7555E641F /* [Expo] Configure project */, - FB9B77136D1334AB107FD00C /* [CP] Embed Pods Frameworks */, - 17344D2847CBD7141D4AF748 /* [CP] Copy Pods Resources */, + 83E5556DD496DD97055F0B21 /* [CP] Embed Pods Frameworks */, + CFB901605AECDF9D4106D152 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1326,12 +1351,12 @@ isa = PBXNativeTarget; buildConfigurationList = 1EFEB5A02493B6640072EDC0 /* Build configuration list for PBXNativeTarget "NotificationService" */; buildPhases = ( - AE4FAA0798304640E5A8569E /* [CP] Check Pods Manifest.lock */, + 974065F792CC0D60E1F63EBD /* [CP] Check Pods Manifest.lock */, 86A998705576AFA7CE938617 /* [Expo] Configure project */, 1EFEB5912493B6640072EDC0 /* Sources */, 1EFEB5922493B6640072EDC0 /* Frameworks */, 1EFEB5932493B6640072EDC0 /* Resources */, - B0026F317D2C2B79CFC7EF36 /* [CP] Copy Pods Resources */, + 8F403F7E8D286C6AB01BD57C /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1346,7 +1371,7 @@ isa = PBXNativeTarget; buildConfigurationList = 7AAB3E4F257E6A6E00707CF6 /* Build configuration list for PBXNativeTarget "Rocket.Chat" */; buildPhases = ( - 9B389A140FE9388CC31876E6 /* [CP] Check Pods Manifest.lock */, + D03AD0E345B1F71DFB6BF874 /* [CP] Check Pods Manifest.lock */, 7AAB3E13257E6A6E00707CF6 /* Start Packager */, 84028E94C77DEBDD5200728D /* [Expo] Configure project */, 7AAB3E14257E6A6E00707CF6 /* Sources */, @@ -1357,8 +1382,8 @@ 7AAB3E4B257E6A6E00707CF6 /* ShellScript */, 1ED1ECE32B8699DD00F6620C /* Embed Watch Content */, 7A10288726B1D15200E47EF8 /* Upload source maps to Bugsnag */, - A7499040756F25E0CD61079D /* [CP] Embed Pods Frameworks */, - 2F74017C365FD1194B768CF7 /* [CP] Copy Pods Resources */, + 2DAD6E48B817688F7DC701E4 /* [CP] Embed Pods Frameworks */, + A21A863DFE0C97848CFA22B1 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1530,77 +1555,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export EXTRA_PACKAGER_ARGS=\"--sourcemap-output $TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\"\nexport NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n"; - }; - 17344D2847CBD7141D4AF748 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/BugsnagReactNative/Bugsnag.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCrashlytics/FirebaseCrashlytics_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInstallations/FirebaseInstallations_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle", - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Bugsnag.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreExtension_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCrashlytics_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseInstallations_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "export EXTRA_PACKAGER_ARGS=\"--sourcemap-output $TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\"\nexport NODE_BINARY=node \n../node_modules/react-native/scripts/react-native-xcode.sh\n"; }; 1E1EA8082326CCE300E22452 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1619,74 +1574,22 @@ shellPath = /bin/sh; shellScript = "echo \"Target architectures: $ARCHS\"\n\nAPP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\nFRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\nFRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\necho \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\necho $(lipo -info \"$FRAMEWORK_EXECUTABLE_PATH\")\n\nFRAMEWORK_TMP_PATH=\"$FRAMEWORK_EXECUTABLE_PATH-tmp\"\n\n# remove simulator's archs if location is not simulator's directory\ncase \"${TARGET_BUILD_DIR}\" in\n*\"iphonesimulator\")\necho \"No need to remove archs\"\n;;\n*)\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"i386\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"i386\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"i386 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"x86_64\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"x86_64\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"x86_64 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\n;;\nesac\n\necho \"Completed for executable $FRAMEWORK_EXECUTABLE_PATH\"\necho $\n\ndone\n"; }; - 2F74017C365FD1194B768CF7 /* [CP] Copy Pods Resources */ = { + 2DAD6E48B817688F7DC701E4 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/BugsnagReactNative/Bugsnag.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCrashlytics/FirebaseCrashlytics_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInstallations/FirebaseInstallations_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle", + "${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); - name = "[CP] Copy Pods Resources"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Bugsnag.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreExtension_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCrashlytics_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseInstallations_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 407D3EDE3DABEE15D27BD87D /* ShellScript */ = { @@ -1726,6 +1629,28 @@ shellPath = /bin/sh; shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-RocketChatRN/expo-configure-project.sh\"\n"; }; + 601E2DCB3D2AE923C1934115 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-defaults-RocketChatRN-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 7A10288726B1D15200E47EF8 /* Upload source maps to Bugsnag */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1734,7 +1659,7 @@ inputFileListPaths = ( ); inputPaths = ( - "$TARGET_BUILD_DIR/$INFOPLIST_PATH", + $TARGET_BUILD_DIR/$INFOPLIST_PATH, ); name = "Upload source maps to Bugsnag"; outputFileListPaths = ( @@ -1820,7 +1745,7 @@ inputFileListPaths = ( ); inputPaths = ( - "$TARGET_BUILD_DIR/$INFOPLIST_PATH", + $TARGET_BUILD_DIR/$INFOPLIST_PATH, ); name = "Upload source maps to Bugsnag"; outputFileListPaths = ( @@ -1831,6 +1756,24 @@ shellPath = /bin/sh; shellScript = "SOURCE_MAP=\"$TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\" ../node_modules/@bugsnag/react-native/bugsnag-react-native-xcode.sh\n"; }; + 83E5556DD496DD97055F0B21 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 84028E94C77DEBDD5200728D /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -1869,7 +1812,77 @@ shellPath = /bin/sh; shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-NotificationService/expo-configure-project.sh\"\n"; }; - 9B389A140FE9388CC31876E6 /* [CP] Check Pods Manifest.lock */ = { + 8F403F7E8D286C6AB01BD57C /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/BugsnagReactNative/Bugsnag.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCrashlytics/FirebaseCrashlytics_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInstallations/FirebaseInstallations_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Bugsnag.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreExtension_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCrashlytics_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseInstallations_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 974065F792CC0D60E1F63EBD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1884,7 +1897,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-defaults-Rocket.Chat-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-defaults-NotificationService-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1910,53 +1923,83 @@ shellPath = /bin/sh; shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-NotificationService/expo-configure-project.sh\"\n"; }; - A7499040756F25E0CD61079D /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - AE4FAA0798304640E5A8569E /* [CP] Check Pods Manifest.lock */ = { + A21A863DFE0C97848CFA22B1 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/BugsnagReactNative/Bugsnag.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCrashlytics/FirebaseCrashlytics_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInstallations/FirebaseInstallations_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle", ); + name = "[CP] Copy Pods Resources"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-defaults-NotificationService-checkManifestLockResult.txt", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Bugsnag.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreExtension_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCrashlytics_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseInstallations_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-resources.sh\"\n"; showEnvVarsInLog = 0; }; - B0026F317D2C2B79CFC7EF36 /* [CP] Copy Pods Resources */ = { + CFB901605AECDF9D4106D152 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService-resources.sh", + "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/BugsnagReactNative/Bugsnag.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", @@ -2017,10 +2060,10 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh\"\n"; showEnvVarsInLog = 0; }; - E162B0C32CD33B9B9A1F4361 /* [CP] Check Pods Manifest.lock */ = { + D03AD0E345B1F71DFB6BF874 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -2035,31 +2078,13 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-defaults-RocketChatRN-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-defaults-Rocket.Chat-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - FB9B77136D1334AB107FD00C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -2072,6 +2097,7 @@ 1E5141182B856673007BE94A /* SSLPinning.swift in Sources */, 1E76CBD825152C870067298C /* Request.swift in Sources */, 1E51411C2B85683C007BE94A /* SSLPinning.m in Sources */, + 66C2701B2EBBCB570062725F /* MMKVKeyManager.mm in Sources */, 1ED00BB12513E04400A1331F /* ReplyNotification.swift in Sources */, 1E76CBC2251529560067298C /* Storage.swift in Sources */, 1E76CBD925152C8C0067298C /* Push.swift in Sources */, @@ -2088,6 +2114,7 @@ 7ACFE7DA2DDE48760090D9BC /* AppDelegate.swift in Sources */, 1E9A71742B59F36E00477BA2 /* ClientSSL.swift in Sources */, A48B46D92D3FFBD200945489 /* A11yFlowModule.m in Sources */, + 3F56D232A9EBA1C9C749F15F /* MMKVBridge.mm in Sources */, 7AACF8AC2C94B28B0082844E /* DecryptedContent.swift in Sources */, 1ED038A52B50900800C007D4 /* Bundle+Extensions.swift in Sources */, 1E76CBC325152A460067298C /* String+Extensions.swift in Sources */, @@ -2108,7 +2135,9 @@ 7A8B30762BCD9D3F00146A40 /* SSLPinning.mm in Sources */, 1E76CBCF25152C310067298C /* NotificationType.swift in Sources */, 1E76CBDA25152C8E0067298C /* SendMessage.swift in Sources */, + 66C270212EBBCB780062725F /* SecureStorage.m in Sources */, 4C4C8603EF082F0A33A95522 /* ExpoModulesProvider.swift in Sources */, + A2C6E2DD38F8BEE19BFB2E1D /* SecureStorage.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2320,6 +2349,7 @@ 1E01C8212511301400FEF824 /* PushResponse.swift in Sources */, 1E680ED92512990700C9257A /* Request.swift in Sources */, 1E2F61642512955D00871711 /* HTTPMethod.swift in Sources */, + 66C2701D2EBBCB570062725F /* MMKVKeyManager.mm in Sources */, 1E598AE925151A63002BDFBD /* SendMessage.swift in Sources */, 1E2F61662512958900871711 /* Push.swift in Sources */, 1EFEB5982493B6640072EDC0 /* NotificationService.swift in Sources */, @@ -2328,6 +2358,8 @@ 1E2F615B25128F9A00871711 /* API.swift in Sources */, 1E01C8272511303900FEF824 /* Payload.swift in Sources */, 85160EB6C143E0493FE5F014 /* ExpoModulesProvider.swift in Sources */, + 3F56D232A9EBA1C9C749F15D /* SecureStorage.m in Sources */, + 3F56D232A9EBA1C9C749F15E /* MMKVBridge.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2340,6 +2372,7 @@ 1E5141192B856673007BE94A /* SSLPinning.swift in Sources */, 7AAB3E16257E6A6E00707CF6 /* Request.swift in Sources */, 1E51411D2B85683C007BE94A /* SSLPinning.m in Sources */, + 66C2701C2EBBCB570062725F /* MMKVKeyManager.mm in Sources */, 7AAB3E17257E6A6E00707CF6 /* ReplyNotification.swift in Sources */, 7AAB3E18257E6A6E00707CF6 /* Storage.swift in Sources */, 7AAB3E19257E6A6E00707CF6 /* Push.swift in Sources */, @@ -2356,6 +2389,7 @@ 7ACFE7D92DDE48760090D9BC /* AppDelegate.swift in Sources */, 1E9A71752B59F36E00477BA2 /* ClientSSL.swift in Sources */, A48B46DA2D3FFBD200945489 /* A11yFlowModule.m in Sources */, + 3F56D232A9EBA1C9C749F160 /* MMKVBridge.mm in Sources */, 7AACF8AE2C94B28B0082844E /* DecryptedContent.swift in Sources */, 1ED038A72B50900800C007D4 /* Bundle+Extensions.swift in Sources */, 7AAB3E26257E6A6E00707CF6 /* String+Extensions.swift in Sources */, @@ -2376,7 +2410,9 @@ 7A8B30772BCD9D3F00146A40 /* SSLPinning.mm in Sources */, 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 */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2438,7 +2474,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 23CCE114AF2A329EBFA536D1 /* Pods-defaults-RocketChatRN.debug.xcconfig */; + baseConfigurationReference = 265A77689414BCF7977F3E07 /* Pods-defaults-RocketChatRN.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; @@ -2458,11 +2494,15 @@ "$(inherited)", "$(PROJECT_DIR)", ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FORCE_POSIX=1", + ); HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/../../../react-native/React/**", "$(SRCROOT)/../node_modules/@nozbe/watermelondb/native/ios/WatermelonDB/SupportingFiles/**", - "$(SRCROOT)/../node_modules/react-native-mmkv-storage/ios/**", + "$(SRCROOT)/../node_modules/react-native-mmkv/MMKV/Core/**", ); INFOPLIST_FILE = RocketChatRN/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -2499,7 +2539,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8B02168DA5A28406D0331D30 /* Pods-defaults-RocketChatRN.release.xcconfig */; + baseConfigurationReference = EC3A37AC948D1CBA680F8A21 /* Pods-defaults-RocketChatRN.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; @@ -2520,11 +2560,15 @@ "$(inherited)", "$(PROJECT_DIR)", ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FORCE_POSIX=1", + ); HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/../../../react-native/React/**", "$(SRCROOT)/../node_modules/@nozbe/watermelondb/native/ios/WatermelonDB/SupportingFiles/**", - "$(SRCROOT)/../node_modules/react-native-mmkv-storage/ios/**", + "$(SRCROOT)/../node_modules/react-native-mmkv/MMKV/Core/**", ); INFOPLIST_FILE = RocketChatRN/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -2602,8 +2646,7 @@ "$(inherited)", "$(SRCROOT)/../node_modules/rn-extensions-share/ios/**", "$(SRCROOT)/../node_modules/react-native-firebase/ios/RNFirebase/**", - "$PODS_CONFIGURATION_BUILD_DIR/Firebase", - "$(SRCROOT)/../node_modules/react-native-mmkv-storage/ios/**", + $PODS_CONFIGURATION_BUILD_DIR/Firebase, ); INFOPLIST_FILE = ShareRocketChatRN/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -2679,8 +2722,7 @@ "$(inherited)", "$(SRCROOT)/../node_modules/rn-extensions-share/ios/**", "$(SRCROOT)/../node_modules/react-native-firebase/ios/RNFirebase/**", - "$PODS_CONFIGURATION_BUILD_DIR/Firebase", - "$(SRCROOT)/../node_modules/react-native-mmkv-storage/ios/**", + $PODS_CONFIGURATION_BUILD_DIR/Firebase, ); INFOPLIST_FILE = ShareRocketChatRN/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -2912,7 +2954,7 @@ }; 1EFEB59D2493B6640072EDC0 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 1B8E154274568748124AD479 /* Pods-defaults-NotificationService.debug.xcconfig */; + baseConfigurationReference = 6F50F2B44849C3F5AF549047 /* Pods-defaults-NotificationService.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(EMBEDDED_CONTENT_CONTAINS_SWIFT)"; CLANG_ANALYZER_NONNULL = YES; @@ -2931,6 +2973,14 @@ ENABLE_USER_SCRIPT_SANDBOXING = NO; EXCLUDED_ARCHS = ""; GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FORCE_POSIX=1", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)", + ); INFOPLIST_FILE = NotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -2956,7 +3006,7 @@ }; 1EFEB59E2493B6640072EDC0 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8596BEE43152ACCF33F41AB6 /* Pods-defaults-NotificationService.release.xcconfig */; + baseConfigurationReference = E508F5F0D3A212E54C74BC1D /* Pods-defaults-NotificationService.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(EMBEDDED_CONTENT_CONTAINS_SWIFT)"; CLANG_ANALYZER_NONNULL = YES; @@ -2976,6 +3026,14 @@ ENABLE_USER_SCRIPT_SANDBOXING = NO; EXCLUDED_ARCHS = ""; GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FORCE_POSIX=1", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)", + ); INFOPLIST_FILE = NotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -2999,7 +3057,7 @@ }; 7AAB3E50257E6A6E00707CF6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = EB54E3717238BA2E5971176E /* Pods-defaults-Rocket.Chat.debug.xcconfig */; + baseConfigurationReference = 338E74D5495AE57AB52BC764 /* Pods-defaults-Rocket.Chat.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; @@ -3019,11 +3077,15 @@ "$(inherited)", "$(PROJECT_DIR)", ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FORCE_POSIX=1", + ); HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/../../../react-native/React/**", "$(SRCROOT)/../node_modules/@nozbe/watermelondb/native/ios/WatermelonDB/SupportingFiles/**", - "$(SRCROOT)/../node_modules/react-native-mmkv-storage/ios/**", + "$(SRCROOT)/../node_modules/react-native-mmkv/MMKV/Core/**", ); INFOPLIST_FILE = RocketChatRN/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -3060,7 +3122,7 @@ }; 7AAB3E51257E6A6E00707CF6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7B573F5E14B1C36153C67BB7 /* Pods-defaults-Rocket.Chat.release.xcconfig */; + baseConfigurationReference = 72BE2DC82482F10567C83FD7 /* Pods-defaults-Rocket.Chat.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; @@ -3079,11 +3141,15 @@ "$(inherited)", "$(PROJECT_DIR)", ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FORCE_POSIX=1", + ); HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/../../../react-native/React/**", "$(SRCROOT)/../node_modules/@nozbe/watermelondb/native/ios/WatermelonDB/SupportingFiles/**", - "$(SRCROOT)/../node_modules/react-native-mmkv-storage/ios/**", + "$(SRCROOT)/../node_modules/react-native-mmkv/MMKV/Core/**", ); INFOPLIST_FILE = RocketChatRN/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -3181,10 +3247,7 @@ ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_CPLUSPLUSFLAGS = "$(inherited)"; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -3248,10 +3311,7 @@ MTL_ENABLE_DEBUG_INFO = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_CPLUSPLUSFLAGS = "$(inherited)"; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/ios/SSLPinning.mm b/ios/SSLPinning.mm index 8037b88b4ed..ef863c3fd22 100644 --- a/ios/SSLPinning.mm +++ b/ios/SSLPinning.mm @@ -8,13 +8,26 @@ #import #import "SSLPinning.h" -#import +#import "Shared/RocketChat/MMKVBridge.h" #import #import "SecureStorage.h" #import "SRWebSocket.h" #import "EXSessionTaskDispatcher.h" @implementation Challenge : NSObject + +// Helper method to get MMKVBridge instance ++(MMKVBridge *)getMMKVInstance { + SecureStorage *secureStorage = [[SecureStorage alloc] init]; + NSString *hexKey = [self stringToHex:@"com.MMKV.default"]; + NSString *key = [secureStorage getSecureKey:hexKey]; + + NSData *cryptKey = (key && [key length] > 0) ? [key dataUsingEncoding:NSUTF8StringEncoding] : nil; + MMKVBridge *mmkvBridge = [[MMKVBridge alloc] initWithID:@"default" cryptKey:cryptKey rootPath:nil]; + + return mmkvBridge; +} + +(NSURLCredential *)getUrlCredential:(NSURLAuthenticationChallenge *)challenge path:(NSString *)path password:(NSString *)password { NSString *authMethod = [[challenge protectionSpace] authenticationMethod]; @@ -84,23 +97,17 @@ +(void)runChallenge:(NSURLSession *)session completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { NSString *host = challenge.protectionSpace.host; - - // Read the clientSSL info from MMKV - __block NSString *clientSSL; - SecureStorage *secureStorage = [[SecureStorage alloc] init]; - - // https://github.com/ammarahm-ed/react-native-mmkv-storage/blob/master/src/loader.js#L31 - NSString *key = [secureStorage getSecureKey:[self stringToHex:@"com.MMKV.default"]]; NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; - if (key == NULL) { + // Read the clientSSL info from MMKV using MMKVBridge + MMKVBridge *mmkvBridge = [self getMMKVInstance]; + + if (!mmkvBridge) { return completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, credential); } - NSData *cryptKey = [key dataUsingEncoding:NSUTF8StringEncoding]; - MMKV *mmkv = [MMKV mmkvWithID:@"default" cryptKey:cryptKey mode:MMKVMultiProcess]; - clientSSL = [mmkv getStringForKey:host]; - + NSString *clientSSL = [mmkvBridge stringForKey:host]; + if (clientSSL) { NSData *data = [clientSSL dataUsingEncoding:NSUTF8StringEncoding]; id dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; @@ -173,20 +180,16 @@ +(void)load - (void)xxx_updateSecureStreamOptions { [self xxx_updateSecureStreamOptions]; - // Read the clientSSL info from MMKV + // Read the clientSSL info from MMKV using MMKVBridge NSMutableDictionary *SSLOptions = [NSMutableDictionary new]; - __block NSString *clientSSL; - SecureStorage *secureStorage = [[SecureStorage alloc] init]; - - // https://github.com/ammarahm-ed/react-native-mmkv-storage/blob/master/src/loader.js#L31 - NSString *key = [secureStorage getSecureKey:[Challenge stringToHex:@"com.MMKV.default"]]; - - if (key != NULL) { - NSData *cryptKey = [key dataUsingEncoding:NSUTF8StringEncoding]; - MMKV *mmkv = [MMKV mmkvWithID:@"default" cryptKey:cryptKey mode:MMKVMultiProcess]; + + MMKVBridge *mmkvBridge = [Challenge getMMKVInstance]; + + if (mmkvBridge) { NSURLRequest *_urlRequest = [self valueForKey:@"_urlRequest"]; - - clientSSL = [mmkv getStringForKey:_urlRequest.URL.host]; + NSString *host = _urlRequest.URL.host; + NSString *clientSSL = [mmkvBridge stringForKey:host]; + if (clientSSL) { NSData *data = [clientSSL dataUsingEncoding:NSUTF8StringEncoding]; id dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; diff --git a/ios/SSLPinning/SSLPinning.swift b/ios/SSLPinning/SSLPinning.swift index d8250a5dbf8..549b23d1927 100644 --- a/ios/SSLPinning/SSLPinning.swift +++ b/ios/SSLPinning/SSLPinning.swift @@ -6,7 +6,7 @@ final class SSLPinning: NSObject { } private let database = Database(name: "default") - private let mmkv = MMKV.build() + private let mmkv = MMKVBridge.build() @objc func setCertificate(_ server: String, _ path: String, _ password: String) { guard FileManager.default.fileExists(atPath: path) else { @@ -17,8 +17,8 @@ final class SSLPinning: NSObject { return } - mmkv.set(Data(referencing: certificate), forKey: Constants.certificateKey.appending(server)) - mmkv.set(password, forKey: Constants.passwordKey.appending(server)) + mmkv.setData(Data(referencing: certificate), forKey: Constants.certificateKey.appending(server)) + mmkv.setString(password, forKey: Constants.passwordKey.appending(server)) } @objc func migrate() { diff --git a/ios/SecureStorage.h b/ios/SecureStorage.h new file mode 100644 index 00000000000..e2dd7f61e5a --- /dev/null +++ b/ios/SecureStorage.h @@ -0,0 +1,27 @@ +#import +#import +#import + +@interface SecureStorage: NSObject + +- (void) setSecureKey: (nonnull NSString *)key value:(nonnull NSString *)value + options: (nonnull NSDictionary *)options; +- (nullable NSString *) getSecureKey:(nonnull NSString *)key; +- (BOOL) deleteSecureKey:(nonnull NSString *)key; + +- (nonnull NSString *)searchKeychainCopyMatching:(nonnull NSString *)identifier; + +- (nonnull NSMutableDictionary *)newSearchDictionary:(nonnull NSString *)identifier; + +- (BOOL)createKeychainValue:(nonnull NSString *)value forIdentifier:(nonnull NSString *)identifier options: (NSDictionary * __nullable)options; + +- (BOOL)updateKeychainValue:(nonnull NSString *)password forIdentifier:(nonnull NSString *)identifier options:(NSDictionary * __nullable)options; + +- (BOOL)deleteKeychainValue:(nonnull NSString *)identifier; + +- (void)handleAppUninstallation; + +// Returns the MMKV encryption key for use by JavaScript +- (nullable NSString *)getMMKVEncryptionKey; + +@end diff --git a/ios/SecureStorage.m b/ios/SecureStorage.m new file mode 100644 index 00000000000..652265ba377 --- /dev/null +++ b/ios/SecureStorage.m @@ -0,0 +1,210 @@ +#import +#import "SecureStorage.h" +#import + +@implementation SecureStorage : NSObject + +RCT_EXPORT_MODULE(SecureStorage); + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getSecureKeySync:(NSString *)key) +{ + NSString *value = [self getSecureKey:key]; + return value ? value : [NSNull null]; +} + +NSString *serviceName = nil; + +- (void) setSecureKey: (NSString *)key value:(NSString *)value + options: (NSDictionary *)options +{ + @try { + [self handleAppUninstallation]; + BOOL status = [self createKeychainValue: value forIdentifier: key options: options]; + if (!status) { + [self updateKeychainValue: value forIdentifier: key options: options]; + } + } + @catch (NSException *exception) { + // Handle exception + } +} + +- (NSString *) getSecureKey:(NSString *)key +{ + @try { + [self handleAppUninstallation]; + NSString *value = [self searchKeychainCopyMatching:key]; + if (value == nil) { + return NULL; + } else { + return value; + } + } + @catch (NSException *exception) { + return NULL; + } +} + +- (BOOL) deleteSecureKey:(NSString *)key +{ + @try { + return [self deleteKeychainValue:key]; + } + @catch (NSException *exception) { + return NO; + } +} + +- (NSMutableDictionary *)newSearchDictionary:(NSString *)identifier { + NSMutableDictionary *searchDictionary = [[NSMutableDictionary alloc] init]; + + // this value is shared by main app and extensions, so, is the best to be used here + serviceName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"AppGroup"]; + + if(serviceName == nil){ + serviceName = [[NSBundle mainBundle] bundleIdentifier]; + } + + [searchDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; + + NSData *encodedIdentifier = [identifier dataUsingEncoding:NSUTF8StringEncoding]; + [searchDictionary setObject:encodedIdentifier forKey:(id)kSecAttrGeneric]; + [searchDictionary setObject:encodedIdentifier forKey:(id)kSecAttrAccount]; + [searchDictionary setObject:serviceName forKey:(id)kSecAttrService]; + + return searchDictionary; +} + +- (NSString *)searchKeychainCopyMatching:(NSString *)identifier { + NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier]; + + // Add search attributes + [searchDictionary setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit]; + + // Add search return types + [searchDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData]; + + NSDictionary *found = nil; + CFTypeRef result = NULL; + OSStatus status = SecItemCopyMatching((CFDictionaryRef)searchDictionary, + (CFTypeRef *)&result); + + NSString *value = nil; + found = (__bridge NSDictionary*)(result); + if (found) { + value = [[NSString alloc] initWithData:found encoding:NSUTF8StringEncoding]; + } + return value; +} + +- (BOOL)createKeychainValue:(NSString *)value forIdentifier:(NSString *)identifier options: (NSDictionary * __nullable)options { + CFStringRef accessibleVal = _accessibleValue(options); + NSMutableDictionary *dictionary = [self newSearchDictionary:identifier]; + + NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding]; + [dictionary setObject:valueData forKey:(id)kSecValueData]; + dictionary[(__bridge NSString *)kSecAttrAccessible] = (__bridge id)accessibleVal; + + OSStatus status = SecItemAdd((CFDictionaryRef)dictionary, NULL); + + if (status == errSecSuccess) { + return YES; + } + return NO; +} + +- (BOOL)updateKeychainValue:(NSString *)password forIdentifier:(NSString *)identifier options:(NSDictionary * __nullable)options { + CFStringRef accessibleVal = _accessibleValue(options); + NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier]; + NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init]; + NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; + [updateDictionary setObject:passwordData forKey:(id)kSecValueData]; + updateDictionary[(__bridge NSString *)kSecAttrAccessible] = (__bridge id)accessibleVal; + OSStatus status = SecItemUpdate((CFDictionaryRef)searchDictionary, + (CFDictionaryRef)updateDictionary); + + if (status == errSecSuccess) { + return YES; + } + return NO; +} + +- (BOOL)deleteKeychainValue:(NSString *)identifier { + NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier]; + OSStatus status = SecItemDelete((CFDictionaryRef)searchDictionary); + if (status == errSecSuccess) { + return YES; + } + return NO; +} + +- (void)handleAppUninstallation +{ + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +CFStringRef _accessibleValue(NSDictionary *options) +{ + if (options && options[@"accessible"] != nil) { + NSDictionary *keyMap = @{ + @"AccessibleWhenUnlocked": (__bridge NSString *)kSecAttrAccessibleWhenUnlocked, + @"AccessibleAfterFirstUnlock": (__bridge NSString *)kSecAttrAccessibleAfterFirstUnlock, + @"AccessibleAlways": (__bridge NSString *)kSecAttrAccessibleAlways, + @"AccessibleWhenPasscodeSetThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, + @"AccessibleWhenUnlockedThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + @"AccessibleAfterFirstUnlockThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + @"AccessibleAlwaysThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleAlwaysThisDeviceOnly + }; + + NSString *result = keyMap[options[@"accessible"]]; + if (result) { + return (__bridge CFStringRef)result; + } + } + return kSecAttrAccessibleAfterFirstUnlock; +} + +// Helper function to convert string to hex (same as react-native-mmkv-storage) +NSString* toHex(NSString *input) { + NSData *data = [input dataUsingEncoding:NSUTF8StringEncoding]; + NSMutableString *hexString = [NSMutableString stringWithCapacity:data.length * 2]; + const unsigned char *bytes = data.bytes; + for (NSUInteger i = 0; i < data.length; i++) { + [hexString appendFormat:@"%02x", bytes[i]]; + } + return hexString; +} + +/** + * Synchronous method to get the MMKV encryption key. + * - For existing users: returns the key stored in Keychain + * - For fresh installs: generates a new key, stores it, and returns it + * Used by JavaScript to initialize MMKV with the same encryption key as native code. + */ +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getMMKVEncryptionKey) +{ + @try { + NSString *alias = toHex(@"com.MMKV.default"); + NSString *key = [self getSecureKey:alias]; + + if (key == nil || key.length == 0) { + // Fresh install - generate a new key + key = [[NSUUID UUID] UUIDString]; + [self setSecureKey:alias value:key options:nil]; + NSLog(@"[SecureStorage] Generated new MMKV encryption key"); + } + + return key; + } + @catch (NSException *exception) { + NSLog(@"[SecureStorage] Error getting MMKV encryption key: %@", exception.reason); + return [NSNull null]; + } +} + +@end diff --git a/ios/Shared/RocketChat/ClientSSL.swift b/ios/Shared/RocketChat/ClientSSL.swift index 52d9f842d71..5b6aac67b96 100644 --- a/ios/Shared/RocketChat/ClientSSL.swift +++ b/ios/Shared/RocketChat/ClientSSL.swift @@ -5,7 +5,7 @@ struct ClientSSL: Codable { let password: String } -extension MMKV { +extension MMKVBridge { func clientSSL(for url: URL) -> ClientSSL? { let server = url.absoluteString.removeTrailingSlash() let host = url.host ?? "" diff --git a/ios/Shared/RocketChat/MMKV.swift b/ios/Shared/RocketChat/MMKV.swift index 1359270592d..f825b4d2cfc 100644 --- a/ios/Shared/RocketChat/MMKV.swift +++ b/ios/Shared/RocketChat/MMKV.swift @@ -1,24 +1,27 @@ import Foundation -extension MMKV { - static func build() -> MMKV { +extension MMKVBridge { + static func build() -> MMKVBridge { let password = SecureStorage().getSecureKey("com.MMKV.default".toHex()) let groupDir = FileManager.default.groupDir() - MMKV.initialize(rootDir: nil, groupDir: groupDir, logLevel: MMKVLogLevel.info) - - guard let mmkv = MMKV(mmapID: "default", cryptKey: password?.data(using: .utf8), mode: MMKVMode.multiProcess) else { - fatalError("Could not initialize MMKV instance.") + var mmkvPath: String? + if !groupDir.isEmpty { + mmkvPath = "\(groupDir)/mmkv" + // Ensure the directory exists + if let path = mmkvPath { + try? FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) + } } - return mmkv + let cryptKey = password?.data(using: .utf8) + return MMKVBridge(id: "default", cryptKey: cryptKey, rootPath: mmkvPath) } func userToken(for userId: String) -> String? { guard let userToken = string(forKey: "reactnativemeteor_usertoken-\(userId)") else { return nil } - return userToken } @@ -26,7 +29,6 @@ extension MMKV { guard let userId = string(forKey: "reactnativemeteor_usertoken-\(server)") else { return nil } - return userId } @@ -34,7 +36,6 @@ extension MMKV { guard let privateKey = string(forKey: "\(server)-RC_E2E_PRIVATE_KEY") else { return nil } - return privateKey } } diff --git a/ios/Shared/RocketChat/MMKVBridge.h b/ios/Shared/RocketChat/MMKVBridge.h new file mode 100644 index 00000000000..1c4cf510cd4 --- /dev/null +++ b/ios/Shared/RocketChat/MMKVBridge.h @@ -0,0 +1,29 @@ +// +// MMKVBridge.h +// RocketChatRN +// +// Bridge to access react-native-mmkv from Swift +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MMKVBridge : NSObject + +- (instancetype)initWithID:(NSString *)mmapID + cryptKey:(nullable NSData *)cryptKey + rootPath:(nullable NSString *)rootPath; + +- (nullable NSString *)stringForKey:(NSString *)key; +- (BOOL)setString:(NSString *)value forKey:(NSString *)key; +- (nullable NSData *)dataForKey:(NSString *)key; +- (BOOL)setData:(NSData *)value forKey:(NSString *)key; +- (void)removeValueForKey:(NSString *)key; +- (NSArray *)allKeys; +- (NSUInteger)count; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/ios/Shared/RocketChat/MMKVBridge.mm b/ios/Shared/RocketChat/MMKVBridge.mm new file mode 100644 index 00000000000..eb9e8e64872 --- /dev/null +++ b/ios/Shared/RocketChat/MMKVBridge.mm @@ -0,0 +1,112 @@ +// +// MMKVBridge.mm +// RocketChatRN +// +// Bridge to access react-native-mmkv from Swift +// Requires FORCE_POSIX=1 preprocessor definition +// + +#import "MMKVBridge.h" +#import "MMKV.h" +#import + +@interface MMKVBridge() +@property (nonatomic, assign) MMKV *mmkvInstance; +@end + +@implementation MMKVBridge + +- (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); + } + } + return self; +} + +- (nullable NSString *)stringForKey:(NSString *)key { + if (!_mmkvInstance) return nil; + + std::string keyStr = [key UTF8String]; + std::string valueStr; + bool hasValue = _mmkvInstance->getString(keyStr, valueStr); + + if (hasValue && !valueStr.empty()) { + return [NSString stringWithUTF8String:valueStr.c_str()]; + } + + return nil; +} + +- (BOOL)setString:(NSString *)value forKey:(NSString *)key { + if (!_mmkvInstance) return NO; + + std::string keyStr = [key UTF8String]; + std::string valueStr = [value UTF8String]; + + return _mmkvInstance->set(valueStr, keyStr); +} + +- (nullable NSData *)dataForKey:(NSString *)key { + if (!_mmkvInstance) return nil; + + std::string keyStr = [key UTF8String]; + auto buffer = _mmkvInstance->getBytes(keyStr); + + if (buffer.length() > 0) { + return [NSData dataWithBytes:buffer.getPtr() length:buffer.length()]; + } + + return nil; +} + +- (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); +} + +- (void)removeValueForKey:(NSString *)key { + if (!_mmkvInstance) return; + + std::string keyStr = [key UTF8String]; + _mmkvInstance->removeValueForKey(keyStr); +} + +- (NSArray *)allKeys { + if (!_mmkvInstance) return @[]; + + auto cppKeys = _mmkvInstance->allKeys(); + NSMutableArray *keys = [NSMutableArray arrayWithCapacity:cppKeys.size()]; + + for (const auto& key : cppKeys) { + [keys addObject:[NSString stringWithUTF8String:key.c_str()]]; + } + + return keys; +} + +- (NSUInteger)count { + if (!_mmkvInstance) return 0; + return _mmkvInstance->count(); +} + +@end diff --git a/ios/Shared/RocketChat/Storage.swift b/ios/Shared/RocketChat/Storage.swift index 6793ab5c9df..63ce28eeef3 100644 --- a/ios/Shared/RocketChat/Storage.swift +++ b/ios/Shared/RocketChat/Storage.swift @@ -1,5 +1,4 @@ import Foundation -import Security struct Credentials { let userId: String @@ -7,41 +6,16 @@ struct Credentials { } final class Storage { - private let mmkv = MMKV.build() - - private var appGroupIdentifier: String? { - return Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String - } + private let mmkv = MMKVBridge.build() func getCredentials(server: String) -> Credentials? { - guard let appGroup = appGroupIdentifier else { - return nil - } - - let query: [String: Any] = [ - kSecClass as String: kSecClassInternetPassword, - kSecAttrServer as String: server, - kSecAttrAccessGroup as String: appGroup, - kSecMatchLimit as String: kSecMatchLimitOne, - kSecReturnAttributes as String: true, - kSecReturnData as String: true - ] - - var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) - - guard status == errSecSuccess else { - return nil - } - - guard let existingItem = item as? [String: Any], - let account = existingItem[kSecAttrAccount as String] as? String, - let passwordData = existingItem[kSecValueData as String] as? Data, - let password = String(data: passwordData, encoding: .utf8) else { + // 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) } func getPrivateKey(server: String) -> String? { diff --git a/ios/Watch/WatchConnection.swift b/ios/Watch/WatchConnection.swift index 311d8c4c898..4e7069fff6e 100644 --- a/ios/Watch/WatchConnection.swift +++ b/ios/Watch/WatchConnection.swift @@ -4,7 +4,7 @@ import WatchConnectivity @objc final class WatchConnection: NSObject { private let database = Database(name: "default") - private let mmkv = MMKV.build() + private let mmkv = MMKVBridge.build() private let session: WCSession @objc init(session: WCSession) { diff --git a/metro.config.js b/metro.config.js index 5cd379b3805..234dd67481b 100644 --- a/metro.config.js +++ b/metro.config.js @@ -12,7 +12,9 @@ const config = { unstable_allowRequireContext: true }, resolver: { - sourceExts: process.env.RUNNING_E2E_TESTS ? ['mock.ts', ...sourceExts] : sourceExts + // When running E2E tests, prioritize .mock.ts files for app code + // Note: react-native-mmkv's internal mock file is disabled via patch-package + sourceExts: process.env.RUNNING_E2E_TESTS === 'true' ? ['mock.ts', ...sourceExts] : sourceExts } }; diff --git a/package.json b/package.json index 18036f1628e..0b25573a0b4 100644 --- a/package.json +++ b/package.json @@ -101,12 +101,11 @@ "react-native-image-crop-picker": "RocketChat/react-native-image-crop-picker#5346870b0be10d300dc53924309dc6adc9946d50", "react-native-katex": "git+https://github.com/RocketChat/react-native-katex.git", "react-native-keyboard-controller": "^1.17.1", - "react-native-keychain": "^8.2.0", "react-native-linear-gradient": "2.6.2", "react-native-localize": "2.1.1", "react-native-math-view": "3.9.5", "react-native-mime-types": "2.3.0", - "react-native-mmkv-storage": "^12.0.0", + "react-native-mmkv": "3.3.3", "react-native-modal": "13.0.1", "react-native-notifications": "5.1.0", "react-native-notifier": "1.6.1", diff --git a/patches/react-native-mmkv+3.3.3.patch b/patches/react-native-mmkv+3.3.3.patch new file mode 100644 index 00000000000..de7f8be8ce6 --- /dev/null +++ b/patches/react-native-mmkv+3.3.3.patch @@ -0,0 +1,39 @@ +diff --git a/node_modules/react-native-mmkv/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties b/node_modules/react-native-mmkv/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties +new file mode 100644 +index 0000000..1211b1e +--- /dev/null ++++ b/node_modules/react-native-mmkv/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties +@@ -0,0 +1,6 @@ ++aarFormatVersion=1.0 ++aarMetadataVersion=1.0 ++minCompileSdk=1 ++minCompileSdkExtension=0 ++minAndroidGradlePluginVersion=1.0.0 ++coreLibraryDesugaringEnabled=false +diff --git a/node_modules/react-native-mmkv/src/MMKV.ts b/node_modules/react-native-mmkv/src/MMKV.ts +index 5ee1f01..ea3f444 100644 +--- a/node_modules/react-native-mmkv/src/MMKV.ts ++++ b/node_modules/react-native-mmkv/src/MMKV.ts +@@ -1,5 +1,5 @@ + import { createMMKV } from './createMMKV'; +-import { createMockMMKV } from './createMMKV.mock'; ++// import { createMockMMKV } from './createMMKV.mock'; // Disabled via patch-package for E2E tests + import { isTest } from './PlatformChecker'; + import type { + Configuration, +@@ -25,9 +25,8 @@ export class MMKV implements MMKVInterface { + */ + constructor(configuration: Configuration = { id: 'mmkv.default' }) { + this.id = configuration.id; +- this.nativeInstance = isTest() +- ? createMockMMKV() +- : createMMKV(configuration); ++ // Always use real MMKV (mock disabled via patch-package for E2E tests) ++ this.nativeInstance = createMMKV(configuration); + this.functionCache = {}; + + addMemoryWarningListener(this); +diff --git a/node_modules/react-native-mmkv/src/createMMKV.mock.ts b/node_modules/react-native-mmkv/src/createMMKV.mock.ts.disabled +similarity index 100% +rename from node_modules/react-native-mmkv/src/createMMKV.mock.ts +rename to node_modules/react-native-mmkv/src/createMMKV.mock.ts.disabled diff --git a/patches/react-native-mmkv-storage+12.0.0.patch b/patches/react-native-mmkv-storage+12.0.0.patch deleted file mode 100644 index 98703b5791e..00000000000 --- a/patches/react-native-mmkv-storage+12.0.0.patch +++ /dev/null @@ -1,38 +0,0 @@ -diff --git a/node_modules/react-native-mmkv-storage/ios/SecureStorage.m b/node_modules/react-native-mmkv-storage/ios/SecureStorage.m -index 34703df..a754918 100644 ---- a/node_modules/react-native-mmkv-storage/ios/SecureStorage.m -+++ b/node_modules/react-native-mmkv-storage/ios/SecureStorage.m -@@ -40,14 +40,14 @@ NSString *serviceName = nil; - @try { - [self handleAppUninstallation]; - NSString *value = [self searchKeychainCopyMatching:key]; -- dispatch_sync(dispatch_get_main_queue(), ^{ -- int readAttempts = 0; -- // See: https://github.com/ammarahm-ed/react-native-mmkv-storage/issues/195 -- while (![[UIApplication sharedApplication] isProtectedDataAvailable] && readAttempts++ < 100) { -- // sleep 25ms before another attempt -- usleep(25000); -- } -- }); -+ // dispatch_sync(dispatch_get_main_queue(), ^{ -+ // int readAttempts = 0; -+ // // See: https://github.com/ammarahm-ed/react-native-mmkv-storage/issues/195 -+ // while (![[UIApplication sharedApplication] isProtectedDataAvailable] && readAttempts++ < 100) { -+ // // sleep 25ms before another attempt -+ // usleep(25000); -+ // } -+ // }); - if (value == nil) { - NSString* errorMessage = @"key does not present"; - -@@ -100,6 +100,10 @@ NSString *serviceName = nil; - - - (NSMutableDictionary *)newSearchDictionary:(NSString *)identifier { - NSMutableDictionary *searchDictionary = [[NSMutableDictionary alloc] init]; -+ -+ // this value is shared by main app and extensions, so, is the best to be used here -+ serviceName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"AppGroup"]; -+ - if(serviceName == nil){ - serviceName = [[NSBundle mainBundle] bundleIdentifier]; - } diff --git a/patches/react-native-webview+13.15.0.patch b/patches/react-native-webview+13.15.0.patch index a026aade098..bbcde36abc5 100644 --- a/patches/react-native-webview+13.15.0.patch +++ b/patches/react-native-webview+13.15.0.patch @@ -1,3 +1,37 @@ +diff --git a/node_modules/react-native-webview/android/.project b/node_modules/react-native-webview/android/.project +new file mode 100644 +index 0000000..122755b +--- /dev/null ++++ b/node_modules/react-native-webview/android/.project +@@ -0,0 +1,28 @@ ++ ++ ++ react-native-webview ++ Project react-native-webview created by Buildship. ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectbuilder ++ ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectnature ++ ++ ++ ++ 1760118463063 ++ ++ 30 ++ ++ org.eclipse.core.resources.regexFilterMatcher ++ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ ++ ++ ++ ++ diff --git a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java index 5932fc1..1ce86e8 100644 --- a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java @@ -91,14 +125,14 @@ index 251939e..763cc50 100644 public void onPageFinished(WebView webView, String url) { super.onPageFinished(webView, url); diff --git a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModuleImpl.java b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModuleImpl.java -index 951749c..05792b8 100644 +index 951749c..12a0bd2 100644 --- a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModuleImpl.java +++ b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModuleImpl.java @@ -551,4 +551,8 @@ public class RNCWebViewModuleImpl implements ActivityEventListener { } return (PermissionAwareActivity) activity; } -+ ++ + public static void setCertificateAlias(String alias) { + RNCWebViewClient.setCertificateAlias(alias); + } @@ -164,130 +198,3 @@ index 526cc16..8f3318d 100644 @NonNull @Override public String getName() { -diff --git a/node_modules/react-native-webview/apple/RNCWebViewImpl.m b/node_modules/react-native-webview/apple/RNCWebViewImpl.m -index 7f5c24d..8e11e4c 100644 ---- a/node_modules/react-native-webview/apple/RNCWebViewImpl.m -+++ b/node_modules/react-native-webview/apple/RNCWebViewImpl.m -@@ -17,6 +17,9 @@ - - #import "objc/runtime.h" - -+#import "SecureStorage.h" -+#import -+ - static NSTimer *keyboardTimer; - static NSString *const HistoryShimName = @"ReactNativeHistoryShim"; - static NSString *const MessageHandlerName = @"ReactNativeWebView"; -@@ -1149,6 +1152,68 @@ + (void)setCustomCertificatesForHost:(nullable NSDictionary*)certificates { - customCertificatesForHost = certificates; - } - -+-(NSURLCredential *)getUrlCredential:(NSURLAuthenticationChallenge *)challenge path:(NSString *)path password:(NSString *)password -+{ -+ NSString *authMethod = [[challenge protectionSpace] authenticationMethod]; -+ SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; -+ -+ if ([authMethod isEqualToString:NSURLAuthenticationMethodServerTrust] || path == nil || password == nil) { -+ return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; -+ } else if (path && password) { -+ NSMutableArray *policies = [NSMutableArray array]; -+ [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host)]; -+ SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies); -+ -+ SecTrustResultType result; -+ SecTrustEvaluate(serverTrust, &result); -+ -+ if (![[NSFileManager defaultManager] fileExistsAtPath:path]) -+ { -+ return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; -+ } -+ -+ NSData *p12data = [NSData dataWithContentsOfFile:path]; -+ NSDictionary* options = @{ (id)kSecImportExportPassphrase:password }; -+ CFArrayRef rawItems = NULL; -+ OSStatus status = SecPKCS12Import((__bridge CFDataRef)p12data, -+ (__bridge CFDictionaryRef)options, -+ &rawItems); -+ -+ if (status != noErr) { -+ return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; -+ } -+ -+ NSArray* items = (NSArray*)CFBridgingRelease(rawItems); -+ NSDictionary* firstItem = nil; -+ if ((status == errSecSuccess) && ([items count]>0)) { -+ firstItem = items[0]; -+ } -+ -+ SecIdentityRef identity = (SecIdentityRef)CFBridgingRetain(firstItem[(id)kSecImportItemIdentity]); -+ SecCertificateRef certificate = NULL; -+ if (identity) { -+ SecIdentityCopyCertificate(identity, &certificate); -+ if (certificate) { CFRelease(certificate); } -+ } -+ -+ NSMutableArray *certificates = [[NSMutableArray alloc] init]; -+ [certificates addObject:CFBridgingRelease(certificate)]; -+ -+ return [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceNone]; -+ } -+ -+ return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; -+} -+ -+- (NSString *)stringToHex:(NSString *)string -+{ -+ char *utf8 = (char *)[string UTF8String]; -+ NSMutableString *hex = [NSMutableString string]; -+ while (*utf8) [hex appendFormat:@"%02X", *utf8++ & 0x00FF]; -+ -+ return [[NSString stringWithFormat:@"%@", hex] lowercaseString]; -+} -+ - - (void) webView:(WKWebView *)webView - didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge - completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable))completionHandler -@@ -1158,7 +1223,31 @@ - (void) webView:(WKWebView *)webView - host = webView.URL.host; - } - if ([[challenge protectionSpace] authenticationMethod] == NSURLAuthenticationMethodClientCertificate) { -- completionHandler(NSURLSessionAuthChallengeUseCredential, clientAuthenticationCredential); -+ NSString *host = challenge.protectionSpace.host; -+ -+ // Read the clientSSL info from MMKV -+ __block NSDictionary *clientSSL; -+ SecureStorage *secureStorage = [[SecureStorage alloc] init]; -+ -+ // https://github.com/ammarahm-ed/react-native-mmkv-storage/blob/master/src/loader.js#L31 -+ NSString *key = [secureStorage getSecureKey:[self stringToHex:@"com.MMKV.default"]]; -+ NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; -+ -+ if (key == NULL) { -+ return completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, credential); -+ } -+ -+ NSData *cryptKey = [key dataUsingEncoding:NSUTF8StringEncoding]; -+ MMKV *mmkv = [MMKV mmkvWithID:@"default" cryptKey:cryptKey mode:MMKVMultiProcess]; -+ clientSSL = [mmkv getObjectOfClass:[NSDictionary class] forKey:host]; -+ -+ if (clientSSL != (id)[NSNull null]) { -+ NSString *path = [clientSSL objectForKey:@"path"]; -+ NSString *password = [clientSSL objectForKey:@"password"]; -+ credential = [self getUrlCredential:challenge path:path password:password]; -+ } -+ -+ completionHandler(NSURLSessionAuthChallengeUseCredential, credential); - return; - } - if ([[challenge protectionSpace] serverTrust] != nil && customCertificatesForHost != nil && host != nil) { -diff --git a/node_modules/react-native-webview/react-native-webview.podspec b/node_modules/react-native-webview/react-native-webview.podspec -index 24e7a13..c7304b0 100644 ---- a/node_modules/react-native-webview/react-native-webview.podspec -+++ b/node_modules/react-native-webview/react-native-webview.podspec -@@ -43,4 +43,6 @@ Pod::Spec.new do |s| - s.dependency "React-Core" - end - end -+ -+ s.dependency 'MMKV', '~> 1.3.9' - end diff --git a/yarn.lock b/yarn.lock index 42818383dc4..616e8c9ab37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12522,11 +12522,6 @@ react-native-keyboard-controller@^1.17.1: dependencies: react-native-is-edge-to-edge "^1.1.6" -react-native-keychain@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-8.2.0.tgz#aea82df37aacbb04f8b567a8e0e6d7292025610a" - integrity sha512-SkRtd9McIl1Ss2XSWNLorG+KMEbgeVqX+gV+t3u1EAAqT8q2/OpRmRbxpneT2vnb/dMhiU7g6K/pf3nxLUXRvA== - react-native-linear-gradient@2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.6.2.tgz#56598a76832724b2afa7889747635b5c80948f38" @@ -12554,10 +12549,10 @@ react-native-mime-types@2.3.0: dependencies: mime-db "~1.37.0" -react-native-mmkv-storage@^12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/react-native-mmkv-storage/-/react-native-mmkv-storage-12.0.0.tgz#9d15cd6dff8abbb7f52992446eef3954dc8f9c3a" - integrity sha512-sssZInILQBquytDDfjosjEdvevwMPk3fqQQjdjfnH362IcsBsApvtT8yJS406Mz9aJ6VhqVsZw/awltsUuPSYg== +react-native-mmkv@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-3.3.3.tgz#1f0326f6314e23725af8145964343224d074e6cd" + integrity sha512-GMsfOmNzx0p5+CtrCFRVtpOOMYNJXuksBVARSQrCFaZwjUyHJdQzcN900GGaFFNTxw2fs8s5Xje//RDKj9+PZA== react-native-modal@13.0.1: version "13.0.1"