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 56c88d46277..10da15e8945 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt @@ -19,6 +19,7 @@ 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.storage.DatabaseKeyStoreTurboPackage; import chat.rocket.reactnative.notification.VideoConfTurboPackage import chat.rocket.reactnative.notification.PushNotificationTurboPackage import chat.rocket.reactnative.VoipTurboPackage @@ -43,6 +44,7 @@ open class MainApplication : Application(), ReactApplication { object : DefaultReactNativeHost(this) { override fun getPackages(): List = PackageList(this).packages.apply { + add(DatabaseKeyStoreTurboPackage()) add(SSLPinningTurboPackage()) add(WatermelonDBJSIPackage()) add(VideoConfTurboPackage()) diff --git a/android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.java b/android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.java new file mode 100644 index 00000000000..72468d1eef8 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreModule.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 java.nio.charset.StandardCharsets; +import java.security.KeyStore; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +/** + * Stores and retrieves per-database SQLCipher keys using Android Keystore AES-GCM. + * + * Each storage key maps to an AndroidKeyStore AES entry and a SharedPreferences entry + * that holds the IV-prefixed ciphertext. The AES key is never exported; only the + * encrypted value is written to disk. + * + * getItemInternal / setItemInternal / removeItemInternal are intentionally static so + * Encryption.java can call them without a ReactApplicationContext (same app process, + * plain Context is enough). + * + * SharedPreferences file: "RCDatabaseKeyStore" — separate from MMKV and SecureStoragePrefs + * so these keys can be found unambiguously from native-only callers. + */ +public class DatabaseKeyStoreModule extends NativeDatabaseKeyStoreSpec { + + private static final String TAG = "RocketChat.DBKeyStore"; + private static final String KEYSTORE_PROVIDER = "AndroidKeyStore"; + private static final String PREFS_NAME = "RCDatabaseKeyStore"; + private static final String TRANSFORMATION = "AES/GCM/NoPadding"; + private static final int GCM_IV_LENGTH = 12; + private static final int GCM_TAG_LENGTH = 128; + + public DatabaseKeyStoreModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + // ------------------------------------------------------------------------- + // JS-facing TurboModule methods + // ------------------------------------------------------------------------- + + @Override + public void getItem(String key, Promise promise) { + try { + promise.resolve(getItemInternal(getReactApplicationContext(), key)); + } catch (Exception e) { + Log.e(TAG, "getItem failed for key: " + key, e); + promise.reject("KEYSTORE_READ_ERROR", e); + } + } + + @Override + public void setItem(String key, String value, Promise promise) { + try { + setItemInternal(getReactApplicationContext(), key, value); + promise.resolve(null); + } catch (Exception e) { + Log.e(TAG, "setItem failed for key: " + key, e); + promise.reject("KEYSTORE_WRITE_ERROR", e); + } + } + + @Override + public void removeItem(String key, Promise promise) { + try { + removeItemInternal(getReactApplicationContext(), key); + promise.resolve(null); + } catch (Exception e) { + Log.e(TAG, "removeItem failed for key: " + key, e); + promise.resolve(null); + } + } + + // ------------------------------------------------------------------------- + // Native-side helpers — callable from Encryption.java (same process, plain Context) + // ------------------------------------------------------------------------- + + /** + * Returns the stored plaintext value, or null if the key is genuinely absent + * (SharedPreferences blob not present). Throws on any other failure — corrupt + * data, missing Keystore alias for an existing blob, decryption error, etc. — + * so callers can distinguish a true not-found from a read/decrypt failure. + */ + public static String getItemInternal(Context context, String key) throws Exception { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + String encoded = prefs.getString(key, null); + if (encoded == null) { + // Genuine not-found: no blob stored for this key. + return null; + } + + KeyStore ks = KeyStore.getInstance(KEYSTORE_PROVIDER); + ks.load(null); + if (!ks.containsAlias(key)) { + // Blob exists but Keystore alias is gone — this is corruption, not a miss. + throw new IllegalStateException("keystore alias missing for existing key blob: " + key); + } + + SecretKey secretKey = (SecretKey) ks.getKey(key, null); + byte[] combined = Base64.decode(encoded, Base64.DEFAULT); + + byte[] iv = new byte[GCM_IV_LENGTH]; + byte[] encrypted = new byte[combined.length - GCM_IV_LENGTH]; + System.arraycopy(combined, 0, iv, 0, GCM_IV_LENGTH); + System.arraycopy(combined, GCM_IV_LENGTH, encrypted, 0, encrypted.length); + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv)); + byte[] plain = cipher.doFinal(encrypted); + return new String(plain, StandardCharsets.UTF_8); + } + + /** Stores value under key. Write-once: an existing key is never overwritten. */ + public static void setItemInternal(Context context, String key, String value) throws Exception { + // Write-once: if a key is already stored, confirm it matches and return. + // A differing value means another caller already minted a key — overwriting + // it would strand a database encrypted with the first key. + String existing = getItemInternal(context, key); + if (existing != null) { + if (!existing.equals(value)) { + throw new IllegalStateException("refusing to overwrite existing database key: " + key); + } + return; + } + + KeyStore ks = KeyStore.getInstance(KEYSTORE_PROVIDER); + ks.load(null); + + // Create the AES-GCM wrapping key once; never export it + if (!ks.containsAlias(key)) { + KeyGenerator kg = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER); + kg.init( + new KeyGenParameterSpec.Builder(key, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setUserAuthenticationRequired(false) + .setRandomizedEncryptionRequired(true) + .build() + ); + kg.generateKey(); + } + + SecretKey secretKey = (SecretKey) ks.getKey(key, 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)); + + // Prepend IV to ciphertext; single Base64 blob stored in SharedPreferences + 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); + + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + boolean persisted = prefs.edit().putString(key, Base64.encodeToString(combined, Base64.DEFAULT)).commit(); + if (!persisted) { + throw new IllegalStateException("failed to persist encrypted database key blob: " + key); + } + } + + /** Removes the SharedPreferences entry. Does not delete the AndroidKeyStore key. */ + public static void removeItemInternal(Context context, String key) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + prefs.edit().remove(key).apply(); + } +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreTurboPackage.java b/android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreTurboPackage.java new file mode 100644 index 00000000000..0749f31e81b --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/storage/DatabaseKeyStoreTurboPackage.java @@ -0,0 +1,43 @@ +package chat.rocket.reactnative.storage; + +import androidx.annotation.Nullable; + +import com.facebook.react.TurboReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; + +import java.util.HashMap; +import java.util.Map; + +public class DatabaseKeyStoreTurboPackage extends TurboReactPackage { + + @Nullable + @Override + public NativeModule getModule(String name, ReactApplicationContext reactContext) { + if (name.equals(NativeDatabaseKeyStoreSpec.NAME)) { + return new DatabaseKeyStoreModule(reactContext); + } + return null; + } + + @Override + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return () -> { + final Map moduleInfos = new HashMap<>(); + moduleInfos.put( + NativeDatabaseKeyStoreSpec.NAME, + new ReactModuleInfo( + NativeDatabaseKeyStoreSpec.NAME, + NativeDatabaseKeyStoreSpec.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false, // isCxxModule + true // isTurboModule + )); + return moduleInfos; + }; + } +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/storage/NativeDatabaseKeyStoreSpec.java b/android/app/src/main/java/chat/rocket/reactnative/storage/NativeDatabaseKeyStoreSpec.java new file mode 100644 index 00000000000..43f64b94493 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/storage/NativeDatabaseKeyStoreSpec.java @@ -0,0 +1,30 @@ +package chat.rocket.reactnative.storage; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.Promise; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; + +public abstract class NativeDatabaseKeyStoreSpec extends ReactContextBaseJavaModule implements TurboModule { + + public static final String NAME = "DatabaseKeyStoreModule"; + + public NativeDatabaseKeyStoreSpec(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return NAME; + } + + @ReactMethod + public abstract void getItem(String key, Promise promise); + + @ReactMethod + public abstract void setItem(String key, String value, Promise promise); + + @ReactMethod + public abstract void removeItem(String key, Promise promise); +} diff --git a/app/lib/encryption/__tests__/keyStore.test.ts b/app/lib/encryption/__tests__/keyStore.test.ts new file mode 100644 index 00000000000..4a0af548060 --- /dev/null +++ b/app/lib/encryption/__tests__/keyStore.test.ts @@ -0,0 +1,86 @@ +import { getOrCreateDatabaseKey } from '../keyStore'; + +// --------------------------------------------------------------------------- +// Mocks +// --------------------------------------------------------------------------- + +const mockGetItem = jest.fn, [string]>(); +const mockSetItem = jest.fn, [string, string]>(); + +jest.mock('../../native/NativeDatabaseKeyStore', () => ({ + __esModule: true, + default: { + getItem: (...args: [string]) => mockGetItem(...args), + setItem: (...args: [string, string]) => mockSetItem(...args), + removeItem: jest.fn() + } +})); + +jest.mock('@rocket.chat/mobile-crypto', () => ({ + randomKey: jest.fn(async () => 'a'.repeat(64)) +})); + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('getOrCreateDatabaseKey', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockSetItem.mockResolvedValue(undefined); + }); + + it('returns the existing key unchanged when one is stored', async () => { + const existingKey = 'b'.repeat(64); + mockGetItem.mockResolvedValueOnce(existingKey); + + const result = await getOrCreateDatabaseKey('test.db'); + + expect(result).toBe(existingKey); + expect(result).toMatch(/^[0-9a-f]{64}$/i); + expect(mockSetItem).not.toHaveBeenCalled(); + }); + + it('mints a 64-char hex key, stores it, and returns it when none exists', async () => { + mockGetItem.mockResolvedValueOnce(null); + + const result = await getOrCreateDatabaseKey('new.db'); + + expect(typeof result).toBe('string'); + expect(result).toHaveLength(64); + expect(result).toMatch(/^[0-9a-f]{64}$/i); + expect(mockSetItem).toHaveBeenCalledTimes(1); + expect(mockSetItem).toHaveBeenCalledWith('db_key_v1:new.db', result); + }); + + it('propagates a read error and does NOT call setItem (fail-closed)', async () => { + const readError = Object.assign(new Error('Keychain read failed'), { code: 'KEYCHAIN_READ_ERROR' }); + mockGetItem.mockRejectedValueOnce(readError); + + await expect(getOrCreateDatabaseKey('encrypted.db')).rejects.toThrow('Keychain read failed'); + expect(mockSetItem).not.toHaveBeenCalled(); + }); + + it('treats resolved null as not-found and mints a new key', async () => { + mockGetItem.mockResolvedValueOnce(null); + + const result = await getOrCreateDatabaseKey('absent.db'); + + expect(mockSetItem).toHaveBeenCalledTimes(1); + expect(result).toHaveLength(64); + }); + + it('throws on undefined (contract violation) and does NOT call setItem', async () => { + mockGetItem.mockResolvedValueOnce(undefined as unknown as null); + + await expect(getOrCreateDatabaseKey('undef.db')).rejects.toThrow('unexpected value'); + expect(mockSetItem).not.toHaveBeenCalled(); + }); + + it('rejects a malformed stored key (fail-closed) and does NOT mint', async () => { + mockGetItem.mockResolvedValueOnce('z'.repeat(64)); // 64 chars but not hex + + await expect(getOrCreateDatabaseKey('corrupt.db')).rejects.toThrow('malformed'); + expect(mockSetItem).not.toHaveBeenCalled(); + }); +}); diff --git a/app/lib/encryption/keyStore.ts b/app/lib/encryption/keyStore.ts new file mode 100644 index 00000000000..79b09d32c6d --- /dev/null +++ b/app/lib/encryption/keyStore.ts @@ -0,0 +1,40 @@ +import { randomKey } from '@rocket.chat/mobile-crypto'; + +import NativeDatabaseKeyStore from '../native/NativeDatabaseKeyStore'; + +const KEY_PREFIX = 'db_key_v1:'; +const DATABASE_KEY_PATTERN = /^[0-9a-f]{64}$/i; + +/** + * Returns the stored 64-hex-char SQLCipher key for dbName, creating and persisting + * a fresh one if none exists yet. + * + * Fail-closed: a read rejection propagates (we never mint over a read failure), and a + * stored value that isn't a 64-hex-char key is rejected rather than handed to SQLCipher — + * either case would otherwise risk locking or corrupting an existing encrypted database. + */ +export async function getOrCreateDatabaseKey(dbName: string): Promise { + const storageKey = KEY_PREFIX + dbName; + + const existing = await NativeDatabaseKeyStore.getItem(storageKey); + + if (typeof existing === 'string') { + if (!DATABASE_KEY_PATTERN.test(existing)) { + throw new Error(`DatabaseKeyStore returned a malformed key for ${storageKey}`); + } + return existing; + } + + if (existing !== null) { + // undefined or any unexpected value is a contract violation — never mint + throw new Error(`DatabaseKeyStore.getItem returned unexpected value for key ${storageKey}`); + } + + // True not-found (null) — mint a new 32-byte key as 64 hex chars + const newKey = await randomKey(32); + if (!DATABASE_KEY_PATTERN.test(newKey)) { + throw new Error(`Generated a malformed database key for ${storageKey}`); + } + await NativeDatabaseKeyStore.setItem(storageKey, newKey); + return newKey; +} diff --git a/app/lib/native/NativeDatabaseKeyStore.ts b/app/lib/native/NativeDatabaseKeyStore.ts new file mode 100644 index 00000000000..ee3da24d93c --- /dev/null +++ b/app/lib/native/NativeDatabaseKeyStore.ts @@ -0,0 +1,10 @@ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + getItem(key: string): Promise; + setItem(key: string, value: string): Promise; + removeItem(key: string): Promise; +} + +export default TurboModuleRegistry.getEnforcing('DatabaseKeyStoreModule'); diff --git a/ios/Libraries/DatabaseKeyStore.mm b/ios/Libraries/DatabaseKeyStore.mm new file mode 100644 index 00000000000..0a12dae7533 --- /dev/null +++ b/ios/Libraries/DatabaseKeyStore.mm @@ -0,0 +1,66 @@ +#import + +#import + +// Forward declaration of the Swift Keychain helper. Same translation-unit-only +// declaration VoipModule.mm uses for VoipService — the symbol links from the +// same target at link time; importing the generated -Swift.h here is unnecessary. +@interface DatabaseKeyStore : NSObject ++ (NSString * _Nullable)readAccount:(NSString *)account error:(NSError * _Nullable * _Nullable)error; ++ (BOOL)write:(NSString *)account value:(NSString *)value; ++ (void)delete:(NSString *)account; +@end + +@interface DatabaseKeyStoreModule : NSObject +@end + +@implementation DatabaseKeyStoreModule + +RCT_EXPORT_MODULE(DatabaseKeyStoreModule) + ++ (BOOL)requiresMainQueueSetup { + return NO; +} + +- (void)getItem:(NSString *)key + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + NSError *err = nil; + NSString *value = [DatabaseKeyStore readAccount:key error:&err]; + if (err != nil) { + reject(@"KEYCHAIN_READ_ERROR", @"Failed to read key from Keychain", err); + return; + } + // Resolve explicit JS null (not nil) on a genuine miss: nil bridges to `undefined`, + // which breaks the `Promise` contract and the `!== null` check + // in getOrCreateDatabaseKey. Android already resolves null on a miss. + resolve(value ?: (id)[NSNull null]); +} + +- (void)setItem:(NSString *)key + value:(NSString *)value + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + BOOL ok = [DatabaseKeyStore write:key value:value]; + if (ok) { + resolve(nil); + } else { + reject(@"KEYCHAIN_WRITE_ERROR", @"Failed to store item in Keychain", nil); + } +} + +- (void)removeItem:(NSString *)key + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [DatabaseKeyStore delete:key]; + resolve(nil); +} + +#pragma mark - TurboModule + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +@end diff --git a/ios/Libraries/DatabaseKeyStore.swift b/ios/Libraries/DatabaseKeyStore.swift new file mode 100644 index 00000000000..daa5cd1207c --- /dev/null +++ b/ios/Libraries/DatabaseKeyStore.swift @@ -0,0 +1,105 @@ +// +// DatabaseKeyStore.swift +// Rocket.Chat +// +// Stores and retrieves per-database SQLCipher keys in the iOS Keychain. +// +// Attributes: +// kSecClass: kSecClassGenericPassword +// kSecAttrService: kDatabaseKeyStoreService ("chat.rocket.reactnative.dbkeys") +// kSecAttrAccount: the storage key ("db_key_v1:") +// kSecAttrAccessGroup: "S6UPZG7ZR3.chat.rocket.reactnative" +// Full team-prefixed form required — the Security framework does +// NOT prepend the team ID; the bare suffix fails with +// errSecMissingEntitlement (-34018). +// kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly +// Allows NSE access after the device has been unlocked once +// (e.g. after boot before the user opens the app). +// kSecAttrSynchronizable: false — device-only, not synced via iCloud Keychain + +import Foundation +import Security + +@objc(DatabaseKeyStore) +final class DatabaseKeyStore: NSObject { + + // kSecAttrService shared between this module and Database.swift + static let service = "chat.rocket.reactnative.dbkeys" + + // Full team-prefixed access group — bare suffix fails with errSecMissingEntitlement + private static let accessGroup = "S6UPZG7ZR3.chat.rocket.reactnative" + + // MARK: - Native-side helpers (called from DatabaseKeyStore.mm and Database.swift in extensions) + + /// Read a key by account name. + /// Returns the stored string on success. + /// Returns nil with error == nil on a genuine not-found (errSecItemNotFound). + /// Returns nil with error set on any other Keychain failure or undecodable data. + /// Safe to call from the NotificationService extension — the access group is shared. + @objc(readAccount:error:) static func read(account: String, error: NSErrorPointer) -> String? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecAttrAccessGroup as String: accessGroup, + kSecAttrSynchronizable as String: kCFBooleanFalse!, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecReturnData as String: kCFBooleanTrue! + ] + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + if status == errSecItemNotFound { + return nil // true not-found; error stays nil + } + guard status == errSecSuccess, let data = result as? Data, let value = String(data: data, encoding: .utf8) else { + error?.pointee = NSError( + domain: "DatabaseKeyStore", + code: Int(status), + userInfo: [NSLocalizedDescriptionKey: "Keychain read failed for account \(account)"] + ) + return nil // failure; error is set + } + return value + } + + /// Write a key. Write-once: never overwrites an existing key — on a duplicate it + /// succeeds only if the stored value already matches the value being written. + /// Returns false on an unexpected Keychain error or a conflicting existing value. + @discardableResult + @objc(write:value:) static func write(account: String, value: String) -> Bool { + let data = Data(value.utf8) + let attrs: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecAttrAccessGroup as String: accessGroup, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String, + kSecAttrSynchronizable as String: kCFBooleanFalse!, + kSecValueData as String: data + ] + + let addStatus = SecItemAdd(attrs as CFDictionary, nil) + if addStatus == errSecSuccess { + return true + } + if addStatus == errSecDuplicateItem { + var readError: NSError? + let existing = read(account: account, error: &readError) + return readError == nil && existing == value + } + NSLog("[DatabaseKeyStore] write failed for account %@, status=%d", account, addStatus) + return false + } + + /// Delete an item. No-op if not found. + @objc(delete:) static func delete(account: String) { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecAttrAccessGroup as String: accessGroup, + kSecAttrSynchronizable as String: kCFBooleanFalse! + ] + SecItemDelete(query as CFDictionary) + } +} diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index 543f9a4aadc..42a9e120b1a 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -148,6 +148,9 @@ 79D8C97F8CE2EC1B6882826B /* SecureStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B215A42CFB843397273C7EA /* SecureStorage.m */; }; 7A0000052F1BAFA700B6B4BD /* VoipService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0000032F1BAFA700B6B4BD /* VoipService.swift */; }; 7A0000062F1BAFA700B6B4BD /* VoipModule.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A0000042F1BAFA700B6B4BD /* VoipModule.mm */; }; + 7ADBC0032F80000100000003 /* DatabaseKeyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADBC0012F80000100000001 /* DatabaseKeyStore.swift */; }; + 7ADBC0042F80000100000004 /* DatabaseKeyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADBC0012F80000100000001 /* DatabaseKeyStore.swift */; }; + 7ADBC0062F80000100000006 /* DatabaseKeyStore.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7ADBC0022F80000100000002 /* DatabaseKeyStore.mm */; }; 7A0000072F1BAFA700B6B4BD /* MediaCallsAnswerRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F98EE33D91A93DF8E69F34 /* MediaCallsAnswerRequest.swift */; }; 7A0129D42C6E8EC800F84A97 /* ShareRocketChatRN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0129D22C6E8B5900F84A97 /* ShareRocketChatRN.swift */; }; 7A0129D62C6E8F0700F84A97 /* ShareRocketChatRN.entitlements in Resources */ = {isa = PBXBuildFile; fileRef = 1EC6AD6022CBA20C00A41C61 /* ShareRocketChatRN.entitlements */; }; @@ -432,6 +435,8 @@ 6FA7E2372F28A9D300A1D45E /* ExternalInputModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExternalInputModule.m; sourceTree = ""; }; 7A0000032F1BAFA700B6B4BD /* VoipService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoipService.swift; sourceTree = ""; }; 7A0000042F1BAFA700B6B4BD /* VoipModule.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VoipModule.mm; sourceTree = ""; }; + 7ADBC0012F80000100000001 /* DatabaseKeyStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseKeyStore.swift; sourceTree = ""; }; + 7ADBC0022F80000100000002 /* DatabaseKeyStore.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = DatabaseKeyStore.mm; 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 = ""; }; @@ -934,6 +939,8 @@ 7AVR00002F5F5900002A6BDE /* VoipRegion.swift */, 7A1B58402F5F58FF002A6BDE /* VoipPayload.swift */, 7A0000042F1BAFA700B6B4BD /* VoipModule.mm */, + 7ADBC0012F80000100000001 /* DatabaseKeyStore.swift */, + 7ADBC0022F80000100000002 /* DatabaseKeyStore.mm */, 7A0000032F1BAFA700B6B4BD /* VoipService.swift */, B179038FDD7AAF285047814B /* SecureStorage.h */, 9B215A42CFB843397273C7EA /* SecureStorage.m */, @@ -1674,6 +1681,7 @@ 1E01C8252511303100FEF824 /* Notification.swift in Sources */, 1E0426E7251A54B4008F022C /* RoomType.swift in Sources */, 1E1C2F80250FCB69005DCE7D /* Database.swift in Sources */, + 7ADBC0042F80000100000004 /* DatabaseKeyStore.swift in Sources */, 1E67380424DC529B0009E081 /* String+Extensions.swift in Sources */, 1ED038A62B50900800C007D4 /* Bundle+Extensions.swift in Sources */, 1E01C8292511304100FEF824 /* Sender.swift in Sources */, @@ -1724,6 +1732,8 @@ 7AAB3E23257E6A6E00707CF6 /* Data+Extensions.swift in Sources */, 7AAB3E24257E6A6E00707CF6 /* Date+Extensions.swift in Sources */, 7AAB3E25257E6A6E00707CF6 /* Database.swift in Sources */, + 7ADBC0032F80000100000003 /* DatabaseKeyStore.swift in Sources */, + 7ADBC0062F80000100000006 /* DatabaseKeyStore.mm in Sources */, 7ACFE7D92DDE48760090D9BC /* AppDelegate.swift in Sources */, 7A3704572F7DB36E009085FC /* VoipPerCallDdpRegistry.swift in Sources */, 1E9A71752B59F36E00477BA2 /* ClientSSL.swift in Sources */,