From c1421c3aa77d0b13805943d452163561fdf5bca2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 19 Mar 2025 13:29:00 +1100 Subject: [PATCH] Added SQLCipher dependency, started testing database access --- CMakeLists.txt | 3 + cmake/StaticBuild.cmake | 41 +++++++++ include/session/database/connection.hpp | 35 ++++++++ src/CMakeLists.txt | 13 +++ src/database/connection.cpp | 115 ++++++++++++++++++++++++ tests/CMakeLists.txt | 8 ++ tests/test_database.cpp | 22 +++++ 7 files changed, 237 insertions(+) create mode 100644 include/session/database/connection.hpp create mode 100644 src/database/connection.cpp create mode 100644 tests/test_database.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 79cfcd5..56bdea6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,9 @@ option(USE_LTO "Use Link-Time Optimization" ${use_lto_default}) # Provide this as an option for now because GMP and Desktop are sometimes unhappy with each other. option(ENABLE_ONIONREQ "Build with onion request functionality" ON) +# Provide this as an option for now so clients that don't use any database logic can exclude it. +option(ENABLE_DATABASE "Build with database functionality" ON) + if(USE_LTO) include(CheckIPOSupported) check_ipo_supported(RESULT IPO_ENABLED OUTPUT ipo_error) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 49863f8..7e4ac1d 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -5,6 +5,14 @@ set(LOCAL_MIRROR "" CACHE STRING "local mirror path/URL for lib downloads") +set(SQLCIPHER_VERSION v4.6.1 CACHE STRING "sqlcipher version") +set(SQLCIPHER_MIRROR ${LOCAL_MIRROR} https://github.com/sqlcipher/sqlcipher/archive/refs/tags/${SQLCIPHER_VERSION} + CACHE STRING "sqlcipher mirror(s)") +set(SQLCIPHER_SOURCE ${SQLCIPHER_VERSION}.tar.gz) +set(SQLCIPHER_HASH SHA512=023b2fc7248fe38b758ef93dd8436677ff0f5d08b1061e7eab0adb9e38ad92d523e0ab69016ee69bd35c1fd53c10f61e99b01f7a2987a1f1d492e1f7216a0a9c + CACHE STRING "sqlcipher source hash") + + include(ExternalProject) set(DEPS_DESTDIR ${CMAKE_BINARY_DIR}/static-deps) @@ -222,3 +230,36 @@ endif() if(MINGW) link_libraries(-Wl,-Bstatic -lpthread) endif() + +# SQLCipher configuration +if(ENABLE_DATABASE) + set(sqlcipher_extra_configure) + set(sqlcipher_extra_cflags) + set(sqlcipher_extra_ldflags) + + if(APPLE) + # On macOS, use CommonCrypto (installed by default). + set(sqlcipher_extra_configure "--with-crypto-lib=commoncrypto") + set(sqlcipher_extra_cflags " -DSQLITE_HAS_CODEC -DSQLITE_TEMP_STORE=3 -DSQLITE_ENABLE_FTS5 -DSQLCIPHER_CRYPTO_COMMONCRYPTO") + set(sqlcipher_extra_ldflags " -framework Security -framework Foundation -framework CoreFoundation") + else() + # On Linux, Windows, etc., use OpenSSL. + find_package(OpenSSL REQUIRED) + set(sqlcipher_extra_cflags " -DSQLITE_HAS_CODEC -DSQLITE_TEMP_STORE=3 -DSQLITE_ENABLE_FTS5 -DSQLCIPHER_CRYPTO_OPENSSL") + endif() + + build_external(sqlcipher + CONFIGURE_COMMAND ./configure ${build_host} --disable-shared --prefix=${DEPS_DESTDIR} --with-pic --enable-fts5 --enable-static ${sqlcipher_extra_configure} + "CC=${deps_cc}" "CXX=${deps_cxx}" "CFLAGS=${deps_CFLAGS}${apple_cflags_arch}${sqlcipher_extra_cflags}" "CXXFLAGS=${deps_CXXFLAGS}${apple_cxxflags_arch}" + "LDFLAGS=-L${DEPS_DESTDIR}/lib${apple_ldflags_arch}${sqlcipher_extra_ldflags}" ${cross_rc} CC_FOR_BUILD=cc CPP_FOR_BUILD=cpp + "--disable-tcl" "--disable-readline" + ) + add_static_target(sqlcipher::sqlcipher sqlcipher_external libsqlcipher.a) + + if(APPLE) + add_library(sqlcipher_frameworks INTERFACE) + target_link_libraries(sqlcipher_frameworks INTERFACE "-framework CoreFoundation" "-framework Security" "-framework Foundation") + add_dependencies(sqlcipher::sqlcipher sqlcipher_frameworks) + target_link_libraries(sqlcipher::sqlcipher INTERFACE sqlcipher_frameworks) + endif() +endif() \ No newline at end of file diff --git a/include/session/database/connection.hpp b/include/session/database/connection.hpp new file mode 100644 index 0000000..6ae4b68 --- /dev/null +++ b/include/session/database/connection.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +struct sqlite3; +struct sqlite3_stmt; + +namespace session::database { + +class Connection { +private: + sqlite3* db_{nullptr}; + std::string key_; + +public: + Connection(const std::string& path, const std::string& key); + ~Connection(); + + // Prevent copying + Connection(const Connection&) = delete; + Connection& operator=(const Connection&) = delete; + + // Allow moving + Connection(Connection&& other) noexcept; + Connection& operator=(Connection&& other) noexcept; + + sqlite3* handle() { return db_; } + + void exec(const std::string& sql); + void query(const std::string& sql, std::function callback); +}; + +} // namespace session::database \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f5f2c28..5707e92 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -118,6 +118,19 @@ if(ENABLE_ONIONREQ) ) endif() +if(ENABLE_DATABASE) + add_libsession_util_library(database + database/connection.cpp + ) + + target_link_libraries(database + PUBLIC + util + PRIVATE + sqlcipher::sqlcipher + ) +endif() + if(WARNINGS_AS_ERRORS AND NOT USE_LTO AND CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION MATCHES "^11\\.") # GCC 11 has an overzealous (and false) stringop-overread warning, but only when LTO is off. # Um, yeah. diff --git a/src/database/connection.cpp b/src/database/connection.cpp new file mode 100644 index 0000000..9ea37cd --- /dev/null +++ b/src/database/connection.cpp @@ -0,0 +1,115 @@ +#include "session/database/connection.hpp" +#include +#include +#include + +#define SQLITE_HAS_CODEC +#if(APPLE) +#define SQLCIPHER_CRYPTO_COMMONCRYPTO +#endif +#include + +namespace session::database { + +Connection::Connection(const std::string& path, const std::string& key) : key_(key) { + int rc = sqlite3_open(path.c_str(), &db_); + + if (rc != SQLITE_OK) { + std::string error = sqlite3_errmsg(db_); + sqlite3_close(db_); + db_ = nullptr; + throw std::runtime_error("Failed to open database: " + error); + } + + // Set encryption key + if (!key_.empty()) { + std::string formatted_key = "x'" + key_ + "'"; + rc = sqlite3_key(db_, formatted_key.c_str(), formatted_key.size()); + + if (rc != SQLITE_OK) { + std::string error = sqlite3_errmsg(db_); + sqlite3_close(db_); + db_ = nullptr; + throw std::runtime_error("Failed to set encryption key: " + error); + } + } + + // According to the SQLCipher docs iOS needs the 'cipher_plaintext_header_size' value set to at least + // 32 as iOS extends special privileges to the database and needs this header to be in plaintext + // to determine the file type + // + // For more info see: https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_plaintext_header_size + rc = sqlite3_exec(db_, "PRAGMA cipher_plaintext_header_size = 32", nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) { + std::string error = sqlite3_errmsg(db_); + sqlite3_close(db_); + db_ = nullptr; + throw std::runtime_error("Failed to configure database: " + error); + } + + // Verify the key works by reading the sqlite_master table + rc = sqlite3_exec(db_, "SELECT count(*) FROM sqlite_master", nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) { + std::string error = sqlite3_errmsg(db_); + sqlite3_close(db_); + db_ = nullptr; + throw std::runtime_error("Failed to decrypt database: " + error); + } + + // oxen::log::debug("Database connection established successfully"); +} + +Connection::~Connection() { + if (db_) { + sqlite3_close(db_); + db_ = nullptr; + } +} + +Connection::Connection(Connection&& other) noexcept + : db_(other.db_), key_(std::move(other.key_)) { + other.db_ = nullptr; +} + +Connection& Connection::operator=(Connection&& other) noexcept { + if (this != &other) { + if (db_) sqlite3_close(db_); + db_ = other.db_; + key_ = std::move(other.key_); + other.db_ = nullptr; + } + return *this; +} + +void Connection::exec(const std::string& sql) { + char* error_msg = nullptr; + int rc = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &error_msg); + + if (rc != SQLITE_OK) { + std::string error = error_msg ? error_msg : "unknown error"; + sqlite3_free(error_msg); + throw std::runtime_error("SQL execution failed: " + error); + } +} + +void Connection::query(const std::string& sql, std::function callback) { + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr); + + if (rc != SQLITE_OK) { + throw std::runtime_error("Failed to prepare statement: " + std::string(sqlite3_errmsg(db_))); + } + + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + callback(stmt); + } + + if (rc != SQLITE_DONE) { + sqlite3_finalize(stmt); + throw std::runtime_error("Error executing query: " + std::string(sqlite3_errmsg(db_))); + } + + sqlite3_finalize(stmt); +} + +} // namespace session::database \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7ef8063..3f1511d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -34,6 +34,10 @@ if (ENABLE_ONIONREQ) list(APPEND LIB_SESSION_UTESTS_SOURCES test_onionreq.cpp) endif() +if (ENABLE_DATABASE) + list(APPEND LIB_SESSION_UTESTS_SOURCES test_database.cpp) +endif() + add_library(test_libs INTERFACE) target_link_libraries(test_libs INTERFACE @@ -48,6 +52,10 @@ else() target_compile_definitions(test_libs INTERFACE DISABLE_ONIONREQ) endif() +if (ENABLE_DATABASE) + target_link_libraries(test_libs INTERFACE libsession::database) +endif() + add_executable(testAll main.cpp ${LIB_SESSION_UTESTS_SOURCES}) target_link_libraries(testAll PRIVATE test_libs diff --git a/tests/test_database.cpp b/tests/test_database.cpp new file mode 100644 index 0000000..9662a3f --- /dev/null +++ b/tests/test_database.cpp @@ -0,0 +1,22 @@ +#include + +#include +#include +#include +#include + +#include "session/database/connection.hpp" +#include "utils.hpp" + +const std::string test_db_path = ""; +const std::string test_db_key = ""; + +TEST_CASE("Database", "[database][open]") { + auto db = session::database::Connection(test_db_path, test_db_key); + + db.query("SELECT id, name FROM profile", [&](sqlite3_stmt* stmt) { + std::string id = reinterpret_cast(sqlite3_column_text(stmt, 0)); + std::string name = reinterpret_cast(sqlite3_column_text(stmt, 1)); + std::cout << "RAWR " + id + ", " + name << std::endl; + }); +}