Compare commits

...

10 Commits

Author SHA1 Message Date
99d239ea08 open file without editing action 2026-04-25 15:05:19 +02:00
48fc35e16c mark as deleted and zero content on deletion instead of shifting everything 2026-04-25 13:37:53 +02:00
85541e1db4 retry on weak password, Vault::create 2026-04-25 13:12:46 +02:00
47f1f7dd74 manage nonces in crypto.h 2026-04-25 13:06:54 +02:00
0f4d4f96d6 add macos build instructions 2026-03-18 17:36:24 +01:00
1ad1571273 LICENSE 2026-03-08 23:12:45 +01:00
7873020e72 delete in chunks 2026-03-08 23:08:53 +01:00
53a222a7da handle invalid passwords 2025-11-11 12:20:35 +01:00
f00b5221c1 extract all 2025-11-11 11:59:51 +01:00
30ca8b6262 bit of ux 2025-11-04 21:21:03 +01:00
14 changed files with 338 additions and 187 deletions

View File

@@ -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-*,-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'

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
/.cache /.cache
/build /build
*.py *.py
*.dull *.dull

View File

@@ -4,6 +4,11 @@ project(dull)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# fix clang
foreach(flag_var CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_RELEASE CMAKE_CXX_FLAGS_DEBUG)
string(REPLACE "-mno-direct-extern-access" "" ${flag_var} "${${flag_var}}")
endforeach()
find_package(Qt6 REQUIRED COMPONENTS Core Widgets) find_package(Qt6 REQUIRED COMPONENTS Core Widgets)
if(NOT WIN32) if(NOT WIN32)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)

28
LICENSE Normal file
View File

@@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2025-2026, Antoni Piasecki
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
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
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,22 +0,0 @@
pkgname=dull
pkgver=0.1
pkgrel=1
pkgdesc="Desktop app for securely storing sensitive files"
arch=('x86_64')
url="https://github.com/antpiasecki/dull"
depends=('qt6-base' 'botan')
makedepends=('cmake')
source=("${url}/archive/refs/tags/${pkgver}.tar.gz")
sha256sums=('SKIP')
DEBUGPKG=()
build() {
cd "${pkgname}-${pkgver}"
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
cmake --build build -j$(nproc)
}
package() {
cd "${pkgname}-${pkgver}/build"
make DESTDIR="${pkgdir}" install
}

View File

@@ -4,32 +4,44 @@ Desktop app for securely storing sensitive files
## Features ## Features
* **Pretty usable UI** * **Pretty usable UI**
* **Overkill encryption:** XChaCha20-Poly1305 + Argon2id(m=1GB,t=6,p=4) key derivation * **Overkill encryption:** XChaCha20-Poly1305 + Argon2id key derivation
* **Cross-platform-ish:** Builds on Linux, Windows and macOS * **Cross-platform:** Tested on Linux, Windows and macOS
* **Drag and Drop support** * **Drag and Drop support**
## Building ## Building
### Debian/Ubuntu ### Debian
``` ```
sudo apt update && sudo apt install build-essential qt6-base-dev libbotan-3-dev cmake sudo apt update && sudo apt install build-essential qt6-base-dev libbotan-3-dev cmake
cmake -S . -B build cmake -B build
cmake --build build -j $(nproc) cmake --build build -j $(nproc)
./build/dull ./build/dull
``` ```
### Arch ### Arch
``` ```
sudo pacman -S base-devel sudo pacman -Syu base-devel cmake qt6-base botan
makepkg -si
cmake -B build
cmake --build build -j $(nproc)
./build/dull
``` ```
### Windows / MSYS2 ### macOS
```
brew install qtbase cmake pkg-config botan
cmake -B build
cmake --build build -j $(sysctl -n hw.physicalcpu)
./build/dull
```
### Windows (MSYS2)
``` ```
pacman -S --needed mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-qt6-base mingw-w64-x86_64-qt6-tools mingw-w64-x86_64-libbotan pacman -S --needed mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-qt6-base mingw-w64-x86_64-qt6-tools mingw-w64-x86_64-libbotan
cmake -S . -B build -DBOTAN_INCLUDE_DIRS=/mingw64/include/botan-3 -DBOTAN_LIBRARIES=/mingw64/lib/libbotan-3.a cmake -B build -DBOTAN_INCLUDE_DIRS=/mingw64/include/botan-3 -DBOTAN_LIBRARIES=/mingw64/lib/libbotan-3.a
cmake --build build -j $(nproc) cmake --build build -j $(nproc)
./build/dull ./build/dull
``` ```

