Compare commits

3 Commits
0.1 ... main

Author SHA1 Message Date
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
9 changed files with 116 additions and 28 deletions

View File

@@ -1,22 +1,27 @@
pkgname=dull
pkgname=dull-git
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")
makedepends=('cmake' 'git')
source=("git+${url}.git")
sha256sums=('SKIP')
DEBUGPKG=()
options=('!debug')
pkgver() {
cd "${pkgname%-git}"
git describe --long --tags | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g'
}
build() {
cd "${pkgname}-${pkgver}"
cd "${pkgname%-git}"
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
cmake --build build -j$(nproc)
}
package() {
cd "${pkgname}-${pkgver}/build"
cd "${pkgname%-git}/build"
make DESTDIR="${pkgdir}" install
}

View File

@@ -25,7 +25,7 @@ sudo pacman -S base-devel
makepkg -si
```
### Windows / MSYS2
### 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

View File

@@ -1,7 +1,6 @@
#pragma once
#include "common.h"
#include <botan/aead.h>
#include <botan/hex.h>
#include <botan/pwdhash.h>
namespace Crypto {
@@ -60,4 +59,4 @@ derive_key_argon2id(const std::string &password,
return key;
}
}; // namespace Crypto
}; // namespace Crypto

View File

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

View File

@@ -1,6 +1,7 @@
// TODO: actual fs
#include "mainwindow.h"
#include "crypto.h"
#include <QDesktopServices>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QFileDialog>
#include <QInputDialog>
@@ -8,7 +9,6 @@
#include <QMimeData>
#include <QTemporaryDir>
#include <botan/auto_rng.h>
#include <botan/hex.h>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), ui(std::make_unique<Ui::MainWindow>()) {
@@ -21,12 +21,12 @@ MainWindow::MainWindow(QWidget *parent)
connect(ui->actionNew, &QAction::triggered, this, [this]() {
QString path = QFileDialog::getSaveFileName(this, "Choose vault location",
QDir::currentPath(),
"Dull Vaults (*.dull)");
"Dull vaults (*.dull)");
if (path.isEmpty()) {
return;
}
if (!path.contains(".dull")) {
if (!path.endsWith(".dull")) {
path += ".dull";
}
@@ -38,19 +38,34 @@ MainWindow::MainWindow(QWidget *parent)
return;
}
ui->statusbar->showMessage("Creating the vault...");
QCoreApplication::processEvents();
static Botan::AutoSeeded_RNG rng;
auto salt_sv = rng.random_vec(16);
std::vector<u8> salt(salt_sv.begin(), salt_sv.end());
auto salt = rng.random_array<16>();
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);
create.write("DULL", 4);
create.write(to_char_ptr(&VERSION), sizeof(VERSION));
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();
m_vault =
std::make_unique<Vault>(path.toStdString(), password.toStdString());
reload_fs_tree();
ui->statusbar->clearMessage();
});
connect(ui->actionOpen, &QAction::triggered, this, [this]() {
@@ -63,12 +78,23 @@ MainWindow::MainWindow(QWidget *parent)
QString password = QInputDialog::getText(
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 =
std::make_unique<Vault>(path.toStdString(), password.toStdString());
reload_fs_tree();
try {
m_vault =
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(
@@ -95,16 +121,43 @@ MainWindow::MainWindow(QWidget *parent)
}
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() {
setWindowTitle(QString::fromStdString(m_vault->path()) + " - dull");
ui->menuFiles->setEnabled(true);
ui->fsTreeWidget->clear();
auto headers = m_vault->read_file_headers();
for (const auto &header : headers) {
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::number(header.content_ciphertext_size));
}
@@ -136,6 +189,8 @@ void MainWindow::extract_file(const std::string &filename) {
std::ofstream file(path.toStdString(), std::ios::binary);
file.write(content->data(), static_cast<i64>(content->size()));
ui->statusbar->showMessage("Extracted to " + path);
} else {
qWarning() << "File to extract not found";
}
@@ -166,6 +221,7 @@ void MainWindow::edit_file(const std::string &filename) {
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 {
qWarning() << "File to edit not found";
@@ -240,7 +296,9 @@ void MainWindow::dropEvent(QDropEvent *event) {
m_vault->create_file(path_to_filename(u.toLocalFile().toStdString()),
content);
}
ui->statusbar->showMessage(
"Added " + QString::number(event->mimeData()->urls().size()) + " files");
reload_fs_tree();
event->acceptProposedAction();
}
}

View File

@@ -2,8 +2,6 @@
#include "ui_mainwindow.h"
#include "vault.h"
#include <QMainWindow>
#include <memory>
class MainWindow : public QMainWindow {
Q_OBJECT

View File

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

View File

@@ -1,7 +1,6 @@
#include "vault.h"
#include "common.h"
#include "crypto.h"
#include <array>
#include <botan/auto_rng.h>
#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));
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() {
@@ -194,4 +202,4 @@ std::optional<FileHeader> Vault::read_file_header() {
return std::nullopt;
}
return header;
}
}

View File

@@ -7,7 +7,7 @@
#include <optional>
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
struct FileHeader {
@@ -28,6 +28,8 @@ public:
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; }
private:
std::string m_path;
std::fstream m_file;