Compare commits

...

6 Commits

11 changed files with 312 additions and 273 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-*,-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'

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@
/build /build
*.py *.py
*.dull *.dull
/TODO
/.clangd

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)
@@ -16,10 +21,9 @@ set(CMAKE_AUTOUIC ON)
qt6_wrap_ui(UI_HEADERS src/mainwindow.ui) 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 dull DESTINATION bin)
install(TARGETS ${PROJECT_NAME} DESTINATION bin)

View File

@@ -1,4 +1,4 @@
BSD 3-Clause License BSD 2-Clause License
Copyright (c) 2025-2026, Antoni Piasecki 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 this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. 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" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

View File

@@ -3,9 +3,8 @@
Desktop app for securely storing sensitive files Desktop app for securely storing sensitive files
## Features ## Features
* **Pretty usable UI**
* **Overkill encryption:** XChaCha20-Poly1305 + Argon2id(m=1GB,t=8,p=4) key derivation
* **Cross-platform:** Tested on Linux, Windows and macOS * **Cross-platform:** Tested on Linux, Windows and macOS
* **Overkill encryption:** XChaCha20-Poly1305 + Argon2id key derivation
* **Drag and Drop support** * **Drag and Drop support**
## Building ## Building

View File

@@ -2,39 +2,22 @@
#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 u16 = uint16_t; using u16 = uint16_t;
using i32 = int32_t; using i32 = int32_t;
using u32 = uint32_t; using u32 = uint32_t;
using i64 = int64_t; using i64 = int64_t;
using u64 = uint64_t; using u64 = uint64_t;
static_assert(sizeof(float) * 8 == 32);
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,57 +1,63 @@
#pragma once #pragma once
#include "common.h" #include "common.h"
#include <algorithm>
#include <botan/aead.h> #include <botan/aead.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 thread_local auto cipher = Botan::AEAD_Mode::create_or_throw(
"ChaCha20Poly1305", Botan::Cipher_Dir::Encryption); "ChaCha20Poly1305", Botan::Cipher_Dir::Encryption);
cipher->clear();
auto nonce = rng.random_array<24>();
cipher->set_key(key);
cipher->start(nonce);
Botan::secure_vector<u8> res;
res.resize(24 + plaintext.size());
std::ranges::copy(nonce, res.begin());
std::ranges::copy(plaintext, res.begin() + 24);
cipher->finish(res, 24);
return res;
}
inline Botan::secure_vector<u8>
decrypt(const Botan::secure_vector<u8> &key,
const Botan::secure_vector<u8> &ciphertext_with_nonce) {
ASSERT(ciphertext_with_nonce.size() >= 40);
ASSERT(key.size() == 32);
std::array<u8, 24> nonce{};
std::copy_n(ciphertext_with_nonce.begin(), 24, nonce.begin());
static thread_local auto cipher = Botan::AEAD_Mode::create_or_throw(
"ChaCha20Poly1305", Botan::Cipher_Dir::Decryption);
cipher->clear();
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(ciphertext_with_nonce.begin() + 24,
ciphertext_with_nonce.end());
cipher->finish(ciphertext); cipher->finish(ciphertext);
return ciphertext; return ciphertext;
} }
inline Botan::secure_vector<u8> inline Botan::secure_vector<u8> derive_key(const std::string &password,
decrypt_xchacha20_poly1305(const Botan::secure_vector<u8> &ciphertext,
const Botan::secure_vector<u8> &key,
const std::array<u8, 24> &nonce) {
ASSERT(ciphertext.size() >= 16);
ASSERT(key.size() == 32);
ASSERT(nonce.size() == 24);
auto cipher = Botan::AEAD_Mode::create_or_throw(
"ChaCha20Poly1305", Botan::Cipher_Dir::Decryption);
cipher->set_key(key);
cipher->start(nonce);
Botan::secure_vector<u8> plaintext = ciphertext;
cipher->finish(plaintext);
return plaintext;
}
inline Botan::secure_vector<u8>
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(),

View File

@@ -1,6 +1,5 @@
// TODO: actual fs // TODO: actual fs
#include "mainwindow.h" #include "mainwindow.h"
#include "crypto.h"
#include <QDesktopServices> #include <QDesktopServices>
#include <QDropEvent> #include <QDropEvent>
#include <QFileDialog> #include <QFileDialog>
@@ -8,8 +7,33 @@
#include <QMessageBox> #include <QMessageBox>
#include <QMimeData> #include <QMimeData>
#include <QTemporaryDir> #include <QTemporaryDir>
#include <botan/auto_rng.h> #include <array>
#include <botan/exceptn.h> #include <botan/exceptn.h>
#include <stdexcept>
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>()) {
@@ -31,42 +55,27 @@ MainWindow::MainWindow(QWidget *parent)
path += ".dull"; path += ".dull";
} }
QString password = QInputDialog::getText( QString password;
while (true) {
password = QInputDialog::getText(
this, "Choose a password", "Choose a password", QLineEdit::Password); this, "Choose a password", "Choose a password", QLineEdit::Password);
if (password.length() < 8) { if (password.length() < 8) {
QMessageBox::critical(this, "Error", QMessageBox::critical(this, "Error",
"Password must be at least 8 characters long."); "Password must be at least 8 characters long.");
return; } else {
break;
}
} }
ui->statusbar->showMessage("Creating the vault..."); ui->statusbar->showMessage("Creating the vault...");
QCoreApplication::processEvents(); QCoreApplication::processEvents();
static Botan::AutoSeeded_RNG rng;
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 = m_vault =
std::make_unique<Vault>(path.toStdString(), password.toStdString()); Vault::create_and_open(path.toStdString(), password.toStdString());
reload_fs_tree();
ui->statusbar->clearMessage(); ui->statusbar->showMessage("Opened " + path);
reload_fs_tree();
}); });
connect(ui->actionOpen, &QAction::triggered, this, [this]() { connect(ui->actionOpen, &QAction::triggered, this, [this]() {
@@ -90,7 +99,7 @@ MainWindow::MainWindow(QWidget *parent)
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(); ui->statusbar->showMessage("Opened " + path);
} catch (const Botan::Invalid_Authentication_Tag &e) { } catch (const Botan::Invalid_Authentication_Tag &e) {
QMessageBox::critical(this, "Error", "Invalid password."); QMessageBox::critical(this, "Error", "Invalid password.");
ui->statusbar->clearMessage(); ui->statusbar->clearMessage();
@@ -113,13 +122,18 @@ 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);
} }
m_vault->flush();
reload_fs_tree(); reload_fs_tree();
ui->statusbar->showMessage("Added " + QString::number(paths.size()) + ui->statusbar->showMessage("Added " + QString::number(paths.size()) +
@@ -137,14 +151,14 @@ MainWindow::MainWindow(QWidget *parent)
return; return;
} }
auto headers = m_vault->read_file_headers(); const auto &file_map = m_vault->file_map();
for (const auto &header : headers) { for (const auto &[k, _] : file_map) {
auto content = m_vault->read_file(header.name); auto content = m_vault->read_file(k);
if (content) { ui->statusbar->showMessage("Extracting " + QString::fromStdString(k) +
std::ofstream file(path.toStdString() + "/" + header.name, "...");
std::ios::binary); QCoreApplication::processEvents();
file.write(content->data(), static_cast<i64>(content->size())); std::ofstream file(path.toStdString() + "/" + k, std::ios::binary);
} file.write(content.data(), static_cast<i64>(content.size()));
} }
ui->statusbar->showMessage("Extracted all files to " + path); ui->statusbar->showMessage("Extracted all files to " + path);
}); });
@@ -155,12 +169,13 @@ void MainWindow::reload_fs_tree() {
ui->menuFiles->setEnabled(true); ui->menuFiles->setEnabled(true);
ui->fsTreeWidget->clear(); ui->fsTreeWidget->clear();
auto headers = m_vault->read_file_headers(); const auto &file_map = m_vault->file_map();
for (const auto &header : headers) { for (const auto &[k, v] : file_map) {
auto *item = new QTreeWidgetItem(ui->fsTreeWidget); auto *item = new QTreeWidgetItem(ui->fsTreeWidget);
item->setIcon(0, style()->standardIcon(QStyle::SP_FileIcon)); item->setIcon(0, style()->standardIcon(QStyle::SP_FileIcon));
item->setText(0, QString::fromStdString(header.name)); item->setText(0, QString::fromStdString(k));
item->setText(1, QString::number(header.content_ciphertext_size)); item->setText(
1, QString::fromStdString(format_bytes(v.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);
@@ -170,17 +185,17 @@ void MainWindow::reload_fs_tree() {
void MainWindow::preview_file(const std::string &filename) { void MainWindow::preview_file(const std::string &filename) {
ui->filePreview->setVisible(true); ui->filePreview->setVisible(true);
try {
auto content = m_vault->read_file(filename); auto content = m_vault->read_file(filename);
if (content) { ui->filePreview->setText(QString::fromStdString(content));
ui->filePreview->setText(QString::fromStdString(content.value())); } catch (std::out_of_range &) {
} else {
qWarning() << "File to preview not found"; qWarning() << "File to preview not found";
} }
} }
void MainWindow::extract_file(const std::string &filename) { void MainWindow::extract_file(const std::string &filename) {
try {
auto content = m_vault->read_file(filename); auto content = m_vault->read_file(filename);
if (content) {
QString path = QFileDialog::getSaveFileName( QString path = QFileDialog::getSaveFileName(
this, "Choose location to extract", this, "Choose location to extract",
QDir::currentPath() + "/" + QString::fromStdString(filename)); QDir::currentPath() + "/" + QString::fromStdString(filename));
@@ -189,24 +204,47 @@ 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); ui->statusbar->showMessage("Extracted to " + path);
} else { } catch (std::out_of_range &) {
qWarning() << "File to extract not found"; qWarning() << "File to extract not found";
} }
} }
void MainWindow::edit_file(const std::string &filename) { void MainWindow::open_file(const std::string &filename) {
try {
auto content = m_vault->read_file(filename); auto content = m_vault->read_file(filename);
if (content) {
QTemporaryDir dir; QTemporaryDir dir;
ASSERT(dir.isValid()); ASSERT(dir.isValid());
std::string path = dir.path().toStdString() + "/" + filename; std::string path = dir.path().toStdString() + "/" + filename;
std::ofstream file(path); std::ofstream file(path);
file.write(content->data(), static_cast<i64>(content->size())); 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
} catch (std::out_of_range &) {
qWarning() << "File to open not found";
}
}
void MainWindow::edit_file(const std::string &filename) {
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<i64>(content.size()));
file.close(); file.close();
QDesktopServices::openUrl( QDesktopServices::openUrl(
@@ -215,16 +253,18 @@ void MainWindow::edit_file(const std::string &filename) {
"Please edit the file in the opened editor and " "Please edit the file in the opened editor and "
"save it. Click OK when done."); "save it. Click OK when done.");
std::ifstream file2(path, std::ios::binary); std::ifstream file2(path, std::ios::binary | std::ios::ate);
std::string new_content((std::istreambuf_iterator<char>(file2)), ASSERT(file2.good());
std::istreambuf_iterator<char>()); 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); m_vault->update_file(filename, new_content);
reload_fs_tree(); reload_fs_tree();
ui->statusbar->showMessage("File updated"); ui->statusbar->showMessage("File updated");
// QTemporaryDir gets deleted when it goes out of scope // QTemporaryDir gets deleted when it goes out of scope
} else { } catch (std::out_of_range &) {
qWarning() << "File to edit not found"; qWarning() << "File to edit not found";
} }
} }
@@ -237,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()); });
@@ -256,6 +304,7 @@ void MainWindow::file_context_menu(const QPoint &pos) {
style()->standardIcon(QStyle::SP_DialogCancelButton), "Delete"); style()->standardIcon(QStyle::SP_DialogCancelButton), "Delete");
connect(delete_action, &QAction::triggered, this, [this, item]() { connect(delete_action, &QAction::triggered, this, [this, item]() {
m_vault->delete_file(item->text(0).toStdString()); m_vault->delete_file(item->text(0).toStdString());
m_vault->flush();
reload_fs_tree(); reload_fs_tree();
}); });
@@ -288,15 +337,17 @@ 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);
} }
m_vault->flush();
ui->statusbar->showMessage( ui->statusbar->showMessage(
"Added " + QString::number(event->mimeData()->urls().size()) + " files"); "Added " + QString::number(event->mimeData()->urls().size()) + " files");
reload_fs_tree(); reload_fs_tree();

