Add automatic crypted databases open via dotenvs (#1404)

* Rename confusing variables

* Fix some project warnings

* Fix code style

* Add constant for the default page size

* Move KeyFormats enum to CipherSettings

* Fix code style

* Fix memory leak

* Stop relying on CipherDialog for encryption settings management

* Fix code style

* Add .env format for QSettings

* Add automatic crypted databases open via dotenvs

This adds support for `.env` files next to the crypted databases that
are to be opened that contains the needed cipher settings.

The only required one is the plain-text password as a value for the key
with the name of the database like this:

    myCryptedDatabase.sqlite = MyPassword

This way, databases with a different extension are supported too:

    myCryptedDatabase.db = MyPassword

You can also specify a custom page size adding a different line
(anywhere in the file) like this:

    myCryptedDatabase.db_pageSize = 2048

If not specified, `1024` is used.

You can also specify the format of the specified key using the
associated integer id:

    anotherCryptedDatabase.sqlite = 0xCAFEBABE
    anotherCryptedDatabase.sqlite_keyFormat = 1

where `1` means a Raw key. If not specified, `0` is used, which means a
simple text Passphrase.

Dotenv files (`.env`) are already used on other platforms and by
different tools to manage environment variables, and it's recommended
to be ignored from version control systems, so they won't leak.

* Add new files to CMakeLists

* Move DotenvFormat include to the implementation

* Fix build error

* Remove superfluous method

(related to ac51c23)

* Remove superfluous checks

* Fix memory leaks

(introduced by 94bbb46)

* Fix code style

* Make dotenv related variable and comment clearer

* Remove duplicated code

* Remove unused forward declaration

(introduced by e5a0293)
This commit is contained in:
Iulian Onofrei
2018-07-10 21:46:17 +03:00
committed by Martin Kleusberg
parent c861f1b9d9
commit 3cdc65a63f
13 changed files with 267 additions and 76 deletions

View File

@@ -49,34 +49,33 @@ CipherDialog::~CipherDialog()
delete ui;
}
CipherDialog::KeyFormats CipherDialog::keyFormat() const
CipherSettings CipherDialog::getCipherSettings() const
{
return static_cast<CipherDialog::KeyFormats>(ui->comboKeyFormat->currentIndex());
}
CipherSettings::KeyFormats keyFormat = CipherSettings::getKeyFormat(ui->comboKeyFormat->currentIndex());
QString password = ui->editPassword->text();
int pageSize = ui->comboPageSize->itemData(ui->comboPageSize->currentIndex()).toInt();
QString CipherDialog::password() const
{
if(keyFormat() == KeyFormats::Passphrase)
return QString("'%1'").arg(ui->editPassword->text().replace("'", "''"));
else
return QString("\"x'%1'\"").arg(ui->editPassword->text().mid(2)); // Remove the '0x' part at the beginning
}
CipherSettings cipherSettings;
int CipherDialog::pageSize() const
{
return ui->comboPageSize->itemData(ui->comboPageSize->currentIndex()).toInt();
cipherSettings.setKeyFormat(keyFormat);
cipherSettings.setPassword(password);
cipherSettings.setPageSize(pageSize);
return cipherSettings;
}
void CipherDialog::checkInputFields()
{
if(sender() == ui->comboKeyFormat)
{
if(keyFormat() == KeyFormats::Passphrase)
CipherSettings::KeyFormats keyFormat = CipherSettings::getKeyFormat(ui->comboKeyFormat->currentIndex());
if(keyFormat == CipherSettings::KeyFormats::Passphrase)
{
ui->editPassword->setValidator(nullptr);
ui->editPassword2->setValidator(nullptr);
ui->editPassword->setPlaceholderText("");
} else if(keyFormat() == KeyFormats::RawKey) {
} else if(keyFormat == CipherSettings::KeyFormats::RawKey) {
ui->editPassword->setValidator(rawKeyValidator);
ui->editPassword2->setValidator(rawKeyValidator);
ui->editPassword->setPlaceholderText("0x...");

View File

@@ -3,6 +3,8 @@
#include <QDialog>
#include "CipherSettings.h"
class QRegExpValidator;
namespace Ui {
@@ -14,21 +16,12 @@ class CipherDialog : public QDialog
Q_OBJECT
public:
enum KeyFormats
{
Passphrase,
RawKey
};
// Set the encrypt parameter to true when the dialog is used to encrypt a database;
// set it to false if the dialog is used to ask the user for the key to decrypt a file.
explicit CipherDialog(QWidget* parent, bool encrypt);
~CipherDialog();
// Allow read access to the input fields
KeyFormats keyFormat() const;
QString password() const;
int pageSize() const;
CipherSettings getCipherSettings() const;
private:
Ui::CipherDialog* ui;

49
src/CipherSettings.cpp Normal file
View File

@@ -0,0 +1,49 @@
#include "CipherSettings.h"
CipherSettings::KeyFormats CipherSettings::getKeyFormat() const
{
return keyFormat;
}
void CipherSettings::setKeyFormat(const KeyFormats &value)
{
keyFormat = value;
}
QString CipherSettings::getPassword() const
{
if(keyFormat == Passphrase)
{
QString tempPassword = password;
tempPassword.replace("'", "''");
return QString("'%1'").arg(tempPassword);
} else {
// Remove the '0x' part at the beginning
return QString("\"x'%1'\"").arg(password.mid(2));
}
}
void CipherSettings::setPassword(const QString &value)
{
password = value;
}
int CipherSettings::getPageSize() const
{
if (pageSize == 0)
return defaultPageSize;
return pageSize;
}
void CipherSettings::setPageSize(int value)
{
pageSize = value;
}
CipherSettings::KeyFormats CipherSettings::getKeyFormat(int rawKeyFormat)
{
return static_cast<CipherSettings::KeyFormats>(rawKeyFormat);
}

34
src/CipherSettings.h Normal file
View File

@@ -0,0 +1,34 @@
#ifndef CIPHERSETTINGS_H
#define CIPHERSETTINGS_H
#include <QString>
class CipherSettings
{
public:
enum KeyFormats
{
Passphrase,
RawKey
};
static const int defaultPageSize = 1024;
KeyFormats getKeyFormat() const;
void setKeyFormat(const KeyFormats &value);
QString getPassword() const;
void setPassword(const QString &value);
int getPageSize() const;
void setPageSize(int value);
static KeyFormats getKeyFormat(int rawKeyFormat);
private:
KeyFormats keyFormat;
QString password;
int pageSize;
};
#endif // CIPHERSETTINGS_H

28
src/DotenvFormat.cpp Normal file
View File

@@ -0,0 +1,28 @@
#include "DotenvFormat.h"
#include <QRegularExpression>
#include <QTextStream>
bool DotenvFormat::readEnvFile(QIODevice &device, QSettings::SettingsMap &map)
{
QTextStream in(&device);
QString line;
QRegularExpression keyValueRegex("^\\s*([\\w\\.\\-]+)\\s*=\\s*(.*)\\s*$");
while (in.readLineInto(&line)) {
QRegularExpressionMatch match = keyValueRegex.match(line);
if (match.capturedLength() < 3) {
continue;
}
QString key = match.captured(1);
QString value = match.captured(2);
map.insert(key, value);
}
return true;
}

13
src/DotenvFormat.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef DOTENVFORMAT_H
#define DOTENVFORMAT_H
#include <QIODevice>
#include <QSettings>
class DotenvFormat
{
public:
static bool readEnvFile(QIODevice &device, QSettings::SettingsMap &map);
};
#endif // DOTENVFORMAT_H

View File

@@ -2530,8 +2530,8 @@ void MainWindow::updateFilter(int column, const QString& value)
void MainWindow::editEncryption()
{
#ifdef ENABLE_SQLCIPHER
CipherDialog dialog(this, true);
if(dialog.exec())
CipherDialog cipherDialog(this, true);
if(cipherDialog.exec())
{
// Show progress dialog even though we can't provide any detailed progress information but this
// process might take some time.
@@ -2553,14 +2553,16 @@ void MainWindow::editEncryption()
file.close();
}
CipherSettings cipherSettings = cipherDialog.getCipherSettings();
// Attach a new database using the new settings
qApp->processEvents();
if(ok)
ok = db.executeSQL(QString("ATTACH DATABASE '%1' AS sqlitebrowser_edit_encryption KEY %2;").arg(db.currentFile() + ".enctemp").arg(dialog.password()),
ok = db.executeSQL(QString("ATTACH DATABASE '%1' AS sqlitebrowser_edit_encryption KEY %2;").arg(db.currentFile() + ".enctemp").arg(cipherSettings.getPassword()),
false, false);
qApp->processEvents();
if(ok)
ok = db.executeSQL(QString("PRAGMA sqlitebrowser_edit_encryption.cipher_page_size = %1").arg(dialog.pageSize()), false, false);
ok = db.executeSQL(QString("PRAGMA sqlitebrowser_edit_encryption.cipher_page_size = %1").arg(cipherSettings.getPageSize()), false, false);
// Export the current database to the new one
qApp->processEvents();

View File

@@ -2,8 +2,8 @@
#define SETTINGS_H
#include <QApplication>
#include <QVariant>
#include <QHash>
#include <QVariant>
class Settings
{

View File

@@ -2,6 +2,8 @@
#include "sqlite.h"
#include "sqlitetablemodel.h"
#include "CipherDialog.h"
#include "CipherSettings.h"
#include "DotenvFormat.h"
#include "Settings.h"
#include <QFile>
@@ -104,8 +106,8 @@ bool DBBrowserDB::open(const QString& db, bool readOnly)
dontCheckForStructureUpdates = false;
// Get encryption settings for database file
CipherDialog* cipher = nullptr;
if(tryEncryptionSettings(db, &isEncrypted, cipher) == false)
CipherSettings* cipherSettings = nullptr;
if(tryEncryptionSettings(db, &isEncrypted, cipherSettings) == false)
return false;
// Open database file
@@ -117,14 +119,14 @@ bool DBBrowserDB::open(const QString& db, bool readOnly)
// Set encryption details if database is encrypted
#ifdef ENABLE_SQLCIPHER
if(isEncrypted && cipher)
if(isEncrypted && cipherSettings)
{
executeSQL(QString("PRAGMA key = %1").arg(cipher->password()), false, false);
if(cipher->pageSize() != 1024)
executeSQL(QString("PRAGMA cipher_page_size = %1;").arg(cipher->pageSize()), false, false);
executeSQL(QString("PRAGMA key = %1").arg(cipherSettings->getPassword()), false, false);
if(cipherSettings->getPageSize() != CipherSettings::defaultPageSize)
executeSQL(QString("PRAGMA cipher_page_size = %1;").arg(cipherSettings->getPageSize()), false, false);
}
#endif
delete cipher;
delete cipherSettings;
if (_db)
{
@@ -172,7 +174,7 @@ bool DBBrowserDB::open(const QString& db, bool readOnly)
}
}
bool DBBrowserDB::attach(const QString& filename, QString attach_as)
bool DBBrowserDB::attach(const QString& filePath, QString attach_as)
{
if(!_db)
return false;
@@ -186,7 +188,7 @@ bool DBBrowserDB::attach(const QString& filename, QString attach_as)
if(sqlite3_prepare_v2(_db, sql.toUtf8(), sql.toUtf8().length(), &db_vm, nullptr) == SQLITE_OK)
{
// Loop through all the databases
QFileInfo fi(filename);
QFileInfo fi(filePath);
while(sqlite3_step(db_vm) == SQLITE_ROW)
{
QFileInfo path(QString::fromUtf8((const char*)sqlite3_column_text(db_vm, 2)));
@@ -205,38 +207,39 @@ bool DBBrowserDB::attach(const QString& filename, QString attach_as)
qApp->applicationName(),
tr("Please specify the database name under which you want to access the attached database"),
QLineEdit::Normal,
QFileInfo(filename).baseName()
QFileInfo(filePath).baseName()
).trimmed();
if(attach_as.isNull())
return false;
#ifdef ENABLE_SQLCIPHER
// Try encryption settings
CipherDialog* cipher = nullptr;
CipherSettings* cipherSettings = nullptr;
bool is_encrypted;
if(tryEncryptionSettings(filename, &is_encrypted, cipher) == false)
if(tryEncryptionSettings(filePath, &is_encrypted, cipherSettings) == false)
return false;
// Attach database
QString key;
if(cipher && is_encrypted)
key = "KEY " + cipher->password();
if(!executeSQL(QString("ATTACH '%1' AS %2 %3").arg(filename).arg(sqlb::escapeIdentifier(attach_as)).arg(key), false))
if(cipherSettings && is_encrypted)
key = "KEY " + cipherSettings->getPassword();
if(!executeSQL(QString("ATTACH '%1' AS %2 %3").arg(filePath).arg(sqlb::escapeIdentifier(attach_as)).arg(key), false))
{
QMessageBox::warning(nullptr, qApp->applicationName(), lastErrorMessage);
return false;
}
if(cipher && cipher->pageSize() != 1024)
if(cipherSettings && cipherSettings->getPageSize() != CipherSettings::defaultPageSize)
{
if(!executeSQL(QString("PRAGMA %1.cipher_page_size = %2").arg(sqlb::escapeIdentifier(attach_as)).arg(cipher->pageSize()), false))
if(!executeSQL(QString("PRAGMA %1.cipher_page_size = %2").arg(sqlb::escapeIdentifier(attach_as)).arg(cipherSettings->getPageSize()), false))
{
QMessageBox::warning(nullptr, qApp->applicationName(), lastErrorMessage);
return false;
}
}
delete cipherSettings;
#else
// Attach database
if(!executeSQL(QString("ATTACH '%1' AS %2").arg(filename).arg(sqlb::escapeIdentifier(attach_as)), false))
if(!executeSQL(QString("ATTACH '%1' AS %2").arg(filePath).arg(sqlb::escapeIdentifier(attach_as)), false))
{
QMessageBox::warning(nullptr, qApp->applicationName(), lastErrorMessage);
return false;
@@ -249,16 +252,21 @@ bool DBBrowserDB::attach(const QString& filename, QString attach_as)
return true;
}
bool DBBrowserDB::tryEncryptionSettings(const QString& filename, bool* encrypted, CipherDialog*& cipherSettings)
bool DBBrowserDB::tryEncryptionSettings(const QString& filePath, bool* encrypted, CipherSettings*& cipherSettings)
{
lastErrorMessage = tr("Invalid file format");
// Open database file
sqlite3* dbHandle;
if(sqlite3_open_v2(filename.toUtf8(), &dbHandle, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK)
if(sqlite3_open_v2(filePath.toUtf8(), &dbHandle, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK)
return false;
// Try reading from database
#ifdef ENABLE_SQLCIPHER
bool isDotenvChecked = false;
#endif
*encrypted = false;
cipherSettings = nullptr;
while(true)
@@ -279,32 +287,81 @@ bool DBBrowserDB::tryEncryptionSettings(const QString& filename, bool* encrypted
{
sqlite3_finalize(vm);
#ifdef ENABLE_SQLCIPHER
delete cipherSettings;
cipherSettings = new CipherDialog(nullptr, false);
if(cipherSettings->exec())
{
// Close and reopen database first to be in a clean state after the failed read attempt from above
sqlite3_close(dbHandle);
if(sqlite3_open_v2(filename.toUtf8(), &dbHandle, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK)
bool foundDotenvPassword = false;
// Being in a while loop, we don't want to check the same file multiple times
if (!isDotenvChecked) {
QFile databaseFile(filePath);
QFileInfo databaseFileInfo(databaseFile);
QString databaseDirectoryPath = databaseFileInfo.dir().path();
QString databaseFileName(databaseFileInfo.fileName());
QString dotenvFilePath = databaseDirectoryPath + "/.env";
static const QSettings::Format dotenvFormat = QSettings::registerFormat("env", &DotenvFormat::readEnvFile, nullptr);
QSettings dotenv(dotenvFilePath, dotenvFormat);
QVariant passwordValue = dotenv.value(databaseFileName);
foundDotenvPassword = !passwordValue.isNull();
isDotenvChecked = true;
if (foundDotenvPassword)
{
QString password = passwordValue.toString();
QVariant keyFormatValue = dotenv.value(databaseFileName + "_keyFormat", QVariant(CipherSettings::KeyFormats::Passphrase));
CipherSettings::KeyFormats keyFormat = CipherSettings::getKeyFormat(keyFormatValue.toInt());
QVariant pageSizeValue = dotenv.value(databaseFileName + "_pageSize", QVariant(CipherSettings::defaultPageSize));
int pageSize = pageSizeValue.toInt();
delete cipherSettings;
cipherSettings = nullptr;
return false;
cipherSettings = new CipherSettings();
cipherSettings->setKeyFormat(keyFormat);
cipherSettings->setPassword(password);
cipherSettings->setPageSize(pageSize);
}
}
// Set key and, if it differs from the default value, the page size
sqlite3_exec(dbHandle, QString("PRAGMA key = %1").arg(cipherSettings->password()).toUtf8(), NULL, NULL, NULL);
if(cipherSettings->pageSize() != 1024)
sqlite3_exec(dbHandle, QString("PRAGMA cipher_page_size = %1;").arg(cipherSettings->pageSize()).toUtf8(), NULL, NULL, NULL);
*encrypted = true;
if(foundDotenvPassword)
{
// Skip the CipherDialog prompt for now to test if the dotenv password was correct
} else {
sqlite3_close(dbHandle);
*encrypted = false;
CipherDialog *cipherDialog = new CipherDialog(nullptr, false);
if(cipherDialog->exec())
{
delete cipherSettings;
cipherSettings = new CipherSettings(cipherDialog->getCipherSettings());
} else {
sqlite3_close(dbHandle);
*encrypted = false;
delete cipherSettings;
cipherSettings = nullptr;
return false;
}
}
// Close and reopen database first to be in a clean state after the failed read attempt from above
sqlite3_close(dbHandle);
if(sqlite3_open_v2(filePath.toUtf8(), &dbHandle, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK)
{
delete cipherSettings;
cipherSettings = nullptr;
return false;
}
// Set the key
sqlite3_exec(dbHandle, QString("PRAGMA key = %1").arg(cipherSettings->getPassword()).toUtf8(), nullptr, nullptr, nullptr);
// Set the page size if it differs from the default value
if(cipherSettings->getPageSize() != CipherSettings::defaultPageSize)
sqlite3_exec(dbHandle, QString("PRAGMA cipher_page_size = %1;").arg(cipherSettings->getPageSize()).toUtf8(), nullptr, nullptr, nullptr);
*encrypted = true;
#else
lastErrorMessage = QString::fromUtf8((const char*)sqlite3_errmsg(dbHandle));
sqlite3_close(dbHandle);
@@ -540,7 +597,7 @@ void DBBrowserDB::waitForDbRelease()
}
}
bool DBBrowserDB::dump(const QString& filename,
bool DBBrowserDB::dump(const QString& filePath,
const QStringList& tablesToDump,
bool insertColNames,
bool insertNewSyntx,
@@ -551,7 +608,7 @@ bool DBBrowserDB::dump(const QString& filename,
waitForDbRelease();
// Open file
QFile file(filename);
QFile file(filePath);
if(file.open(QIODevice::WriteOnly))
{
QApplication::setOverrideCursor(Qt::WaitCursor);
@@ -1650,14 +1707,14 @@ bool DBBrowserDB::setPragma(const QString& pragma, int value, int& originalvalue
return false;
}
bool DBBrowserDB::loadExtension(const QString& filename)
bool DBBrowserDB::loadExtension(const QString& filePath)
{
waitForDbRelease();
if(!_db)
return false;
// Check if file exists
if(!QFile::exists(filename))
if(!QFile::exists(filePath))
{
lastErrorMessage = tr("File not found.");
return false;
@@ -1665,7 +1722,7 @@ bool DBBrowserDB::loadExtension(const QString& filename)
// Try to load extension
char* error;
if(sqlite3_load_extension(_db, filename.toUtf8(), nullptr, &error) == SQLITE_OK)
if(sqlite3_load_extension(_db, filePath.toUtf8(), nullptr, &error) == SQLITE_OK)
{
return true;
} else {

View File

@@ -12,7 +12,7 @@
#include <QByteArray>
struct sqlite3;
class CipherDialog;
class CipherSettings;
enum
{
@@ -205,7 +205,7 @@ private:
void collationNeeded(void* pData, sqlite3* db, int eTextRep, const char* sCollationName);
bool tryEncryptionSettings(const QString& filename, bool* encrypted, CipherDialog*& cipherSettings);
bool tryEncryptionSettings(const QString& filename, bool* encrypted, CipherSettings*& cipherSettings);
bool dontCheckForStructureUpdates;

View File

@@ -65,7 +65,9 @@ HEADERS += \
FindReplaceDialog.h \
ExtendedScintilla.h \
FileExtensionManager.h \
Data.h
Data.h \
CipherSettings.h \
DotenvFormat.h
SOURCES += \
sqlitedb.cpp \
@@ -107,7 +109,9 @@ SOURCES += \
FindReplaceDialog.cpp \
ExtendedScintilla.cpp \
FileExtensionManager.cpp \
Data.cpp
Data.cpp \
CipherSettings.cpp \
DotenvFormat.cpp
RESOURCES += icons/icons.qrc \
translations/flags/flags.qrc \

View File

@@ -17,6 +17,8 @@ set(TESTSQLOBJECTS_SRC
../Settings.cpp
testsqlobjects.cpp
../Data.cpp
../CipherSettings.cpp
../DotenvFormat.cpp
)
set(TESTSQLOBJECTS_HDR
@@ -32,6 +34,8 @@ set(TESTSQLOBJECTS_MOC_HDR
../sqlitetablemodel.h
../Settings.h
testsqlobjects.h
../CipherSettings.h
../DotenvFormat.h
)
if(sqlcipher)
@@ -92,6 +96,8 @@ set(TESTREGEX_SRC
../Settings.cpp
TestRegex.cpp
../Data.cpp
../CipherSettings.cpp
../DotenvFormat.cpp
)
set(TESTREGEX_HDR
@@ -107,6 +113,8 @@ set(TESTREGEX_MOC_HDR
../sqlitetablemodel.h
../Settings.h
TestRegex.h
../CipherSettings.h
../DotenvFormat.h
)
if(sqlcipher)