// // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // #include "td/db/SqliteDb.h" #include "td/utils/common.h" #include "td/utils/format.h" #include "td/utils/logging.h" #include "td/utils/port/path.h" #include "td/utils/port/Stat.h" #include "td/utils/SliceBuilder.h" #include "td/utils/Status.h" #include "td/utils/StringBuilder.h" #include "td/utils/Timer.h" #include "sqlite/sqlite3.h" namespace td { namespace { string quote_string(Slice str) { size_t cnt = 0; for (auto &c : str) { if (c == '\'') { cnt++; } } if (cnt == 0) { return str.str(); } string result; result.reserve(str.size() + cnt); for (auto &c : str) { if (c == '\'') { result += '\''; } result += c; } return result; } string db_key_to_sqlcipher_key(const DbKey &db_key) { if (db_key.is_empty()) { return "''"; } if (db_key.is_password()) { return PSTRING() << "'" << quote_string(db_key.data()) << "'"; } CHECK(db_key.is_raw_key()); Slice raw_key = db_key.data(); CHECK(raw_key.size() == 32); size_t expected_size = 64 + 5; string res(expected_size + 50, ' '); StringBuilder sb(res); sb << '"'; sb << 'x'; sb << '\''; sb << format::as_hex_dump<0>(raw_key); sb << '\''; sb << '"'; CHECK(!sb.is_error()); CHECK(sb.as_cslice().size() == expected_size); res.resize(expected_size); return res; } } // namespace SqliteDb::~SqliteDb() = default; Status SqliteDb::init(CSlice path, bool allow_creation) { // if database does not exist, delete all other files which could have been left from the old database auto database_stat = stat(path); if (database_stat.is_error()) { if (!allow_creation) { bool was_destroyed = detail::RawSqliteDb::was_any_database_destroyed(); auto reason = was_destroyed ? Slice("was corrupted and deleted") : Slice("disappeared"); LOG(FATAL) << "Database " << reason << " during execution and can't be recreated: " << database_stat.error(); } TRY_STATUS(destroy(path)); } sqlite3 *db; CHECK(sqlite3_threadsafe() != 0); int rc = sqlite3_open_v2(path.c_str(), &db, SQLITE_OPEN_READWRITE | (allow_creation ? SQLITE_OPEN_CREATE : 0), nullptr); if (rc != SQLITE_OK) { auto res = detail::RawSqliteDb::last_error(db, path); sqlite3_close(db); return res; } sqlite3_busy_timeout(db, 1000 * 5 /* 5 seconds */); raw_ = std::make_shared(db, path.str()); return Status::OK(); } static void trace_callback(void *ptr, const char *query) { LOG(ERROR) << query; } static int trace_v2_callback(unsigned code, void *ctx, void *p_raw, void *x_raw) { CHECK(code == SQLITE_TRACE_STMT); auto x = static_cast(x_raw); if (x[0] == '-' && x[1] == '-') { trace_callback(ctx, x); } else { trace_callback(ctx, sqlite3_expanded_sql(static_cast(p_raw))); } return 0; } void SqliteDb::trace(bool flag) { sqlite3_trace_v2(raw_->db(), SQLITE_TRACE_STMT, flag ? trace_v2_callback : nullptr, nullptr); } Status SqliteDb::exec(CSlice cmd) { CHECK(!empty()); char *msg; if (enable_logging_) { VLOG(sqlite) << "Start exec " << tag("query", cmd) << tag("database", raw_->db()); } auto rc = sqlite3_exec(raw_->db(), cmd.c_str(), nullptr, nullptr, &msg); if (enable_logging_) { VLOG(sqlite) << "Finish exec " << tag("query", cmd) << tag("database", raw_->db()); } if (rc != SQLITE_OK) { CHECK(msg != nullptr); return Status::Error(PSLICE() << tag("query", cmd) << " to database \"" << raw_->path() << "\" failed: " << msg); } CHECK(msg == nullptr); return Status::OK(); } Result SqliteDb::has_table(Slice table) { TRY_RESULT(stmt, get_statement(PSLICE() << "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='" << table << "'")); TRY_STATUS(stmt.step()); CHECK(stmt.has_row()); auto cnt = stmt.view_int32(0); return cnt == 1; } Result SqliteDb::get_pragma(Slice name) { TRY_RESULT(stmt, get_statement(PSLICE() << "PRAGMA " << name)); TRY_STATUS(stmt.step()); CHECK(stmt.has_row()); auto res = stmt.view_blob(0).str(); TRY_STATUS(stmt.step()); CHECK(!stmt.can_step()); return std::move(res); } Result SqliteDb::get_pragma_string(Slice name) { TRY_RESULT(stmt, get_statement(PSLICE() << "PRAGMA " << name)); TRY_STATUS(stmt.step()); CHECK(stmt.has_row()); auto res = stmt.view_string(0).str(); TRY_STATUS(stmt.step()); CHECK(!stmt.can_step()); return std::move(res); } Result SqliteDb::user_version() { TRY_RESULT(get_version_stmt, get_statement("PRAGMA user_version")); TRY_STATUS(get_version_stmt.step()); if (!get_version_stmt.has_row()) { return Status::Error(PSLICE() << "PRAGMA user_version failed for database \"" << raw_->path() << '"'); } return get_version_stmt.view_int32(0); } Status SqliteDb::set_user_version(int32 version) { return exec(PSLICE() << "PRAGMA user_version = " << version); } Status SqliteDb::begin_read_transaction() { if (raw_->on_begin()) { return exec("BEGIN"); } return Status::OK(); } Status SqliteDb::begin_write_transaction() { if (raw_->on_begin()) { return exec("BEGIN IMMEDIATE"); } return Status::OK(); } Status SqliteDb::commit_transaction() { TRY_RESULT(need_commit, raw_->on_commit()); if (need_commit) { return exec("COMMIT"); } return Status::OK(); } Status SqliteDb::check_encryption() { auto status = exec("SELECT count(*) FROM sqlite_master"); if (status.is_ok()) { enable_logging_ = true; } return status; } Result SqliteDb::open_with_key(CSlice path, bool allow_creation, const DbKey &db_key, optional cipher_version) { auto res = do_open_with_key(path, allow_creation, db_key, cipher_version ? cipher_version.value() : 0); if (res.is_error() && !cipher_version && !db_key.is_empty()) { return do_open_with_key(path, false, db_key, 3); } return res; } Result SqliteDb::do_open_with_key(CSlice path, bool allow_creation, const DbKey &db_key, int32 cipher_version) { SqliteDb db; TRY_STATUS(db.init(path, allow_creation)); if (!db_key.is_empty()) { if (db.check_encryption().is_ok()) { return Status::Error(PSLICE() << "No key is needed for database \"" << path << '"'); } auto key = db_key_to_sqlcipher_key(db_key); TRY_STATUS(db.exec(PSLICE() << "PRAGMA key = " << key)); if (cipher_version != 0) { LOG(INFO) << "Trying SQLCipher compatibility mode with version = " << cipher_version; TRY_STATUS(db.exec(PSLICE() << "PRAGMA cipher_compatibility = " << cipher_version)); } db.set_cipher_version(cipher_version); } TRY_STATUS_PREFIX(db.check_encryption(), "Can't check database: "); return std::move(db); } void SqliteDb::set_cipher_version(int32 cipher_version) { raw_->set_cipher_version(cipher_version); } optional SqliteDb::get_cipher_version() const { return raw_->get_cipher_version(); } Result SqliteDb::change_key(CSlice path, bool allow_creation, const DbKey &new_db_key, const DbKey &old_db_key) { // fast path { PerfWarningTimer perf("open database", 0.05); auto r_db = open_with_key(path, allow_creation, new_db_key); if (r_db.is_ok()) { return r_db; } } PerfWarningTimer perf("change database key", 0.5); auto create_database = [](CSlice tmp_path) -> Status { TRY_STATUS(destroy(tmp_path)); SqliteDb db; return db.init(tmp_path, true); }; TRY_RESULT(db, open_with_key(path, false, old_db_key)); TRY_RESULT(user_version, db.user_version()); auto new_key = db_key_to_sqlcipher_key(new_db_key); if (old_db_key.is_empty() && !new_db_key.is_empty()) { LOG(DEBUG) << "ENCRYPT"; PerfWarningTimer timer("Encrypt SQLite database", 0.1); auto tmp_path = path.str() + ".encrypted"; TRY_STATUS(create_database(tmp_path)); // make sure that database is not empty TRY_STATUS(db.exec("CREATE TABLE IF NOT EXISTS encryption_dummy_table(id INT PRIMARY KEY)")); TRY_STATUS(db.exec(PSLICE() << "ATTACH DATABASE '" << quote_string(tmp_path) << "' AS encrypted KEY " << new_key)); TRY_STATUS(db.exec("SELECT sqlcipher_export('encrypted')")); TRY_STATUS(db.exec(PSLICE() << "PRAGMA encrypted.user_version = " << user_version)); TRY_STATUS(db.exec("DETACH DATABASE encrypted")); db.close(); TRY_STATUS(rename(tmp_path, path)); } else if (!old_db_key.is_empty() && new_db_key.is_empty()) { LOG(DEBUG) << "DECRYPT"; PerfWarningTimer timer("Decrypt SQLite database", 0.1); auto tmp_path = path.str() + ".encrypted"; TRY_STATUS(create_database(tmp_path)); TRY_STATUS(db.exec(PSLICE() << "ATTACH DATABASE '" << quote_string(tmp_path) << "' AS decrypted KEY ''")); TRY_STATUS(db.exec("SELECT sqlcipher_export('decrypted')")); TRY_STATUS(db.exec(PSLICE() << "PRAGMA decrypted.user_version = " << user_version)); TRY_STATUS(db.exec("DETACH DATABASE decrypted")); db.close(); TRY_STATUS(rename(tmp_path, path)); } else { LOG(DEBUG) << "REKEY"; PerfWarningTimer timer("Rekey SQLite database", 0.1); TRY_STATUS(db.exec(PSLICE() << "PRAGMA rekey = " << new_key)); } TRY_RESULT(new_db, open_with_key(path, false, new_db_key)); CHECK(new_db.user_version().ok() == user_version); return std::move(new_db); } Status SqliteDb::destroy(Slice path) { return detail::RawSqliteDb::destroy(path); } Result SqliteDb::get_statement(CSlice statement) { sqlite3_stmt *stmt = nullptr; auto rc = sqlite3_prepare_v2(get_native(), statement.c_str(), static_cast(statement.size()) + 1, &stmt, nullptr); if (rc != SQLITE_OK) { return Status::Error(PSLICE() << "Failed to prepare SQLite " << tag("statement", statement) << raw_->last_error()); } LOG_CHECK(stmt != nullptr) << statement; return SqliteStatement(stmt, raw_); } } // namespace td