From 32641f97de31ea7d674ab89c849d11b44b30bd83 Mon Sep 17 00:00:00 2001 From: Toni Date: Mon, 27 Apr 2026 11:00:23 +0200 Subject: [PATCH] maintain an in-memory file map instead of searching the entire vault on every operation --- .clang-tidy | 2 +- CMakeLists.txt | 9 ++-- LICENSE | 6 +-- README.md | 3 +- src/common.h | 3 -- src/crypto.h | 13 ++--- src/mainwindow.cc | 72 ++++++++++++++-------------- src/vault.cc | 118 +++++++++++++++++++++------------------------- src/vault.h | 14 ++++-- 9 files changed, 114 insertions(+), 126 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 377737c..5c23db4 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1 +1 @@ -Checks: '*,clang-analyzer-*,-llvmlibc-*,-fuchsia-*,-altera-*,-abseil-*,-android-*,-modernize-use-trailing-return-type,-readability-identifier-length,-*-readability-todo,-*-magic-numbers,-readability-function-cognitive-complexity,-*-easily-swappable-parameters,-*-pro-type-reinterpret-cast,-*-owning-memory,-concurrency-mt-unsafe,-cert-env33-c,-*-avoid-do-while' +Checks: '*,clang-analyzer-*,-llvmlibc-*,-fuchsia-*,-altera-*,-abseil-*,-android-*,-boost-*,-modernize-use-trailing-return-type,-readability-identifier-length,-*-readability-todo,-*-magic-numbers,-readability-function-cognitive-complexity,-*-easily-swappable-parameters,-*-pro-type-reinterpret-cast,-*-owning-memory,-concurrency-mt-unsafe,-cert-env33-c,-*-avoid-do-while' diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c61f3c..8349e69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,10 +21,9 @@ set(CMAKE_AUTOUIC ON) qt6_wrap_ui(UI_HEADERS src/mainwindow.ui) -add_executable(${PROJECT_NAME} src/main.cc src/mainwindow.cc src/vault.cc ${UI_HEADERS}) +add_executable(dull src/main.cc src/mainwindow.cc src/vault.cc ${UI_HEADERS}) -target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${BOTAN_INCLUDE_DIRS}) +target_include_directories(dull PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${BOTAN_INCLUDE_DIRS}) +target_link_libraries(dull Qt6::Core Qt6::Widgets ${BOTAN_LIBRARIES}) -target_link_libraries(${PROJECT_NAME} Qt6::Core Qt6::Widgets ${BOTAN_LIBRARIES}) - -install(TARGETS ${PROJECT_NAME} DESTINATION bin) +install(TARGETS dull DESTINATION bin) diff --git a/LICENSE b/LICENSE index 1875e2c..2e16d76 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -BSD 3-Clause License +BSD 2-Clause License Copyright (c) 2025-2026, Antoni Piasecki @@ -12,10 +12,6 @@ modification, are permitted provided that the following conditions are met: this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE diff --git a/README.md b/README.md index 3a39dc8..2c463ec 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,8 @@ Desktop app for securely storing sensitive files ## Features -* **Pretty usable UI** -* **Overkill encryption:** XChaCha20-Poly1305 + Argon2id key derivation * **Cross-platform:** Tested on Linux, Windows and macOS +* **Overkill encryption:** XChaCha20-Poly1305 + Argon2id key derivation * **Drag and Drop support** ## Building diff --git a/src/common.h b/src/common.h index 4195a4d..9a3608d 100644 --- a/src/common.h +++ b/src/common.h @@ -14,13 +14,10 @@ } while (0) using u8 = uint8_t; -using i16 = int16_t; using u16 = uint16_t; using i32 = int32_t; using u32 = uint32_t; using i64 = int64_t; using u64 = uint64_t; -static_assert(sizeof(float) * 8 == 32); -using f32 = float; static_assert(sizeof(double) * 8 == 64); using f64 = double; diff --git a/src/crypto.h b/src/crypto.h index e9007f6..b2ef1c0 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -4,6 +4,8 @@ #include #include +#include + namespace Crypto { inline Botan::secure_vector encrypt(const Botan::secure_vector &key, @@ -21,12 +23,11 @@ inline Botan::secure_vector encrypt(const Botan::secure_vector &key, cipher->set_key(key); cipher->start(nonce); - Botan::secure_vector ciphertext(plaintext.begin(), plaintext.end()); - cipher->finish(ciphertext); - Botan::secure_vector res; - res.insert(res.end(), nonce.begin(), nonce.end()); - res.insert(res.end(), ciphertext.begin(), ciphertext.end()); + res.resize(24 + plaintext.size() + 16); + std::ranges::copy(nonce, res.begin()); + std::ranges::copy(plaintext, res.begin() + 24); + cipher->finish(res, 24); return res; } @@ -39,7 +40,7 @@ decrypt(const Botan::secure_vector &key, std::array nonce{}; std::copy_n(ciphertext_with_nonce.begin(), 24, nonce.begin()); - auto cipher = Botan::AEAD_Mode::create_or_throw( + static auto cipher = Botan::AEAD_Mode::create_or_throw( "ChaCha20Poly1305", Botan::Cipher_Dir::Decryption); cipher->set_key(key); diff --git a/src/mainwindow.cc b/src/mainwindow.cc index 756f6a2..86786a3 100644 --- a/src/mainwindow.cc +++ b/src/mainwindow.cc @@ -9,6 +9,7 @@ #include #include #include +#include namespace { @@ -150,17 +151,14 @@ MainWindow::MainWindow(QWidget *parent) return; } - auto headers = m_vault->read_file_headers(); - for (const auto &header : headers) { - auto content = m_vault->read_file(header.name); - if (content) { - ui->statusbar->showMessage("Extracting " + - QString::fromStdString(header.name) + "..."); - QCoreApplication::processEvents(); - std::ofstream file(path.toStdString() + "/" + header.name, - std::ios::binary); - file.write(content->data(), static_cast(content->size())); - } + const auto &file_map = m_vault->file_map(); + for (const auto &[k, _] : file_map) { + auto content = m_vault->read_file(k); + ui->statusbar->showMessage("Extracting " + QString::fromStdString(k) + + "..."); + QCoreApplication::processEvents(); + std::ofstream file(path.toStdString() + "/" + k, std::ios::binary); + file.write(content.data(), static_cast(content.size())); } ui->statusbar->showMessage("Extracted all files to " + path); }); @@ -171,13 +169,13 @@ void MainWindow::reload_fs_tree() { ui->menuFiles->setEnabled(true); ui->fsTreeWidget->clear(); - auto headers = m_vault->read_file_headers(); - for (const auto &header : headers) { + const auto &file_map = m_vault->file_map(); + for (const auto &[k, v] : file_map) { auto *item = new QTreeWidgetItem(ui->fsTreeWidget); item->setIcon(0, style()->standardIcon(QStyle::SP_FileIcon)); - item->setText(0, QString::fromStdString(header.name)); - item->setText(1, QString::fromStdString( - format_bytes(header.content_ciphertext_size))); + item->setText(0, QString::fromStdString(k)); + item->setText( + 1, QString::fromStdString(format_bytes(v.content_ciphertext_size))); } for (int i = 0; i < ui->fsTreeWidget->columnCount(); ++i) { ui->fsTreeWidget->resizeColumnToContents(i); @@ -187,17 +185,17 @@ void MainWindow::reload_fs_tree() { void MainWindow::preview_file(const std::string &filename) { ui->filePreview->setVisible(true); - auto content = m_vault->read_file(filename); - if (content) { - ui->filePreview->setText(QString::fromStdString(content.value())); - } else { + try { + auto content = m_vault->read_file(filename); + ui->filePreview->setText(QString::fromStdString(content)); + } catch (std::out_of_range &) { qWarning() << "File to preview not found"; } } void MainWindow::extract_file(const std::string &filename) { - auto content = m_vault->read_file(filename); - if (content) { + try { + auto content = m_vault->read_file(filename); QString path = QFileDialog::getSaveFileName( this, "Choose location to extract", QDir::currentPath() + "/" + QString::fromStdString(filename)); @@ -206,24 +204,24 @@ void MainWindow::extract_file(const std::string &filename) { } std::ofstream file(path.toStdString(), std::ios::binary); - file.write(content->data(), static_cast(content->size())); + file.write(content.data(), static_cast(content.size())); ui->statusbar->showMessage("Extracted to " + path); - } else { + } catch (std::out_of_range &) { qWarning() << "File to extract not found"; } } void MainWindow::open_file(const std::string &filename) { - auto content = m_vault->read_file(filename); - if (content) { + try { + auto content = m_vault->read_file(filename); QTemporaryDir dir; ASSERT(dir.isValid()); std::string path = dir.path().toStdString() + "/" + filename; std::ofstream file(path); - file.write(content->data(), static_cast(content->size())); + file.write(content.data(), static_cast(content.size())); file.close(); QDesktopServices::openUrl( @@ -232,21 +230,21 @@ void MainWindow::open_file(const std::string &filename) { "Click OK to delete the opened file."); // QTemporaryDir gets deleted when it goes out of scope - } else { + } catch (std::out_of_range &) { qWarning() << "File to open not found"; } } void MainWindow::edit_file(const std::string &filename) { - auto content = m_vault->read_file(filename); - if (content) { + try { + auto content = m_vault->read_file(filename); QTemporaryDir dir; ASSERT(dir.isValid()); std::string path = dir.path().toStdString() + "/" + filename; std::ofstream file(path); - file.write(content->data(), static_cast(content->size())); + file.write(content.data(), static_cast(content.size())); file.close(); QDesktopServices::openUrl( @@ -255,16 +253,18 @@ void MainWindow::edit_file(const std::string &filename) { "Please edit the file in the opened editor and " "save it. Click OK when done."); - std::ifstream file2(path, std::ios::binary); - std::string new_content((std::istreambuf_iterator(file2)), - std::istreambuf_iterator()); - + std::ifstream file2(path, std::ios::binary | std::ios::ate); + ASSERT(file2.good()); + auto size = file2.tellg(); + file2.seekg(0); + std::string new_content(size, '\0'); + file2.read(new_content.data(), size); m_vault->update_file(filename, new_content); reload_fs_tree(); ui->statusbar->showMessage("File updated"); // QTemporaryDir gets deleted when it goes out of scope - } else { + } catch (std::out_of_range &) { qWarning() << "File to edit not found"; } } diff --git a/src/vault.cc b/src/vault.cc index 035628b..83b11ad 100644 --- a/src/vault.cc +++ b/src/vault.cc @@ -31,7 +31,7 @@ Vault::Vault(std::string path, const std::string &password) ASSERT(m_file.read(magic.data(), 4)); ASSERT(std::string_view(magic.data(), magic.size()) == "DULL"); - i16 version = 0; + u16 version = 0; ASSERT(m_file.read(to_char_ptr(&version), sizeof(version))); ASSERT(version == VERSION); @@ -45,6 +45,8 @@ Vault::Vault(std::string path, const std::string &password) ASSERT(m_file.read(to_char_ptr(check_ciphertext.data()), 46)); Crypto::decrypt(m_key, check_ciphertext); + + build_file_map(); } void Vault::create(const std::string &path, const std::string &password) { @@ -64,8 +66,8 @@ void Vault::create(const std::string &path, const std::string &password) { file.close(); } -std::vector Vault::read_file_headers() { - std::vector headers; +void Vault::build_file_map() { + m_file_map.clear(); m_file.clear(); m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg); @@ -74,7 +76,7 @@ std::vector Vault::read_file_headers() { auto header = read_file_header(); if (header) { if (!header->deleted) { - headers.push_back(header.value()); + m_file_map[header->name] = header.value(); } m_file.seekg(static_cast(header->content_ciphertext_size), std::ios::cur); @@ -82,37 +84,22 @@ std::vector Vault::read_file_headers() { break; } } - - return headers; } -std::optional Vault::read_file(const std::string &filename) { +std::string Vault::read_file(const std::string &filename) { + auto header = m_file_map.at(filename); + ASSERT(!header.deleted); + m_file.clear(); - m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg); + m_file.seekg(static_cast(header.content_offset), std::ios::beg); - while (true) { - auto header = read_file_header(); - if (!header) { - break; - } + Botan::secure_vector ciphertext; + ciphertext.resize(header.content_ciphertext_size); + ASSERT(m_file.read(to_char_ptr(ciphertext.data()), + static_cast(header.content_ciphertext_size))); - if (!header->deleted && header->name == filename) { - Botan::secure_vector ciphertext; - ciphertext.resize(header->content_ciphertext_size); - if (!m_file.read(to_char_ptr(ciphertext.data()), - static_cast(header->content_ciphertext_size))) { - break; - } - - auto plaintext = Crypto::decrypt(m_key, ciphertext); - return std::string(to_char_ptr(plaintext.data()), plaintext.size()); - } - - m_file.seekg(static_cast(header->content_ciphertext_size), - std::ios::cur); - } - - return std::nullopt; + auto plaintext = Crypto::decrypt(m_key, ciphertext); + return {to_char_ptr(plaintext.data()), plaintext.size()}; } void Vault::create_file(const std::string &filename, @@ -120,54 +107,54 @@ void Vault::create_file(const std::string &filename, m_file.clear(); m_file.seekp(0, std::ios::end); - auto filename_ciphertext = Crypto::encrypt(m_key, to_span(filename)); - u64 filename_ciphertext_size = filename_ciphertext.size(); + auto offset = m_file.tellp(); + + auto name_ciphertext = Crypto::encrypt(m_key, to_span(filename)); + u64 name_ciphertext_size = name_ciphertext.size(); auto ciphertext = Crypto::encrypt(m_key, to_span(content)); u64 ciphertext_size = ciphertext.size(); bool deleted = false; WRITE(m_file, &deleted, sizeof(bool)); // deleted flag - WRITE(m_file, &filename_ciphertext_size, sizeof(u64)); - WRITE(m_file, filename_ciphertext.data(), filename_ciphertext_size); + WRITE(m_file, &name_ciphertext_size, sizeof(u64)); + WRITE(m_file, name_ciphertext.data(), name_ciphertext_size); WRITE(m_file, &ciphertext_size, sizeof(u64)); + auto content_offset = m_file.tellp(); WRITE(m_file, ciphertext.data(), ciphertext_size); m_file.flush(); + + m_file_map[filename] = FileHeader{ + .offset = static_cast(offset), + .deleted = false, + .name_ciphertext_size = name_ciphertext_size, + .name = filename, + .content_ciphertext_size = ciphertext_size, + .content_offset = static_cast(content_offset), + }; } void Vault::delete_file(const std::string &filename) { + auto header = m_file_map.at(filename); + + // zero the ciphertext and mark as deleted m_file.clear(); - m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg); - - while (true) { - i64 current_pos = m_file.tellg(); - - auto header = read_file_header(); - if (!header) { - break; - } - - if (!header->deleted && header->name == filename) { - // zero the ciphertext and mark as deleted - m_file.seekp(m_file.tellg()); - std::array zeros = {0}; - u64 remaining = header->content_ciphertext_size; - while (remaining > 0) { - u64 chunk = std::min(remaining, sizeof(zeros)); - WRITE(m_file, zeros.data(), chunk); - remaining -= chunk; - } - - m_file.seekp(current_pos, std::ios::beg); - bool deleted = true; - WRITE(m_file, &deleted, sizeof(bool)); - m_file.flush(); - break; - } - - m_file.seekg(static_cast(header->content_ciphertext_size), - std::ios::cur); + m_file.seekp(static_cast(header.content_offset), std::ios::beg); + std::array zeros = {0}; + u64 remaining = header.content_ciphertext_size; + while (remaining > 0) { + u64 chunk = std::min(remaining, sizeof(zeros)); + WRITE(m_file, zeros.data(), chunk); + remaining -= chunk; } + + m_file.clear(); + m_file.seekp(static_cast(header.offset), std::ios::beg); + bool deleted = true; + WRITE(m_file, &deleted, sizeof(bool)); + m_file.flush(); + + m_file_map.erase(filename); } void Vault::update_file(const std::string &filename, @@ -179,6 +166,8 @@ void Vault::update_file(const std::string &filename, std::optional Vault::read_file_header() { FileHeader header{}; + header.offset = m_file.tellg(); + if (!m_file.read(to_char_ptr(&header.deleted), 1)) { return std::nullopt; } @@ -205,5 +194,6 @@ std::optional Vault::read_file_header() { return std::nullopt; } + header.content_offset = m_file.tellg(); return header; } diff --git a/src/vault.h b/src/vault.h index d71cd56..53980aa 100644 --- a/src/vault.h +++ b/src/vault.h @@ -4,16 +4,18 @@ #include #include #include +#include -constexpr i16 VERSION = 1; +constexpr u16 VERSION = 1; constexpr u64 AFTER_HEADER_OFFSET = 68; -// !!! REMEMBER TO UPDATE entry_total_size IN Vault::delete_file struct FileHeader { + u64 offset; bool deleted; u64 name_ciphertext_size; std::string name; u64 content_ciphertext_size; + u64 content_offset; }; class Vault { @@ -22,18 +24,22 @@ public: static void create(const std::string &path, const std::string &password); - std::vector read_file_headers(); - std::optional read_file(const std::string &name); + std::string read_file(const std::string &name); void create_file(const std::string &name, const std::string &content); void delete_file(const std::string &name); void update_file(const std::string &name, const std::string &content); const std::string &path() const { return m_path; } + const std::unordered_map &file_map() const { + return m_file_map; + } private: std::string m_path; std::fstream m_file; Botan::secure_vector m_key; + std::unordered_map m_file_map; + void build_file_map(); std::optional read_file_header(); };