diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp index c4bc8dc6f..f87ba00b5 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp @@ -1,1123 +1,959 @@ #include "SQLiteQueryExecutor.h" #include "CommSecureStore.h" #include "Logger.h" #include "sqlite_orm.h" #include "entities/Metadata.h" #include #include #include #ifdef __ANDROID__ #include #endif #define ACCOUNT_ID 1 namespace comm { using namespace sqlite_orm; std::string SQLiteQueryExecutor::sqliteFilePath; std::string SQLiteQueryExecutor::encryptionKey; std::once_flag SQLiteQueryExecutor::initialized; int SQLiteQueryExecutor::sqlcipherEncryptionKeySize = 64; std::string SQLiteQueryExecutor::secureStoreEncryptionKeyID = "comm.encryptionKey"; bool create_table(sqlite3 *db, std::string query, std::string tableName) { char *error; sqlite3_exec(db, query.c_str(), nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error creating '" << tableName << "' table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_drafts_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS drafts (threadID TEXT UNIQUE PRIMARY KEY, " "text TEXT);"; return create_table(db, query, "drafts"); } bool rename_threadID_to_key(sqlite3 *db) { sqlite3_stmt *key_column_stmt; sqlite3_prepare_v2( db, "SELECT name AS col_name FROM pragma_table_xinfo ('drafts') WHERE " "col_name='key';", -1, &key_column_stmt, nullptr); sqlite3_step(key_column_stmt); auto num_bytes = sqlite3_column_bytes(key_column_stmt, 0); sqlite3_finalize(key_column_stmt); if (num_bytes) { return true; } char *error; sqlite3_exec( db, "ALTER TABLE drafts RENAME COLUMN `threadID` TO `key`;", nullptr, nullptr, &error); if (error) { std::ostringstream stringStream; stringStream << "Error occurred renaming threadID column in drafts table " << "to key: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } return true; } bool create_persist_account_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS olm_persist_account(" "id INTEGER UNIQUE PRIMARY KEY NOT NULL, " "account_data TEXT NOT NULL);"; return create_table(db, query, "olm_persist_account"); } bool create_persist_sessions_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS olm_persist_sessions(" "target_user_id TEXT UNIQUE PRIMARY KEY NOT NULL, " "session_data TEXT NOT NULL);"; return create_table(db, query, "olm_persist_sessions"); } bool drop_messages_table(sqlite3 *db) { char *error; sqlite3_exec(db, "DROP TABLE IF EXISTS messages;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error dropping 'messages' table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool recreate_messages_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS messages ( " "id TEXT UNIQUE PRIMARY KEY NOT NULL, " "local_id TEXT, " "thread TEXT NOT NULL, " "user TEXT NOT NULL, " "type INTEGER NOT NULL, " "future_type INTEGER, " "content TEXT, " "time INTEGER NOT NULL);"; return create_table(db, query, "messages"); } bool create_messages_idx_thread_time(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE INDEX IF NOT EXISTS messages_idx_thread_time " "ON messages (thread, time);", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error creating (thread, time) index on messages table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_media_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS media ( " "id TEXT UNIQUE PRIMARY KEY NOT NULL, " "container TEXT NOT NULL, " "thread TEXT NOT NULL, " "uri TEXT NOT NULL, " "type TEXT NOT NULL, " "extras TEXT NOT NULL);"; return create_table(db, query, "media"); } bool create_media_idx_container(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE INDEX IF NOT EXISTS media_idx_container " "ON media (container);", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error creating (container) index on media table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_threads_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS threads ( " "id TEXT UNIQUE PRIMARY KEY NOT NULL, " "type INTEGER NOT NULL, " "name TEXT, " "description TEXT, " "color TEXT NOT NULL, " "creation_time BIGINT NOT NULL, " "parent_thread_id TEXT, " "containing_thread_id TEXT, " "community TEXT, " "members TEXT NOT NULL, " "roles TEXT NOT NULL, " "current_user TEXT NOT NULL, " "source_message_id TEXT, " "replies_count INTEGER NOT NULL);"; return create_table(db, query, "threads"); } bool update_threadID_for_pending_threads_in_drafts(sqlite3 *db) { char *error; sqlite3_exec( db, "UPDATE drafts SET key = " "REPLACE(REPLACE(REPLACE(REPLACE(key, 'type4/', '')," "'type5/', ''),'type6/', ''),'type7/', '')" "WHERE key LIKE 'pending/%'", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error update pending threadIDs on drafts table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool enable_write_ahead_logging_mode(sqlite3 *db) { char *error; sqlite3_exec(db, "PRAGMA journal_mode=wal;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error enabling write-ahead logging mode: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_metadata_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS metadata ( " "name TEXT UNIQUE PRIMARY KEY NOT NULL, " "data TEXT);"; return create_table(db, query, "metadata"); } bool add_not_null_constraint_to_drafts(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE TABLE IF NOT EXISTS temporary_drafts (" "key TEXT UNIQUE PRIMARY KEY NOT NULL, " "text TEXT NOT NULL);" "INSERT INTO temporary_drafts SELECT * FROM drafts " "WHERE key IS NOT NULL AND text IS NOT NULL;" "DROP TABLE drafts;" "ALTER TABLE temporary_drafts RENAME TO drafts;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error adding NOT NULL constraint to drafts table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool add_not_null_constraint_to_metadata(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE TABLE IF NOT EXISTS temporary_metadata (" "name TEXT UNIQUE PRIMARY KEY NOT NULL, " "data TEXT NOT NULL);" "INSERT INTO temporary_metadata SELECT * FROM metadata " "WHERE data IS NOT NULL;" "DROP TABLE metadata;" "ALTER TABLE temporary_metadata RENAME TO metadata;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error adding NOT NULL constraint to metadata table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } -bool create_schema(sqlite3 *db) { - char *error; - sqlite3_exec( - db, - "CREATE TABLE IF NOT EXISTS drafts (" - " key TEXT UNIQUE PRIMARY KEY NOT NULL," - " text TEXT NOT NULL" - ");" - - "CREATE TABLE IF NOT EXISTS messages (" - " id TEXT UNIQUE PRIMARY KEY NOT NULL," - " local_id TEXT," - " thread TEXT NOT NULL," - " user TEXT NOT NULL," - " type INTEGER NOT NULL," - " future_type INTEGER," - " content TEXT," - " time INTEGER NOT NULL" - ");" - - "CREATE TABLE IF NOT EXISTS olm_persist_account (" - " id INTEGER UNIQUE PRIMARY KEY NOT NULL," - " account_data TEXT NOT NULL" - ");" - - "CREATE TABLE IF NOT EXISTS olm_persist_sessions (" - " target_user_id TEXT UNIQUE PRIMARY KEY NOT NULL," - " session_data TEXT NOT NULL" - ");" - - "CREATE TABLE IF NOT EXISTS media (" - " id TEXT UNIQUE PRIMARY KEY NOT NULL," - " container TEXT NOT NULL," - " thread TEXT NOT NULL," - " uri TEXT NOT NULL," - " type TEXT NOT NULL," - " extras TEXT NOT NULL" - ");" - - "CREATE TABLE IF NOT EXISTS threads (" - " id TEXT UNIQUE PRIMARY KEY NOT NULL," - " type INTEGER NOT NULL," - " name TEXT," - " description TEXT," - " color TEXT NOT NULL," - " creation_time BIGINT NOT NULL," - " parent_thread_id TEXT," - " containing_thread_id TEXT," - " community TEXT," - " members TEXT NOT NULL," - " roles TEXT NOT NULL," - " current_user TEXT NOT NULL," - " source_message_id TEXT," - " replies_count INTEGER NOT NULL" - ");" - - "CREATE TABLE IF NOT EXISTS metadata (" - " name TEXT UNIQUE PRIMARY KEY NOT NULL," - " data TEXT NOT NULL" - ");" - - "CREATE INDEX IF NOT EXISTS media_idx_container" - " ON media (container);" - - "CREATE INDEX IF NOT EXISTS messages_idx_thread_time" - " ON messages (thread, time);", - nullptr, - nullptr, - &error); - - if (!error) { - return true; - } - - std::ostringstream stringStream; - stringStream << "Error creating tables: " << error; - Logger::log(stringStream.str()); - - sqlite3_free(error); - return false; -} - void set_encryption_key(sqlite3 *db) { std::string set_encryption_key_query = "PRAGMA key = \"x'" + SQLiteQueryExecutor::encryptionKey + "'\";"; char *error_set_key; sqlite3_exec( db, set_encryption_key_query.c_str(), nullptr, nullptr, &error_set_key); if (error_set_key) { std::ostringstream error_message; error_message << "Failed to set encryption key: " << error_set_key; throw std::system_error( ECANCELED, std::generic_category(), error_message.str()); } } int get_database_version(sqlite3 *db) { sqlite3_stmt *user_version_stmt; sqlite3_prepare_v2( db, "PRAGMA user_version;", -1, &user_version_stmt, nullptr); sqlite3_step(user_version_stmt); int current_user_version = sqlite3_column_int(user_version_stmt, 0); sqlite3_finalize(user_version_stmt); return current_user_version; } bool set_database_version(sqlite3 *db, int db_version) { std::stringstream update_version; update_version << "PRAGMA user_version=" << db_version << ";"; auto update_version_str = update_version.str(); char *error; sqlite3_exec(db, update_version_str.c_str(), nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream errorStream; errorStream << "Error setting database version to " << db_version << ": " << error; Logger::log(errorStream.str()); sqlite3_free(error); return false; } void trace_queries(sqlite3 *db) { int error_code = sqlite3_trace_v2( db, SQLITE_TRACE_PROFILE, [](unsigned, void *, void *preparedStatement, void *) { sqlite3_stmt *statement = (sqlite3_stmt *)preparedStatement; char *sql = sqlite3_expanded_sql(statement); if (sql != nullptr) { std::string sqlStr(sql); // TODO: send logs to backup here } return 0; }, NULL); if (error_code != SQLITE_OK) { std::ostringstream error_message; error_message << "Failed to set trace callback, error code: " << error_code; throw std::system_error( ECANCELED, std::generic_category(), error_message.str()); } } void on_database_open(sqlite3 *db) { set_encryption_key(db); trace_queries(db); } bool file_exists(const std::string &file_path) { std::ifstream file(file_path.c_str()); return file.good(); } void attempt_delete_file( const std::string &file_path, const char *error_message) { if (std::remove(file_path.c_str())) { throw std::system_error(errno, std::generic_category(), error_message); } } void attempt_rename_file( const std::string &old_path, const std::string &new_path, const char *error_message) { if (std::rename(old_path.c_str(), new_path.c_str())) { throw std::system_error(errno, std::generic_category(), error_message); } } bool is_database_queryable(sqlite3 *db, bool use_encryption_key) { char *err_msg; sqlite3_open(SQLiteQueryExecutor::sqliteFilePath.c_str(), &db); // According to SQLCipher documentation running some SELECT is the only way to // check for key validity if (use_encryption_key) { set_encryption_key(db); } sqlite3_exec( db, "SELECT COUNT(*) FROM sqlite_master;", nullptr, nullptr, &err_msg); sqlite3_close(db); return !err_msg; } void run_with_native_accessible(std::function &&task) { // Some methods of SQLiteQueryExecutor are meant to be executed on // auxiliary threads. In case they require access to native Java // API we need to temporarily attach the thread to JVM // This function attaches thread to JVM for the time // lambda passed to this function will be executing. #ifdef __ANDROID__ facebook::jni::ThreadScope::WithClassLoader(std::move(task)); #else task(); #endif } void validate_encryption() { std::string temp_encrypted_db_path = SQLiteQueryExecutor::sqliteFilePath + "_temp_encrypted"; bool temp_encrypted_exists = file_exists(temp_encrypted_db_path); bool default_location_exists = file_exists(SQLiteQueryExecutor::sqliteFilePath); if (temp_encrypted_exists && default_location_exists) { Logger::log( "Previous encryption attempt failed. Repeating encryption process from " "the beginning."); attempt_delete_file( temp_encrypted_db_path, "Failed to delete corrupted encrypted database."); } else if (temp_encrypted_exists && !default_location_exists) { Logger::log( "Moving temporary encrypted database to default location failed in " "previous encryption attempt. Repeating rename step."); attempt_rename_file( temp_encrypted_db_path, SQLiteQueryExecutor::sqliteFilePath, "Failed to move encrypted database to default location."); return; } else if (!default_location_exists) { Logger::log( "Database not present yet. It will be created encrypted under default " "path."); return; } sqlite3 *db; if (is_database_queryable(db, true)) { Logger::log( "Database exists under default path and it is correctly encrypted."); return; } if (!is_database_queryable(db, false)) { Logger::log( "Database exists but it is encrypted with key that was lost. " "Attempting database deletion. New encrypted one will be created."); attempt_delete_file( SQLiteQueryExecutor::sqliteFilePath.c_str(), "Failed to delete database encrypted with lost key."); return; } else { Logger::log( "Database exists but it is not encrypted. Attempting encryption " "process."); } sqlite3_open(SQLiteQueryExecutor::sqliteFilePath.c_str(), &db); std::string createEncryptedCopySQL = "ATTACH DATABASE '" + temp_encrypted_db_path + "' AS encrypted_comm " "KEY \"x'" + SQLiteQueryExecutor::encryptionKey + "'\";" "SELECT sqlcipher_export('encrypted_comm');" "DETACH DATABASE encrypted_comm;"; char *encryption_error; sqlite3_exec( db, createEncryptedCopySQL.c_str(), nullptr, nullptr, &encryption_error); if (encryption_error) { throw std::system_error( ECANCELED, std::generic_category(), "Failed to create encrypted copy of the original database."); } sqlite3_close(db); attempt_delete_file( SQLiteQueryExecutor::sqliteFilePath, "Failed to delete unencrypted database."); attempt_rename_file( temp_encrypted_db_path, SQLiteQueryExecutor::sqliteFilePath, "Failed to move encrypted database to default location."); Logger::log("Encryption completed successfully."); } typedef bool ShouldBeInTransaction; -typedef std::function MigrateFunction; -typedef std::pair SQLiteMigration; +typedef std::pair, ShouldBeInTransaction> + SQLiteMigration; std::vector> migrations{ {{1, {create_drafts_table, true}}, {2, {rename_threadID_to_key, true}}, {4, {create_persist_account_table, true}}, {5, {create_persist_sessions_table, true}}, {15, {create_media_table, true}}, {16, {drop_messages_table, true}}, {17, {recreate_messages_table, true}}, {18, {create_messages_idx_thread_time, true}}, {19, {create_media_idx_container, true}}, {20, {create_threads_table, true}}, {21, {update_threadID_for_pending_threads_in_drafts, true}}, {22, {enable_write_ahead_logging_mode, false}}, {23, {create_metadata_table, true}}, {24, {add_not_null_constraint_to_drafts, true}}, {25, {add_not_null_constraint_to_metadata, true}}}}; -enum class MigrationResult { SUCCESS, FAILURE, NOT_APPLIED }; - -MigrationResult applyMigrationWithTransaction( - sqlite3 *db, - const MigrateFunction &migrate, - int index) { - sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); - auto db_version = get_database_version(db); - if (index <= db_version) { - sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); - return MigrationResult::NOT_APPLIED; - } - auto rc = migrate(db); - if (!rc) { - sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); - return MigrationResult::FAILURE; - } - auto database_version_set = set_database_version(db, index); - if (!database_version_set) { - sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); - return MigrationResult::FAILURE; - } - sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, nullptr); - return MigrationResult::SUCCESS; -} - -MigrationResult applyMigrationWithoutTransaction( - sqlite3 *db, - const MigrateFunction &migrate, - int index) { - auto db_version = get_database_version(db); - if (index <= db_version) { - return MigrationResult::NOT_APPLIED; - } - auto rc = migrate(db); - if (!rc) { - return MigrationResult::FAILURE; - } - sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); - auto inner_db_version = get_database_version(db); - if (index <= inner_db_version) { - sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); - return MigrationResult::NOT_APPLIED; - } - auto database_version_set = set_database_version(db, index); - if (!database_version_set) { - sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); - return MigrationResult::FAILURE; - } - sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, nullptr); - return MigrationResult::SUCCESS; -} - -bool set_up_database(sqlite3 *db) { - auto write_ahead_enabled = enable_write_ahead_logging_mode(db); - if (!write_ahead_enabled) { - return false; - } - - sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); - auto db_version = get_database_version(db); - auto latest_version = migrations.back().first; - if (db_version == latest_version) { - sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); - return true; - } - if (db_version != 0 || !create_schema(db) || - !set_database_version(db, latest_version)) { - sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); - return false; - } - sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, nullptr); - return true; -} - void SQLiteQueryExecutor::migrate() const { validate_encryption(); sqlite3 *db; sqlite3_open(SQLiteQueryExecutor::sqliteFilePath.c_str(), &db); on_database_open(db); std::stringstream db_path; db_path << "db path: " << SQLiteQueryExecutor::sqliteFilePath.c_str() << std::endl; Logger::log(db_path.str()); auto db_version = get_database_version(db); std::stringstream version_msg; version_msg << "db version: " << db_version << std::endl; Logger::log(version_msg.str()); - if (db_version == 0) { - set_up_database(db); - Logger::log("Database structure created."); - - sqlite3_close(db); - return; - } - for (const auto &[idx, migration] : migrations) { + if (idx <= db_version) { + continue; + } const auto &[applyMigration, shouldBeInTransaction] = migration; - MigrationResult migrationResult; - if (shouldBeInTransaction) { - migrationResult = applyMigrationWithTransaction(db, applyMigration, idx); - } else { - migrationResult = - applyMigrationWithoutTransaction(db, applyMigration, idx); - } + std::stringstream migration_msg; - if (migrationResult == MigrationResult::NOT_APPLIED) { - continue; + if (shouldBeInTransaction) { + sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); } - std::stringstream migration_msg; - if (migrationResult == MigrationResult::FAILURE) { + auto rc = applyMigration(db); + if (!rc) { migration_msg << "migration " << idx << " failed." << std::endl; Logger::log(migration_msg.str()); break; } - if (migrationResult == MigrationResult::SUCCESS) { - migration_msg << "migration " << idx << " succeeded." << std::endl; - Logger::log(migration_msg.str()); + + set_database_version(db, idx); + + if (shouldBeInTransaction) { + sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, nullptr); } + migration_msg << "migration " << idx << " succeeded." << std::endl; + Logger::log(migration_msg.str()); } sqlite3_close(db); } void SQLiteQueryExecutor::assign_encryption_key() { CommSecureStore commSecureStore{}; std::string encryptionKey = comm::crypto::Tools::generateRandomHexString( SQLiteQueryExecutor::sqlcipherEncryptionKeySize); commSecureStore.set( SQLiteQueryExecutor::secureStoreEncryptionKeyID, encryptionKey); SQLiteQueryExecutor::encryptionKey = encryptionKey; } auto &SQLiteQueryExecutor::getStorage() { static auto storage = make_storage( SQLiteQueryExecutor::sqliteFilePath, make_index("messages_idx_thread_time", &Message::thread, &Message::time), make_index("media_idx_container", &Media::container), make_table( "drafts", make_column("key", &Draft::key, unique(), primary_key()), make_column("text", &Draft::text)), make_table( "messages", make_column("id", &Message::id, unique(), primary_key()), make_column("local_id", &Message::local_id), make_column("thread", &Message::thread), make_column("user", &Message::user), make_column("type", &Message::type), make_column("future_type", &Message::future_type), make_column("content", &Message::content), make_column("time", &Message::time)), make_table( "olm_persist_account", make_column("id", &OlmPersistAccount::id, unique(), primary_key()), make_column("account_data", &OlmPersistAccount::account_data)), make_table( "olm_persist_sessions", make_column( "target_user_id", &OlmPersistSession::target_user_id, unique(), primary_key()), make_column("session_data", &OlmPersistSession::session_data)), make_table( "media", make_column("id", &Media::id, unique(), primary_key()), make_column("container", &Media::container), make_column("thread", &Media::thread), make_column("uri", &Media::uri), make_column("type", &Media::type), make_column("extras", &Media::extras)), make_table( "threads", make_column("id", &Thread::id, unique(), primary_key()), make_column("type", &Thread::type), make_column("name", &Thread::name), make_column("description", &Thread::description), make_column("color", &Thread::color), make_column("creation_time", &Thread::creation_time), make_column("parent_thread_id", &Thread::parent_thread_id), make_column("containing_thread_id", &Thread::containing_thread_id), make_column("community", &Thread::community), make_column("members", &Thread::members), make_column("roles", &Thread::roles), make_column("current_user", &Thread::current_user), make_column("source_message_id", &Thread::source_message_id), make_column("replies_count", &Thread::replies_count)), make_table( "metadata", make_column("name", &Metadata::name, unique(), primary_key()), make_column("data", &Metadata::data))); storage.on_open = on_database_open; return storage; } void SQLiteQueryExecutor::initialize(std::string &databasePath) { std::call_once(SQLiteQueryExecutor::initialized, [&databasePath]() { SQLiteQueryExecutor::sqliteFilePath = databasePath; CommSecureStore commSecureStore{}; folly::Optional maybeEncryptionKey = commSecureStore.get(SQLiteQueryExecutor::secureStoreEncryptionKeyID); if (file_exists(databasePath) && maybeEncryptionKey) { SQLiteQueryExecutor::encryptionKey = maybeEncryptionKey.value(); return; } SQLiteQueryExecutor::assign_encryption_key(); }); } SQLiteQueryExecutor::SQLiteQueryExecutor() { this->migrate(); } std::string SQLiteQueryExecutor::getDraft(std::string key) const { std::unique_ptr draft = SQLiteQueryExecutor::getStorage().get_pointer(key); return (draft == nullptr) ? "" : draft->text; } std::unique_ptr SQLiteQueryExecutor::getThread(std::string threadID) const { return SQLiteQueryExecutor::getStorage().get_pointer(threadID); } void SQLiteQueryExecutor::updateDraft(std::string key, std::string text) const { Draft draft = {key, text}; SQLiteQueryExecutor::getStorage().replace(draft); } bool SQLiteQueryExecutor::moveDraft(std::string oldKey, std::string newKey) const { std::unique_ptr draft = SQLiteQueryExecutor::getStorage().get_pointer(oldKey); if (draft == nullptr) { return false; } draft->key = newKey; SQLiteQueryExecutor::getStorage().replace(*draft); SQLiteQueryExecutor::getStorage().remove(oldKey); return true; } std::vector SQLiteQueryExecutor::getAllDrafts() const { return SQLiteQueryExecutor::getStorage().get_all(); } void SQLiteQueryExecutor::removeAllDrafts() const { SQLiteQueryExecutor::getStorage().remove_all(); } void SQLiteQueryExecutor::removeAllMessages() const { SQLiteQueryExecutor::getStorage().remove_all(); } std::vector>> SQLiteQueryExecutor::getAllMessages() const { auto rows = SQLiteQueryExecutor::getStorage().select( columns( &Message::id, &Message::local_id, &Message::thread, &Message::user, &Message::type, &Message::future_type, &Message::content, &Message::time, &Media::id, &Media::container, &Media::thread, &Media::uri, &Media::type, &Media::extras), left_join(on(c(&Message::id) == &Media::container)), order_by(&Message::id)); std::vector>> allMessages; allMessages.reserve(rows.size()); std::string prev_msg_idx{}; for (auto &row : rows) { auto msg_id = std::get<0>(row); if (msg_id == prev_msg_idx) { allMessages.back().second.push_back(Media{ std::get<8>(row), std::move(std::get<9>(row)), std::move(std::get<10>(row)), std::move(std::get<11>(row)), std::move(std::get<12>(row)), std::move(std::get<13>(row)), }); } else { std::vector mediaForMsg; if (!std::get<8>(row).empty()) { mediaForMsg.push_back(Media{ std::get<8>(row), std::move(std::get<9>(row)), std::move(std::get<10>(row)), std::move(std::get<11>(row)), std::move(std::get<12>(row)), std::move(std::get<13>(row)), }); } allMessages.push_back(std::make_pair( Message{ msg_id, std::move(std::get<1>(row)), std::move(std::get<2>(row)), std::move(std::get<3>(row)), std::get<4>(row), std::move(std::get<5>(row)), std::move(std::get<6>(row)), std::get<7>(row)}, mediaForMsg)); prev_msg_idx = msg_id; } } return allMessages; } void SQLiteQueryExecutor::removeMessages( const std::vector &ids) const { SQLiteQueryExecutor::getStorage().remove_all( where(in(&Message::id, ids))); } void SQLiteQueryExecutor::removeMessagesForThreads( const std::vector &threadIDs) const { SQLiteQueryExecutor::getStorage().remove_all( where(in(&Message::thread, threadIDs))); } void SQLiteQueryExecutor::replaceMessage(const Message &message) const { SQLiteQueryExecutor::getStorage().replace(message); } void SQLiteQueryExecutor::rekeyMessage(std::string from, std::string to) const { auto msg = SQLiteQueryExecutor::getStorage().get(from); msg.id = to; SQLiteQueryExecutor::getStorage().replace(msg); SQLiteQueryExecutor::getStorage().remove(from); } void SQLiteQueryExecutor::removeAllMedia() const { SQLiteQueryExecutor::getStorage().remove_all(); } void SQLiteQueryExecutor::removeMediaForMessages( const std::vector &msg_ids) const { SQLiteQueryExecutor::getStorage().remove_all( where(in(&Media::container, msg_ids))); } void SQLiteQueryExecutor::removeMediaForMessage(std::string msg_id) const { SQLiteQueryExecutor::getStorage().remove_all( where(c(&Media::container) == msg_id)); } void SQLiteQueryExecutor::removeMediaForThreads( const std::vector &thread_ids) const { SQLiteQueryExecutor::getStorage().remove_all( where(in(&Media::thread, thread_ids))); } void SQLiteQueryExecutor::replaceMedia(const Media &media) const { SQLiteQueryExecutor::getStorage().replace(media); } void SQLiteQueryExecutor::rekeyMediaContainers(std::string from, std::string to) const { SQLiteQueryExecutor::getStorage().update_all( set(c(&Media::container) = to), where(c(&Media::container) == from)); } std::vector SQLiteQueryExecutor::getAllThreads() const { return SQLiteQueryExecutor::getStorage().get_all(); }; void SQLiteQueryExecutor::removeThreads(std::vector ids) const { SQLiteQueryExecutor::getStorage().remove_all( where(in(&Thread::id, ids))); }; void SQLiteQueryExecutor::replaceThread(const Thread &thread) const { SQLiteQueryExecutor::getStorage().replace(thread); }; void SQLiteQueryExecutor::removeAllThreads() const { SQLiteQueryExecutor::getStorage().remove_all(); }; void SQLiteQueryExecutor::beginTransaction() const { SQLiteQueryExecutor::getStorage().begin_transaction(); } void SQLiteQueryExecutor::commitTransaction() const { SQLiteQueryExecutor::getStorage().commit(); } void SQLiteQueryExecutor::rollbackTransaction() const { SQLiteQueryExecutor::getStorage().rollback(); } std::vector SQLiteQueryExecutor::getOlmPersistSessionsData() const { return SQLiteQueryExecutor::getStorage().get_all(); } folly::Optional SQLiteQueryExecutor::getOlmPersistAccountData() const { std::vector result = SQLiteQueryExecutor::getStorage().get_all(); if (result.size() > 1) { throw std::system_error( ECANCELED, std::generic_category(), "Multiple records found for the olm_persist_account table"); } return (result.size() == 0) ? folly::none : folly::Optional(result[0].account_data); } void SQLiteQueryExecutor::storeOlmPersistData(crypto::Persist persist) const { OlmPersistAccount persistAccount = { ACCOUNT_ID, std::string(persist.account.begin(), persist.account.end())}; SQLiteQueryExecutor::getStorage().replace(persistAccount); for (auto it = persist.sessions.begin(); it != persist.sessions.end(); it++) { OlmPersistSession persistSession = { it->first, std::string(it->second.begin(), it->second.end())}; SQLiteQueryExecutor::getStorage().replace(persistSession); } } void SQLiteQueryExecutor::setNotifyToken(std::string token) const { this->setMetadata("notify_token", token); } void SQLiteQueryExecutor::clearNotifyToken() const { this->clearMetadata("notify_token"); } void SQLiteQueryExecutor::setCurrentUserID(std::string userID) const { this->setMetadata("current_user_id", userID); } std::string SQLiteQueryExecutor::getCurrentUserID() const { return this->getMetadata("current_user_id"); } void SQLiteQueryExecutor::setDeviceID(std::string deviceID) const { this->setMetadata("device_id", deviceID); }; std::string SQLiteQueryExecutor::getDeviceID() const { return this->getMetadata("device_id"); }; void SQLiteQueryExecutor::setMetadata(std::string entry_name, std::string data) const { Metadata entry{ entry_name, data, }; SQLiteQueryExecutor::getStorage().replace(entry); } void SQLiteQueryExecutor::clearMetadata(std::string entry_name) const { SQLiteQueryExecutor::getStorage().remove(entry_name); } std::string SQLiteQueryExecutor::getMetadata(std::string entry_name) const { std::unique_ptr entry = SQLiteQueryExecutor::getStorage().get_pointer(entry_name); return (entry == nullptr) ? "" : entry->data; } void SQLiteQueryExecutor::clearSensitiveData() const { if (file_exists(SQLiteQueryExecutor::sqliteFilePath) && std::remove(SQLiteQueryExecutor::sqliteFilePath.c_str())) { std::ostringstream errorStream; errorStream << "Failed to delete database file. Details: " << strerror(errno); throw std::system_error(errno, std::generic_category(), errorStream.str()); } auto native_dependent_task = []() { SQLiteQueryExecutor::assign_encryption_key(); }; run_with_native_accessible(native_dependent_task); this->migrate(); } } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h index 94e73fb9b..8d68165b8 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h @@ -1,72 +1,71 @@ #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 c8c2e6c0e..2e38de4d1 100644 --- a/native/ios/Comm/AppDelegate.mm +++ b/native/ios/Comm/AppDelegate.mm @@ -1,313 +1,306 @@ #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 "GlobalNetworkSingleton.h" #import "Logger.h" #import "MessageOperationsUtilities.h" #import "NetworkModule.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:@"PING"]) { comm::GlobalNetworkSingleton::instance.scheduleOrRun( [=](comm::NetworkModule &networkModule) { networkModule.sendPong(); dispatch_async(dispatch_get_main_queue(), ^{ completionHandler(UIBackgroundFetchResultNewData); }); }); return YES; } else 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 9406e5b9e..ef7b40166 100644 --- a/native/ios/Comm/CommSecureStoreIOSWrapper.h +++ b/native/ios/Comm/CommSecureStoreIOSWrapper.h @@ -1,13 +1,12 @@ #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 c8a5f8ce4..c309fd23a 100644 --- a/native/ios/Comm/CommSecureStoreIOSWrapper.mm +++ b/native/ios/Comm/CommSecureStoreIOSWrapper.mm @@ -1,99 +1,67 @@ #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