chore: auto-commit (3 archivos)
- cpp/functions/infra/secret_store.cpp - cpp/functions/infra/secret_store.h - cpp/functions/infra/secret_store.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
# include <wincrypt.h>
|
||||
# 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> encrypt(const std::string& plaintext) {
|
||||
#ifdef _WIN32
|
||||
DATA_BLOB in_blob { (DWORD)plaintext.size(),
|
||||
(BYTE*)const_cast<char*>(plaintext.data()) };
|
||||
DATA_BLOB out_blob {};
|
||||
if (!CryptProtectData(&in_blob, L"fn_agents_dashboard", nullptr,
|
||||
nullptr, nullptr, 0, &out_blob)) {
|
||||
return {};
|
||||
}
|
||||
std::vector<uint8_t> result(out_blob.pbData,
|
||||
out_blob.pbData + out_blob.cbData);
|
||||
LocalFree(out_blob.pbData);
|
||||
return result;
|
||||
#else
|
||||
// Linux: 1-byte magic + XOR
|
||||
std::vector<uint8_t> key = linux_key();
|
||||
std::vector<uint8_t> 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<uint8_t>& blob) {
|
||||
if (blob.empty()) return {};
|
||||
#ifdef _WIN32
|
||||
DATA_BLOB in_blob { (DWORD)blob.size(),
|
||||
(BYTE*)const_cast<uint8_t*>(blob.data()) };
|
||||
DATA_BLOB out_blob {};
|
||||
if (!CryptUnprotectData(&in_blob, nullptr, nullptr,
|
||||
nullptr, nullptr, 0, &out_blob)) {
|
||||
return {};
|
||||
}
|
||||
std::string result(reinterpret_cast<char*>(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<uint8_t> 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
|
||||
@@ -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 <string>
|
||||
#include <vector>
|
||||
|
||||
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<uint8_t> 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<uint8_t>& 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
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: secret_store
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "fn_secret::encrypt(plaintext) -> vector<uint8_t>; 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: "error_go_core"
|
||||
imports: [infra/secret_store.h]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/infra/secret_store.cpp"
|
||||
framework: ""
|
||||
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 column."
|
||||
output: "encrypt returns vector<uint8_t> 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<uint8_t> 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`.
|
||||
Reference in New Issue
Block a user