From a59b12d46717a8115d1079f3dbe0b4a2fdb77386 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Fri, 22 May 2026 21:42:44 +0200 Subject: [PATCH] =?UTF-8?q?feat(auto):=20construir=20iter=201=20=E2=80=94?= =?UTF-8?q?=20add=20secret=5Fstore=5Fcpp=5Finfra=20registry=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DPAPI Windows + XOR Linux fallback para almacenar credentials sensibles en SQLite local. Usado por agents_dashboard para cifrar apikeys. Incluye encrypt/decrypt/is_strong + base64 helpers. Issue: 0129 Co-Authored-By: fn-constructor --- cpp/functions/infra/secret_store.cpp | 167 +++++++++++++++++++++++++++ cpp/functions/infra/secret_store.h | 37 ++++++ cpp/functions/infra/secret_store.md | 76 ++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 cpp/functions/infra/secret_store.cpp create mode 100644 cpp/functions/infra/secret_store.h create mode 100644 cpp/functions/infra/secret_store.md diff --git a/cpp/functions/infra/secret_store.cpp b/cpp/functions/infra/secret_store.cpp new file mode 100644 index 00000000..e0d45c6e --- /dev/null +++ b/cpp/functions/infra/secret_store.cpp @@ -0,0 +1,167 @@ +// secret_store.cpp — implementation of fn_secret (issue 0129). +// +// See secret_store.h for API docs and platform notes. + +#include "infra/secret_store.h" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +# include +# pragma comment(lib, "crypt32.lib") +#endif + +namespace fn_secret { + +// --------------------------------------------------------------------------- +// Base64 helpers (no external deps, RFC 4648 alphabet) +// --------------------------------------------------------------------------- + +static const char kB64Chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static std::string base64_encode(const uint8_t* data, size_t len) { + std::string out; + out.reserve(((len + 2) / 3) * 4); + for (size_t i = 0; i < len; i += 3) { + uint32_t b = (uint32_t)data[i] << 16; + if (i + 1 < len) b |= (uint32_t)data[i + 1] << 8; + if (i + 2 < len) b |= (uint32_t)data[i + 2]; + out += kB64Chars[(b >> 18) & 63]; + out += kB64Chars[(b >> 12) & 63]; + out += (i + 1 < len) ? kB64Chars[(b >> 6) & 63] : '='; + out += (i + 2 < len) ? kB64Chars[(b) & 63] : '='; + } + return out; +} + +static std::vector base64_decode(const std::string& s) { + auto decode_char = [](char c) -> int { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; + }; + std::vector out; + out.reserve(s.size() * 3 / 4); + for (size_t i = 0; i + 3 < s.size(); i += 4) { + int a = decode_char(s[i]); + int b = decode_char(s[i + 1]); + int c = decode_char(s[i + 2]); + int d = decode_char(s[i + 3]); + if (a < 0 || b < 0) break; + out.push_back((uint8_t)((a << 2) | (b >> 4))); + if (c >= 0) out.push_back((uint8_t)((b << 4) | (c >> 2))); + if (d >= 0) out.push_back((uint8_t)((c << 2) | d)); + } + return out; +} + +// --------------------------------------------------------------------------- +// Linux fallback: XOR with a stable per-user key +// --------------------------------------------------------------------------- + +#ifndef _WIN32 +static std::vector linux_key() { + // Key = first 32 bytes of SHA-256-like mixing of LOGNAME + HOSTNAME. + // Good enough to prevent casual plaintext inspection; NOT crypto-secure. + const char* user = getenv("LOGNAME"); + const char* host = getenv("HOSTNAME"); + if (!user) user = "user"; + if (!host) host = "localhost"; + std::string seed = std::string(user) + "@" + host + ":fn_agents_dashboard_key_v1"; + std::vector key(32, 0); + for (size_t i = 0; i < seed.size(); i++) { + key[i % 32] ^= (uint8_t)seed[i]; + key[(i + 7) % 32] += (uint8_t)(seed[i] * 31 + i); + key[(i + 13) % 32] ^= (uint8_t)(seed[i] + i * 7); + } + return key; +} +#endif + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +bool is_strong() { +#ifdef _WIN32 + return true; +#else + return false; +#endif +} + +std::vector encrypt(const std::string& plaintext) { +#ifdef _WIN32 + DATA_BLOB in_blob { (DWORD)plaintext.size(), + (BYTE*)const_cast(plaintext.data()) }; + DATA_BLOB out_blob {}; + if (!CryptProtectData(&in_blob, L"fn_agents_dashboard", nullptr, + nullptr, nullptr, 0, &out_blob)) { + return {}; + } + std::vector result(out_blob.pbData, + out_blob.pbData + out_blob.cbData); + LocalFree(out_blob.pbData); + return result; +#else + // Linux: 1-byte magic + XOR + std::vector key = linux_key(); + std::vector out; + out.reserve(1 + plaintext.size()); + out.push_back(0xAF); // magic marker + for (size_t i = 0; i < plaintext.size(); i++) { + out.push_back((uint8_t)plaintext[i] ^ key[i % key.size()]); + } + return out; +#endif +} + +std::string decrypt(const std::vector& blob) { + if (blob.empty()) return {}; +#ifdef _WIN32 + DATA_BLOB in_blob { (DWORD)blob.size(), + (BYTE*)const_cast(blob.data()) }; + DATA_BLOB out_blob {}; + if (!CryptUnprotectData(&in_blob, nullptr, nullptr, + nullptr, nullptr, 0, &out_blob)) { + return {}; + } + std::string result(reinterpret_cast(out_blob.pbData), + out_blob.cbData); + LocalFree(out_blob.pbData); + return result; +#else + // Linux: check magic, XOR decode + if (blob[0] != 0xAF) return {}; + std::vector key = linux_key(); + std::string out; + out.reserve(blob.size() - 1); + for (size_t i = 1; i < blob.size(); i++) { + out += (char)(blob[i] ^ key[(i - 1) % key.size()]); + } + return out; +#endif +} + +std::string encrypt_b64(const std::string& plaintext) { + auto blob = encrypt(plaintext); + if (blob.empty()) return {}; + return base64_encode(blob.data(), blob.size()); +} + +std::string decrypt_b64(const std::string& b64) { + auto blob = base64_decode(b64); + return decrypt(blob); +} + +} // namespace fn_secret diff --git a/cpp/functions/infra/secret_store.h b/cpp/functions/infra/secret_store.h new file mode 100644 index 00000000..3fc8269e --- /dev/null +++ b/cpp/functions/infra/secret_store.h @@ -0,0 +1,37 @@ +// secret_store.h — encrypt/decrypt sensitive strings for local storage. +// +// Windows: uses DPAPI (CryptProtectData / CryptUnprotectData). +// The encrypted blob is bound to the current user account on the local +// machine. Key never leaves the machine. The blob can be stored in +// SQLite as a BLOB column. +// +// Linux/WSL fallback: XOR-encode with a stable per-user key derived from +// username + hostname. NOT cryptographically strong — but prevents +// plaintext credentials sitting in SQLite and shows a warning in the UI. +// Production use should switch to libsecret / KDE Wallet on Linux. +// +// Part of issue 0129 (agents_dashboard credential storage). +#pragma once + +#include +#include + +namespace fn_secret { + +// Encrypt `plaintext` into an opaque blob suitable for storage in a BLOB column. +// Returns empty vector on failure; never throws. +std::vector encrypt(const std::string& plaintext); + +// Decrypt a blob produced by `encrypt()`. +// Returns empty string on failure (wrong key, corrupted data, etc.). +std::string decrypt(const std::vector& blob); + +// Convenience: encrypt returns base64 string for TEXT storage. +std::string encrypt_b64(const std::string& plaintext); +std::string decrypt_b64(const std::string& b64); + +// Returns true if running with strong DPAPI encryption (Windows). +// Returns false on Linux fallback — callers may show a warning. +bool is_strong(); + +} // namespace fn_secret diff --git a/cpp/functions/infra/secret_store.md b/cpp/functions/infra/secret_store.md new file mode 100644 index 00000000..d08db9f6 --- /dev/null +++ b/cpp/functions/infra/secret_store.md @@ -0,0 +1,76 @@ +--- +id: secret_store_cpp_infra +name: secret_store +kind: function +lang: cpp +domain: infra +version: 1.0.0 +purity: impure +signature: "fn_secret::encrypt(plaintext) -> blob; fn_secret::decrypt(blob) -> string; fn_secret::is_strong() -> bool" +description: "Encrypt/decrypt sensitive strings for local SQLite storage. Windows: DPAPI (user-bound, machine-local, cryptographically strong). Linux/WSL fallback: XOR with per-user seed key (not crypto-secure, shows warning). Used by agents_dashboard to store API keys." +tags: [security, credentials, dpapi, encrypt, infra, agents] +uses_functions: [] +uses_types: [] +returns: "" +returns_optional: false +error_type: "" +imports: "infra/secret_store.h" +example: | + #include "infra/secret_store.h" + + // Encrypt an API key before storing in SQLite: + std::string apikey = "sk-mykey-123"; + auto blob = fn_secret::encrypt(apikey); + // store blob in SQLite BLOB column... + + // Decrypt when needed: + std::string recovered = fn_secret::decrypt(blob); + assert(recovered == apikey); + + // Check platform strength: + if (!fn_secret::is_strong()) { + // Show warning: Linux fallback is NOT crypto-secure + } +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/infra/secret_store.cpp" +params_schema: '{"params":[{"name":"plaintext","desc":"Sensitive string to encrypt (API key, password, token)."},{"name":"blob","desc":"Opaque byte vector returned by encrypt(), stored as SQLite BLOB."}],"output":"encrypt returns vector blob (empty on failure). decrypt returns plaintext string (empty on failure). is_strong() returns true on Windows (DPAPI), false on Linux (XOR fallback)."}' +--- + +# secret_store + +Encrypt/decrypt sensitive credentials for local SQLite storage. + +## Ejemplo + +```cpp +#include "infra/secret_store.h" + +// Store API key encrypted: +std::vector blob = fn_secret::encrypt("my-api-key-here"); +// Insert blob into SQLite BLOB column via sqlite3_bind_blob()... + +// Recover: +std::string key = fn_secret::decrypt(blob); + +// Base64 helpers for TEXT columns: +std::string b64 = fn_secret::encrypt_b64("my-api-key-here"); +std::string back = fn_secret::decrypt_b64(b64); + +// Platform check (show warning on Linux): +if (!fn_secret::is_strong()) { + fn_log::warn("[security] apikey stored with weak Linux fallback encryption"); +} +``` + +## Cuando usarla + +Antes de guardar una API key, token o contrasena en SQLite local. Siempre usar `fn::local_path("app.db")` para la DB. En Windows (DPAPI) la clave nunca sale de la maquina. En Linux, mostrar aviso en UI de que la proteccion es basica. + +## Gotchas + +- **DPAPI is Windows-only**: el blob cifrado en Windows NO se puede descifrar en Linux y viceversa. Si el usuario mueve la DB entre plataformas, las credenciales se pierden — debe reingresar la apikey. +- **Linux fallback NO es criptograficamente seguro**: XOR con semilla derivada de username+hostname. Previene lectura casual pero no protege contra atacante con acceso al sistema. +- **CryptProtectData es sincrono**: no llamar desde el thread principal con datos grandes. Para una apikey (tipicamente <200 bytes) el coste es despreciable. +- Linkear `crypt32.lib` en Windows: el `.cpp` tiene `#pragma comment(lib, "crypt32.lib")` — no necesita entry en CMakeLists para MSVC. Con MinGW se enlaza automaticamente si se incluye `wincrypt.h`.