View File

@@ -2,19 +2,18 @@
#include <cstdint> #include <cstdint>
#include <iostream> #include <iostream>
#include <string>
#define ASSERT(cond) \ #define ASSERT(cond) \
do { \ do { \
if (!(cond)) { \ if (!(cond)) { \
std::cerr << "ASSERTION FAILED at " << __FILE__ << ":" << __LINE__ \ std::cerr << "ASSERTION FAILED at " << __FILE__ << ":" << __LINE__ \
<< "\n" \ << "\n" \
<< #cond << std::endl; \ << #cond << '\n'; \
abort(); \ abort(); \
} \ } \
} while (0) } while (0)
using u8 = unsigned char; using u8 = uint8_t;
using i16 = int16_t; using i16 = int16_t;
using u16 = uint16_t; using u16 = uint16_t;
using i32 = int32_t; using i32 = int32_t;
@@ -25,16 +24,3 @@ static_assert(sizeof(float) * 8 == 32);
using f32 = float; using f32 = float;
static_assert(sizeof(double) * 8 == 64); static_assert(sizeof(double) * 8 == 64);
using f64 = double; using f64 = double;
inline std::string path_to_filename(const std::string &path) {
u64 pos = path.find_last_of("/\\");
return (pos == std::string::npos) ? path : path.substr(pos + 1);
}
template <typename T> constexpr char *to_char_ptr(T *ptr) noexcept {
return reinterpret_cast<char *>(ptr);
}
template <typename T> constexpr const char *to_char_ptr(const T *ptr) noexcept {
return reinterpret_cast<const char *>(ptr);
}

View File

