diff --git a/src/PreferencesDialog.cpp b/src/PreferencesDialog.cpp
index 7c98f9905..775acc654 100644
--- a/src/PreferencesDialog.cpp
+++ b/src/PreferencesDialog.cpp
@@ -100,6 +100,7 @@ void PreferencesDialog::loadSettings()
ui->spinStructureFontSize->setValue(Settings::getValue("db", "fontsize").toInt());
ui->watcherCheckBox->setChecked(Settings::getValue("db", "watcher").toBool());
+ ui->checkSQLCipherParentDotenvLookup->setChecked(Settings::getValue("db", "sqlcipherparentdotenvlookup").toBool());
// Gracefully handle the preferred Data Browser font not being available
int matchingFont = ui->comboDataBrowserFont->findText(Settings::getValue("databrowser", "font").toString(), Qt::MatchExactly);
@@ -199,6 +200,7 @@ void PreferencesDialog::saveSettings(bool accept)
Settings::setValue("db", "defaultfieldtype", ui->defaultFieldTypeComboBox->currentIndex());
Settings::setValue("db", "fontsize", ui->spinStructureFontSize->value());
Settings::setValue("db", "watcher", ui->watcherCheckBox->isChecked());
+ Settings::setValue("db", "sqlcipherparentdotenvlookup", ui->checkSQLCipherParentDotenvLookup->isChecked());
Settings::setValue("checkversion", "enabled", ui->checkUpdates->isChecked());
diff --git a/src/PreferencesDialog.ui b/src/PreferencesDialog.ui
index 45f9f1ee3..bef09cc63 100644
--- a/src/PreferencesDialog.ui
+++ b/src/PreferencesDialog.ui
@@ -752,16 +752,36 @@ in new project file
- -
-
-
- enabled
-
-
- true
-
-
-
+ -
+
+
+ enabled
+
+
+ true
+
+
+
+ -
+
+
+ When enabled, DB4S searches parent folders for .db4s.env files, which can slow down opening encrypted databases.
+
+
+ Search parent folders for .db4s.env SQLCipher passwords
+
+
+ checkSQLCipherParentDotenvLookup
+
+
+
+ -
+
+
+ enabled
+
+
+
-
diff --git a/src/Settings.cpp b/src/Settings.cpp
index 6fe1b5d7f..856680d32 100644
--- a/src/Settings.cpp
+++ b/src/Settings.cpp
@@ -163,6 +163,10 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string&
if(group == "db" && name == "watcher")
return false;
+ // db/sqlcipherparentdotenvlookup?
+ if(group == "db" && name == "sqlcipherparentdotenvlookup")
+ return false;
+
// exportcsv/firstrowheader?
if(group == "exportcsv" && name == "firstrowheader")
return true;
diff --git a/src/sqlitedb.cpp b/src/sqlitedb.cpp
index 329a57012..6e58e94ab 100644
--- a/src/sqlitedb.cpp
+++ b/src/sqlitedb.cpp
@@ -16,6 +16,7 @@
#include
#include
#include
+#include
#include
#include
@@ -478,32 +479,31 @@ bool DBBrowserDB::tryEncryptionSettings(const QString& filePath, bool* encrypted
// Being in a while loop, we don't want to check the same file multiple times
if (!isDotenvChecked) {
- QFile databaseFile(filePath);
- QFileInfo databaseFileInfo(databaseFile);
+ const QFileInfo databaseFileInfo(filePath);
- QString databaseDirectoryPath = databaseFileInfo.dir().path();
- QString databaseFileName(databaseFileInfo.fileName());
+ const QDir databaseDirectory(databaseFileInfo.absoluteDir());
+ const QString databaseFileName(databaseFileInfo.fileName());
- QString dotenvFilePath = databaseDirectoryPath + "/.env";
- QSettings dotenv(dotenvFilePath, QSettings::IniFormat);
+ auto tryLoadDotenv = [&](const QString& dotenvFilePath) -> bool {
+ if (!QFile::exists(dotenvFilePath))
+ return false;
- QVariant passwordValue = dotenv.value(databaseFileName);
+ const QSettings dotenv(dotenvFilePath, QSettings::IniFormat);
+ const QVariant passwordValue = dotenv.value(databaseFileName);
- foundDotenvPassword = !passwordValue.isNull();
- isDotenvChecked = true;
+ if (passwordValue.isNull())
+ return false;
- if (foundDotenvPassword)
- {
- std::string password = passwordValue.toString().toStdString();
+ const std::string password = passwordValue.toString().toStdString();
- QVariant keyFormatValue = dotenv.value(databaseFileName + "_keyFormat", QVariant(CipherSettings::KeyFormats::Passphrase));
- CipherSettings::KeyFormats keyFormat = CipherSettings::getKeyFormat(keyFormatValue.toInt());
+ const QVariant keyFormatValue = dotenv.value(databaseFileName + "_keyFormat", QVariant(CipherSettings::KeyFormats::Passphrase));
+ const CipherSettings::KeyFormats keyFormat = CipherSettings::getKeyFormat(keyFormatValue.toInt());
- int pageSize = dotenv.value(databaseFileName + "_pageSize", enc_default_page_size).toInt();
- int kdfIterations = dotenv.value(databaseFileName + "_kdfIter", enc_default_kdf_iter).toInt();
- int plaintextHeaderSize = dotenv.value(databaseFileName + "_plaintextHeaderSize", enc_default_plaintext_header_size).toInt();
- std::string hmacAlgorithm = dotenv.value(databaseFileName + "_hmacAlgorithm", QString::fromStdString(enc_default_hmac_algorithm)).toString().toStdString();
- std::string kdfAlgorithm = dotenv.value(databaseFileName + "_kdfAlgorithm", QString::fromStdString(enc_default_kdf_algorithm)).toString().toStdString();
+ const int pageSize = dotenv.value(databaseFileName + "_pageSize", enc_default_page_size).toInt();
+ const int kdfIterations = dotenv.value(databaseFileName + "_kdfIter", enc_default_kdf_iter).toInt();
+ const int plaintextHeaderSize = dotenv.value(databaseFileName + "_plaintextHeaderSize", enc_default_plaintext_header_size).toInt();
+ const std::string hmacAlgorithm = dotenv.value(databaseFileName + "_hmacAlgorithm", QString::fromStdString(enc_default_hmac_algorithm)).toString().toStdString();
+ const std::string kdfAlgorithm = dotenv.value(databaseFileName + "_kdfAlgorithm", QString::fromStdString(enc_default_kdf_algorithm)).toString().toStdString();
cipherSettings->setKeyFormat(keyFormat);
cipherSettings->setPassword(password);
@@ -512,7 +512,36 @@ bool DBBrowserDB::tryEncryptionSettings(const QString& filePath, bool* encrypted
cipherSettings->setHmacAlgorithm("HMAC_" + hmacAlgorithm);
cipherSettings->setKdfAlgorithm("PBKDF2_HMAC_" + kdfAlgorithm);
cipherSettings->setPlaintextHeaderSize(plaintextHeaderSize);
+
+ return true;
+ };
+
+ const QString db4sEnvPath = databaseDirectory.filePath(".db4s.env");
+ const bool hasDB4SEnv = QFile::exists(db4sEnvPath);
+
+ if (hasDB4SEnv)
+ {
+ foundDotenvPassword = tryLoadDotenv(db4sEnvPath);
+ } else {
+ // Deprecated: legacy .env in the database directory only. Remove once .db4s.env is adopted.
+ foundDotenvPassword = tryLoadDotenv(databaseDirectory.filePath(".env"));
+
+ // Parent folder search is opt-in because it can slow down opening encrypted databases.
+ if (!foundDotenvPassword && Settings::getValue("db", "sqlcipherparentdotenvlookup").toBool())
+ {
+ QDir searchDirectory(databaseDirectory);
+ while (searchDirectory.cdUp())
+ {
+ if (tryLoadDotenv(searchDirectory.filePath(".db4s.env")))
+ {
+ foundDotenvPassword = true;
+ break;
+ }
+ }
+ }
}
+
+ isDotenvChecked = true;
}
if(foundDotenvPassword)