From 9ba36d02b2b25c782c74857c190a6fbb028ed5cb Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Sat, 1 Nov 2014 12:49:14 +0100 Subject: [PATCH] Add initial SQLCipher support Add some basic initial support for SQLCipher. Note that this is more of a POC than a final implementation. This commit adds an option called 'sqlcipher' to the cmake and qmake projects which - when enabled - replaces the default SQLite3 include and library files by their SQLCipher counter-parts. Especially on MacOS X there might be some more work required in finding the correct include paths. The SQLCipher library supports unencrypted databases, too, so even if the option is enabled the program behaves like before. You can see the difference, though, in the About Dialog where the SQLite version string will say 'SQLCipher version xy'. When the sqlcipher option is enabled and you try to open a file which is neither a project file nor a normal SQLite3 database it is assumed now that the file is an encypted database. There is no way to tell between an invalid file and an encypted file, so in both cases a password dialog pops up. When the correct password and page size are entered the file is opened and can be edited like any other database before. Creating encrypted databases isn't supported yet. So for testing you need to fall back to the sqlcipher command line tool. See issue #12. --- CMakeLists.txt | 15 +++++++-- src/AboutDialog.cpp | 6 +++- src/ExportCsvDialog.cpp | 2 +- src/ImportCsvDialog.cpp | 2 +- src/MainWindow.cpp | 28 ++++++++++------- src/sqlite.h | 12 +++++++ src/sqlitedb.cpp | 67 +++++++++++++++++++++++++++++----------- src/sqlitetablemodel.cpp | 2 +- src/src.pro | 12 +++++-- 9 files changed, 107 insertions(+), 39 deletions(-) create mode 100644 src/sqlite.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 19ee9532..5ae40fa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ endif() set(SQLB_HDR src/gen_version.h src/sqlitetypes.h - src/csvparser.h + src/csvparser.h src/grammar/sqlite3TokenTypes.hpp src/grammar/Sqlite3Lexer.hpp src/grammar/Sqlite3Parser.hpp @@ -54,6 +54,7 @@ set(SQLB_MOC_HDR src/sqltextedit.h src/DbStructureModel.h src/Application.h + src/sqlite.h ) set(SQLB_SRC @@ -179,13 +180,21 @@ if(APPLE) add_definitions(-DCHECKNEWVERSION) endif(APPLE) +# SQLCipher option +if(sqlcipher) + add_definitions(-DSQLCIPHER) + set(LIBSQLITE_NAME sqlcipher) +else(sqlcipher) + set(LIBSQLITE_NAME sqlite3) +endif(sqlcipher) + # add extra library path for MacOS and FreeBSD set(EXTRAPATH APPLE OR ${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") if(EXTRAPATH) - find_library(LIBSQLITE sqlite3 HINTS /usr/local/lib /usr/local/opt/sqlite/lib) + find_library(LIBSQLITE ${LIBSQLITE_NAME} HINTS /usr/local/lib /usr/local/opt/sqlite/lib) set(ADDITIONAL_INCLUDE_PATHS /usr/local/include /usr/local/opt/sqlite/include) else(EXTRAPATH) - find_library(LIBSQLITE sqlite3) + find_library(LIBSQLITE ${LIBSQLITE_NAME}) endif(EXTRAPATH) include_directories( diff --git a/src/AboutDialog.cpp b/src/AboutDialog.cpp index d41519a0..39e4300b 100644 --- a/src/AboutDialog.cpp +++ b/src/AboutDialog.cpp @@ -1,7 +1,7 @@ #include "AboutDialog.h" #include "ui_AboutDialog.h" #include "gen_version.h" -#include +#include "sqlite.h" AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), @@ -12,7 +12,11 @@ AboutDialog::AboutDialog(QWidget *parent) : ui->label_version->setText(ui->label_version->text() + " " + APP_VERSION); ui->label_versionqt->setText(ui->label_versionqt->text() + " " + QT_VERSION_STR); +#ifdef SQLCIPHER + ui->label_versionsqlite->setText(ui->label_versionsqlite->text().replace("SQLite", "SQLCipher") + " " + SQLITE_VERSION); +#else ui->label_versionsqlite->setText(ui->label_versionsqlite->text() + " " + SQLITE_VERSION); +#endif } AboutDialog::~AboutDialog() diff --git a/src/ExportCsvDialog.cpp b/src/ExportCsvDialog.cpp index 03a8e67f..7ed1a8ed 100644 --- a/src/ExportCsvDialog.cpp +++ b/src/ExportCsvDialog.cpp @@ -3,12 +3,12 @@ #include "sqlitedb.h" #include "PreferencesDialog.h" #include "sqlitetablemodel.h" +#include "sqlite.h" #include #include #include #include -#include ExportCsvDialog::ExportCsvDialog(DBBrowserDB* db, QWidget* parent, const QString& query, const QString& selection) : QDialog(parent), diff --git a/src/ImportCsvDialog.cpp b/src/ImportCsvDialog.cpp index 5d442ac8..fc07dc2b 100644 --- a/src/ImportCsvDialog.cpp +++ b/src/ImportCsvDialog.cpp @@ -2,6 +2,7 @@ #include "ui_ImportCsvDialog.h" #include "sqlitedb.h" #include "csvparser.h" +#include "sqlite.h" #include #include @@ -10,7 +11,6 @@ #include #include #include -#include #include #include #include diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 4917acae..3c028a13 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -15,6 +15,7 @@ #include "VacuumDialog.h" #include "DbStructureModel.h" #include "gen_version.h" +#include "sqlite.h" #include #include @@ -28,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -195,23 +195,30 @@ bool MainWindow::fileOpen(const QString& fileName, bool dontAddToRecentFiles) if(QFile::exists(wFile) ) { fileClose(); - if(db.open(wFile)) + // Try opening it as a project file first + if(loadProject(wFile)) { - statusEncodingLabel->setText(db.getPragma("encoding")); - setCurrentFile(wFile); - if(!dontAddToRecentFiles) - addToRecentFilesMenu(wFile); retval = true; } else { - // Failed opening file; so it might be a project file instead - return loadProject(wFile); + // No project file; so it should be a database file + if(db.open(wFile)) + { + statusEncodingLabel->setText(db.getPragma("encoding")); + setCurrentFile(wFile); + if(!dontAddToRecentFiles) + addToRecentFilesMenu(wFile); + openSqlTab(true); + retval = true; + } else { + QMessageBox::warning(this, qApp->applicationName(), tr("Invalid file format.")); + return false; + } } loadExtensionsFromSettings(); populateStructure(); resetBrowser(); if(ui->mainTab->currentIndex() == 2) loadPragmas(); - openSqlTab(true); } return retval; @@ -1749,10 +1756,7 @@ bool MainWindow::loadProject(QString filename) xml.readNext(); // token == QXmlStreamReader::StartDocument xml.readNext(); // name == sqlb_project if(xml.name() != "sqlb_project") - { - QMessageBox::warning(this, qApp->applicationName(), tr("Invalid file format.")); return false; - } addToRecentFilesMenu(filename); diff --git a/src/sqlite.h b/src/sqlite.h new file mode 100644 index 00000000..d4945f85 --- /dev/null +++ b/src/sqlite.h @@ -0,0 +1,12 @@ +#ifndef SQLITE_H +#define SQLITE_H + +#ifdef SQLCIPHER + #define SQLITE_TEMP_STORE 2 + #define SQLITE_HAS_CODEC + #include +#else + #include +#endif + +#endif diff --git a/src/sqlitedb.cpp b/src/sqlitedb.cpp index 95ec31bc..2a82d56b 100644 --- a/src/sqlitedb.cpp +++ b/src/sqlitedb.cpp @@ -1,5 +1,6 @@ #include "sqlitedb.h" #include "sqlitetablemodel.h" +#include "sqlite.h" #include #include @@ -7,7 +8,7 @@ #include #include #include -#include +#include // collation callbacks int collCompare(void* /*pArg*/, int /*eTextRepA*/, const void* sA, int /*eTextRepB*/, const void* sB) @@ -43,32 +44,62 @@ bool DBBrowserDB::getDirty() const return !savepointList.empty(); } -bool DBBrowserDB::open ( const QString & db) +bool DBBrowserDB::open(const QString& db) { bool ok=false; int err; if (isOpen()) close(); - //try to verify the SQLite version 3 file header - QFile dbfile(db); - if ( dbfile.open( QIODevice::ReadOnly ) ) { - char buffer[16+1]; - dbfile.readLine(buffer, 16); - QString contents = QString(buffer); - dbfile.close(); - if (!contents.startsWith("SQLite format 3")) { - lastErrorMessage = QObject::tr("File is not a SQLite 3 database"); - return false; - } - } else { - lastErrorMessage = QObject::tr("File could not be read"); + lastErrorMessage = QObject::tr("no error"); + + // Open database file + if(sqlite3_open_v2(db.toUtf8(), &_db, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK) + { + lastErrorMessage = QString::fromUtf8((const char*)sqlite3_errmsg(_db)); return false; } - lastErrorMessage = QObject::tr("no error"); - - err = sqlite3_open_v2(db.toUtf8(), &_db, SQLITE_OPEN_READWRITE, NULL); + // Try reading from database + bool done = false; + do + { + QString statement = "SELECT COUNT(*) FROM sqlite_master;"; + QByteArray utf8Statement = statement.toUtf8(); + sqlite3_stmt* vm; + const char* tail; + err = sqlite3_prepare_v2(_db, utf8Statement, utf8Statement.length(), &vm, &tail); + if(sqlite3_step(vm) != SQLITE_ROW) + { +#ifdef SQLCIPHER + QString pass = QInputDialog::getText(0, qApp->applicationName(), + QObject::tr("Couldn't read from database file. This means it is either not a valid SQLite3 " + "database or it is encrypted.\nIn the latter case you can provide a passphrase to open the file. Note" + "that only databases encrypted using SQLCipher are supported.")); + if(pass.isEmpty()) + { + sqlite3_close(_db); + _db = 0; + return false; + } else { + int pagesize = QInputDialog::getInt(0, qApp->applicationName(), + QObject::tr("If the database has been encrypted using a different page size than normally (i.e. 1024 bytes) this " + "value is required, too."), + 1024, 0); + if(pagesize == 0) pagesize = 1024; + sqlite3_key(_db, pass.toUtf8(), pass.toUtf8().length()); + sqlite3_exec(_db, QString("PRAGMA cipher_page_size = %1;").arg(pagesize).toUtf8(), NULL, NULL, NULL); + } +#else + sqlite3_close(_db); + _db = 0; + return false; +#endif + } else { + done = true; + } + sqlite3_finalize(vm); + } while(!done); // register collation callback sqlite3_collation_needed(_db, NULL, collation_needed); diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index 26ced406..2af27dd8 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -1,10 +1,10 @@ #include "sqlitetablemodel.h" #include "sqlitedb.h" +#include "sqlite.h" #include #include #include -#include SqliteTableModel::SqliteTableModel(QObject* parent, DBBrowserDB* db, size_t chunkSize) : QAbstractTableModel(parent) diff --git a/src/src.pro b/src/src.pro index e14f6b9e..ee481803 100644 --- a/src/src.pro +++ b/src/src.pro @@ -42,7 +42,8 @@ HEADERS += \ SqlExecutionArea.h \ VacuumDialog.h \ DbStructureModel.h \ - Application.h + Application.h \ + sqlite.h SOURCES += \ sqlitedb.cpp \ @@ -88,6 +89,13 @@ TRANSLATIONS += \ translations/sqlb_fr.ts \ translations/sqlb_ru.ts +CONFIG(sqlcipher) { + QMAKE_CXXFLAGS += -DENABLE_SQLCIPHER + LIBS += -lsqlcipher +} else { + LIBS += -lsqlite3 +} + LIBPATH_QHEXEDIT=$$PWD/../libs/qhexedit LIBPATH_ANTLR=$$PWD/../libs/antlr-2.7.7 LIBPATH_QCUSTOMPLOT=$$PWD/../libs/qcustomplot-source @@ -121,7 +129,7 @@ mac { UI_DIR = .ui INCLUDEPATH += $$PWD/../libs/antlr-2.7.7 $$PWD/../libs/qhexedit $$PWD/../libs/qcustomplot-source $$PWD/.. -LIBS += -L$$LIBPATH_QHEXEDIT -L$$LIBPATH_ANTLR -L$$LIBPATH_QCUSTOMPLOT -lantlr -lqhexedit -lqcustomplot -lsqlite3 +LIBS += -L$$LIBPATH_QHEXEDIT -L$$LIBPATH_ANTLR -L$$LIBPATH_QCUSTOMPLOT -lantlr -lqhexedit -lqcustomplot DEPENDPATH += $$PWD/../libs/antlr-2.7.7 $$PWD/../libs/qhexedit $$PWD/../libs/qcustomplot-source # Rules for creating/updating {ts|qm}-files