@@ -1,39 +1,43 @@
#pragma once #pragma once
#include "common.h" #include "common.h"
#include <botan/aead.h> #include <botan/aead.h>
#include <botan/hex.h> #include <botan/auto_rng.h>
#include <botan/pwdhash.h> #include <botan/pwdhash.h>
namespace Crypto { namespace Crypto {
inline Botan::secure_vector<u8> inline Botan::secure_vector<u8> encrypt(const Botan::secure_vector<u8> &key,
encrypt_xchacha20_poly1305(const Botan::secure_vector<u8> &plaintext, const std::span<const u8> &plaintext) {
const Botan::secure_vector<u8> &key,
const std::array<u8, 24> &nonce) {
ASSERT(key.size() == 32); ASSERT(key.size() == 32);
ASSERT(nonce.size() == 24);
static Botan::AutoSeeded_RNG rng;
// XChaCha20 is selected automatically based on the nonce size // XChaCha20 is selected automatically based on the nonce size
// https://github.com/randombit/botan/blob/master/src/lib/stream/chacha/chacha.cpp#L375 // https://github.com/randombit/botan/blob/master/src/lib/stream/chacha/chacha.cpp#L375
auto cipher = Botan::AEAD_Mode::create_or_throw( static auto cipher = Botan::AEAD_Mode::create_or_throw(
"ChaCha20Poly1305", Botan::Cipher_Dir::Encryption); "ChaCha20Poly1305", Botan::Cipher_Dir::Encryption);
auto nonce = rng.random_array<24>();
cipher->set_key(key); cipher->set_key(key);
cipher->start(nonce); cipher->start(nonce);
Botan::secure_vector<u8> ciphertext = plaintext; Botan::secure_vector<u8> ciphertext(plaintext.begin(), plaintext.end());
cipher->finish(ciphertext); cipher->finish(ciphertext);
return ciphertext; Botan::secure_vector<u8> res;
res.insert(res.end(), nonce.begin(), nonce.end());
res.insert(res.end(), ciphertext.begin(), ciphertext.end());
return res;
} }
inline Botan::secure_vector<u8> inline Botan::secure_vector<u8>
decrypt_xchacha20_poly1305(const Botan::secure_vector<u8> &ciphertext, decrypt(const Botan::secure_vector<u8> &key,
const Botan::secure_vector<u8> &key, const Botan::secure_vector<u8> &ciphertext_with_nonce) {
const std::array<u8, 24> &nonce) { ASSERT(ciphertext_with_nonce.size() >= 40);
ASSERT(ciphertext.size() >= 16);
ASSERT(key.size() == 32); ASSERT(key.size() == 32);
ASSERT(nonce.size() == 24);
std::array<u8, 24> nonce{};
std::copy_n(ciphertext_with_nonce.begin(), 24, nonce.begin());
auto cipher = Botan::AEAD_Mode::create_or_throw( auto cipher = Botan::AEAD_Mode::create_or_throw(
"ChaCha20Poly1305", Botan::Cipher_Dir::Decryption); "ChaCha20Poly1305", Botan::Cipher_Dir::Decryption);
@@ -41,18 +45,17 @@ decrypt_xchacha20_poly1305(const Botan::secure_vector<u8> &ciphertext,
cipher->set_key(key); cipher->set_key(key);
cipher->start(nonce); cipher->start(nonce);
Botan::secure_vector<u8> plaintext = ciphertext; Botan::secure_vector<u8> ciphertext(ciphertext_with_nonce.begin() + 24,
cipher->finish(plaintext); ciphertext_with_nonce.end());
cipher->finish(ciphertext);
return plaintext; return ciphertext;
} }
inline Botan::secure_vector<u8> inline Botan::secure_vector<u8> derive_key(const std::string &password,
derive_key_argon2id(const std::string &password, const std::array<u8, 16> &salt) {
const std::array<u8, 16> &salt) {
// thousands of years to crack a random 8 char password on a 100 GPUs
auto pwdhash = Botan::PasswordHashFamily::create_or_throw("Argon2id") auto pwdhash = Botan::PasswordHashFamily::create_or_throw("Argon2id")
->from_params(static_cast<u64>(1024 * 1024), 8, 4); ->from_params(static_cast<u64>(512 * 1024), 8, 4);
Botan::secure_vector<u8> key(32); Botan::secure_vector<u8> key(32);
pwdhash->derive_key(key.data(), key.size(), password.data(), password.size(), pwdhash->derive_key(key.data(), key.size(), password.data(), password.size(),
@@ -60,4 +63,4 @@ derive_key_argon2id(const std::string &password,
return key; return key;
} }
}; // namespace Crypto }; // namespace Crypto

View File

@@ -1,5 +1,4 @@
#include "mainwindow.h" #include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
QApplication app(argc, argv); QApplication app(argc, argv);

View File

@@ -1,14 +1,38 @@
// TODO: actual fs
#include "mainwindow.h" #include "mainwindow.h"
#include <QDesktopServices> #include <QDesktopServices>
#include <QDragEnterEvent>
#include <QDropEvent> #include <QDropEvent>
#include <QFileDialog> #include <QFileDialog>
#include <QInputDialog> #include <QInputDialog>
#include <QMessageBox> #include <QMessageBox>
#include <QMimeData> #include <QMimeData>
#include <QTemporaryDir> #include <QTemporaryDir>
#include <botan/auto_rng.h> #include <array>
#include <botan/hex.h> #include <botan/exceptn.h>
namespace {
std::string basename(const std::string &path) {
u64 pos = path.find_last_of("/\\");
return (pos == std::string::npos) ? path : path.substr(pos + 1);
}
std::string format_bytes(u64 bytes) {
const std::array<std::string_view, 5> units = {"B", "KB", "MB", "GB", "TB"};
auto size = static_cast<f64>(bytes);
i32 unit = 0;
while (size >= 1024.0 && unit < 5) {
size /= 1024.0;
++unit;
}
std::ostringstream out;
out << std::fixed << std::setprecision(2) << size << " " << units.at(unit);
return out.str();
}
} // namespace
MainWindow::MainWindow(QWidget *parent) MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), ui(std::make_unique<Ui::MainWindow>()) { : QMainWindow(parent), ui(std::make_unique<Ui::MainWindow>()) {
@@ -21,35 +45,36 @@ MainWindow::MainWindow(QWidget *parent)
connect(ui->actionNew, &QAction::triggered, this, [this]() { connect(ui->actionNew, &QAction::triggered, this, [this]() {
QString path = QFileDialog::getSaveFileName(this, "Choose vault location", QString path = QFileDialog::getSaveFileName(this, "Choose vault location",
QDir::currentPath(), QDir::currentPath(),
"Dull Vaults (*.dull)"); "Dull vaults (*.dull)");
if (path.isEmpty()) { if (path.isEmpty()) {
return; return;
} }
if (!path.contains(".dull")) { if (!path.endsWith(".dull")) {
path += ".dull"; path += ".dull";
} }
QString password = QInputDialog::getText( QString password;
this, "Choose a password", "Choose a password", QLineEdit::Password); while (true) {
if (password.length() < 8) { password = QInputDialog::getText(
QMessageBox::critical(this, "Error", this, "Choose a password", "Choose a password", QLineEdit::Password);
"Password must be at least 8 characters long."); if (password.length() < 8) {
return; QMessageBox::critical(this, "Error",
"Password must be at least 8 characters long.");
} else {
break;
}
} }
static Botan::AutoSeeded_RNG rng; ui->statusbar->showMessage("Creating the vault...");
auto salt_sv = rng.random_vec(16); QCoreApplication::processEvents();
std::vector<u8> salt(salt_sv.begin(), salt_sv.end());
std::ofstream create(path.toStdString(), std::ios::binary); Vault::create(path.toStdString(), password.toStdString());
create.write("DULL", 4);
create.write(to_char_ptr(&VERSION), sizeof(VERSION));
create.write(to_char_ptr(salt.data()), 16);
create.close();
m_vault = m_vault =
std::make_unique<Vault>(path.toStdString(), password.toStdString()); std::make_unique<Vault>(path.toStdString(), password.toStdString());
ui->statusbar->showMessage("Opened " + path);
reload_fs_tree(); reload_fs_tree();
}); });
@@ -63,12 +88,23 @@ MainWindow::MainWindow(QWidget *parent)
QString password = QInputDialog::getText( QString password = QInputDialog::getText(
this, "Unlock the vault", "Enter vault password", QLineEdit::Password); this, "Unlock the vault", "Enter vault password", QLineEdit::Password);
if (password.isEmpty()) {
return;
}
// TODO: check if password valid ui->statusbar->showMessage("Opening the vault...");
QCoreApplication::processEvents();
m_vault = try {
std::make_unique<Vault>(path.toStdString(), password.toStdString()); m_vault =
reload_fs_tree(); std::make_unique<Vault>(path.toStdString(), password.toStdString());
reload_fs_tree();
ui->statusbar->showMessage("Opened " + path);
} catch (const Botan::Invalid_Authentication_Tag &e) {
QMessageBox::critical(this, "Error", "Invalid password.");
ui->statusbar->clearMessage();
return;
}
}); });
connect( connect(
@@ -86,27 +122,62 @@ MainWindow::MainWindow(QWidget *parent)
QStringList paths = QStringList paths =
QFileDialog::getOpenFileNames(this, "Choose files to add"); QFileDialog::getOpenFileNames(this, "Choose files to add");
for (const auto &path : paths) { for (const auto &path : paths) {
std::ifstream file(path.toStdString(), std::ios::binary); ui->statusbar->showMessage("Adding " + path + "...");
QCoreApplication::processEvents();
std::string content((std::istreambuf_iterator<char>(file)), std::ifstream file(path.toStdString(), std::ios::binary | std::ios::ate);
std::istreambuf_iterator<char>()); ASSERT(file.good());
auto size = file.tellg();
m_vault->create_file(path_to_filename(path.toStdString()), content); file.seekg(0);
std::string content(size, '\0');
file.read(content.data(), size);
m_vault->create_file(basename(path.toStdString()), content);
} }
reload_fs_tree(); reload_fs_tree();
ui->statusbar->showMessage("Added " + QString::number(paths.size()) +
" files");
});
connect(ui->actionExtract_All, &QAction::triggered, this, [this]() {
if (!m_vault) {
return;
}
QString path =
QFileDialog::getExistingDirectory(this, "Choose location to extract");
if (path.isEmpty()) {
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<i64>(content->size()));
}
}
ui->statusbar->showMessage("Extracted all files to " + path);
}); });
} }
void MainWindow::reload_fs_tree() { void MainWindow::reload_fs_tree() {
setWindowTitle(QString::fromStdString(m_vault->path()) + " - dull");
ui->menuFiles->setEnabled(true); ui->menuFiles->setEnabled(true);
ui->fsTreeWidget->clear(); ui->fsTreeWidget->clear();
auto headers = m_vault->read_file_headers(); auto headers = m_vault->read_file_headers();
for (const auto &header : headers) { for (const auto &header : headers) {
auto *item = new QTreeWidgetItem(ui->fsTreeWidget); auto *item = new QTreeWidgetItem(ui->fsTreeWidget);
item->setIcon(0, style()->standardIcon(QStyle::SP_FileIcon));
item->setText(0, QString::fromStdString(header.name)); item->setText(0, QString::fromStdString(header.name));
item->setText(1, QString::number(header.content_ciphertext_size)); item->setText(1, QString::fromStdString(
format_bytes(header.content_ciphertext_size)));
} }
for (int i = 0; i < ui->fsTreeWidget->columnCount(); ++i) { for (int i = 0; i < ui->fsTreeWidget->columnCount(); ++i) {
ui->fsTreeWidget->resizeColumnToContents(i); ui->fsTreeWidget->resizeColumnToContents(i);
@@ -136,11 +207,36 @@ void MainWindow::extract_file(const std::string &filename) {
std::ofstream file(path.toStdString(), std::ios::binary); std::ofstream file(path.toStdString(), std::ios::binary);
file.write(content->data(), static_cast<i64>(content->size())); file.write(content->data(), static_cast<i64>(content->size()));
ui->statusbar->showMessage("Extracted to " + path);
} else { } else {
qWarning() << "File to extract not found"; qWarning() << "File to extract not found";
} }
} }
void MainWindow::open_file(const std::string &filename) {
auto content = m_vault->read_file(filename);
if (content) {
QTemporaryDir dir;
ASSERT(dir.isValid());
std::string path = dir.path().toStdString() + "/" + filename;
std::ofstream file(path);
file.write(content->data(), static_cast<i64>(content->size()));
file.close();
QDesktopServices::openUrl(
QUrl::fromLocalFile(QString::fromStdString(path)));
QMessageBox::information(this, "Open",
"Click OK to delete the opened file.");
// QTemporaryDir gets deleted when it goes out of scope
} else {
qWarning() << "File to open not found";
}
}
void MainWindow::edit_file(const std::string &filename) { void MainWindow::edit_file(const std::string &filename) {
auto content = m_vault->read_file(filename); auto content = m_vault->read_file(filename);
if (content) { if (content) {
@@ -166,6 +262,7 @@ void MainWindow::edit_file(const std::string &filename) {
m_vault->update_file(filename, new_content); m_vault->update_file(filename, new_content);
reload_fs_tree(); reload_fs_tree();
ui->statusbar->showMessage("File updated");
// QTemporaryDir gets deleted when it goes out of scope // QTemporaryDir gets deleted when it goes out of scope
} else { } else {
qWarning() << "File to edit not found"; qWarning() << "File to edit not found";
@@ -180,13 +277,21 @@ void MainWindow::file_context_menu(const QPoint &pos) {
QMenu menu(this); QMenu menu(this);
QAction *preview_action = menu.addAction( QAction *preview_action =
style()->standardIcon(QStyle::SP_FileDialogContentsView), "Preview"); menu.addAction(style()->standardIcon(QStyle::SP_FileDialogContentsView),
"Preview as text");
connect(preview_action, &QAction::triggered, this, connect(preview_action, &QAction::triggered, this,
[this, item]() { preview_file(item->text(0).toStdString()); }); [this, item]() { preview_file(item->text(0).toStdString()); });
QAction *edit_action = menu.addAction( QAction *open_action =
style()->standardIcon(QStyle::SP_FileDialogDetailedView), "Edit"); menu.addAction(style()->standardIcon(QStyle::SP_FileDialogDetailedView),
"Open in external app");
connect(open_action, &QAction::triggered, this,
[this, item]() { open_file(item->text(0).toStdString()); });
QAction *edit_action =
menu.addAction(style()->standardIcon(QStyle::SP_FileDialogDetailedView),
"Edit in external app");
connect(edit_action, &QAction::triggered, this, connect(edit_action, &QAction::triggered, this,
[this, item]() { edit_file(item->text(0).toStdString()); }); [this, item]() { edit_file(item->text(0).toStdString()); });
@@ -231,16 +336,19 @@ void MainWindow::dropEvent(QDropEvent *event) {
if (!u.isLocalFile()) { if (!u.isLocalFile()) {
continue; continue;
} }
std::ifstream file(u.toLocalFile().toStdString(), std::ios::binary);
std::ifstream file(u.toLocalFile().toStdString(),
std::ios::binary | std::ios::ate);
ASSERT(file.good()); ASSERT(file.good());
auto size = file.tellg();
std::string content((std::istreambuf_iterator<char>(file)), file.seekg(0);
std::istreambuf_iterator<char>()); std::string content(size, '\0');
file.read(content.data(), size);
m_vault->create_file(path_to_filename(u.toLocalFile().toStdString()), m_vault->create_file(basename(u.toLocalFile().toStdString()), content);
content);
} }
ui->statusbar->showMessage(
"Added " + QString::number(event->mimeData()->urls().size()) + " files");
reload_fs_tree(); reload_fs_tree();
event->acceptProposedAction(); event->acceptProposedAction();
} }

View File

@@ -2,8 +2,6 @@
#include "ui_mainwindow.h" #include "ui_mainwindow.h"
#include "vault.h" #include "vault.h"
#include <QMainWindow>
#include <memory>
class MainWindow : public QMainWindow { class MainWindow : public QMainWindow {
Q_OBJECT Q_OBJECT
@@ -23,6 +21,7 @@ private:
void reload_fs_tree(); void reload_fs_tree();
void preview_file(const std::string &filename); void preview_file(const std::string &filename);
void extract_file(const std::string &filename); void extract_file(const std::string &filename);
void open_file(const std::string &filename);
void edit_file(const std::string &filename); void edit_file(const std::string &filename);
void file_context_menu(const QPoint &pos); void file_context_menu(const QPoint &pos);
}; };

View File

@@ -40,13 +40,14 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QMenuBar" name="menubar"> <widget class="QMenuBar" name="menubar">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>800</width> <width>900</width>
<height>32</height> <height>30</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuVault"> <widget class="QMenu" name="menuVault">
@@ -64,25 +65,43 @@
<string>Files</string> <string>Files</string>
</property> </property>
<addaction name="actionAddFiles"/> <addaction name="actionAddFiles"/>
<addaction name="actionExtract_All"/>
</widget> </widget>
<addaction name="menuVault"/> <addaction name="menuVault"/>
<addaction name="menuFiles"/> <addaction name="menuFiles"/>
</widget> </widget>
<action name="actionNew"> <action name="actionNew">
<property name="icon">
<iconset theme="document-new"/>
</property>
<property name="text"> <property name="text">
<string>New</string> <string>New</string>
</property> </property>
</action> </action>
<action name="actionOpen"> <action name="actionOpen">
<property name="icon">
<iconset theme="document-open"/>
</property>
<property name="text"> <property name="text">
<string>Open</string> <string>Open</string>
</property> </property>
</action> </action>
<action name="actionAddFiles"> <action name="actionAddFiles">
<property name="icon">
<iconset theme="list-add"/>
</property>
<property name="text"> <property name="text">
<string>Add</string> <string>Add</string>
</property> </property>
</action> </action>
<action name="actionExtract_All">
<property name="icon">
<iconset theme="document-save-as"/>
</property>
<property name="text">
<string>Extract All</string>
</property>
</action>
</widget> </widget>
<resources/> <resources/>
<connections/> <connections/>

View File

@@ -1,9 +1,26 @@
#include "vault.h" #include "vault.h"
#include "common.h" #include "common.h"
#include "crypto.h" #include "crypto.h"
#include <array>
#include <botan/auto_rng.h> #include <botan/auto_rng.h>
#include <filesystem>
#define WRITE(file, data, size) \
ASSERT(file.write(to_char_ptr(data), static_cast<i64>(size)))
namespace {
template <typename T> constexpr char *to_char_ptr(T *ptr) noexcept {
return reinterpret_cast<char *>(ptr);
}
template <typename T> constexpr const char *to_char_ptr(const T *ptr) noexcept {
return reinterpret_cast<const char *>(ptr);
}
template <typename T> constexpr std::span<const u8> to_span(const T &x) {
return std::span<const u8>(reinterpret_cast<const u8 *>(x.data()), x.size());
}
} // namespace
Vault::Vault(std::string path, const std::string &password) Vault::Vault(std::string path, const std::string &password)
: m_path(std::move(path)) { : m_path(std::move(path)) {
@@ -21,7 +38,30 @@ Vault::Vault(std::string path, const std::string &password)
std::array<u8, 16> salt{}; std::array<u8, 16> salt{};
ASSERT(m_file.read(to_char_ptr(salt.data()), 16)); ASSERT(m_file.read(to_char_ptr(salt.data()), 16));
m_key = Crypto::derive_key_argon2id(password, salt); m_key = Crypto::derive_key(password, salt);
Botan::secure_vector<u8> check_ciphertext;
check_ciphertext.resize(46);
ASSERT(m_file.read(to_char_ptr(check_ciphertext.data()), 46));
Crypto::decrypt(m_key, check_ciphertext);
}
void Vault::create(const std::string &path, const std::string &password) {
static Botan::AutoSeeded_RNG rng;
auto salt = rng.random_array<16>();
auto key = Crypto::derive_key(password, salt);
const std::string_view content = "LETSGO";
auto check_ciphertext = Crypto::encrypt(key, to_span(content));
std::ofstream file(path, std::ios::binary);
WRITE(file, "DULL", 4);
WRITE(file, to_char_ptr(&VERSION), sizeof(VERSION));
WRITE(file, to_char_ptr(salt.data()), 16);
WRITE(file, to_char_ptr(check_ciphertext.data()), 46);
file.close();
} }
std::vector<FileHeader> Vault::read_file_headers() { std::vector<FileHeader> Vault::read_file_headers() {
@@ -33,7 +73,9 @@ std::vector<FileHeader> Vault::read_file_headers() {
while (true) { while (true) {
auto header = read_file_header(); auto header = read_file_header();
if (header) { if (header) {
headers.push_back(header.value()); if (!header->deleted) {
headers.push_back(header.value());
}
m_file.seekg(static_cast<i64>(header->content_ciphertext_size), m_file.seekg(static_cast<i64>(header->content_ciphertext_size),
std::ios::cur); std::ios::cur);
} else { } else {
@@ -54,7 +96,7 @@ std::optional<std::string> Vault::read_file(const std::string &filename) {
break; break;
} }
if (header->name == filename) { if (!header->deleted && header->name == filename) {
Botan::secure_vector<u8> ciphertext; Botan::secure_vector<u8> ciphertext;
ciphertext.resize(header->content_ciphertext_size); ciphertext.resize(header->content_ciphertext_size);
if (!m_file.read(to_char_ptr(ciphertext.data()), if (!m_file.read(to_char_ptr(ciphertext.data()),
@@ -62,8 +104,7 @@ std::optional<std::string> Vault::read_file(const std::string &filename) {
break; break;
} }
auto plaintext = Crypto::decrypt_xchacha20_poly1305( auto plaintext = Crypto::decrypt(m_key, ciphertext);
ciphertext, m_key, header->content_nonce);
return std::string(to_char_ptr(plaintext.data()), plaintext.size()); return std::string(to_char_ptr(plaintext.data()), plaintext.size());
} }
@@ -79,28 +120,18 @@ void Vault::create_file(const std::string &filename,
m_file.clear(); m_file.clear();
m_file.seekp(0, std::ios::end); m_file.seekp(0, std::ios::end);
static Botan::AutoSeeded_RNG rng; auto filename_ciphertext = Crypto::encrypt(m_key, to_span(filename));
auto name_nonce = rng.random_array<24>();
auto content_nonce = rng.random_array<24>();
Botan::secure_vector<u8> filename_sv(filename.begin(), filename.end());
auto filename_ciphertext =
Crypto::encrypt_xchacha20_poly1305(filename_sv, m_key, name_nonce);
u64 filename_ciphertext_size = filename_ciphertext.size(); u64 filename_ciphertext_size = filename_ciphertext.size();
Botan::secure_vector<u8> content_sv(content.begin(), content.end()); auto ciphertext = Crypto::encrypt(m_key, to_span(content));
auto ciphertext =
Crypto::encrypt_xchacha20_poly1305(content_sv, m_key, content_nonce);
u64 ciphertext_size = ciphertext.size(); u64 ciphertext_size = ciphertext.size();
ASSERT(m_file.write(to_char_ptr(name_nonce.data()), name_nonce.size())); bool deleted = false;
ASSERT(m_file.write(to_char_ptr(&filename_ciphertext_size), sizeof(u64))); WRITE(m_file, &deleted, sizeof(bool)); // deleted flag
ASSERT(m_file.write(to_char_ptr(filename_ciphertext.data()), WRITE(m_file, &filename_ciphertext_size, sizeof(u64));
static_cast<i64>(filename_ciphertext_size))); WRITE(m_file, filename_ciphertext.data(), filename_ciphertext_size);
ASSERT(m_file.write(to_char_ptr(content_nonce.data()), content_nonce.size())); WRITE(m_file, &ciphertext_size, sizeof(u64));
ASSERT(m_file.write(to_char_ptr(&ciphertext_size), sizeof(u64))); WRITE(m_file, ciphertext.data(), ciphertext_size);
ASSERT(m_file.write(to_char_ptr(ciphertext.data()),
static_cast<i64>(ciphertext_size)));
m_file.flush(); m_file.flush();
} }
@@ -108,9 +139,6 @@ void Vault::delete_file(const std::string &filename) {
m_file.clear(); m_file.clear();
m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg); m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg);
i64 entry_start = -1;
u64 entry_total_size = 0;
while (true) { while (true) {
i64 current_pos = m_file.tellg(); i64 current_pos = m_file.tellg();
@@ -119,41 +147,27 @@ void Vault::delete_file(const std::string &filename) {
break; break;
} }
if (header->name == filename) { if (!header->deleted && header->name == filename) {
entry_start = current_pos; // zero the ciphertext and mark as deleted
entry_total_size = 24 + sizeof(u64) + header->name_ciphertext_size + 24 + m_file.seekp(m_file.tellg());
sizeof(u64) + header->content_ciphertext_size; std::array<u8, 4096> zeros = {0};
m_file.seekg(static_cast<i64>(header->content_ciphertext_size), u64 remaining = header->content_ciphertext_size;
std::ios::cur); 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; break;
} }
m_file.seekg(static_cast<i64>(header->content_ciphertext_size), m_file.seekg(static_cast<i64>(header->content_ciphertext_size),
std::ios::cur); std::ios::cur);
} }
if (entry_start != -1) {
i64 entry_end = entry_start + static_cast<i64>(entry_total_size);
m_file.clear();
m_file.seekg(entry_end, std::ios::beg);
std::string remaining;
remaining.assign(std::istreambuf_iterator<char>(m_file),
std::istreambuf_iterator<char>());
m_file.clear();
m_file.seekp(entry_start, std::ios::beg);
ASSERT(m_file.write(remaining.data(), static_cast<i64>(remaining.size())));
i64 new_size = entry_start + static_cast<i64>(remaining.size());
m_file.flush();
m_file.close();
std::filesystem::resize_file(m_path, new_size);
m_file.open(m_path, std::ios::in | std::ios::out | std::ios::binary);
ASSERT(m_file.good());
}
} }
void Vault::update_file(const std::string &filename, void Vault::update_file(const std::string &filename,
@@ -165,7 +179,7 @@ void Vault::update_file(const std::string &filename,
std::optional<FileHeader> Vault::read_file_header() { std::optional<FileHeader> Vault::read_file_header() {
FileHeader header{}; FileHeader header{};
if (!m_file.read(to_char_ptr(header.name_nonce.data()), 24)) { if (!m_file.read(to_char_ptr(&header.deleted), 1)) {
return std::nullopt; return std::nullopt;
} }
@@ -182,16 +196,14 @@ std::optional<FileHeader> Vault::read_file_header() {
return std::nullopt; return std::nullopt;
} }
auto name = Crypto::decrypt_xchacha20_poly1305(name_ciphertext, m_key, if (!header.deleted) {
header.name_nonce); auto name = Crypto::decrypt(m_key, name_ciphertext);
header.name = std::string(name.begin(), name.end()); header.name = std::string(name.begin(), name.end());
if (!m_file.read(to_char_ptr(header.content_nonce.data()), 24)) {
return std::nullopt;
} }
if (!m_file.read(to_char_ptr(&header.content_ciphertext_size), sizeof(u64))) { if (!m_file.read(to_char_ptr(&header.content_ciphertext_size), sizeof(u64))) {
return std::nullopt; return std::nullopt;
} }
return header; return header;
} }

View File

@@ -1,20 +1,18 @@
#pragma once #pragma once
#include "common.h" #include "common.h"
#include <array>
#include <botan/secmem.h> #include <botan/secmem.h>
#include <fstream> #include <fstream>
#include <optional> #include <optional>
constexpr i16 VERSION = 1; constexpr i16 VERSION = 1;
constexpr u64 AFTER_HEADER_OFFSET = 22; constexpr u64 AFTER_HEADER_OFFSET = 68;
// !!! REMEMBER TO UPDATE entry_total_size IN Vault::delete_file // !!! REMEMBER TO UPDATE entry_total_size IN Vault::delete_file
struct FileHeader { struct FileHeader {
std::array<u8, 24> name_nonce; bool deleted;
u64 name_ciphertext_size; u64 name_ciphertext_size;
std::string name; std::string name;
std::array<u8, 24> content_nonce;
u64 content_ciphertext_size; u64 content_ciphertext_size;
}; };
@@ -22,12 +20,16 @@ class Vault {
public: public:
explicit Vault(std::string path, const std::string &password); explicit Vault(std::string path, const std::string &password);
static void create(const std::string &path, const std::string &password);
std::vector<FileHeader> read_file_headers(); std::vector<FileHeader> read_file_headers();
std::optional<std::string> read_file(const std::string &name); std::optional<std::string> read_file(const std::string &name);
void create_file(const std::string &name, const std::string &content); void create_file(const std::string &name, const std::string &content);
void delete_file(const std::string &name); void delete_file(const std::string &name);
void update_file(const std::string &name, const std::string &content); void update_file(const std::string &name, const std::string &content);
const std::string &path() const { return m_path; }
private: private:
std::string m_path; std::string m_path;
std::fstream m_file; std::fstream m_file;