Compare commits

...

6 Commits

Author SHA1 Message Date
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
12 changed files with 174 additions and 64 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

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(m=1GB,t=8,p=4) 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

@@ -1,7 +1,6 @@
#pragma once #pragma once
#include "common.h" #include "common.h"
#include <botan/aead.h> #include <botan/aead.h>
#include <botan/hex.h>
#include <botan/pwdhash.h> #include <botan/pwdhash.h>
namespace Crypto { namespace Crypto {
@@ -60,4 +59,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,6 +1,7 @@
// TODO: actual fs
#include "mainwindow.h" #include "mainwindow.h"
#include "crypto.h"
#include <QDesktopServices> #include <QDesktopServices>
#include <QDragEnterEvent>
#include <QDropEvent> #include <QDropEvent>
#include <QFileDialog> #include <QFileDialog>
#include <QInputDialog> #include <QInputDialog>
@@ -8,7 +9,7 @@
#include <QMimeData> #include <QMimeData>
#include <QTemporaryDir> #include <QTemporaryDir>
#include <botan/auto_rng.h> #include <botan/auto_rng.h>
#include <botan/hex.h> #include <botan/exceptn.h>
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,12 +22,12 @@ 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";
} }
@@ -38,19 +39,34 @@ MainWindow::MainWindow(QWidget *parent)
return; return;
} }
ui->statusbar->showMessage("Creating the vault...");
QCoreApplication::processEvents();
static Botan::AutoSeeded_RNG rng; static Botan::AutoSeeded_RNG rng;
auto salt_sv = rng.random_vec(16); auto salt = rng.random_array<16>();
std::vector<u8> salt(salt_sv.begin(), salt_sv.end());
auto key = Crypto::derive_key_argon2id(password.toStdString(), salt);
auto check_nonce = rng.random_array<24>();
const std::string content = "LETSGO";
Botan::secure_vector<u8> content_sv(content.begin(), content.end());
auto check_ciphertext =
Crypto::encrypt_xchacha20_poly1305(content_sv, key, check_nonce);
std::ofstream create(path.toStdString(), std::ios::binary); std::ofstream create(path.toStdString(), std::ios::binary);
create.write("DULL", 4); create.write("DULL", 4);
create.write(to_char_ptr(&VERSION), sizeof(VERSION)); create.write(to_char_ptr(&VERSION), sizeof(VERSION));
create.write(to_char_ptr(salt.data()), 16); create.write(to_char_ptr(salt.data()), 16);
create.write(to_char_ptr(check_nonce.data()), 24);
create.write(to_char_ptr(check_ciphertext.data()), 22);
create.close(); create.close();
m_vault = m_vault =
std::make_unique<Vault>(path.toStdString(), password.toStdString()); std::make_unique<Vault>(path.toStdString(), password.toStdString());
reload_fs_tree(); reload_fs_tree();
ui->statusbar->clearMessage();
}); });
connect(ui->actionOpen, &QAction::triggered, this, [this]() { connect(ui->actionOpen, &QAction::triggered, this, [this]() {
@@ -63,12 +79,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->clearMessage();
} catch (const Botan::Invalid_Authentication_Tag &e) {
QMessageBox::critical(this, "Error", "Invalid password.");
ui->statusbar->clearMessage();
return;
}
}); });
connect( connect(
@@ -95,16 +122,43 @@ MainWindow::MainWindow(QWidget *parent)
} }
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) {
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::number(header.content_ciphertext_size));
} }
@@ -136,6 +190,8 @@ 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";
} }
@@ -166,6 +222,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";
@@ -240,7 +297,9 @@ void MainWindow::dropEvent(QDropEvent *event) {
m_vault->create_file(path_to_filename(u.toLocalFile().toStdString()), m_vault->create_file(path_to_filename(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

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,7 +1,6 @@
#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> #include <filesystem>
@@ -22,6 +21,15 @@ Vault::Vault(std::string path, const std::string &password)
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_argon2id(password, salt);
std::array<u8, 24> check_nonce{};
ASSERT(m_file.read(to_char_ptr(check_nonce.data()), 24));
Botan::secure_vector<u8> check_ciphertext;
check_ciphertext.resize(22);
ASSERT(m_file.read(to_char_ptr(check_ciphertext.data()), 22));
Crypto::decrypt_xchacha20_poly1305(check_ciphertext, m_key, check_nonce);
} }
std::vector<FileHeader> Vault::read_file_headers() { std::vector<FileHeader> Vault::read_file_headers() {
@@ -135,21 +143,29 @@ void Vault::delete_file(const std::string &filename) {
if (entry_start != -1) { if (entry_start != -1) {
i64 entry_end = entry_start + static_cast<i64>(entry_total_size); i64 entry_end = entry_start + static_cast<i64>(entry_total_size);
m_file.clear(); i64 read_pos = entry_end;
m_file.seekg(entry_end, std::ios::beg); i64 write_pos = entry_start;
std::string remaining; std::array<u8, 65536> buffer{};
remaining.assign(std::istreambuf_iterator<char>(m_file), while (true) {
std::istreambuf_iterator<char>()); m_file.clear();
m_file.seekg(read_pos);
m_file.read(to_char_ptr(buffer.data()), buffer.size());
auto bytes = m_file.gcount();
if (bytes == 0) {
break;
}
m_file.clear(); m_file.seekp(write_pos);
m_file.seekp(entry_start, std::ios::beg); ASSERT(m_file.write(to_char_ptr(buffer.data()), bytes));
ASSERT(m_file.write(remaining.data(), static_cast<i64>(remaining.size())));
read_pos += bytes;
write_pos += bytes;
}
i64 new_size = entry_start + static_cast<i64>(remaining.size());
m_file.flush(); m_file.flush();
m_file.close(); m_file.close();
std::filesystem::resize_file(m_path, new_size); std::filesystem::resize_file(m_path, write_pos);
m_file.open(m_path, std::ios::in | std::ios::out | std::ios::binary); m_file.open(m_path, std::ios::in | std::ios::out | std::ios::binary);
ASSERT(m_file.good()); ASSERT(m_file.good());
@@ -194,4 +210,4 @@ std::optional<FileHeader> Vault::read_file_header() {
return std::nullopt; return std::nullopt;
} }
return header; return header;
} }

View File

@@ -7,7 +7,7 @@
#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 {
@@ -28,6 +28,8 @@ public:
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;