Compare commits
6 Commits
0.1
...
0f4d4f96d6
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f4d4f96d6 | |||
| 1ad1571273 | |||
| 7873020e72 | |||
| 53a222a7da | |||
| f00b5221c1 | |||
| 30ca8b6262 |
28
LICENSE
Normal file
28
LICENSE
Normal 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.
|
||||
22
PKGBUILD
22
PKGBUILD
@@ -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
|
||||
}
|
||||
36
README.md
36
README.md
@@ -4,32 +4,44 @@ Desktop app for securely storing sensitive files
|
||||
|
||||
## Features
|
||||
* **Pretty usable UI**
|
||||
* **Overkill encryption:** XChaCha20-Poly1305 + Argon2id(m=1GB,t=6,p=4) key derivation
|
||||
* **Cross-platform-ish:** Builds on Linux, Windows and macOS
|
||||
* **Overkill encryption:** XChaCha20-Poly1305 + Argon2id(m=1GB,t=8,p=4) key derivation
|
||||
* **Cross-platform:** Tested on Linux, Windows and macOS
|
||||
* **Drag and Drop support**
|
||||
|
||||
## Building
|
||||
|
||||
### Debian/Ubuntu
|
||||
### Debian
|
||||
```
|
||||
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)
|
||||
./build/dull
|
||||
```
|
||||
|
||||
### Arch
|
||||
```
|
||||
sudo pacman -S base-devel
|
||||
makepkg -si
|
||||
```
|
||||
sudo pacman -Syu base-devel cmake qt6-base botan
|
||||
|
||||
### 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
|
||||
|
||||
cmake -S . -B build -DBOTAN_INCLUDE_DIRS=/mingw64/include/botan-3 -DBOTAN_LIBRARIES=/mingw64/lib/libbotan-3.a
|
||||
cmake -B build
|
||||
cmake --build build -j $(nproc)
|
||||
./build/dull
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
cmake -B build -DBOTAN_INCLUDE_DIRS=/mingw64/include/botan-3 -DBOTAN_LIBRARIES=/mingw64/lib/libbotan-3.a
|
||||
cmake --build build -j $(nproc)
|
||||
./build/dull
|
||||
```
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
#include "common.h"
|
||||
#include <botan/aead.h>
|
||||
#include <botan/hex.h>
|
||||
#include <botan/pwdhash.h>
|
||||
|
||||
namespace Crypto {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "mainwindow.h"
|
||||
#include <QApplication>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
|
||||
@@ -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,7 @@
|
||||
#include <QMimeData>
|
||||
#include <QTemporaryDir>
|
||||
#include <botan/auto_rng.h>
|
||||
#include <botan/hex.h>
|
||||
#include <botan/exceptn.h>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent), ui(std::make_unique<Ui::MainWindow>()) {
|
||||
@@ -21,12 +22,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 +39,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 +79,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 +122,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 +190,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 +222,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,6 +297,8 @@ 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();
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
#include "ui_mainwindow.h"
|
||||
#include "vault.h"
|
||||
#include <QMainWindow>
|
||||
#include <memory>
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -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/>
|
||||
|
||||
38
src/vault.cc
38
src/vault.cc
@@ -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() {
|
||||
@@ -135,21 +143,29 @@ void Vault::delete_file(const std::string &filename) {
|
||||
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>());
|
||||
i64 read_pos = entry_end;
|
||||
i64 write_pos = entry_start;
|
||||
std::array<u8, 65536> buffer{};
|
||||
while (true) {
|
||||
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(entry_start, std::ios::beg);
|
||||
ASSERT(m_file.write(remaining.data(), static_cast<i64>(remaining.size())));
|
||||
m_file.seekp(write_pos);
|
||||
ASSERT(m_file.write(to_char_ptr(buffer.data()), bytes));
|
||||
|
||||
read_pos += bytes;
|
||||
write_pos += bytes;
|
||||
}
|
||||
|
||||
i64 new_size = entry_start + static_cast<i64>(remaining.size());
|
||||
m_file.flush();
|
||||
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);
|
||||
ASSERT(m_file.good());
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user