Skip to content

[WIP] Add SQLite database #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions cmake/StaticBuild.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
35 changes: 35 additions & 0 deletions include/session/database/connection.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <string>
#include <functional>
#include <memory>

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<void(sqlite3_stmt*)> callback);
};

} // namespace session::database
13 changes: 13 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
115 changes: 115 additions & 0 deletions src/database/connection.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#include "session/database/connection.hpp"
#include <oxen/log.hpp>
#include <oxenc/hex.h>
#include <stdexcept>

#define SQLITE_HAS_CODEC
#if(APPLE)
#define SQLCIPHER_CRYPTO_COMMONCRYPTO
#endif
#include <sqlcipher/sqlite3.h>

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<void(sqlite3_stmt*)> 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
8 changes: 8 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
22 changes: 22 additions & 0 deletions tests/test_database.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#include <oxenc/hex.h>

#include <catch2/catch_test_macros.hpp>
#include <session/util.hpp>
#include <sqlcipher/sqlite3.h>
#include <iostream>

#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<const char*>(sqlite3_column_text(stmt, 0));
std::string name = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
std::cout << "RAWR " + id + ", " + name << std::endl;
});
}