diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h index 8d68165b8..94e73fb9b 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h @@ -1,71 +1,72 @@ #pragma once #include "../CryptoTools/Persist.h" #include "DatabaseQueryExecutor.h" #include "entities/Draft.h" #include #include namespace comm { class SQLiteQueryExecutor : public DatabaseQueryExecutor { void migrate() const; static void assign_encryption_key(); static auto &getStorage(); void setMetadata(std::string entry_name, std::string data) const override; void clearMetadata(std::string entry_name) const override; std::string getMetadata(std::string entry_name) const override; static std::once_flag initialized; static int sqlcipherEncryptionKeySize; - static std::string secureStoreEncryptionKeyID; public: static std::string sqliteFilePath; static std::string encryptionKey; + static std::string secureStoreEncryptionKeyID; + SQLiteQueryExecutor(); static void initialize(std::string &databasePath); std::unique_ptr getThread(std::string threadID) const override; std::string getDraft(std::string key) const override; void updateDraft(std::string key, std::string text) const override; bool moveDraft(std::string oldKey, std::string newKey) const override; std::vector getAllDrafts() const override; void removeAllDrafts() const override; void removeAllMessages() const override; std::vector>> getAllMessages() const override; void removeMessages(const std::vector &ids) const override; void removeMessagesForThreads( const std::vector &threadIDs) const override; void replaceMessage(const Message &message) const override; void rekeyMessage(std::string from, std::string to) const override; void removeAllMedia() const override; void removeMediaForMessages( const std::vector &msg_ids) const override; void removeMediaForMessage(std::string msg_id) const override; void removeMediaForThreads( const std::vector &thread_ids) const override; void replaceMedia(const Media &media) const override; void rekeyMediaContainers(std::string from, std::string to) const override; std::vector getAllThreads() const override; void removeThreads(std::vector ids) const override; void replaceThread(const Thread &thread) const override; void removeAllThreads() const override; void beginTransaction() const override; void commitTransaction() const override; void rollbackTransaction() const override; std::vector getOlmPersistSessionsData() const override; folly::Optional getOlmPersistAccountData() const override; void storeOlmPersistData(crypto::Persist persist) const override; void setNotifyToken(std::string token) const override; void clearNotifyToken() const override; void setCurrentUserID(std::string userID) const override; std::string getCurrentUserID() const override; void setDeviceID(std::string deviceID) const override; std::string getDeviceID() const override; void clearSensitiveData() const override; }; } // namespace comm diff --git a/native/ios/Comm/AppDelegate.mm b/native/ios/Comm/AppDelegate.mm index 5436d2fbf..e53a28677 100644 --- a/native/ios/Comm/AppDelegate.mm +++ b/native/ios/Comm/AppDelegate.mm @@ -1,294 +1,301 @@ #import "AppDelegate.h" #import "Orientation.h" #import "RNNotifications.h" #import #import #import #import #import #import #import #import #import #import #import "CommCoreModule.h" +#import "CommSecureStoreIOSWrapper.h" #import "GlobalDBSingleton.h" #import "Logger.h" #import "MessageOperationsUtilities.h" #import "SQLiteQueryExecutor.h" #import "TemporaryMessageStorage.h" #import "ThreadOperations.h" #import "Tools.h" #import #import #import #ifdef FB_SONARKIT_ENABLED #import #import #import #import #import #import static void InitializeFlipper(UIApplication *application) { FlipperClient *client = [FlipperClient sharedClient]; SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; [client addPlugin:[FlipperKitReactPlugin new]]; [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; [client start]; } #endif #import #import #import NSString *const backgroundNotificationTypeKey = @"backgroundNotifType"; NSString *const setUnreadStatusKey = @"setUnreadStatus"; @interface AppDelegate () < RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate> { } @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + NSString *secureStoreEncryptionKeyID = [NSString + stringWithUTF8String: + (comm::SQLiteQueryExecutor::secureStoreEncryptionKeyID.c_str())]; + [[CommSecureStoreIOSWrapper sharedInstance] + migrateOptionsForKey:secureStoreEncryptionKeyID + withVersion:@"0"]; [self attemptDatabaseInitialization]; return YES; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { #ifdef FB_SONARKIT_ENABLED InitializeFlipper(application); #endif [self moveMessagesToDatabase]; RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"Comm" initialProperties:nil]; if (@available(iOS 13.0, *)) { rootView.backgroundColor = [UIColor systemBackgroundColor]; } else { rootView.backgroundColor = [UIColor whiteColor]; } self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; [super application:application didFinishLaunchingWithOptions:launchOptions]; // This prevents a very small flicker from occurring before expo-splash-screen // is able to display UIView *launchScreenView = [[UIStoryboard storyboardWithName:@"SplashScreen" bundle:nil] instantiateInitialViewController] .view; launchScreenView.frame = self.window.bounds; rootView.loadingView = launchScreenView; rootView.loadingViewFadeDelay = 0; rootView.loadingViewFadeDuration = 0.001; return YES; } - (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge { // If you'd like to export some custom RCTBridgeModules that are not Expo // modules, add them here! return @[]; } - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { [RNNotifications didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [RNNotifications didFailToRegisterForRemoteNotificationsWithError:error]; } // Required for the notification event. You must call the completion handler // after handling the remote notification. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification fetchCompletionHandler: (void (^)(UIBackgroundFetchResult))completionHandler { BOOL handled = NO; if (notification[@"aps"][@"content-available"] && notification[backgroundNotificationTypeKey]) { handled = [self handleBackgroundNotification:notification fetchCompletionHandler:completionHandler]; } if (handled) { return; } [RNNotifications didReceiveRemoteNotification:notification fetchCompletionHandler:completionHandler]; } - (BOOL)handleBackgroundNotification:(NSDictionary *)notification fetchCompletionHandler: (void (^)(UIBackgroundFetchResult))completionHandler { if ([notification[backgroundNotificationTypeKey] isEqualToString:@"CLEAR"]) { if (notification[setUnreadStatusKey] && notification[@"threadID"]) { std::string threadID = std::string([notification[@"threadID"] UTF8String]); // this callback may be called from inactive state so we need // to initialize the database [self attemptDatabaseInitialization]; comm::GlobalDBSingleton::instance.scheduleOrRun([threadID]() mutable { comm::ThreadOperations::updateSQLiteUnreadStatus(threadID, false); }); } [[UNUserNotificationCenter currentNotificationCenter] getDeliveredNotificationsWithCompletionHandler:^( NSArray *notifications) { for (UNNotification *notif in notifications) { if ([notification[@"notificationId"] isEqual:notif.request.content.userInfo[@"id"]]) { NSArray *identifiers = [NSArray arrayWithObjects:notif.request.identifier, nil]; [[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:identifiers]; } } dispatch_async(dispatch_get_main_queue(), ^{ completionHandler(UIBackgroundFetchResultNewData); }); }]; return YES; } return NO; } // Required for the localNotification event. - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { [RNNotifications didReceiveLocalNotification:notification]; } - (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window { return [Orientation getOrientation]; } - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif } using JSExecutorFactory = facebook::react::JSExecutorFactory; using HermesExecutorFactory = facebook::react::HermesExecutorFactory; using Runtime = facebook::jsi::Runtime; - (std::unique_ptr)jsExecutorFactoryForBridge: (RCTBridge *)bridge { __weak __typeof(self) weakSelf = self; const auto commRuntimeInstaller = [weakSelf, bridge](facebook::jsi::Runtime &rt) { if (!bridge) { return; } __typeof(self) strongSelf = weakSelf; if (strongSelf) { std::shared_ptr nativeModule = std::make_shared(bridge.jsCallInvoker); rt.global().setProperty( rt, facebook::jsi::PropNameID::forAscii(rt, "CommCoreModule"), facebook::jsi::Object::createFromHostObject(rt, nativeModule)); } }; const auto installer = reanimated::REAJSIExecutorRuntimeInstaller(bridge, commRuntimeInstaller); return std::make_unique( facebook::react::RCTJSIExecutorRuntimeInstaller(installer), JSIExecutor::defaultTimeoutInvoker, makeRuntimeConfig(3072)); } - (void)attemptDatabaseInitialization { std::string sqliteFilePath = std::string([[Tools getSQLiteFilePath] UTF8String]); // Previous Comm versions used app group location for SQLite // database, so that NotificationService was able to acces it directly. // Unfortunately it caused errores related to system locks. The code // below re-migrates SQLite from app group to app specific location // on devices where previous Comm version was installed. NSString *appGroupSQLiteFilePath = [Tools getAppGroupSQLiteFilePath]; if ([NSFileManager.defaultManager fileExistsAtPath:appGroupSQLiteFilePath] && std::rename( std::string([appGroupSQLiteFilePath UTF8String]).c_str(), sqliteFilePath.c_str())) { throw std::runtime_error( "Failed to move SQLite database from app group to default location"); } comm::SQLiteQueryExecutor::initialize(sqliteFilePath); } - (void)moveMessagesToDatabase { TemporaryMessageStorage *temporaryStorage = [[TemporaryMessageStorage alloc] init]; NSArray *messages = [temporaryStorage readAndClearMessages]; for (NSString *message in messages) { std::string messageInfos = std::string([message UTF8String]); comm::GlobalDBSingleton::instance.scheduleOrRun([messageInfos]() mutable { comm::MessageOperationsUtilities::storeMessageInfos(messageInfos); }); } } // Copied from // ReactAndroid/src/main/java/com/facebook/hermes/reactexecutor/OnLoad.cpp static ::hermes::vm::RuntimeConfig makeRuntimeConfig(::hermes::vm::gcheapsize_t heapSizeMB) { namespace vm = ::hermes::vm; auto gcConfigBuilder = vm::GCConfig::Builder() .withName("RN") // For the next two arguments: avoid GC before TTI by initializing the // runtime to allocate directly in the old generation, but revert to // normal operation when we reach the (first) TTI point. .withAllocInYoung(false) .withRevertToYGAtTTI(true); if (heapSizeMB > 0) { gcConfigBuilder.withMaxHeapSize(heapSizeMB << 20); } return vm::RuntimeConfig::Builder() .withGCConfig(gcConfigBuilder.build()) .build(); } @end diff --git a/native/ios/Comm/CommSecureStoreIOSWrapper.h b/native/ios/Comm/CommSecureStoreIOSWrapper.h index ef7b40166..9406e5b9e 100644 --- a/native/ios/Comm/CommSecureStoreIOSWrapper.h +++ b/native/ios/Comm/CommSecureStoreIOSWrapper.h @@ -1,12 +1,13 @@ #pragma once #import #import @interface CommSecureStoreIOSWrapper : NSObject + (id)sharedInstance; - (void)set:(NSString *)key value:(NSString *)value; - (NSString *)get:(NSString *)key; +- (void)migrateOptionsForKey:(NSString *)key withVersion:(NSString *)version; @end diff --git a/native/ios/Comm/CommSecureStoreIOSWrapper.mm b/native/ios/Comm/CommSecureStoreIOSWrapper.mm index c309fd23a..c8a5f8ce4 100644 --- a/native/ios/Comm/CommSecureStoreIOSWrapper.mm +++ b/native/ios/Comm/CommSecureStoreIOSWrapper.mm @@ -1,67 +1,99 @@ #import "CommSecureStoreIOSWrapper.h" #import "CommSecureStoreIOSWrapper.h" #import @interface CommSecureStoreIOSWrapper () @property(nonatomic, strong) EXSecureStore *secureStore; @property(nonatomic, strong) NSDictionary *options; @end @interface EXSecureStore (CommEXSecureStore) - (BOOL)_setValue:(NSString *)value withKey:(NSString *)key withOptions:(NSDictionary *)options error:(NSError **)error; - (NSString *)_getValueWithKey:(NSString *)key withOptions:(NSDictionary *)options error:(NSError **)error; +- (void)_deleteValueWithKey:(NSString *)key withOptions:(NSDictionary *)options; @end @implementation CommSecureStoreIOSWrapper #pragma mark Singleton Methods + (id)sharedInstance { static CommSecureStoreIOSWrapper *shared = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ shared = [[self alloc] init]; EXModuleRegistryProvider *moduleRegistryProvider = [[EXModuleRegistryProvider alloc] init]; EXSecureStore *secureStore = (EXSecureStore *)[[moduleRegistryProvider moduleRegistry] getExportedModuleOfClass:EXSecureStore.class]; shared.secureStore = secureStore; + shared.options = + @{@"keychainAccessible" : @(EXSecureStoreAccessibleAfterFirstUnlock)}; }); return shared; } - (void)set:(NSString *)key value:(NSString *)value { if ([self secureStore] == nil) { [NSException raise:@"secure store error" format:@"secure store has not been initialized"]; } NSError *error; [[self secureStore] _setValue:value withKey:key withOptions:[self options] error:&error]; if (error != nil) { [NSException raise:@"secure store error" format:@"error occured when setting data"]; } } - (NSString *)get:(NSString *)key { if ([self secureStore] == nil) { [NSException raise:@"secure store error" format:@"secure store has not been initialized"]; } NSError *error; return [[self secureStore] _getValueWithKey:key withOptions:[self options] error:&error]; } +- (void)migrateOptionsForKey:(NSString *)key withVersion:(NSString *)version { + NSString *secureStoreKeyVersionID = [key stringByAppendingString:@".version"]; + NSString *failureProtectionCopyKey = [key stringByAppendingString:@".copy"]; + + NSString *secureStoreKeyVersion = [self get:secureStoreKeyVersionID]; + if (secureStoreKeyVersion && + [secureStoreKeyVersion isEqualToString:version]) { + return; + } + + NSString *value = [self get:key]; + NSString *valueCopy = [self get:failureProtectionCopyKey]; + + if (value) { + [self set:failureProtectionCopyKey value:value]; + [[self secureStore] _deleteValueWithKey:key withOptions:[self options]]; + } else if (valueCopy) { + value = valueCopy; + } else { + [self set:secureStoreKeyVersionID value:version]; + return; + } + + [self set:key value:value]; + [self set:secureStoreKeyVersionID value:version]; + [[self secureStore] _deleteValueWithKey:failureProtectionCopyKey + withOptions:[self options]]; +} + @end diff --git a/native/utils/staff-utils.js b/native/utils/staff-utils.js index 095b440d5..ed5d5e894 100644 --- a/native/utils/staff-utils.js +++ b/native/utils/staff-utils.js @@ -1,24 +1,24 @@ // @flow import { useSelector } from 'react-redux'; import { isStaff } from 'lib/shared/user-utils'; -const isStaffRelease = false; +const isStaffRelease = true; function useIsCurrentUserStaff(): boolean { const isCurrentUserStaff = useSelector( state => state.currentUserInfo && state.currentUserInfo.id && isStaff(state.currentUserInfo.id), ); return isCurrentUserStaff; } function useStaffCanSee(): boolean { const isCurrentUserStaff = useIsCurrentUserStaff(); return __DEV__ || isStaffRelease || isCurrentUserStaff; } export { isStaffRelease, useIsCurrentUserStaff, useStaffCanSee };