diff --git a/include/SQLiteCpp/Database.h b/include/SQLiteCpp/Database.h index 70ca2e5..0129809 100644 --- a/include/SQLiteCpp/Database.h +++ b/include/SQLiteCpp/Database.h @@ -12,7 +12,7 @@ #include -#include +#include // Forward declarations to avoid inclusion of in a header struct sqlite3; @@ -369,6 +369,51 @@ public: */ void loadExtension(const char* apExtensionName, const char* apEntryPointName); + /** + * @brief Set the key for the current sqlite database instance. + * + * This is the equivalent of the sqlite3_key call and should thus be called + * directly after opening the database. + * Open encrypted database -> call db.key("secret") -> database ready + * + * @param[in] aKey Key to decode/encode the database + * + * @throw SQLite::Exception in case of error + */ + void key(const std::string& aKey) const; + + /** + * @brief Reset the key for the current sqlite database instance. + * + * This is the equivalent of the sqlite3_rekey call and should thus be called + * after the database has been opened with a valid key. To decrypt a + * database, call this method with an empty string. + * Open normal database -> call db.rekey("secret") -> encrypted database, database ready + * Open encrypted database -> call db.key("secret") -> call db.rekey("newsecret") -> change key, database ready + * Open encrypted database -> call db.key("secret") -> call db.rekey("") -> decrypted database, database ready + * + * @param[in] aNewKey New key to encode the database + * + * @throw SQLite::Exception in case of error + */ + void rekey(const std::string& aNewKey) const; + + /** + * @brief Test if a file contains an unencrypted database. + * + * This is a simple test that reads the first bytes of a database file and + * compares them to the standard header for unencrypted databases. If the + * header does not match the standard string, we assume that we have an + * encrypted file. + * + * @param[in] aFilename path/uri to a file + * + * @return true if the database has the standard header. + * + * @throw SQLite::Exception in case of error + */ + static const bool isUnencrypted(const std::string& aFilename); + private: /// @{ Database must be non-copyable Database(const Database&); diff --git a/src/Database.cpp b/src/Database.cpp index f640218..cfa5447 100644 --- a/src/Database.cpp +++ b/src/Database.cpp @@ -15,6 +15,8 @@ #include #include +#include +#include #ifndef SQLITE_DETERMINISTIC #define SQLITE_DETERMINISTIC 0x800 @@ -62,7 +64,6 @@ Database::Database(const char* apFilename, sqlite3_close(mpSQLite); // close is required even in case of error on opening throw exception; } - if (aBusyTimeoutMs > 0) { setBusyTimeout(aBusyTimeoutMs); @@ -84,7 +85,6 @@ Database::Database(const std::string& aFilename, sqlite3_close(mpSQLite); // close is required even in case of error on opening throw exception; } - if (aBusyTimeoutMs > 0) { setBusyTimeout(aBusyTimeoutMs); @@ -108,8 +108,8 @@ Database::~Database() noexcept // nothrow * @brief Set a busy handler that sleeps for a specified amount of time when a table is locked. * * This is useful in multithreaded program to handle case where a table is locked for writting by a thread. - * Any other thread cannot access the table and will receive a SQLITE_BUSY error: - * setting a timeout will wait and retry up to the time specified before returning this SQLITE_BUSY error. + * Any other thread cannot access the table and will receive a SQLITE_BUSY error: + * setting a timeout will wait and retry up to the time specified before returning this SQLITE_BUSY error. * Reading the value of timeout for current connection can be done with SQL query "PRAGMA busy_timeout;". * Default busy timeout is 0ms. * @@ -229,4 +229,59 @@ void Database::loadExtension(const char* apExtensionName, const char *apEntryPoi #endif } +// Set the key for the current sqlite database instance. +void Database::key(const std::string& aKey) const +{ + int pass_len = aKey.length(); +#ifdef SQLITE_HAS_CODEC + if (pass_len > 0) { + const int ret = sqlite3_key(mpSQLite, aKey.c_str(), pass_len); + check(ret); + } +#else // SQLITE_HAS_CODEC + if (pass_len > 0) { + const SQLite::Exception exception("No encryption support, recompile with SQLITE_HAS_CODEC to enable."); + throw exception; + } +#endif // SQLITE_HAS_CODEC +} + +// Reset the key for the current sqlite database instance. +void Database::rekey(const std::string& aNewKey) const +{ +#ifdef SQLITE_HAS_CODEC + int pass_len = aNewKey.length(); + if (pass_len > 0) { + const int ret = sqlite3_rekey(mpSQLite, aNewKey.c_str(), pass_len); + check(ret); + } else { + const int ret = sqlite3_rekey(mpSQLite, nullptr, 0); + check(ret); + } +#else // SQLITE_HAS_CODEC + const SQLite::Exception exception("No encryption support, recompile with SQLITE_HAS_CODEC to enable."); + throw exception; +#endif // SQLITE_HAS_CODEC +} + +// Test if a file contains an unencrypted database. +const bool Database::isUnencrypted(const std::string& aFilename) +{ + if (aFilename.length() > 0) { + std::ifstream fileBuffer(aFilename.c_str(), std::ios::in | std::ios::binary); + char header[16]; + if (fileBuffer.is_open()) { + fileBuffer.seekg(0, std::ios::beg); + fileBuffer.getline(header, 16); + fileBuffer.close(); + } else { + const SQLite::Exception exception("Error opening file: " + aFilename); + throw exception; + } + return strncmp(header, "SQLite format 3\000", 16) == 0; + } + const SQLite::Exception exception("Could not open database, the aFilename parameter was empty."); + throw exception; +} + } // namespace SQLite diff --git a/tests/Database_test.cpp b/tests/Database_test.cpp index e38c101..b42e89c 100644 --- a/tests/Database_test.cpp +++ b/tests/Database_test.cpp @@ -258,3 +258,72 @@ TEST(Database, execException) { // TODO: test Database::createFunction() // TODO: test Database::loadExtension() + +#ifdef SQLITE_HAS_CODEC +TEST(Database, encryptAndDecrypt) { + remove("test.db3"); + { + // Try to open the non-existing database + EXPECT_THROW(SQLite::Database not_found("test.db3"), SQLite::Exception); + + // Create a new database + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); + EXPECT_FALSE(db.tableExists("test")); + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); + EXPECT_TRUE(db.tableExists("test")); + } // Close DB test.db3 + { + // Reopen the database file and encrypt it + EXPECT_TRUE(SQLite::Database::isUnencrypted("test.db3")); + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); + // Encrypt the database + db.rekey("123secret"); + } // Close DB test.db3 + { + // Reopen the database file and try to use it + EXPECT_FALSE(SQLite::Database::isUnencrypted("test.db3")); + SQLite::Database db("test.db3", SQLite::OPEN_READONLY); + EXPECT_THROW(db.tableExists("test"), SQLite::Exception); + db.key("123secret"); + EXPECT_TRUE(db.tableExists("test")); + } // Close DB test.db3 + { + // Reopen the database file and decrypt it + EXPECT_FALSE(SQLite::Database::isUnencrypted("test.db3")); + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); + // Decrypt the database + db.key("123secret"); + db.rekey(""); + } // Close DB test.db3 + { + // Reopen the database file and use it + EXPECT_TRUE(SQLite::Database::isUnencrypted("test.db3")); + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); + EXPECT_TRUE(db.tableExists("test")); + } // Close DB test.db3 + remove("test.db3"); +} +#else // SQLITE_HAS_CODEC +TEST(Database, encryptAndDecrypt) { + remove("test.db3"); + { + // Try to open the non-existing database + EXPECT_THROW(SQLite::Database not_found("test.db3"), SQLite::Exception); + + // Create a new database + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); + EXPECT_FALSE(db.tableExists("test")); + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); + EXPECT_TRUE(db.tableExists("test")); + } // Close DB test.db3 + { + // Reopen the database file and encrypt it + EXPECT_TRUE(SQLite::Database::isUnencrypted("test.db3")); + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); + // Encrypt the database + EXPECT_THROW(db.key("123secret"), SQLite::Exception); + EXPECT_THROW(db.rekey("123secret"), SQLite::Exception); + } // Close DB test.db3 + remove("test.db3"); +} +#endif // SQLITE_HAS_CODEC \ No newline at end of file