maintain an in-memory file map instead of searching the entire vault on every operation

This commit is contained in:
2026-04-27 11:00:23 +02:00
parent 99d239ea08
commit 32641f97de
9 changed files with 114 additions and 126 deletions

View File

@@ -14,13 +14,10 @@
} while (0)
using u8 = uint8_t;
using i16 = int16_t;
using u16 = uint16_t;
using i32 = int32_t;
using u32 = uint32_t;
using i64 = int64_t;
using u64 = uint64_t;
static_assert(sizeof(float) * 8 == 32);
using f32 = float;
static_assert(sizeof(double) * 8 == 64);
using f64 = double;

View File

@@ -4,6 +4,8 @@
#include <botan/auto_rng.h>
#include <botan/pwdhash.h>
#include <algorithm>
namespace Crypto {
inline Botan::secure_vector<u8> encrypt(const Botan::secure_vector<u8> &key,
@@ -21,12 +23,11 @@ inline Botan::secure_vector<u8> encrypt(const Botan::secure_vector<u8> &key,
cipher->set_key(key);
cipher->start(nonce);
Botan::secure_vector<u8> ciphertext(plaintext.begin(), plaintext.end());
cipher->finish(ciphertext);
Botan::secure_vector<u8> res;
res.insert(res.end(), nonce.begin(), nonce.end());
res.insert(res.end(), ciphertext.begin(), ciphertext.end());
res.resize(24 + plaintext.size() + 16);
std::ranges::copy(nonce, res.begin());
std::ranges::copy(plaintext, res.begin() + 24);
cipher->finish(res, 24);
return res;
}
@@ -39,7 +40,7 @@ decrypt(const Botan::secure_vector<u8> &key,
std::array<u8, 24> nonce{};
std::copy_n(ciphertext_with_nonce.begin(), 24, nonce.begin());
auto cipher = Botan::AEAD_Mode::create_or_throw(
static auto cipher = Botan::AEAD_Mode::create_or_throw(
"ChaCha20Poly1305", Botan::Cipher_Dir::Decryption);
cipher->set_key(key);

View File

@@ -9,6 +9,7 @@
#include <QTemporaryDir>
#include <array>
#include <botan/exceptn.h>
#include <stdexcept>
namespace {
@@ -150,17 +151,14 @@ MainWindow::MainWindow(QWidget *parent)
return;
}
auto headers = m_vault->read_file_headers();
for (const auto &header : headers) {
auto content = m_vault->read_file(header.name);
if (content) {
ui->statusbar->showMessage("Extracting " +
QString::fromStdString(header.name) + "...");
QCoreApplication::processEvents();
std::ofstream file(path.toStdString() + "/" + header.name,
std::ios::binary);
file.write(content->data(), static_cast<i64>(content->size()));
}
const auto &file_map = m_vault->file_map();
for (const auto &[k, _] : file_map) {
auto content = m_vault->read_file(k);
ui->statusbar->showMessage("Extracting " + QString::fromStdString(k) +
"...");
QCoreApplication::processEvents();
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);
});
@@ -171,13 +169,13 @@ void MainWindow::reload_fs_tree() {
ui->menuFiles->setEnabled(true);
ui->fsTreeWidget->clear();
auto headers = m_vault->read_file_headers();
for (const auto &header : headers) {
const auto &file_map = m_vault->file_map();
for (const auto &[k, v] : file_map) {
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::fromStdString(
format_bytes(header.content_ciphertext_size)));
item->setText(0, QString::fromStdString(k));
item->setText(
1, QString::fromStdString(format_bytes(v.content_ciphertext_size)));
}
for (int i = 0; i < ui->fsTreeWidget->columnCount(); ++i) {
ui->fsTreeWidget->resizeColumnToContents(i);
@@ -187,17 +185,17 @@ void MainWindow::reload_fs_tree() {
void MainWindow::preview_file(const std::string &filename) {
ui->filePreview->setVisible(true);
auto content = m_vault->read_file(filename);
if (content) {
ui->filePreview->setText(QString::fromStdString(content.value()));
} else {
try {
auto content = m_vault->read_file(filename);
ui->filePreview->setText(QString::fromStdString(content));
} catch (std::out_of_range &) {
qWarning() << "File to preview not found";
}
}
void MainWindow::extract_file(const std::string &filename) {
auto content = m_vault->read_file(filename);
if (content) {
try {
auto content = m_vault->read_file(filename);
QString path = QFileDialog::getSaveFileName(
this, "Choose location to extract",
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);
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 {
} catch (std::out_of_range &) {
qWarning() << "File to extract not found";
}
}
void MainWindow::open_file(const std::string &filename) {
auto content = m_vault->read_file(filename);
if (content) {
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.write(content.data(), static_cast<i64>(content.size()));
file.close();
QDesktopServices::openUrl(
@@ -232,21 +230,21 @@ void MainWindow::open_file(const std::string &filename) {
"Click OK to delete the opened file.");
// QTemporaryDir gets deleted when it goes out of scope
} else {
} catch (std::out_of_range &) {
qWarning() << "File to open not found";
}
}
void MainWindow::edit_file(const std::string &filename) {
auto content = m_vault->read_file(filename);
if (content) {
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.write(content.data(), static_cast<i64>(content.size()));
file.close();
QDesktopServices::openUrl(
@@ -255,16 +253,18 @@ void MainWindow::edit_file(const std::string &filename) {
"Please edit the file in the opened editor and "
"save it. Click OK when done.");
std::ifstream file2(path, std::ios::binary);
std::string new_content((std::istreambuf_iterator<char>(file2)),
std::istreambuf_iterator<char>());
std::ifstream file2(path, std::ios::binary | std::ios::ate);
ASSERT(file2.good());
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);
reload_fs_tree();
ui->statusbar->showMessage("File updated");
// QTemporaryDir gets deleted when it goes out of scope
} else {
} catch (std::out_of_range &) {
qWarning() << "File to edit not found";
}
}

View File

@@ -31,7 +31,7 @@ Vault::Vault(std::string path, const std::string &password)
ASSERT(m_file.read(magic.data(), 4));
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(version == VERSION);
@@ -45,6 +45,8 @@ Vault::Vault(std::string path, const std::string &password)
ASSERT(m_file.read(to_char_ptr(check_ciphertext.data()), 46));
Crypto::decrypt(m_key, check_ciphertext);
build_file_map();
}
void Vault::create(const std::string &path, const std::string &password) {
@@ -64,8 +66,8 @@ void Vault::create(const std::string &path, const std::string &password) {
file.close();
}
std::vector<FileHeader> Vault::read_file_headers() {
std::vector<FileHeader> headers;
void Vault::build_file_map() {
m_file_map.clear();
m_file.clear();
m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg);
@@ -74,7 +76,7 @@ std::vector<FileHeader> Vault::read_file_headers() {
auto header = read_file_header();
if (header) {
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),
std::ios::cur);
@@ -82,37 +84,22 @@ std::vector<FileHeader> Vault::read_file_headers() {
break;
}
}
return headers;
}
std::optional<std::string> Vault::read_file(const std::string &filename) {
std::string Vault::read_file(const std::string &filename) {
auto header = m_file_map.at(filename);
ASSERT(!header.deleted);
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;
}
Botan::secure_vector<u8> ciphertext;
ciphertext.resize(header.content_ciphertext_size);
ASSERT(m_file.read(to_char_ptr(ciphertext.data()),
static_cast<i64>(header.content_ciphertext_size)));
if (!header->deleted && header->name == filename) {
Botan::secure_vector<u8> ciphertext;
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;
auto plaintext = Crypto::decrypt(m_key, ciphertext);
return {to_char_ptr(plaintext.data()), plaintext.size()};
}
void Vault::create_file(const std::string &filename,
@@ -120,54 +107,54 @@ void Vault::create_file(const std::string &filename,
m_file.clear();
m_file.seekp(0, std::ios::end);
auto filename_ciphertext = Crypto::encrypt(m_key, to_span(filename));
u64 filename_ciphertext_size = filename_ciphertext.size();
auto offset = m_file.tellp();
auto name_ciphertext = Crypto::encrypt(m_key, to_span(filename));
u64 name_ciphertext_size = name_ciphertext.size();
auto ciphertext = Crypto::encrypt(m_key, to_span(content));
u64 ciphertext_size = ciphertext.size();
bool deleted = false;
WRITE(m_file, &deleted, sizeof(bool)); // deleted flag
WRITE(m_file, &filename_ciphertext_size, sizeof(u64));
WRITE(m_file, filename_ciphertext.data(), filename_ciphertext_size);
WRITE(m_file, &name_ciphertext_size, sizeof(u64));
WRITE(m_file, name_ciphertext.data(), name_ciphertext_size);
WRITE(m_file, &ciphertext_size, sizeof(u64));
auto content_offset = m_file.tellp();
WRITE(m_file, ciphertext.data(), ciphertext_size);
m_file.flush();
m_file_map[filename] = FileHeader{
.offset = static_cast<u64>(offset),
.deleted = false,
.name_ciphertext_size = name_ciphertext_size,
.name = filename,
.content_ciphertext_size = ciphertext_size,
.content_offset = static_cast<u64>(content_offset),
};
}
void Vault::delete_file(const std::string &filename) {
auto header = m_file_map.at(filename);
// zero the ciphertext and mark as deleted
m_file.clear();
m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg);
while (true) {
i64 current_pos = m_file.tellg();
auto header = read_file_header();
if (!header) {
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.seekp(static_cast<i64>(header.content_offset), std::ios::beg);
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.clear();
m_file.seekp(static_cast<i64>(header.offset), std::ios::beg);
bool deleted = true;
WRITE(m_file, &deleted, sizeof(bool));
m_file.flush();
m_file_map.erase(filename);
}
void Vault::update_file(const std::string &filename,
@@ -179,6 +166,8 @@ void Vault::update_file(const std::string &filename,
std::optional<FileHeader> Vault::read_file_header() {
FileHeader header{};
header.offset = m_file.tellg();
if (!m_file.read(to_char_ptr(&header.deleted), 1)) {
return std::nullopt;
}
@@ -205,5 +194,6 @@ std::optional<FileHeader> Vault::read_file_header() {
return std::nullopt;
}
header.content_offset = m_file.tellg();
return header;
}

View File

@@ -4,16 +4,18 @@
#include <botan/secmem.h>
#include <fstream>
#include <optional>
#include <unordered_map>
constexpr i16 VERSION = 1;
constexpr u16 VERSION = 1;
constexpr u64 AFTER_HEADER_OFFSET = 68;
// !!! REMEMBER TO UPDATE entry_total_size IN Vault::delete_file
struct FileHeader {
u64 offset;
bool deleted;
u64 name_ciphertext_size;
std::string name;
u64 content_ciphertext_size;
u64 content_offset;
};
class Vault {
@@ -22,18 +24,22 @@ public:
static void create(const std::string &path, const std::string &password);
std::vector<FileHeader> read_file_headers();
std::optional<std::string> read_file(const std::string &name);
std::string read_file(const std::string &name);
void create_file(const std::string &name, const std::string &content);
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; }
const std::unordered_map<std::string, FileHeader> &file_map() const {
return m_file_map;
}
private:
std::string m_path;
std::fstream m_file;
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();
};