View File

@@ -21,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

@@ -2,7 +2,25 @@
#include "common.h" #include "common.h"
#include "crypto.h" #include "crypto.h"
#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)) {
@@ -13,27 +31,46 @@ Vault::Vault(std::string path, const std::string &password)
ASSERT(m_file.read(magic.data(), 4)); ASSERT(m_file.read(magic.data(), 4));
ASSERT(std::string_view(magic.data(), magic.size()) == "DULL"); 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(m_file.read(to_char_ptr(&version), sizeof(version)));
ASSERT(version == VERSION); ASSERT(version == VERSION);
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);
std::array<u8, 24> check_nonce{};
ASSERT(m_file.read(to_char_ptr(check_nonce.data()), 24));
Botan::secure_vector<u8> check_ciphertext; Botan::secure_vector<u8> check_ciphertext;
check_ciphertext.resize(22); check_ciphertext.resize(46);
ASSERT(m_file.read(to_char_ptr(check_ciphertext.data()), 22)); ASSERT(m_file.read(to_char_ptr(check_ciphertext.data()), 46));
Crypto::decrypt_xchacha20_poly1305(check_ciphertext, m_key, check_nonce); Crypto::decrypt(m_key, check_ciphertext);
build_file_map();
} }
std::vector<FileHeader> Vault::read_file_headers() { std::unique_ptr<Vault> Vault::create_and_open(const std::string &path,
std::vector<FileHeader> headers; 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();
return std::make_unique<Vault>(path, password);
}
void Vault::build_file_map() {
m_file_map.clear();
m_file.clear(); m_file.clear();
m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg); m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg);
@@ -41,147 +78,99 @@ 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) {
m_file_map[header->name] = 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 {
break; break;
} }
} }
return headers;
} }
std::optional<std::string> Vault::read_file(const std::string &filename) { std::string Vault::read_file(const std::string &name) {
auto header = m_file_map.at(name);
ASSERT(!header.deleted);
m_file.clear(); m_file.clear();
m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg); m_file.seekg(static_cast<i64>(header.content_offset), std::ios::beg);
while (true) {
auto header = read_file_header();
if (!header) {
break;
}
if (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()), ASSERT(m_file.read(to_char_ptr(ciphertext.data()),
static_cast<i64>(header->content_ciphertext_size))) { static_cast<i64>(header.content_ciphertext_size)));
break;
}
auto plaintext = Crypto::decrypt_xchacha20_poly1305( auto plaintext = Crypto::decrypt(m_key, ciphertext);
ciphertext, m_key, header->content_nonce); return {to_char_ptr(plaintext.data()), plaintext.size()};
return std::string(to_char_ptr(plaintext.data()), plaintext.size());
}
m_file.seekg(static_cast<i64>(header->content_ciphertext_size),
std::ios::cur);
}
return std::nullopt;
} }
void Vault::create_file(const std::string &filename, void Vault::create_file(const std::string &name, const std::string &content) {
const std::string &content) { ASSERT(!m_file_map.contains(name));
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 offset = m_file.tellp();
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 name_ciphertext = Crypto::encrypt(m_key, to_span(name));
auto filename_ciphertext = u64 name_ciphertext_size = name_ciphertext.size();
Crypto::encrypt_xchacha20_poly1305(filename_sv, m_key, name_nonce);
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, &name_ciphertext_size, sizeof(u64));
static_cast<i64>(filename_ciphertext_size))); WRITE(m_file, name_ciphertext.data(), name_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))); auto content_offset = m_file.tellp();
ASSERT(m_file.write(to_char_ptr(ciphertext.data()), WRITE(m_file, ciphertext.data(), ciphertext_size);
static_cast<i64>(ciphertext_size)));
m_file.flush(); m_file_map[name] = FileHeader{
.offset = static_cast<u64>(offset),
.deleted = false,
.name_ciphertext_size = name_ciphertext_size,
.name = name,
.content_ciphertext_size = ciphertext_size,
.content_offset = static_cast<u64>(content_offset),
};
} }
void Vault::delete_file(const std::string &filename) { void Vault::delete_file(const std::string &name) {
auto header = m_file_map.at(name);
// zero the ciphertext and mark as deleted
m_file.clear(); m_file.clear();
m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg); m_file.seekp(static_cast<i64>(header.content_offset), std::ios::beg);
std::array<u8, 4096> zeros = {0};
i64 entry_start = -1; u64 remaining = header.content_ciphertext_size;
u64 entry_total_size = 0; while (remaining > 0) {
u64 chunk = std::min(remaining, sizeof(zeros));
while (true) { WRITE(m_file, zeros.data(), chunk);
i64 current_pos = m_file.tellg(); remaining -= chunk;
auto header = read_file_header();
if (!header) {
break;
} }
if (header->name == filename) {
entry_start = current_pos;
entry_total_size = 24 + sizeof(u64) + header->name_ciphertext_size + 24 +
sizeof(u64) + header->content_ciphertext_size;
m_file.seekg(static_cast<i64>(header->content_ciphertext_size),
std::ios::cur);
break;
}
m_file.seekg(static_cast<i64>(header->content_ciphertext_size),
std::ios::cur);
}
if (entry_start != -1) {
i64 entry_end = entry_start + static_cast<i64>(entry_total_size);
i64 read_pos = entry_end;
i64 write_pos = entry_start;
std::array<u8, 65536> buffer{};
while (true) {
m_file.clear(); m_file.clear();
m_file.seekg(read_pos); m_file.seekp(static_cast<i64>(header.offset), std::ios::beg);
m_file.read(to_char_ptr(buffer.data()), buffer.size()); bool deleted = true;
auto bytes = m_file.gcount(); WRITE(m_file, &deleted, sizeof(bool));
if (bytes == 0) {
break;
}
m_file.seekp(write_pos); m_file_map.erase(name);
ASSERT(m_file.write(to_char_ptr(buffer.data()), bytes));
read_pos += bytes;
write_pos += bytes;
}
m_file.flush();
m_file.close();
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());
}
} }
void Vault::update_file(const std::string &filename, void Vault::update_file(const std::string &name, const std::string &content) {
const std::string &content) { delete_file(name);
delete_file(filename); create_file(name, content);
create_file(filename, content); flush();
} }
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)) { header.offset = m_file.tellg();
if (!m_file.read(to_char_ptr(&header.deleted), 1)) {
return std::nullopt; return std::nullopt;
} }
@@ -198,16 +187,15 @@ 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;
} }
header.content_offset = m_file.tellg();
return header; return header;
} }

