Compare commits
10 Commits
0.1
...
99d239ea08
| Author | SHA1 | Date | |
|---|---|---|---|
| 99d239ea08 | |||
| 48fc35e16c | |||
| 85541e1db4 | |||
| 47f1f7dd74 | |||
| 0f4d4f96d6 | |||
| 1ad1571273 | |||
| 7873020e72 | |||
| 53a222a7da | |||
| f00b5221c1 | |||
| 30ca8b6262 |
@@ -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)
|
||||||
|
|||||||
28
LICENSE
Normal file
28
LICENSE
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2025-2026, Antoni Piasecki
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
22
PKGBUILD
22
PKGBUILD
@@ -1,22 +0,0 @@
|
|||||||
pkgname=dull
|
|
||||||
pkgver=0.1
|
|
||||||
pkgrel=1
|
|
||||||
pkgdesc="Desktop app for securely storing sensitive files"
|
|
||||||
arch=('x86_64')
|
|
||||||
url="https://github.com/antpiasecki/dull"
|
|
||||||
depends=('qt6-base' 'botan')
|
|
||||||
makedepends=('cmake')
|
|
||||||
source=("${url}/archive/refs/tags/${pkgver}.tar.gz")
|
|
||||||
sha256sums=('SKIP')
|
|
||||||
DEBUGPKG=()
|
|
||||||
|
|
||||||
build() {
|
|
||||||
cd "${pkgname}-${pkgver}"
|
|
||||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
|
|
||||||
cmake --build build -j$(nproc)
|
|
||||||
}
|
|
||||||
|
|
||||||
package() {
|
|
||||||
cd "${pkgname}-${pkgver}/build"
|
|
||||||
make DESTDIR="${pkgdir}" install
|
|
||||||
}
|
|
||||||
36
README.md
36
README.md
@@ -4,32 +4,44 @@ Desktop app for securely storing sensitive files
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
* **Pretty usable UI**
|
* **Pretty usable UI**
|
||||||
* **Overkill encryption:** XChaCha20-Poly1305 + Argon2id(m=1GB,t=6,p=4) key derivation
|
* **Overkill encryption:** XChaCha20-Poly1305 + Argon2id key derivation
|
||||||
* **Cross-platform-ish:** Builds on Linux, Windows and macOS
|
* **Cross-platform:** Tested on Linux, Windows and macOS
|
||||||
* **Drag and Drop support**
|
* **Drag and Drop support**
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
### Debian/Ubuntu
|
### Debian
|
||||||
```
|
```
|
||||||
sudo apt update && sudo apt install build-essential qt6-base-dev libbotan-3-dev cmake
|
sudo apt update && sudo apt install build-essential qt6-base-dev libbotan-3-dev cmake
|
||||||
|
|
||||||
cmake -S . -B build
|
cmake -B build
|
||||||
cmake --build build -j $(nproc)
|
cmake --build build -j $(nproc)
|
||||||
./build/dull
|
./build/dull
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch
|
### Arch
|
||||||
```
|
```
|
||||||
sudo pacman -S base-devel
|
sudo pacman -Syu base-devel cmake qt6-base botan
|
||||||
makepkg -si
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows / MSYS2
|
cmake -B build
|
||||||
```
|
cmake --build build -j $(nproc)
|
||||||
pacman -S --needed mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-qt6-base mingw-w64-x86_64-qt6-tools mingw-w64-x86_64-libbotan
|
./build/dull
|
||||||
|
```
|
||||||
cmake -S . -B build -DBOTAN_INCLUDE_DIRS=/mingw64/include/botan-3 -DBOTAN_LIBRARIES=/mingw64/lib/libbotan-3.a
|
|
||||||
|
### macOS
|
||||||
|
```
|
||||||
|
brew install qtbase cmake pkg-config botan
|
||||||
|
|
||||||
|
cmake -B build
|
||||||
|
cmake --build build -j $(sysctl -n hw.physicalcpu)
|
||||||
|
./build/dull
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (MSYS2)
|
||||||
|
```
|
||||||
|
pacman -S --needed mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-qt6-base mingw-w64-x86_64-qt6-tools mingw-w64-x86_64-libbotan
|
||||||
|
|
||||||
|
cmake -B build -DBOTAN_INCLUDE_DIRS=/mingw64/include/botan-3 -DBOTAN_LIBRARIES=/mingw64/lib/libbotan-3.a
|
||||||
cmake --build build -j $(nproc)
|
cmake --build build -j $(nproc)
|
||||||
./build/dull
|
./build/dull
|
||||||
```
|
```
|
||||||
18
src/common.h
18
src/common.h
@@ -2,19 +2,18 @@
|
|||||||
|
|
||||||
#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 i16 = int16_t;
|
||||||
using u16 = uint16_t;
|
using u16 = uint16_t;
|
||||||
using i32 = int32_t;
|
using i32 = int32_t;
|
||||||
@@ -25,16 +24,3 @@ static_assert(sizeof(float) * 8 == 32);
|
|||||||
using f32 = float;
|
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);
|
|
||||||
}
|
|
||||||
|
|||||||
45
src/crypto.h
45
src/crypto.h
@@ -1,39 +1,43 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include <botan/aead.h>
|
#include <botan/aead.h>
|
||||||
#include <botan/hex.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 auto cipher = Botan::AEAD_Mode::create_or_throw(
|
||||||
"ChaCha20Poly1305", Botan::Cipher_Dir::Encryption);
|
"ChaCha20Poly1305", Botan::Cipher_Dir::Encryption);
|
||||||
|
|
||||||
|
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;
|
Botan::secure_vector<u8> ciphertext(plaintext.begin(), plaintext.end());
|
||||||
cipher->finish(ciphertext);
|
cipher->finish(ciphertext);
|
||||||
|
|
||||||
return ciphertext;
|
Botan::secure_vector<u8> res;
|
||||||
|
res.insert(res.end(), nonce.begin(), nonce.end());
|
||||||
|
res.insert(res.end(), ciphertext.begin(), ciphertext.end());
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline Botan::secure_vector<u8>
|
inline Botan::secure_vector<u8>
|
||||||
decrypt_xchacha20_poly1305(const Botan::secure_vector<u8> &ciphertext,
|
decrypt(const Botan::secure_vector<u8> &key,
|
||||||
const Botan::secure_vector<u8> &key,
|
const Botan::secure_vector<u8> &ciphertext_with_nonce) {
|
||||||
const std::array<u8, 24> &nonce) {
|
ASSERT(ciphertext_with_nonce.size() >= 40);
|
||||||
ASSERT(ciphertext.size() >= 16);
|
|
||||||
ASSERT(key.size() == 32);
|
ASSERT(key.size() == 32);
|
||||||
ASSERT(nonce.size() == 24);
|
|
||||||
|
std::array<u8, 24> nonce{};
|
||||||
|
std::copy_n(ciphertext_with_nonce.begin(), 24, nonce.begin());
|
||||||
|
|
||||||
auto cipher = Botan::AEAD_Mode::create_or_throw(
|
auto cipher = Botan::AEAD_Mode::create_or_throw(
|
||||||
"ChaCha20Poly1305", Botan::Cipher_Dir::Decryption);
|
"ChaCha20Poly1305", Botan::Cipher_Dir::Decryption);
|
||||||
@@ -41,18 +45,17 @@ decrypt_xchacha20_poly1305(const Botan::secure_vector<u8> &ciphertext,
|
|||||||
cipher->set_key(key);
|
cipher->set_key(key);
|
||||||
cipher->start(nonce);
|
cipher->start(nonce);
|
||||||
|
|
||||||
Botan::secure_vector<u8> plaintext = ciphertext;
|
Botan::secure_vector<u8> ciphertext(ciphertext_with_nonce.begin() + 24,
|
||||||
cipher->finish(plaintext);
|
ciphertext_with_nonce.end());
|
||||||
|
cipher->finish(ciphertext);
|
||||||
|
|
||||||
return plaintext;
|
return ciphertext;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline Botan::secure_vector<u8>
|
inline Botan::secure_vector<u8> derive_key(const std::string &password,
|
||||||
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(),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
#include <QApplication>
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
|
|||||||
@@ -1,14 +1,38 @@
|
|||||||
|
// TODO: actual fs
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QDragEnterEvent>
|
|
||||||
#include <QDropEvent>
|
#include <QDropEvent>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
#include <botan/auto_rng.h>
|
#include <array>
|
||||||
#include <botan/hex.h>
|
#include <botan/exceptn.h>
|
||||||
|
|
||||||
|
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>()) {
|
||||||
@@ -21,35 +45,36 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
connect(ui->actionNew, &QAction::triggered, this, [this]() {
|
connect(ui->actionNew, &QAction::triggered, this, [this]() {
|
||||||
QString path = QFileDialog::getSaveFileName(this, "Choose vault location",
|
QString path = QFileDialog::getSaveFileName(this, "Choose vault location",
|
||||||
QDir::currentPath(),
|
QDir::currentPath(),
|
||||||
"Dull Vaults (*.dull)");
|
"Dull vaults (*.dull)");
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!path.contains(".dull")) {
|
if (!path.endsWith(".dull")) {
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Botan::AutoSeeded_RNG rng;
|
ui->statusbar->showMessage("Creating the vault...");
|
||||||
auto salt_sv = rng.random_vec(16);
|
QCoreApplication::processEvents();
|
||||||
std::vector<u8> salt(salt_sv.begin(), salt_sv.end());
|
|
||||||
|
|
||||||
std::ofstream create(path.toStdString(), std::ios::binary);
|
Vault::create(path.toStdString(), password.toStdString());
|
||||||
create.write("DULL", 4);
|
|
||||||
create.write(to_char_ptr(&VERSION), sizeof(VERSION));
|
|
||||||
create.write(to_char_ptr(salt.data()), 16);
|
|
||||||
create.close();
|
|
||||||
|
|
||||||
m_vault =
|
m_vault =
|
||||||
std::make_unique<Vault>(path.toStdString(), password.toStdString());
|
std::make_unique<Vault>(path.toStdString(), password.toStdString());
|
||||||
|
ui->statusbar->showMessage("Opened " + path);
|
||||||
|
|
||||||
reload_fs_tree();
|
reload_fs_tree();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,12 +88,23 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
|
|
||||||
QString password = QInputDialog::getText(
|
QString password = QInputDialog::getText(
|
||||||
this, "Unlock the vault", "Enter vault password", QLineEdit::Password);
|
this, "Unlock the vault", "Enter vault password", QLineEdit::Password);
|
||||||
|
if (password.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: check if password valid
|
ui->statusbar->showMessage("Opening the vault...");
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
try {
|
||||||
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->showMessage("Opened " + path);
|
||||||
|
} catch (const Botan::Invalid_Authentication_Tag &e) {
|
||||||
|
QMessageBox::critical(this, "Error", "Invalid password.");
|
||||||
|
ui->statusbar->clearMessage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
@@ -86,27 +122,62 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
reload_fs_tree();
|
reload_fs_tree();
|
||||||
|
ui->statusbar->showMessage("Added " + QString::number(paths.size()) +
|
||||||
|
" files");
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(ui->actionExtract_All, &QAction::triggered, this, [this]() {
|
||||||
|
if (!m_vault) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString path =
|
||||||
|
QFileDialog::getExistingDirectory(this, "Choose location to extract");
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto headers = m_vault->read_file_headers();
|
||||||
|
for (const auto &header : headers) {
|
||||||
|
auto content = m_vault->read_file(header.name);
|
||||||
|
if (content) {
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui->statusbar->showMessage("Extracted all files to " + path);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::reload_fs_tree() {
|
void MainWindow::reload_fs_tree() {
|
||||||
|
setWindowTitle(QString::fromStdString(m_vault->path()) + " - dull");
|
||||||
ui->menuFiles->setEnabled(true);
|
ui->menuFiles->setEnabled(true);
|
||||||
ui->fsTreeWidget->clear();
|
ui->fsTreeWidget->clear();
|
||||||
|
|
||||||
auto headers = m_vault->read_file_headers();
|
auto headers = m_vault->read_file_headers();
|
||||||
for (const auto &header : headers) {
|
for (const auto &header : headers) {
|
||||||
auto *item = new QTreeWidgetItem(ui->fsTreeWidget);
|
auto *item = new QTreeWidgetItem(ui->fsTreeWidget);
|
||||||
|
item->setIcon(0, style()->standardIcon(QStyle::SP_FileIcon));
|
||||||
item->setText(0, QString::fromStdString(header.name));
|
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) {
|
for (int i = 0; i < ui->fsTreeWidget->columnCount(); ++i) {
|
||||||
ui->fsTreeWidget->resizeColumnToContents(i);
|
ui->fsTreeWidget->resizeColumnToContents(i);
|
||||||
@@ -136,11 +207,36 @@ 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);
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "File to extract not found";
|
qWarning() << "File to extract not found";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
void MainWindow::edit_file(const std::string &filename) {
|
||||||
auto content = m_vault->read_file(filename);
|
auto content = m_vault->read_file(filename);
|
||||||
if (content) {
|
if (content) {
|
||||||
@@ -166,6 +262,7 @@ void MainWindow::edit_file(const std::string &filename) {
|
|||||||
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");
|
||||||
// QTemporaryDir gets deleted when it goes out of scope
|
// QTemporaryDir gets deleted when it goes out of scope
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "File to edit not found";
|
qWarning() << "File to edit not found";
|
||||||
@@ -180,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()); });
|
||||||
|
|
||||||
@@ -231,15 +336,18 @@ 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);
|
|
||||||
}
|
}
|
||||||
|
ui->statusbar->showMessage(
|
||||||
|
"Added " + QString::number(event->mimeData()->urls().size()) + " files");
|
||||||
reload_fs_tree();
|
reload_fs_tree();
|
||||||
|
|
||||||
event->acceptProposedAction();
|
event->acceptProposedAction();
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
#include "ui_mainwindow.h"
|
#include "ui_mainwindow.h"
|
||||||
#include "vault.h"
|
#include "vault.h"
|
||||||
#include <QMainWindow>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
class MainWindow : public QMainWindow {
|
class MainWindow : public QMainWindow {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -23,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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,13 +40,14 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QStatusBar" name="statusbar"/>
|
||||||
<widget class="QMenuBar" name="menubar">
|
<widget class="QMenuBar" name="menubar">
|
||||||
<property name="geometry">
|
<property name="geometry">
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>800</width>
|
<width>900</width>
|
||||||
<height>32</height>
|
<height>30</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuVault">
|
<widget class="QMenu" name="menuVault">
|
||||||
@@ -64,25 +65,43 @@
|
|||||||
<string>Files</string>
|
<string>Files</string>
|
||||||
</property>
|
</property>
|
||||||
<addaction name="actionAddFiles"/>
|
<addaction name="actionAddFiles"/>
|
||||||
|
<addaction name="actionExtract_All"/>
|
||||||
</widget>
|
</widget>
|
||||||
<addaction name="menuVault"/>
|
<addaction name="menuVault"/>
|
||||||
<addaction name="menuFiles"/>
|
<addaction name="menuFiles"/>
|
||||||
</widget>
|
</widget>
|
||||||
<action name="actionNew">
|
<action name="actionNew">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="document-new"/>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>New</string>
|
<string>New</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionOpen">
|
<action name="actionOpen">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="document-open"/>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Open</string>
|
<string>Open</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionAddFiles">
|
<action name="actionAddFiles">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="list-add"/>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Add</string>
|
<string>Add</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionExtract_All">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="document-save-as"/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Extract All</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections/>
|
<connections/>
|
||||||
|
|||||||
136
src/vault.cc
136
src/vault.cc
@@ -1,9 +1,26 @@
|
|||||||
#include "vault.h"
|
#include "vault.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "crypto.h"
|
#include "crypto.h"
|
||||||
#include <array>
|
|
||||||
#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)) {
|
||||||
@@ -21,7 +38,30 @@ Vault::Vault(std::string path, const std::string &password)
|
|||||||
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);
|
||||||
|
|
||||||
|
Botan::secure_vector<u8> check_ciphertext;
|
||||||
|
check_ciphertext.resize(46);
|
||||||
|
ASSERT(m_file.read(to_char_ptr(check_ciphertext.data()), 46));
|
||||||
|
|
||||||
|
Crypto::decrypt(m_key, check_ciphertext);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Vault::create(const std::string &path, 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<FileHeader> Vault::read_file_headers() {
|
std::vector<FileHeader> Vault::read_file_headers() {
|
||||||
@@ -33,7 +73,9 @@ 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) {
|
||||||
|
if (!header->deleted) {
|
||||||
headers.push_back(header.value());
|
headers.push_back(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 {
|
||||||
@@ -54,7 +96,7 @@ std::optional<std::string> Vault::read_file(const std::string &filename) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header->name == filename) {
|
if (!header->deleted && 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()),
|
if (!m_file.read(to_char_ptr(ciphertext.data()),
|
||||||
@@ -62,8 +104,7 @@ std::optional<std::string> Vault::read_file(const std::string &filename) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto plaintext = Crypto::decrypt_xchacha20_poly1305(
|
auto plaintext = Crypto::decrypt(m_key, ciphertext);
|
||||||
ciphertext, m_key, header->content_nonce);
|
|
||||||
return std::string(to_char_ptr(plaintext.data()), plaintext.size());
|
return std::string(to_char_ptr(plaintext.data()), plaintext.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,28 +120,18 @@ void Vault::create_file(const std::string &filename,
|
|||||||
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 filename_ciphertext = Crypto::encrypt(m_key, to_span(filename));
|
||||||
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 filename_ciphertext =
|
|
||||||
Crypto::encrypt_xchacha20_poly1305(filename_sv, m_key, name_nonce);
|
|
||||||
u64 filename_ciphertext_size = filename_ciphertext.size();
|
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, &filename_ciphertext_size, sizeof(u64));
|
||||||
static_cast<i64>(filename_ciphertext_size)));
|
WRITE(m_file, filename_ciphertext.data(), filename_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)));
|
WRITE(m_file, ciphertext.data(), ciphertext_size);
|
||||||
ASSERT(m_file.write(to_char_ptr(ciphertext.data()),
|
|
||||||
static_cast<i64>(ciphertext_size)));
|
|
||||||
m_file.flush();
|
m_file.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,9 +139,6 @@ void Vault::delete_file(const std::string &filename) {
|
|||||||
m_file.clear();
|
m_file.clear();
|
||||||
m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg);
|
m_file.seekg(AFTER_HEADER_OFFSET, std::ios::beg);
|
||||||
|
|
||||||
i64 entry_start = -1;
|
|
||||||
u64 entry_total_size = 0;
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
i64 current_pos = m_file.tellg();
|
i64 current_pos = m_file.tellg();
|
||||||
|
|
||||||
@@ -119,41 +147,27 @@ void Vault::delete_file(const std::string &filename) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header->name == filename) {
|
if (!header->deleted && header->name == filename) {
|
||||||
entry_start = current_pos;
|
// zero the ciphertext and mark as deleted
|
||||||
entry_total_size = 24 + sizeof(u64) + header->name_ciphertext_size + 24 +
|
m_file.seekp(m_file.tellg());
|
||||||
sizeof(u64) + header->content_ciphertext_size;
|
std::array<u8, 4096> zeros = {0};
|
||||||
m_file.seekg(static_cast<i64>(header->content_ciphertext_size),
|
u64 remaining = header->content_ciphertext_size;
|
||||||
std::ios::cur);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry_start != -1) {
|
|
||||||
i64 entry_end = entry_start + static_cast<i64>(entry_total_size);
|
|
||||||
|
|
||||||
m_file.clear();
|
|
||||||
m_file.seekg(entry_end, std::ios::beg);
|
|
||||||
std::string remaining;
|
|
||||||
remaining.assign(std::istreambuf_iterator<char>(m_file),
|
|
||||||
std::istreambuf_iterator<char>());
|
|
||||||
|
|
||||||
m_file.clear();
|
|
||||||
m_file.seekp(entry_start, std::ios::beg);
|
|
||||||
ASSERT(m_file.write(remaining.data(), static_cast<i64>(remaining.size())));
|
|
||||||
|
|
||||||
i64 new_size = entry_start + static_cast<i64>(remaining.size());
|
|
||||||
m_file.flush();
|
|
||||||
m_file.close();
|
|
||||||
|
|
||||||
std::filesystem::resize_file(m_path, new_size);
|
|
||||||
|
|
||||||
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 &filename,
|
||||||
@@ -165,7 +179,7 @@ void Vault::update_file(const std::string &filename,
|
|||||||
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)) {
|
if (!m_file.read(to_char_ptr(&header.deleted), 1)) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,16 +196,14 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
10
src/vault.h
10
src/vault.h
@@ -1,20 +1,18 @@
|
|||||||
#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 <optional>
|
#include <optional>
|
||||||
|
|
||||||
constexpr i16 VERSION = 1;
|
constexpr i16 VERSION = 1;
|
||||||
constexpr u64 AFTER_HEADER_OFFSET = 22;
|
constexpr u64 AFTER_HEADER_OFFSET = 68;
|
||||||
|
|
||||||
// !!! REMEMBER TO UPDATE entry_total_size IN Vault::delete_file
|
// !!! REMEMBER TO UPDATE entry_total_size IN Vault::delete_file
|
||||||
struct FileHeader {
|
struct FileHeader {
|
||||||
std::array<u8, 24> name_nonce;
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,12 +20,16 @@ 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);
|
||||||
|
|
||||||
std::vector<FileHeader> read_file_headers();
|
std::vector<FileHeader> read_file_headers();
|
||||||
std::optional<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);
|
||||||
|
|
||||||
|
const std::string &path() const { return m_path; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_path;
|
std::string m_path;
|
||||||
std::fstream m_file;
|
std::fstream m_file;
|
||||||
|
|||||||
Reference in New Issue
Block a user