Skip to content
API Documentation
BastionGuard™
PasswordVault.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 *
17 * BastionGuard™ is a trademark of Calogero Scarnà.
18 * The BastionGuard™ name and branding are not licensed under the GPL.
19 */
20
21#include "PasswordVault.hpp"
22
23#include <fstream>
24#include <iomanip>
25#include <random>
26#include <sstream>
27#include <ctime>
28#include <cstring>
29#include <thread>
30#include <unordered_set>
31
32#include <giomm/file.h>
33#include <glibmm/miscutils.h>
34#include <glibmm/dispatcher.h>
35#include <nlohmann/json.hpp>
36#include <openssl/evp.h>
37#include <openssl/rand.h>
38
39using json = nlohmann::json;
40
41static void secureClear(std::string& s) {
42 if (!s.empty()) {
43 volatile char* p = const_cast<volatile char*>(s.data());
44 std::memset(const_cast<char*>(s.data()), 0, s.size());
45 (void)p;
46 s.clear();
47 }
48}
49
50static std::string toHex(const unsigned char* data, size_t len) {
51 std::ostringstream oss;
52 oss << std::hex << std::setfill('0');
53 for (size_t i = 0; i < len; ++i)
54 oss << std::setw(2) << static_cast<int>(data[i]);
55 return oss.str();
56}
57
58static std::string toHex(const std::string& s) {
59 return toHex(reinterpret_cast<const unsigned char*>(s.data()), s.size());
60}
61
62static std::string fromHex(const std::string& hex) {
63 std::string out;
64 out.reserve(hex.size() / 2);
65 for (size_t i = 0; i + 1 < hex.size(); i += 2) {
66 try {
67 out.push_back(static_cast<char>(std::stoi(hex.substr(i, 2), nullptr, 16)));
68 } catch (...) {
69 // byte non valido (vault corrotto o non-hex): restituisci vuoto
70 // così il caller sa che la decodifica è fallita.
71 return {};
72 }
73 }
74 return out;
75}
76
77static std::string csvEscape(const std::string& s) {
78 bool needs = s.find_first_of(",\"\n\r") != std::string::npos;
79 if (!needs) return s;
80 std::string out = "\"";
81 for (char c : s) { if (c == '"') out += "\"\""; else out += c; }
82 return out + '"';
83}
84
85bool PasswordVault::atomicWriteFile(const std::string& path, const std::string& content) {
86 const std::string tmp = path + ".tmp";
87 {
88 std::ofstream out(tmp, std::ios::trunc | std::ios::binary);
89 if (!out.good()) return false;
90 out << content;
91 if (!out.good()) return false;
92 }
93 return std::rename(tmp.c_str(), path.c_str()) == 0;
94}
95
96
97bool PasswordVault::aesEncrypt(const std::string& key32,
98 const std::string& nonce12,
99 const std::string& plaintext,
100 std::string& ciphertext,
101 std::string& tag16) const
102{
103 EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
104 if (!ctx) return false;
105
106 ciphertext.resize(plaintext.size());
107 tag16.resize(16);
108
109 int len = 0;
110 bool ok = true;
111
112 ok = ok && EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) == 1;
113 ok = ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, nullptr) == 1;
114 ok = ok && EVP_EncryptInit_ex(ctx, nullptr, nullptr,
115 reinterpret_cast<const unsigned char*>(key32.data()),
116 reinterpret_cast<const unsigned char*>(nonce12.data())) == 1;
117 ok = ok && EVP_EncryptUpdate(ctx, nullptr, &len, nullptr, 0) == 1; // no AAD
118 if (!plaintext.empty())
119 ok = ok && EVP_EncryptUpdate(ctx,
120 reinterpret_cast<unsigned char*>(ciphertext.data()),
121 &len,
122 reinterpret_cast<const unsigned char*>(plaintext.data()),
123 (int)plaintext.size()) == 1;
124 int finalLen = 0;
125 ok = ok && EVP_EncryptFinal_ex(ctx,
126 reinterpret_cast<unsigned char*>(ciphertext.data()) + len,
127 &finalLen) == 1;
128 ok = ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16,
129 reinterpret_cast<void*>(tag16.data())) == 1;
130 EVP_CIPHER_CTX_free(ctx);
131 return ok;
132}
133
134bool PasswordVault::aesDecrypt(const std::string& key32,
135 const std::string& nonce12,
136 const std::string& ciphertext,
137 const std::string& tag16,
138 std::string& plaintext) const
139{
140 EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
141 if (!ctx) return false;
142
143 plaintext.resize(ciphertext.size());
144 int len = 0;
145 bool ok = true;
146
147 ok = ok && EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) == 1;
148 ok = ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, nullptr) == 1;
149 ok = ok && EVP_DecryptInit_ex(ctx, nullptr, nullptr,
150 reinterpret_cast<const unsigned char*>(key32.data()),
151 reinterpret_cast<const unsigned char*>(nonce12.data())) == 1;
152 if (!ciphertext.empty())
153 ok = ok && EVP_DecryptUpdate(ctx,
154 reinterpret_cast<unsigned char*>(plaintext.data()),
155 &len,
156 reinterpret_cast<const unsigned char*>(ciphertext.data()),
157 (int)ciphertext.size()) == 1;
158 ok = ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16,
159 const_cast<void*>(static_cast<const void*>(tag16.data()))) == 1;
160 int finalLen = 0;
161 bool authOk = ok && EVP_DecryptFinal_ex(ctx,
162 reinterpret_cast<unsigned char*>(plaintext.data()) + len,
163 &finalLen) > 0;
164 EVP_CIPHER_CTX_free(ctx);
165 if (!authOk) { plaintext.clear(); return false; }
166 plaintext.resize(len + finalLen);
167 return true;
168}
169
170std::string PasswordVault::deriveKey(const std::string& saltHex) const {
171 const std::string salt = fromHex(saltHex);
172 unsigned char key[32];
173 PKCS5_PBKDF2_HMAC(masterPassword_.c_str(), (int)masterPassword_.size(),
174 reinterpret_cast<const unsigned char*>(salt.data()), (int)salt.size(),
175 100000, EVP_sha256(), 32, key);
176 return std::string(reinterpret_cast<char*>(key), 32);
177}
178
179std::string PasswordVault::pbkdf2Hex(const std::string& password,
180 const std::string& saltHex,
181 int iterations) const {
182 const std::string salt = fromHex(saltHex);
183 unsigned char out[32];
184 PKCS5_PBKDF2_HMAC(password.c_str(), (int)password.size(),
185 reinterpret_cast<const unsigned char*>(salt.data()), (int)salt.size(),
186 iterations, EVP_sha256(), 32, out);
187 return toHex(out, 32);
188}
189
191 unsigned char salt[16];
192 RAND_bytes(salt, sizeof(salt));
193 return toHex(salt, sizeof(salt));
194}
195
197 unsigned char nonce[12];
198 RAND_bytes(nonce, sizeof(nonce));
199 return toHex(nonce, sizeof(nonce));
200}
201
202
204 const std::unordered_map<std::string,std::string>& secrets) const
205{
206 json plainJ = json::object();
207 for (const auto& [id, pwd] : secrets)
208 plainJ[id] = pwd;
209 const std::string plaintext = plainJ.dump();
210 const std::string saltHex = generateSaltHex();
211 const std::string nonceHex = generateNonceHex();
212 const std::string key32 = deriveKey(saltHex);
213 const std::string nonce12 = fromHex(nonceHex);
214
215 std::string ct, tag;
216 if (!aesEncrypt(key32, nonce12, plaintext, ct, tag)) return false;
217
218 json fileJ = {
219 {"salt", saltHex},
220 {"nonce", nonceHex},
221 {"ct", toHex(ct)},
222 {"tag", toHex(tag)}
223 };
224 return atomicWriteFile(getSecretsPath(), fileJ.dump());
225}
226
228 std::unordered_map<std::string,std::string>& secrets) const
229{
230 secrets.clear();
231 std::ifstream in(getSecretsPath());
232 if (!in.good()) return true;
233
234 json fileJ;
235 try { in >> fileJ; } catch (...) { return false; }
236
237 const std::string saltHex = fileJ.value("salt", "");
238 const std::string nonceHex = fileJ.value("nonce", "");
239 const std::string ctHex = fileJ.value("ct", "");
240 const std::string tagHex = fileJ.value("tag", "");
241 if (saltHex.empty() || nonceHex.empty()) return false;
242
243 const std::string key32 = deriveKey(saltHex);
244 const std::string nonce12 = fromHex(nonceHex);
245 const std::string ct = fromHex(ctHex);
246 const std::string tag = fromHex(tagHex);
247
248 std::string plaintext;
249 if (!aesDecrypt(key32, nonce12, ct, tag, plaintext)) return false;
250
251 json plainJ;
252 try { plainJ = json::parse(plaintext); } catch (...) { return false; }
253 if (!plainJ.is_object()) return false;
254
255 for (auto it = plainJ.begin(); it != plainJ.end(); ++it)
256 if (it.value().is_string())
257 secrets[it.key()] = it.value().get<std::string>();
258
259 return true;
260}
261
262
264
266 const auto dir = Glib::get_home_dir() + "/.config/BastionGuard/password_manager";
267 auto dirFile = Gio::File::create_for_path(dir);
268 try {
269 if (!dirFile->query_exists())
270 dirFile->make_directory_with_parents();
271 } catch (...) { return false; }
272 const auto indexPath = getIndexPath();
273 std::ifstream in(indexPath);
274 if (!in.good())
275 if (!atomicWriteFile(indexPath, "[]")) return false;
276 return true;
277}
278
279bool PasswordVault::isUnlocked() const { return unlocked_; }
280
282 std::ifstream f(getHashPath());
283 return !f.good();
284}
285
286bool PasswordVault::createMasterPassword(const std::string& masterPassword) {
287 if (masterPassword.size() < 8) return false;
288 const std::string saltHex = generateSaltHex();
289 const std::string hashHex = pbkdf2Hex(masterPassword, saltHex);
290 json j = { {"salt", saltHex}, {"hash", hashHex}, {"iterations", 200000} };
291 if (!atomicWriteFile(getHashPath(), j.dump())) return false;
292 masterPassword_ = masterPassword;
293 unlocked_ = true;
294 cacheReady_ = true; // nessun secret da caricare al primo avvio
295 return true;
296}
297
298bool PasswordVault::unlock(const std::string& masterPassword) {
299 if (masterPassword.size() < 8) return false;
300 std::ifstream f(getHashPath());
301 if (!f.good()) return false;
302 json j;
303 try { f >> j; } catch (...) { return false; }
304 const std::string saltHex = j.value("salt", "");
305 const std::string savedHash = j.value("hash", "");
306 const int iterations = j.value("iterations", 200000);
307 if (saltHex.empty() || savedHash.empty()) return false;
308 const std::string derived = pbkdf2Hex(masterPassword, saltHex, iterations);
309 if (derived.size() != savedHash.size()) return false;
310 unsigned char diff = 0;
311 for (size_t i = 0; i < derived.size(); ++i)
312 diff |= static_cast<unsigned char>(derived[i]) ^
313 static_cast<unsigned char>(savedHash[i]);
314 if (diff != 0) return false;
315
316 masterPassword_ = masterPassword;
317 unlocked_ = true;
318 cacheReady_ = false;
319 std::thread([this]() {
320 std::unordered_map<std::string, std::string> loaded;
321 const bool ok = loadSecretsFile(loaded);
322 if (unlocked_) {
323 std::lock_guard<std::mutex> lk(cacheMutex_);
324 if (ok) secretCache_ = std::move(loaded);
325 }
326 cacheReady_ = true;
327 }).detach();
328
329 return true;
330}
331
334 unlocked_ = false;
335 cacheReady_ = false;
336 std::lock_guard<std::mutex> lk(cacheMutex_);
337 secretCache_.clear();
338}
339
340std::string PasswordVault::getIndexPath() const {
341 return Glib::get_home_dir() + "/.config/BastionGuard/password_manager/index.json";
342}
343std::string PasswordVault::getHashPath() const {
344 return Glib::get_home_dir() + "/.config/BastionGuard/password_manager/master.json";
345}
346std::string PasswordVault::getSecretsPath() const {
347 return Glib::get_home_dir() + "/.config/BastionGuard/password_manager/secrets.enc";
348}
349std::string PasswordVault::makeSecretKey(const std::string& id) const {
350 return id;
351}
352
353std::vector<PasswordEntry> PasswordVault::loadIndex() const {
354 std::vector<PasswordEntry> entries;
355 std::ifstream in(getIndexPath());
356 if (!in.good()) return entries;
357 json j;
358 try { in >> j; } catch (...) { return entries; }
359 if (!j.is_array()) return entries;
360 for (const auto& item : j) {
361 if (!item.is_object()) continue;
363 e.id = item.value("id", "");
364 e.name = item.value("name", "");
365 e.username = item.value("username", "");
366 e.url = item.value("url", "");
367 e.notes = item.value("notes", "");
368 e.created_at = item.value("created_at", "");
369 e.updated_at = item.value("updated_at", "");
370 if (e.id.empty()) continue;
371 if (item.contains("tags") && item["tags"].is_array())
372 for (const auto& tag : item["tags"])
373 if (tag.is_string()) e.tags.push_back(tag.get<std::string>());
374 entries.push_back(std::move(e));
375 }
376 return entries;
377}
378
379bool PasswordVault::saveIndex(const std::vector<PasswordEntry>& entries) const {
380 json j = json::array();
381 for (const auto& e : entries)
382 j.push_back({{"id",e.id},{"name",e.name},{"username",e.username},
383 {"url",e.url},{"notes",e.notes},{"tags",e.tags},
384 {"created_at",e.created_at},{"updated_at",e.updated_at}});
385 return atomicWriteFile(getIndexPath(), j.dump(4));
386}
387
388
390 std::unordered_map<std::string,std::string> snapshot) const {
391 std::thread([this, snapshot]() {
392 this->saveSecretsFile(snapshot);
393 }).detach();
394}
395
396bool PasswordVault::addEntry(const PasswordEntry& entry, const std::string& secret) {
397 if (!unlocked_ || entry.id.empty() || entry.name.empty()) return false;
398 auto entries = loadIndex();
399 for (const auto& e : entries)
400 if (e.id == entry.id) return false;
401 entries.push_back(entry);
402 if (!saveIndex(entries)) return false;
403
404 std::unordered_map<std::string,std::string> snap;
405 {
406 std::lock_guard<std::mutex> lk(cacheMutex_);
407 secretCache_[entry.id] = secret;
408 snap = secretCache_;
409 }
410 flushAsync(std::move(snap));
411 return true;
412}
413
414bool PasswordVault::updateEntry(const PasswordEntry& entry, const std::string& secret) {
415 if (!unlocked_ || entry.id.empty()) return false;
416 auto entries = loadIndex();
417 for (auto& e : entries) {
418 if (e.id == entry.id) {
419 e = entry;
420 if (!saveIndex(entries)) return false;
421 std::unordered_map<std::string,std::string> snap;
422 {
423 std::lock_guard<std::mutex> lk(cacheMutex_);
424 secretCache_[entry.id] = secret;
425 snap = secretCache_;
426 }
427 flushAsync(std::move(snap));
428 return true;
429 }
430 }
431 return false;
432}
433
434bool PasswordVault::deleteEntry(const std::string& id) {
435 if (!unlocked_ || id.empty()) return false;
436 auto entries = loadIndex();
437 const auto before = entries.size();
438 std::vector<PasswordEntry> filtered;
439 filtered.reserve(before);
440 for (const auto& e : entries)
441 if (e.id != id) filtered.push_back(e);
442 if (filtered.size() == before) return false;
443 if (!saveIndex(filtered)) return false;
444
445 std::unordered_map<std::string,std::string> snap;
446 {
447 std::lock_guard<std::mutex> lk(cacheMutex_);
448 secretCache_.erase(id);
449 snap = secretCache_;
450 }
451 flushAsync(std::move(snap));
452 return true;
453}
454
455void PasswordVault::deleteAllEntries(std::function<void(bool)> onDone) {
456 if (!unlocked_) { if (onDone) onDone(false); return; }
457 if (!atomicWriteFile(getIndexPath(), "[]")) { if (onDone) onDone(false); return; }
458 {
459 std::lock_guard<std::mutex> lk(cacheMutex_);
460 secretCache_.clear();
461 }
462 auto disp = std::make_shared<Glib::Dispatcher>();
463 auto result = std::make_shared<bool>(false);
464 auto cb = std::make_shared<std::function<void(bool)>>(std::move(onDone));
465 disp->connect([disp, result, cb]() { if (*cb) (*cb)(*result); });
466 const std::string path = getSecretsPath();
467 std::thread([disp, result, path]() {
468 *result = (std::remove(path.c_str()) == 0 || errno == ENOENT);
469 disp->emit();
470 }).detach();
471}
472
474 const std::vector<std::pair<PasswordEntry, std::string>>& items,
475 std::function<void(int ok, int fail)> onDone)
476{
477 if (!unlocked_) { if (onDone) onDone(0, (int)items.size()); return; }
478
479 auto entries = loadIndex();
480 std::unordered_set<std::string> existingIds;
481 existingIds.reserve(entries.size());
482 for (const auto& e : entries) existingIds.insert(e.id);
483
484 int ok = 0, fail = 0;
485 for (const auto& [entry, secret] : items) {
486 if (entry.id.empty() || entry.name.empty() || existingIds.count(entry.id)) {
487 ++fail; continue;
488 }
489 entries.push_back(entry);
490 existingIds.insert(entry.id);
491 ++ok;
492 }
493
494 if (!saveIndex(entries)) { if (onDone) onDone(0, (int)items.size()); return; }
495 {
496 std::lock_guard<std::mutex> lk(cacheMutex_);
497 for (const auto& [entry, secret] : items)
498 if (!entry.id.empty()) secretCache_[entry.id] = secret;
499 }
500 if (onDone) onDone(ok, fail);
501 std::unordered_map<std::string,std::string> snap;
502 { std::lock_guard<std::mutex> lk(cacheMutex_); snap = secretCache_; }
503 flushAsync(std::move(snap));
504}
505
506
508 std::function<void(std::string csv, std::string err)> onDone) const
509{
510 if (!unlocked_) { if (onDone) onDone("", "Vault bloccato."); return; }
511 const auto entries = loadIndex();
512 auto resultCsv = std::make_shared<std::string>();
513 auto disp = std::make_shared<Glib::Dispatcher>();
514 auto cb = std::make_shared<std::function<void(std::string,std::string)>>(onDone);
515 disp->connect([disp, resultCsv, cb]() { if (*cb) (*cb)(std::move(*resultCsv), ""); });
516
517 std::unordered_map<std::string,std::string> snap;
518 { std::lock_guard<std::mutex> lk(cacheMutex_); snap = secretCache_; }
519
520 std::thread([entries, snap, resultCsv, disp]() {
521 std::ostringstream csv;
522 csv << "name,url,username,password,notes\n";
523 for (const auto& e : entries) {
524 std::string pwd;
525 auto it = snap.find(e.id);
526 if (it != snap.end()) pwd = it->second;
527 csv << csvEscape(e.name) << ","
528 << csvEscape(e.url) << ","
529 << csvEscape(e.username) << ","
530 << csvEscape(pwd) << ","
531 << csvEscape(e.notes) << "\n";
532 }
533 *resultCsv = csv.str();
534 disp->emit();
535 }).detach();
536}
537
538
539void PasswordVault::getSecretAsync(const std::string& id,
540 std::function<void(std::string)> callback) const
541{
542 if (!unlocked_ || id.empty()) { if (callback) callback({}); return; }
543
544 {
545 std::lock_guard<std::mutex> lk(cacheMutex_);
546 auto it = secretCache_.find(id);
547 if (it != secretCache_.end()) {
548 const std::string cached = it->second;
549 Glib::signal_idle().connect_once([callback, cached]() {
550 if (callback) callback(cached);
551 });
552 return;
553 }
554 }
555
556 auto retries = std::make_shared<int>(0);
557 Glib::signal_timeout().connect(
558 [this, id, callback, retries]() -> bool {
559 ++(*retries);
560 {
561 std::lock_guard<std::mutex> lk(cacheMutex_);
562 auto it = secretCache_.find(id);
563 if (it != secretCache_.end()) {
564 if (callback) callback(it->second);
565 return false;
566 }
567 }
568 if (cacheReady_.load() || *retries > 100) {
569 if (callback) callback({});
570 return false;
571 }
572 return true;
573 }, 50);
574}
575
576bool PasswordVault::setSecret(const std::string& id, const std::string& secret) {
577 if (id.empty()) return false;
578 std::unordered_map<std::string,std::string> snap;
579 {
580 std::lock_guard<std::mutex> lk(cacheMutex_);
581 secretCache_[id] = secret;
582 snap = secretCache_;
583 }
584 flushAsync(std::move(snap));
585 return true;
586}
587
588std::string PasswordVault::generateId() const {
589 std::random_device rd;
590 std::mt19937_64 gen(rd());
591 std::uniform_int_distribution<uint32_t> dist32;
592 uint32_t data[4] = { dist32(gen), dist32(gen), dist32(gen), dist32(gen) };
593 data[1] = (data[1] & 0xFFFF0FFF) | 0x00004000;
594 data[2] = (data[2] & 0x3FFFFFFF) | 0x80000000;
595 std::ostringstream oss;
596 oss << std::hex << std::setfill('0');
597 oss << std::setw(8) << data[0] << "-"
598 << std::setw(4) << (data[1] >> 16) << "-"
599 << std::setw(4) << (data[1] & 0xFFFF) << "-"
600 << std::setw(4) << (data[2] >> 16) << "-"
601 << std::setw(4) << (data[2] & 0xFFFF)
602 << std::setw(8) << data[3];
603 return oss.str();
604}
605
606std::string PasswordVault::nowIso() const {
607 std::time_t t = std::time(nullptr);
608 std::tm tm{};
609 localtime_r(&t, &tm);
610 std::ostringstream oss;
611 oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
612 return oss.str();
613}
nlohmann::json json
Definition Backend.cpp:2908
static std::string toHex(const unsigned char *data, size_t len)
static std::string csvEscape(const std::string &s)
static std::string fromHex(const std::string &hex)
static void secureClear(std::string &s)
std::string masterPassword_
std::unordered_map< std::string, std::string > secretCache_
bool setSecret(const std::string &id, const std::string &secret)
bool createMasterPassword(const std::string &masterPassword)
std::string generateId() const
std::mutex cacheMutex_
bool isUnlocked() const
std::atomic< bool > cacheReady_
bool addEntry(const PasswordEntry &entry, const std::string &secret)
std::string pbkdf2Hex(const std::string &password, const std::string &saltHex, int iterations=200000) const
std::string getSecretsPath() const
std::atomic< bool > unlocked_
std::string generateNonceHex() const
std::string getHashPath() const
void flushAsync(std::unordered_map< std::string, std::string > snapshot) const
std::string generateSaltHex() const
bool isFirstRun() const
bool loadSecretsFile(std::unordered_map< std::string, std::string > &secrets) const
bool aesEncrypt(const std::string &key32, const std::string &nonce12, const std::string &plaintext, std::string &ciphertext, std::string &tag16) const
std::string nowIso() const
std::string makeSecretKey(const std::string &id) const
void getSecretAsync(const std::string &id, std::function< void(std::string)> callback) const
bool aesDecrypt(const std::string &key32, const std::string &nonce12, const std::string &ciphertext, const std::string &tag16, std::string &plaintext) const
bool unlock(const std::string &masterPassword)
std::string deriveKey(const std::string &saltHex) const
bool updateEntry(const PasswordEntry &entry, const std::string &secret)
static bool atomicWriteFile(const std::string &path, const std::string &content)
bool saveSecretsFile(const std::unordered_map< std::string, std::string > &secrets) const
std::string getIndexPath() const
void deleteAllEntries(std::function< void(bool)> onDone)
std::vector< PasswordEntry > loadIndex() const
void exportToCSV(std::function< void(std::string csv, std::string err)> onDone) const
bool deleteEntry(const std::string &id)
void addEntriesBulk(const std::vector< std::pair< PasswordEntry, std::string > > &items, std::function< void(int ok, int fail)> onDone)
bool saveIndex(const std::vector< PasswordEntry > &entries) const
std::string created_at
std::string url
std::string id
std::string username
std::string updated_at
std::vector< std::string > tags
std::string notes
std::string name