View File

@@ -1,39 +1,48 @@
#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 <memory>
#include <optional> #include <optional>
#include <unordered_map>
constexpr i16 VERSION = 1; constexpr u16 VERSION = 1;
constexpr u64 AFTER_HEADER_OFFSET = 68; constexpr u64 AFTER_HEADER_OFFSET = 68;
// !!! REMEMBER TO UPDATE entry_total_size IN Vault::delete_file
struct FileHeader { struct FileHeader {
std::array<u8, 24> name_nonce; u64 offset;
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;
u64 content_offset;
}; };
class Vault { class Vault {
public: public:
explicit Vault(std::string path, const std::string &password); explicit Vault(std::string path, const std::string &password);
std::vector<FileHeader> read_file_headers(); static std::unique_ptr<Vault> create_and_open(const std::string &path,
std::optional<std::string> read_file(const std::string &name); const std::string &password);
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);
void flush() { m_file.flush(); }
const std::string &path() const { return m_path; } const std::string &path() const { return m_path; }
const std::unordered_map<std::string, FileHeader> &file_map() const {
return m_file_map;
}
private: private:
std::string m_path; std::string m_path;
std::fstream m_file; std::fstream m_file;
Botan::secure_vector<u8> m_key; Botan::secure_vector<u8> m_key;
std::unordered_map<std::string, FileHeader> m_file_map;
void build_file_map();
std::optional<FileHeader> read_file_header(); std::optional<FileHeader> read_file_header();
}; };