Compare commits
2 Commits
85541e1db4
...
99d239ea08
| Author | SHA1 | Date | |
|---|---|---|---|
| 99d239ea08 | |||
| 48fc35e16c |
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#define ASSERT(cond) \
|
||||
do { \
|
||||
@@ -25,11 +24,3 @@ static_assert(sizeof(float) * 8 == 32);
|
||||
using f32 = float;
|
||||
static_assert(sizeof(double) * 8 == 64);
|
||||
using f64 = double;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
11
src/crypto.h
11
src/crypto.h
@@ -6,23 +6,22 @@
|
||||
|
||||
namespace Crypto {
|
||||
|
||||
inline Botan::secure_vector<u8>
|
||||
encrypt(const Botan::secure_vector<u8> &key,
|
||||
const Botan::secure_vector<u8> &plaintext) {
|
||||
inline Botan::secure_vector<u8> encrypt(const Botan::secure_vector<u8> &key,
|
||||
const std::span<const u8> &plaintext) {
|
||||
ASSERT(key.size() == 32);
|
||||
|
||||
static Botan::AutoSeeded_RNG rng;
|
||||
auto nonce = rng.random_array<24>();
|
||||
|
||||
// XChaCha20 is selected automatically based on the nonce size
|
||||
// https://github.com/randombit/botan/blob/master/src/lib/stream/chacha/chacha.cpp#L375
|
||||
auto cipher = Botan::AEAD_Mode::create_or_throw(
|
||||
static auto cipher = Botan::AEAD_Mode::create_or_throw(
|
||||
"ChaCha20Poly1305", Botan::Cipher_Dir::Encryption);
|
||||
|
||||
auto nonce = rng.random_array<24>();
|
||||
cipher->set_key(key);
|
||||
cipher->start(nonce);
|
||||
|
||||
Botan::secure_vector<u8> ciphertext = plaintext;
|
||||
Botan::secure_vector<u8> ciphertext(plaintext.begin(), plaintext.end());
|
||||
cipher->finish(ciphertext);
|
||||
|
||||
Botan::secure_vector<u8> res;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QMessageBox>
|
||||
#include <QMimeData>
|
||||
#include <QTemporaryDir>
|
||||
#include <array>
|
||||
#include <botan/exceptn.h>
|
||||
|
||||
namespace {
|
||||
@@ -16,6 +17,21 @@ std::string basename(const std::string &path) {
|
||||
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)
|
||||
@@ -106,11 +122,15 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
QStringList paths =
|
||||
QFileDialog::getOpenFileNames(this, "Choose files to add");
|
||||
for (const auto &path : paths) {
|
||||
std::ifstream file(path.toStdString(), std::ios::binary);
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
ui->statusbar->showMessage("Adding " + path + "...");
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
std::ifstream file(path.toStdString(), std::ios::binary | std::ios::ate);
|
||||
ASSERT(file.good());
|
||||
auto size = file.tellg();
|
||||
file.seekg(0);
|
||||
std::string content(size, '\0');
|
||||
file.read(content.data(), size);
|
||||
m_vault->create_file(basename(path.toStdString()), content);
|
||||
}
|
||||
|
||||
@@ -134,6 +154,9 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
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()));
|
||||
@@ -153,7 +176,8 @@ void MainWindow::reload_fs_tree() {
|
||||
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));
|
||||
item->setText(1, QString::fromStdString(
|
||||
format_bytes(header.content_ciphertext_size)));
|
||||
}
|
||||
for (int i = 0; i < ui->fsTreeWidget->columnCount(); ++i) {
|
||||
ui->fsTreeWidget->resizeColumnToContents(i);
|
||||
@@ -190,6 +214,29 @@ void MainWindow::extract_file(const std::string &filename) {
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::open_file(const std::string &filename) {
|
||||
auto content = m_vault->read_file(filename);
|
||||
if (content) {
|
||||
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();
|
||||
|
||||
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
|
||||
} else {
|
||||
qWarning() << "File to open not found";
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::edit_file(const std::string &filename) {
|
||||
auto content = m_vault->read_file(filename);
|
||||
if (content) {
|
||||
@@ -230,13 +277,21 @@ void MainWindow::file_context_menu(const QPoint &pos) {
|
||||
|
||||
QMenu menu(this);
|
||||
|
||||
QAction *preview_action = menu.addAction(
|
||||
style()->standardIcon(QStyle::SP_FileDialogContentsView), "Preview");
|
||||
QAction *preview_action =
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_FileDialogContentsView),
|
||||
"Preview as text");
|
||||
connect(preview_action, &QAction::triggered, this,
|
||||
[this, item]() { preview_file(item->text(0).toStdString()); });
|
||||
|
||||
QAction *edit_action = menu.addAction(
|
||||
style()->standardIcon(QStyle::SP_FileDialogDetailedView), "Edit");
|
||||
QAction *open_action =
|
||||
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,
|
||||
[this, item]() { edit_file(item->text(0).toStdString()); });
|
||||
|
||||
@@ -281,12 +336,14 @@ void MainWindow::dropEvent(QDropEvent *event) {
|
||||
if (!u.isLocalFile()) {
|
||||
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());
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
auto size = file.tellg();
|
||||
file.seekg(0);
|
||||
std::string content(size, '\0');
|
||||
file.read(content.data(), size);
|
||||
m_vault->create_file(basename(u.toLocalFile().toStdString()), content);
|
||||
}
|
||||
ui->statusbar->showMessage(
|
||||
|
||||
@@ -21,6 +21,7 @@ private:
|
||||
void reload_fs_tree();
|
||||
void preview_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 file_context_menu(const QPoint &pos);
|
||||
};
|
||||
|
||||
123
src/vault.cc
123
src/vault.cc
@@ -2,7 +2,25 @@
|
||||
#include "common.h"
|
||||
#include "crypto.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)
|
||||
: m_path(std::move(path)) {
|
||||
@@ -36,16 +54,14 @@ void Vault::create(const std::string &path, const std::string &password) {
|
||||
auto key = Crypto::derive_key(password, salt);
|
||||
|
||||
const std::string_view content = "LETSGO";
|
||||
Botan::secure_vector<u8> content_sv(content.begin(), content.end());
|
||||
auto check_ciphertext = Crypto::encrypt(key, content_sv);
|
||||
auto check_ciphertext = Crypto::encrypt(key, to_span(content));
|
||||
|
||||
std::ofstream create(path, 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_ciphertext.data()), 46);
|
||||
create.close();
|
||||
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();
|
||||
}
|
||||
|
||||
std::vector<FileHeader> Vault::read_file_headers() {
|
||||
@@ -57,7 +73,9 @@ std::vector<FileHeader> Vault::read_file_headers() {
|
||||
while (true) {
|
||||
auto header = read_file_header();
|
||||
if (header) {
|
||||
if (!header->deleted) {
|
||||
headers.push_back(header.value());
|
||||
}
|
||||
m_file.seekg(static_cast<i64>(header->content_ciphertext_size),
|
||||
std::ios::cur);
|
||||
} else {
|
||||
@@ -78,7 +96,7 @@ std::optional<std::string> Vault::read_file(const std::string &filename) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (header->name == filename) {
|
||||
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()),
|
||||
@@ -102,20 +120,18 @@ void Vault::create_file(const std::string &filename,
|
||||
m_file.clear();
|
||||
m_file.seekp(0, std::ios::end);
|
||||
|
||||
Botan::secure_vector<u8> filename_sv(filename.begin(), filename.end());
|
||||
auto filename_ciphertext = Crypto::encrypt(m_key, filename_sv);
|
||||
auto filename_ciphertext = Crypto::encrypt(m_key, to_span(filename));
|
||||
u64 filename_ciphertext_size = filename_ciphertext.size();
|
||||
|
||||
Botan::secure_vector<u8> content_sv(content.begin(), content.end());
|
||||
auto ciphertext = Crypto::encrypt(m_key, content_sv);
|
||||
auto ciphertext = Crypto::encrypt(m_key, to_span(content));
|
||||
u64 ciphertext_size = ciphertext.size();
|
||||
|
||||
ASSERT(m_file.write(to_char_ptr(&filename_ciphertext_size), sizeof(u64)));
|
||||
ASSERT(m_file.write(to_char_ptr(filename_ciphertext.data()),
|
||||
static_cast<i64>(filename_ciphertext_size)));
|
||||
ASSERT(m_file.write(to_char_ptr(&ciphertext_size), sizeof(u64)));
|
||||
ASSERT(m_file.write(to_char_ptr(ciphertext.data()),
|
||||
static_cast<i64>(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, &ciphertext_size, sizeof(u64));
|
||||
WRITE(m_file, ciphertext.data(), ciphertext_size);
|
||||
m_file.flush();
|
||||
}
|
||||
|
||||
@@ -123,9 +139,6 @@ void Vault::delete_file(const std::string &filename) {
|
||||
m_file.clear();
|
||||
m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg);
|
||||
|
||||
i64 entry_start = -1;
|
||||
u64 entry_total_size = 0;
|
||||
|
||||
while (true) {
|
||||
i64 current_pos = m_file.tellg();
|
||||
|
||||
@@ -134,48 +147,26 @@ void Vault::delete_file(const std::string &filename) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (header->name == filename) {
|
||||
entry_start = current_pos;
|
||||
entry_total_size = sizeof(u64) + header->name_ciphertext_size +
|
||||
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.seekg(read_pos);
|
||||
m_file.read(to_char_ptr(buffer.data()), buffer.size());
|
||||
auto bytes = m_file.gcount();
|
||||
if (bytes == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
m_file.seekp(write_pos);
|
||||
ASSERT(m_file.write(to_char_ptr(buffer.data()), bytes));
|
||||
|
||||
read_pos += bytes;
|
||||
write_pos += bytes;
|
||||
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();
|
||||
m_file.close();
|
||||
break;
|
||||
}
|
||||
|
||||
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());
|
||||
m_file.seekg(static_cast<i64>(header->content_ciphertext_size),
|
||||
std::ios::cur);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +179,10 @@ void Vault::update_file(const std::string &filename,
|
||||
std::optional<FileHeader> Vault::read_file_header() {
|
||||
FileHeader header{};
|
||||
|
||||
if (!m_file.read(to_char_ptr(&header.deleted), 1)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!m_file.read(to_char_ptr(&header.name_ciphertext_size), sizeof(u64))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -201,10 +196,14 @@ std::optional<FileHeader> Vault::read_file_header() {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!header.deleted) {
|
||||
auto name = Crypto::decrypt(m_key, name_ciphertext);
|
||||
header.name = std::string(name.begin(), name.end());
|
||||
}
|
||||
|
||||
if (!m_file.read(to_char_ptr(&header.content_ciphertext_size), sizeof(u64))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ constexpr u64 AFTER_HEADER_OFFSET = 68;
|
||||
|
||||
// !!! REMEMBER TO UPDATE entry_total_size IN Vault::delete_file
|
||||
struct FileHeader {
|
||||
bool deleted;
|
||||
u64 name_ciphertext_size;
|
||||
std::string name;
|
||||
u64 content_ciphertext_size;
|
||||
|
||||
Reference in New Issue
Block a user