30#include <unordered_set>
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>
39using json = nlohmann::json;
43 volatile char* p =
const_cast<volatile char*
>(s.data());
44 std::memset(
const_cast<char*
>(s.data()), 0, s.size());
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]);
58static std::string
toHex(
const std::string& s) {
59 return toHex(
reinterpret_cast<const unsigned char*
>(s.data()), s.size());
62static std::string
fromHex(
const std::string& hex) {
64 out.reserve(hex.size() / 2);
65 for (
size_t i = 0; i + 1 < hex.size(); i += 2) {
67 out.push_back(
static_cast<char>(std::stoi(hex.substr(i, 2),
nullptr, 16)));
78 bool needs = s.find_first_of(
",\"\n\r") != std::string::npos;
80 std::string out =
"\"";
81 for (
char c : s) {
if (c ==
'"') out +=
"\"\"";
else out += c; }
86 const std::string tmp = path +
".tmp";
88 std::ofstream out(tmp, std::ios::trunc | std::ios::binary);
89 if (!out.good())
return false;
91 if (!out.good())
return false;
93 return std::rename(tmp.c_str(), path.c_str()) == 0;
98 const std::string& nonce12,
99 const std::string& plaintext,
100 std::string& ciphertext,
101 std::string& tag16)
const
103 EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
104 if (!ctx)
return false;
106 ciphertext.resize(plaintext.size());
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;
118 if (!plaintext.empty())
119 ok = ok && EVP_EncryptUpdate(ctx,
120 reinterpret_cast<unsigned char*
>(ciphertext.data()),
122 reinterpret_cast<const unsigned char*
>(plaintext.data()),
123 (
int)plaintext.size()) == 1;
125 ok = ok && EVP_EncryptFinal_ex(ctx,
126 reinterpret_cast<unsigned char*
>(ciphertext.data()) + len,
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);
135 const std::string& nonce12,
136 const std::string& ciphertext,
137 const std::string& tag16,
138 std::string& plaintext)
const
140 EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
141 if (!ctx)
return false;
143 plaintext.resize(ciphertext.size());
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()),
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;
161 bool authOk = ok && EVP_DecryptFinal_ex(ctx,
162 reinterpret_cast<unsigned char*
>(plaintext.data()) + len,
164 EVP_CIPHER_CTX_free(ctx);
165 if (!authOk) { plaintext.clear();
return false; }
166 plaintext.resize(len + finalLen);
171 const std::string salt =
fromHex(saltHex);
172 unsigned char key[32];
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);
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);
191 unsigned char salt[16];
192 RAND_bytes(salt,
sizeof(salt));
193 return toHex(salt,
sizeof(salt));
197 unsigned char nonce[12];
198 RAND_bytes(nonce,
sizeof(nonce));
199 return toHex(nonce,
sizeof(nonce));
204 const std::unordered_map<std::string,std::string>& secrets)
const
206 json plainJ = json::object();
207 for (
const auto& [
id, pwd] : secrets)
209 const std::string plaintext = plainJ.dump();
212 const std::string key32 =
deriveKey(saltHex);
213 const std::string nonce12 =
fromHex(nonceHex);
216 if (!
aesEncrypt(key32, nonce12, plaintext, ct, tag))
return false;
228 std::unordered_map<std::string,std::string>& secrets)
const
232 if (!in.good())
return true;
235 try { in >> fileJ; }
catch (...) {
return false; }
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;
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);
248 std::string plaintext;
249 if (!
aesDecrypt(key32, nonce12, ct, tag, plaintext))
return false;
252 try { plainJ = json::parse(plaintext); }
catch (...) {
return false; }
253 if (!plainJ.is_object())
return false;
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>();
266 const auto dir = Glib::get_home_dir() +
"/.config/BastionGuard/password_manager";
267 auto dirFile = Gio::File::create_for_path(dir);
269 if (!dirFile->query_exists())
270 dirFile->make_directory_with_parents();
271 }
catch (...) {
return false; }
273 std::ifstream in(indexPath);
287 if (masterPassword.size() < 8)
return false;
289 const std::string hashHex =
pbkdf2Hex(masterPassword, saltHex);
290 json j = { {
"salt", saltHex}, {
"hash", hashHex}, {
"iterations", 200000} };
299 if (masterPassword.size() < 8)
return false;
301 if (!f.good())
return false;
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;
319 std::thread([
this]() {
320 std::unordered_map<std::string, std::string> loaded;
341 return Glib::get_home_dir() +
"/.config/BastionGuard/password_manager/index.json";
344 return Glib::get_home_dir() +
"/.config/BastionGuard/password_manager/master.json";
347 return Glib::get_home_dir() +
"/.config/BastionGuard/password_manager/secrets.enc";
354 std::vector<PasswordEntry> entries;
356 if (!in.good())
return entries;
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",
"");
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));
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}});
390 std::unordered_map<std::string,std::string> snapshot)
const {
391 std::thread([
this, snapshot]() {
397 if (!
unlocked_ || entry.
id.empty() || entry.
name.empty())
return false;
399 for (
const auto& e : entries)
400 if (e.id == entry.
id)
return false;
401 entries.push_back(entry);
404 std::unordered_map<std::string,std::string> snap;
417 for (
auto& e : entries) {
418 if (e.id == entry.
id) {
421 std::unordered_map<std::string,std::string> snap;
435 if (!
unlocked_ ||
id.empty())
return false;
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;
445 std::unordered_map<std::string,std::string> snap;
456 if (!
unlocked_) {
if (onDone) onDone(
false);
return; }
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); });
467 std::thread([disp, result, path]() {
468 *result = (std::remove(path.c_str()) == 0 || errno == ENOENT);
474 const std::vector<std::pair<PasswordEntry, std::string>>& items,
475 std::function<
void(
int ok,
int fail)> onDone)
477 if (!
unlocked_) {
if (onDone) onDone(0, (
int)items.size());
return; }
480 std::unordered_set<std::string> existingIds;
481 existingIds.reserve(entries.size());
482 for (
const auto& e : entries) existingIds.insert(e.id);
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)) {
489 entries.push_back(entry);
490 existingIds.insert(entry.id);
494 if (!
saveIndex(entries)) {
if (onDone) onDone(0, (
int)items.size());
return; }
497 for (
const auto& [entry, secret] : items)
500 if (onDone) onDone(ok, fail);
501 std::unordered_map<std::string,std::string> snap;
508 std::function<
void(std::string csv, std::string err)> onDone)
const
510 if (!
unlocked_) {
if (onDone) onDone(
"",
"Vault bloccato.");
return; }
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),
""); });
517 std::unordered_map<std::string,std::string> snap;
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) {
525 auto it = snap.find(e.id);
526 if (it != snap.end()) pwd = it->second;
533 *resultCsv = csv.str();
540 std::function<
void(std::string)> callback)
const
542 if (!
unlocked_ ||
id.empty()) {
if (callback) callback({});
return; }
548 const std::string cached = it->second;
549 Glib::signal_idle().connect_once([callback, cached]() {
550 if (callback) callback(cached);
556 auto retries = std::make_shared<int>(0);
557 Glib::signal_timeout().connect(
558 [
this,
id, callback, retries]() ->
bool {
564 if (callback) callback(it->second);
569 if (callback) callback({});
577 if (
id.empty())
return false;
578 std::unordered_map<std::string,std::string> snap;
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];
607 std::time_t t = std::time(
nullptr);
609 localtime_r(&t, &tm);
610 std::ostringstream oss;
611 oss << std::put_time(&tm,
"%Y-%m-%d %H:%M:%S");
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::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 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::vector< std::string > tags