Compare commits
2 Commits
99d239ea08
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f95cb9aba | |||
| 32641f97de |
@@ -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
2
.gitignore
vendored
@@ -2,3 +2,5 @@
|
|||||||
/build
|
/build
|
||||||
*.py
|
*.py
|
||||||
*.dull
|
*.dull
|
||||||
|
/TODO
|
||||||
|
/.clangd
|
||||||
|
|||||||
@@ -21,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)
|
|
||||||
|
|||||||
6
LICENSE
6
LICENSE
@@ -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
|
||||||
|
|||||||
@@ -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 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
|
||||||
|
|||||||
@@ -14,13 +14,10 @@
|
|||||||
} while (0)
|
} while (0)
|
||||||
|
|
||||||
using u8 = uint8_t;
|
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;
|
||||||
|
|||||||
16
src/crypto.h
16
src/crypto.h
@@ -1,5 +1,6 @@
|
|||||||
#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/auto_rng.h>
|
||||||
#include <botan/pwdhash.h>
|
#include <botan/pwdhash.h>
|
||||||
@@ -14,19 +15,19 @@ inline Botan::secure_vector<u8> encrypt(const Botan::secure_vector<u8> &key,
|
|||||||
|
|
||||||
// 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
|
||||||
static 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>();
|
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.begin(), plaintext.end());
|
|
||||||
cipher->finish(ciphertext);
|
|
||||||
|
|
||||||
Botan::secure_vector<u8> res;
|
Botan::secure_vector<u8> res;
|
||||||
res.insert(res.end(), nonce.begin(), nonce.end());
|
res.resize(24 + plaintext.size());
|
||||||
res.insert(res.end(), ciphertext.begin(), ciphertext.end());
|
std::ranges::copy(nonce, res.begin());
|
||||||
|
std::ranges::copy(plaintext, res.begin() + 24);
|
||||||
|
cipher->finish(res, 24);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +40,9 @@ decrypt(const Botan::secure_vector<u8> &key,
|
|||||||
std::array<u8, 24> nonce{};
|
std::array<u8, 24> nonce{};
|
||||||
std::copy_n(ciphertext_with_nonce.begin(), 24, nonce.begin());
|
std::copy_n(ciphertext_with_nonce.begin(), 24, nonce.begin());
|
||||||
|
|
||||||
auto cipher = Botan::AEAD_Mode::create_or_throw(
|
static thread_local auto cipher = Botan::AEAD_Mode::create_or_throw(
|
||||||
"ChaCha20Poly1305", Botan::Cipher_Dir::Decryption);
|
"ChaCha20Poly1305", Botan::Cipher_Dir::Decryption);
|
||||||
|
cipher->clear();
|
||||||
|
|
||||||
cipher->set_key(key);
|
cipher->set_key(key);
|
||||||
cipher->start(nonce);
|
cipher->start(nonce);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <botan/exceptn.h>
|
#include <botan/exceptn.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -69,10 +70,9 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
ui->statusbar->showMessage("Creating the vault...");
|
ui->statusbar->showMessage("Creating the vault...");
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
Vault::create(path.toStdString(), password.toStdString());
|
|
||||||
|
|
||||||
m_vault =
|
m_vault =
|
||||||
std::make_unique<Vault>(path.toStdString(), password.toStdString());
|
Vault::create_and_open(path.toStdString(), password.toStdString());
|
||||||
|
|
||||||
ui->statusbar->showMessage("Opened " + path);
|
ui->statusbar->showMessage("Opened " + path);
|
||||||
|
|
||||||
reload_fs_tree();
|
reload_fs_tree();
|
||||||
@@ -133,6 +133,7 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
file.read(content.data(), size);
|
file.read(content.data(), size);
|
||||||
m_vault->create_file(basename(path.toStdString()), content);
|
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()) +
|
||||||
@@ -150,17 +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) +
|
||||||
ui->statusbar->showMessage("Extracting " +
|
"...");
|
||||||
QString::fromStdString(header.name) + "...");
|
QCoreApplication::processEvents();
|
||||||
QCoreApplication::processEvents();
|
std::ofstream file(path.toStdString() + "/" + k, std::ios::binary);
|
||||||
std::ofstream file(path.toStdString() + "/" + header.name,
|
file.write(content.data(), static_cast<i64>(content.size()));
|
||||||
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);
|
||||||
});
|
});
|
||||||
@@ -171,13 +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::fromStdString(
|
item->setText(
|
||||||
format_bytes(header.content_ciphertext_size)));
|
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);
|
||||||
@@ -187,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);
|
||||||
|
|
||||||
auto content = m_vault->read_file(filename);
|
try {
|
||||||
if (content) {
|
auto content = m_vault->read_file(filename);
|
||||||
ui->filePreview->setText(QString::fromStdString(content.value()));
|
ui->filePreview->setText(QString::fromStdString(content));
|
||||||
} else {
|
} catch (std::out_of_range &) {
|
||||||
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) {
|
||||||
auto content = m_vault->read_file(filename);
|
try {
|
||||||
if (content) {
|
auto content = m_vault->read_file(filename);
|
||||||
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));
|
||||||
@@ -206,24 +204,24 @@ 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::open_file(const std::string &filename) {
|
void MainWindow::open_file(const std::string &filename) {
|
||||||
auto content = m_vault->read_file(filename);
|
try {
|
||||||
if (content) {
|
auto content = m_vault->read_file(filename);
|
||||||
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();
|
file.close();
|
||||||
|
|
||||||
QDesktopServices::openUrl(
|
QDesktopServices::openUrl(
|
||||||
@@ -232,21 +230,21 @@ void MainWindow::open_file(const std::string &filename) {
|
|||||||
"Click OK to delete the opened file.");
|
"Click OK to delete the opened file.");
|
||||||
|
|
||||||
// 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 open not found";
|
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);
|
try {
|
||||||
if (content) {
|
auto content = m_vault->read_file(filename);
|
||||||
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();
|
file.close();
|
||||||
|
|
||||||
QDesktopServices::openUrl(
|
QDesktopServices::openUrl(
|
||||||
@@ -255,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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -346,6 +347,7 @@ void MainWindow::dropEvent(QDropEvent *event) {
|
|||||||
file.read(content.data(), size);
|
file.read(content.data(), size);
|
||||||
m_vault->create_file(basename(u.toLocalFile().toStdString()), content);
|
m_vault->create_file(basename(u.toLocalFile().toStdString()), 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();
|
||||||
|
|||||||
138
src/vault.cc
138
src/vault.cc
@@ -31,7 +31,7 @@ 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);
|
||||||
|
|
||||||
@@ -45,9 +45,12 @@ Vault::Vault(std::string path, const std::string &password)
|
|||||||
ASSERT(m_file.read(to_char_ptr(check_ciphertext.data()), 46));
|
ASSERT(m_file.read(to_char_ptr(check_ciphertext.data()), 46));
|
||||||
|
|
||||||
Crypto::decrypt(m_key, check_ciphertext);
|
Crypto::decrypt(m_key, check_ciphertext);
|
||||||
|
|
||||||
|
build_file_map();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Vault::create(const std::string &path, const std::string &password) {
|
std::unique_ptr<Vault> Vault::create_and_open(const std::string &path,
|
||||||
|
const std::string &password) {
|
||||||
static Botan::AutoSeeded_RNG rng;
|
static Botan::AutoSeeded_RNG rng;
|
||||||
auto salt = rng.random_array<16>();
|
auto salt = rng.random_array<16>();
|
||||||
|
|
||||||
@@ -62,10 +65,12 @@ void Vault::create(const std::string &path, const std::string &password) {
|
|||||||
WRITE(file, to_char_ptr(salt.data()), 16);
|
WRITE(file, to_char_ptr(salt.data()), 16);
|
||||||
WRITE(file, to_char_ptr(check_ciphertext.data()), 46);
|
WRITE(file, to_char_ptr(check_ciphertext.data()), 46);
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
|
return std::make_unique<Vault>(path, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<FileHeader> Vault::read_file_headers() {
|
void Vault::build_file_map() {
|
||||||
std::vector<FileHeader> headers;
|
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);
|
||||||
@@ -74,7 +79,7 @@ std::vector<FileHeader> Vault::read_file_headers() {
|
|||||||
auto header = read_file_header();
|
auto header = read_file_header();
|
||||||
if (header) {
|
if (header) {
|
||||||
if (!header->deleted) {
|
if (!header->deleted) {
|
||||||
headers.push_back(header.value());
|
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);
|
||||||
@@ -82,103 +87,89 @@ std::vector<FileHeader> Vault::read_file_headers() {
|
|||||||
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) {
|
Botan::secure_vector<u8> ciphertext;
|
||||||
auto header = read_file_header();
|
ciphertext.resize(header.content_ciphertext_size);
|
||||||
if (!header) {
|
ASSERT(m_file.read(to_char_ptr(ciphertext.data()),
|
||||||
break;
|
static_cast<i64>(header.content_ciphertext_size)));
|
||||||
}
|
|
||||||
|
|
||||||
if (!header->deleted && header->name == filename) {
|
auto plaintext = Crypto::decrypt(m_key, ciphertext);
|
||||||
Botan::secure_vector<u8> ciphertext;
|
return {to_char_ptr(plaintext.data()), plaintext.size()};
|
||||||
ciphertext.resize(header->content_ciphertext_size);
|
|
||||||
if (!m_file.read(to_char_ptr(ciphertext.data()),
|
|
||||||
static_cast<i64>(header->content_ciphertext_size))) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto plaintext = Crypto::decrypt(m_key, ciphertext);
|
|
||||||
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);
|
||||||
|
|
||||||
auto filename_ciphertext = Crypto::encrypt(m_key, to_span(filename));
|
auto offset = m_file.tellp();
|
||||||
u64 filename_ciphertext_size = filename_ciphertext.size();
|
|
||||||
|
auto name_ciphertext = Crypto::encrypt(m_key, to_span(name));
|
||||||
|
u64 name_ciphertext_size = name_ciphertext.size();
|
||||||
|
|
||||||
auto ciphertext = Crypto::encrypt(m_key, to_span(content));
|
auto ciphertext = Crypto::encrypt(m_key, to_span(content));
|
||||||
u64 ciphertext_size = ciphertext.size();
|
u64 ciphertext_size = ciphertext.size();
|
||||||
|
|
||||||
bool deleted = false;
|
bool deleted = false;
|
||||||
WRITE(m_file, &deleted, sizeof(bool)); // deleted flag
|
WRITE(m_file, &deleted, sizeof(bool)); // deleted flag
|
||||||
WRITE(m_file, &filename_ciphertext_size, sizeof(u64));
|
WRITE(m_file, &name_ciphertext_size, sizeof(u64));
|
||||||
WRITE(m_file, filename_ciphertext.data(), filename_ciphertext_size);
|
WRITE(m_file, name_ciphertext.data(), name_ciphertext_size);
|
||||||
WRITE(m_file, &ciphertext_size, sizeof(u64));
|
WRITE(m_file, &ciphertext_size, sizeof(u64));
|
||||||
|
auto content_offset = m_file.tellp();
|
||||||
WRITE(m_file, ciphertext.data(), ciphertext_size);
|
WRITE(m_file, ciphertext.data(), 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};
|
||||||
while (true) {
|
u64 remaining = header.content_ciphertext_size;
|
||||||
i64 current_pos = m_file.tellg();
|
while (remaining > 0) {
|
||||||
|
u64 chunk = std::min(remaining, sizeof(zeros));
|
||||||
auto header = read_file_header();
|
WRITE(m_file, zeros.data(), chunk);
|
||||||
if (!header) {
|
remaining -= chunk;
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!header->deleted && header->name == filename) {
|
|
||||||
// zero the ciphertext and mark as deleted
|
|
||||||
m_file.seekp(m_file.tellg());
|
|
||||||
std::array<u8, 4096> zeros = {0};
|
|
||||||
u64 remaining = header->content_ciphertext_size;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_file.seekg(static_cast<i64>(header->content_ciphertext_size),
|
|
||||||
std::ios::cur);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_file.clear();
|
||||||
|
m_file.seekp(static_cast<i64>(header.offset), std::ios::beg);
|
||||||
|
bool deleted = true;
|
||||||
|
WRITE(m_file, &deleted, sizeof(bool));
|
||||||
|
|
||||||
|
m_file_map.erase(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
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{};
|
||||||
|
|
||||||
|
header.offset = m_file.tellg();
|
||||||
|
|
||||||
if (!m_file.read(to_char_ptr(&header.deleted), 1)) {
|
if (!m_file.read(to_char_ptr(&header.deleted), 1)) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
@@ -205,5 +196,6 @@ std::optional<FileHeader> Vault::read_file_header() {
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header.content_offset = m_file.tellg();
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/vault.h
19
src/vault.h
@@ -3,37 +3,46 @@
|
|||||||
#include "common.h"
|
#include "common.h"
|
||||||
#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 {
|
||||||
|
u64 offset;
|
||||||
bool deleted;
|
bool deleted;
|
||||||
u64 name_ciphertext_size;
|
u64 name_ciphertext_size;
|
||||||
std::string name;
|
std::string name;
|
||||||
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);
|
||||||
|
|
||||||
static void create(const std::string &path, const std::string &password);
|
static std::unique_ptr<Vault> create_and_open(const std::string &path,
|
||||||
|
const std::string &password);
|
||||||
|
|
||||||
std::vector<FileHeader> read_file_headers();
|
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);
|
||||||
|
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();
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user