Biblioteca dinâmica C++ (.so) POSIX para gerenciamento criptográfico de segredos multi-tenant com SQLCipher.
- Visão Geral
- Stack
- Build
- API Pública
- Exemplos
- Hierarquia Criptográfica
- Wrapping Key Cache
- Ciclo de Vida da KEK
- Thread Safety
- Audit Log
- Schema SQLite
- Bindings
Vaultine é um cofre de chaves criptográficas multi-tenant. Cada usuário possui um KEK (Key Encryption Key) de 256-bit que protege todos os seus segredos. O KEK é armazenado wrapped (AES-KW-256) e só é deswrapped em memória durante operações, usando uma chave derivada do hash de autenticação do usuário + salt.
| Conceito | Descrição |
|---|---|
| KEK | Key Encryption Key — 256-bit aleatório, único por usuário |
| Wrapping Key | Chave derivada de auth_hash + salt via Argon2id, usada para AES-KW-256 |
| Auth Hash | Hash da senha do usuário (Argon2id crypto_pwhash_str) |
| Secret | Par (private_key, public_key) criptografado com AES-GCM-256 usando o KEK |
Usuário registra → senha hasheada (Argon2id) + KEK gerado + wrapped + armazenado
│
Usuário armazena segredo → wrapping_key (cache) → AES-KW unwrap → AES-GCM encrypt → KEK zerado
│
Usuário lê segredo → wrapping_key (cache) → AES-KW unwrap → AES-GCM decrypt → KEK zerado
│
Usuário lista segredos → callback por secret (sem decrypt)
│
Usuário deleta segredo → busca + remove da tabela secrets
│
Usuário deleta conta → verifica senha → CASCADE (KEK + segredos removidos)
│
Usuário troca senha → re-hash → re-wrap KEK (mesmo salt) → COMMIT atômico
│
KEK expira (90d) → rotação: decrypt tudo com KEK velho → encrypt com KEK novo → COMMIT
│
Backup → AES-256-GCM + HMAC-SHA256 → arquivo .bak portável
│
Export → JSON/CSV metadata (PII redactable) → streaming via callback
│
Schema Migration → PRAGMA user_version → migrações sequenciais atômicas
| Componente | Função |
|---|---|
| C++17 | Linguagem |
| OpenSSL | AES-KW-256, AES-GCM-256 |
| libsodium | Argon2id (hashing + KDF), random_bytes |
| SQLite3 / SQLCipher | Persistência (SQLCipher: encrypt at rest; SQLite3 puro: dev) |
| ncursesw | TUI (Terminal User Interface) |
| CMake | Build system |
| Google Test | Testes unitários e integração |
# Debian / Ubuntu (produção — SQLCipher real)
apt install libsqlcipher-dev libsodium-dev libssl-dev libncursesw5-dev \
cmake pkg-config build-essential
# Termux (dev — SQLite3 puro, sem encrypt-at-rest)
pkg install libsodium openssl sqlite ncursesw cmake ninjacmake -B build
cmake --build build
ctest --test-dir build --output-on-failurecmake -B build-release -DCMAKE_BUILD_TYPE=Release
cmake --build build-release
# Visibility hidden automático em Release (SSM_VISIBILITY_HIDDEN=ON)# Testes completos (182)
ctest --test-dir build --output-on-failure
# Sanitizers (ASan+UBSan)
cmake -B build-san -DSSM_SANITIZE=ON && cmake --build build-san
./build-san/tests/ssm_test
# Fuzzing (clang)
cmake -B build-fuzz -DCMAKE_C_COMPILER=clang -DSSM_FUZZING=ON && cmake --build build-fuzz
./build-fuzz/tests/fuzz_api -max_total_time=10 -runs=100000
# Benchmarks
cmake -B build-bench -DSSM_BUILD_BENCHMARKS=ON -DCMAKE_BUILD_TYPE=Release && cmake --build build-bench
./build-bench/tests/ssm_bench --benchmark_min_time=0.1cmake -B build
cmake --build build
cmake --install build --prefix /usr/local
# Produz: lib/libssm.so.0.3.0, include/ssm/ssm.h, lib/cmake/ssm/ssmConfig.cmakeConsumir via CMake:
find_package(ssm REQUIRED)
target_link_libraries(meu_app PRIVATE ssm::ssm)MIT License
Copyright (c) 2026 Vaultine
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# clang-format
find src/ include/ tests/ -name '*.cc' -o -name '*.h' | xargs clang-format -i
# clang-tidy (requer compile_commands.json)
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
run-clang-tidy -p build src/O Vaultine inclui uma interface ncurses interativa via ssm-cli tui:
┌───────────────────────────────────────────────┐
│ VAULTINE — Terminal Interface │
├───────────────────────────────────────────────┤
│ │
│ ╔═══════════════════╗ │
│ ║ MENU PRINCIPAL ║ │
│ ╚═══════════════════╝ │
│ │
│ [1] User Management │
│ [2] Secret Management │
│ [3] KEK Rotation │
│ [4] Database Info │
│ [5] Cache Statistics │
│ [6] Exit │
│ │
├───────────────────────────────────────────────┤
│ DB: ./ssm.db │ arrows + Enter to select │
└───────────────────────────────────────────────┘
| Tecla | Ação |
|---|---|
↑ ↓ |
Navegar entre itens |
Enter |
Selecionar |
Esc |
Voltar / Cancelar |
1-6 |
Atalho numérico |
q |
Sair |
Telas: Register, Authenticate, Delete, Change Password, Secret Store/Get/Delete/List (scrollável), KEK Rotation, Database Info, Cache Statistics.
ssm-cli --help| Comando | Descrição |
|---|---|
user register/auth/delete/change-password |
Gerenciamento de usuários |
secret store/get/delete/list |
Gerenciamento de segredos |
kek rotate |
Rotação manual de KEK |
db version |
Mostra versão do schema SQLite |
db migrate |
Aplica migrações pendentes |
backup create/restore <file> |
Backup/restore criptografado |
export [--format json|csv] [--redact-pii] |
Exporta metadados para stdout |
server start [--port] [--host] [--daemonize] |
REST API server (libevent evhttp) |
cache-stats |
Estatísticas do cache de wrapping keys |
audit-log <username> |
Consulta log de auditoria |
env exec <username> <cmd> |
Injeta segredos como env vars |
tui |
Interface ncurses interativa |
completion [bash|zsh] |
Gera script de autocomplete para o shell |
ssm-cli tui # inicia a interface
ssm-cli --db /path/db tui # com banco personalizado
ssm-cli backup create vaultine.bak --backup-key <hex64>
ssm-cli export --format json --redact-pii
ssm-cli db versionO ssm-cli carrega automaticamente um arquivo de configuração JSON na inicialização.
As flags CLI têm precedência sobre valores do config.
| Caminho | Descrição |
|---|---|
./vaultine.json |
Config por projeto (junto ao banco de dados) |
~/.vaultinerc |
Config global do usuário |
{
"db": "./ssm.db",
"db_key": "0123456789abcdef...",
"password": "minha-senha",
"backup_key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"json": false
}| Chave | Tipo | Descrição |
|---|---|---|
db |
string | Caminho do SQLite (default: ./ssm.db) |
db_key |
string | Chave hex SQLCipher (opcional) |
password |
string | Senha para todas operações |
backup_key |
string | Chave hex 64 chars para backup/restore |
json |
bool | Habilita saída JSON (default: false) |
# ssm-cli usa ./vaultine.json automaticamente
$ cat vaultine.json
{"db": "./vaultine.db", "password": "admin123"}
$ ssm-cli user register alice # usa db + password do config
OK: user 'alice' registered
$ ssm-cli --db ./other.db ... # flag CLI sobrescreve configAs seguintes variáveis de ambiente podem substituir valores do config file e são sobrescritas por flags CLI. Preferíveis a --password/--db-key/--backup-key na linha de comando pois não aparecem em ps.
| Variável | Valor esperado | Descrição |
|---|---|---|
SSM_PASSWORD |
string | Senha para autenticação do usuário |
SSM_DB_KEY |
hex (até 64 chars) | Chave de criptografia do SQLCipher |
SSM_BACKUP_KEY |
hex (64 chars) | Chave de 32 bytes para backup/restore |
SSM_API_KEY |
string | API key para REST server |
Ordem de precedência: CLI flags > Env vars > Config file
O ssm-cli suporta autocomplete para bash e zsh:
# Bash — adicione ao ~/.bashrc
source <(ssm-cli completion bash)
# Zsh — adicione ao ~/.zshrc
source <(ssm-cli completion zsh)Ou copie o script permanente:
# Bash
ssm-cli completion bash > /etc/bash_completion.d/ssm-cli
# Zsh
ssm-cli completion zsh > /usr/local/share/zsh/site-functions/_ssm-cliScripts prontos também em scripts/completion/.
O Vaultine inclui um servidor HTTP REST via ssm-cli server start, implementado com libevent evhttp + jsoncpp. Todas as respostas são JSON com Content-Type: application/json.
Autenticação via X-API-Key header. Configure com --api-key flag ou SSM_API_KEY env var. Se nenhuma key for configurada, o servidor aceita todas as requisições (compatibilidade com versões anteriores).
ssm-cli server start # localhost:8080, sem auth
ssm-cli server start --api-key "tok3n" # exige X-API-Key: tok3n
ssm-cli server start --port 9090 # porta customizada
ssm-cli server start --host 0.0.0.0 # todas as interfaces
ssm-cli server start --daemonize # background
ssm-cli server start --daemonize --pidfile /run/ssm-cli.pid| Método | Rota | Descrição |
|---|---|---|
GET |
/v1/health |
Health check |
GET |
/v1/version |
Versão da API |
POST |
/v1/users/<username>/register |
Registrar usuário |
POST |
/v1/users/<username>/auth |
Autenticar usuário |
DELETE |
/v1/users/<username> |
Deletar usuário |
PUT |
/v1/users/<username>/password |
Trocar senha |
GET |
/v1/users/<username>/secrets |
Listar segredos |
POST |
/v1/users/<username>/secrets |
Armazenar segredo |
GET |
/v1/users/<username>/secrets/<name> |
Recuperar segredo |
DELETE |
/v1/users/<username>/secrets/<name> |
Deletar segredo |
POST |
/v1/users/<username>/kek/rotate |
Rotacionar KEK |
POST |
/v1/backup/create |
Criar backup |
POST |
/v1/backup/restore |
Restaurar backup |
GET |
/v1/audit |
Log de auditoria |
GET |
/v1/export |
Exportar metadados |
GET |
/v1/cache/stats |
Estatísticas do cache |
GET |
/v1/db/version |
Versão do schema |
POST |
/v1/db/migrate |
Migrar schema |
# Iniciar servidor com API key
ssm-cli server start --port 8080 --api-key "tok3n" &
# Registrar usuário
curl -s -X POST -H "Content-Type: application/json" \
-H "X-API-Key: tok3n" \
-d '{"password":"minha-senha"}' \
http://127.0.0.1:8080/v1/users/alice/register
# Armazenar segredo
curl -s -X POST -H "Content-Type: application/json" \
-H "X-API-Key: tok3n" \
-d '{"name":"minha-chave","private_key":"deadbeef01020304"}' \
http://127.0.0.1:8080/v1/users/alice/secrets
# Recuperar segredo
curl -s -H "X-API-Key: tok3n" \
http://127.0.0.1:8080/v1/users/alice/secrets/minha-chaveOs endpoints /v1/audit e /v1/export usam cabeçalhos HTTP para filtros:
curl -s -H "X-Audit-Username: alice" \
-H "X-Audit-Operation: secret_store" \
-H "X-Audit-Limit: 50" \
http://127.0.0.1:8080/v1/audit
curl -s -H "X-Export-Format: csv" \
-H "X-Export-Redact: true" \
http://127.0.0.1:8080/v1/export#include <ssm/ssm.h>typedef struct ssm_handle ssm_handle;
typedef enum {
SSM_OK = 0,
SSM_ERR_AUTH = 1, // usuário não encontrado ou senha incorreta
SSM_ERR_NOT_FOUND = 2, // segredo não encontrado
SSM_ERR_EXPIRED = 3, // KEK expirado
SSM_ERR_INTEGRITY = 4, // AES-GCM tag mismatch (dados corrompidos)
SSM_ERR_INTERNAL = 5 // erro interno (DB, crypto, OOM)
} ssm_status;
// Callback para ssm_secret_list: recebe metadados de cada segredo
typedef void (*ssm_secret_list_cb)(const char* name, const char* description,
const char* updated_at, size_t public_key_len,
void* user_data);ssm_status ssm_init(ssm_handle** out, const char* db_path,
const unsigned char* db_key, size_t db_key_len);
ssm_status ssm_destroy(ssm_handle* h);| Parâmetro | Descrição |
|---|---|
db_path |
Caminho do arquivo SQLite (:memory: para memória) |
db_key |
Chave SQLCipher (opcional; NULL = sem encrypt) |
ssm_status ssm_user_register(ssm_handle* h, const char* username,
const char* password);
ssm_status ssm_user_authenticate(ssm_handle* h, const char* username,
const char* password, int* is_valid);ssm_user_register: hashea a senha (Argon2id), cria usuário, gera KEK, wrapped + armazenado.ssm_user_authenticate: verifica senha contra hash armazenado.is_valid=1se correta.
ssm_status ssm_user_delete(ssm_handle* h, const char* username, const char* password);Remove permanentemente o usuário e todos os seus dados (KEK + segredos via ON DELETE CASCADE). Requer senha para confirmação.
ssm_status ssm_user_change_password(ssm_handle* h, const char* username,
const char* old_password, const char* new_password);Troca a senha do usuário. Atomicidade via BEGIN IMMEDIATE / COMMIT:
- Verifica senha antiga.
- Hash da nova senha (Argon2id).
- UPDATE
users.password_hash. - Unwrap KEK com wrapping key antiga.
- Re-wrap KEK com nova wrapping key (derivada do novo hash + mesmo salt).
- COMMIT (ou ROLLBACK em qualquer falha).
O KEK permanece o mesmo — segredos não são re-criptografados.
ssm_status ssm_secret_store(ssm_handle* h, const char* username,
const unsigned char* private_key, size_t private_key_len,
const unsigned char* public_key, size_t public_key_len,
const char* name, const char* description);
ssm_status ssm_secret_get(ssm_handle* h, const char* username,
const char* name,
unsigned char* private_key_out, size_t* private_key_len_out,
unsigned char* public_key_out, size_t* public_key_len_out);
ssm_status ssm_secret_delete(ssm_handle* h, const char* username,
const char* name);
ssm_status ssm_secret_list(ssm_handle* h, const char* username,
ssm_secret_list_cb callback, void* user_data);private_key: sempre criptografada com AES-GCM-256 usando o KEK do usuário.public_key: armazenada plaintext (chaves públicas são públicas).name: identificador único do segredo dentro do escopo do usuário.ssm_secret_list: enumera segredos via callback com(name, description, updated_at, public_key_len). Verifica expiração do KEK antes de listar.
ssm_status ssm_kek_rotate(ssm_handle* h, const char* username);Gera novo KEK, descriptografa todos os segredos com KEK antigo em memória, re-criptografa com KEK novo. Tudo dentro de uma transação ACID única — se qualquer passo falhar, ROLLBACK total.
ssm_status ssm_backup_create(ssm_handle* h, const char* backup_path,
const unsigned char* backup_key, size_t backup_key_len);
ssm_status ssm_backup_restore(ssm_handle* h, const char* backup_path,
const unsigned char* backup_key, size_t backup_key_len);Backup do banco completo com AES-256-GCM + HMAC-SHA256:
backup_key: 32 bytes de chave (hex 64 chars no CLI)- Formato portável entre máquinas
- Restore valida HMAC + tag GCM antes de substituir o DB
typedef enum {
SSM_EXPORT_JSON = 0,
SSM_EXPORT_CSV = 1
} ssm_export_format;
typedef void (*ssm_export_cb)(const char* chunk, size_t len, void* user_data);
ssm_status ssm_export(ssm_handle* h, ssm_export_format format, int redact_pii,
ssm_export_cb callback, void* user_data);Exporta metadados (NÃO segredos) via streaming callback:
- JSON ou CSV
redact_pii=1substitui usernames por IDs anônimos- Inclui: usuários, segredos (nomes/tamanhos), metadados KEK
ssm_status ssm_db_version(ssm_handle* h, int* version_out);
ssm_status ssm_db_migrate(ssm_handle* h);ssm_db_version: lêPRAGMA user_versiondo SQLitessm_db_migrate: aplica migrações pendentes automaticamente- Schema v1: schema inicial (4 tabelas). Schema v2: + índice
idx_secrets_user_id
#include <ssm/ssm.h>
#include <stdio.h>
#include <string.h>
int main(void) {
ssm_handle* h = NULL;
// init com banco em memória (sem criptografia em repouso)
if (ssm_init(&h, ":memory:", NULL, 0) != SSM_OK) {
fprintf(stderr, "falha ao inicializar\n");
return 1;
}
// registrar usuário
if (ssm_user_register(h, "alice", "p@ssw0rd") != SSM_OK) {
fprintf(stderr, "falha ao registrar\n");
ssm_destroy(h);
return 1;
}
// autenticar
int valido = 0;
ssm_user_authenticate(h, "alice", "p@ssw0rd", &valido);
printf("alice autenticada: %s\n", valido ? "sim" : "não");
ssm_destroy(h);
return 0;
}#include <ssm/ssm.h>
#include <stdio.h>
#include <string.h>
int main(void) {
ssm_handle* h = NULL;
ssm_init(&h, ":memory:", NULL, 0);
ssm_user_register(h, "bob", "secret");
// --- armazenar ---
unsigned char priv[] = "minha-chave-privada-32bytes-aqui!";
unsigned char pub[] = "chave-publica-aqui!";
if (ssm_secret_store(h, "bob",
priv, sizeof(priv),
pub, sizeof(pub),
"minha-chave",
"chave RSA para projeto X") == SSM_OK)
printf("segredo armazenado\n");
// --- recuperar ---
unsigned char priv_out[64];
size_t priv_len = sizeof(priv_out);
unsigned char pub_out[64];
size_t pub_len = sizeof(pub_out);
if (ssm_secret_get(h, "bob", "minha-chave",
priv_out, &priv_len,
pub_out, &pub_len) == SSM_OK) {
printf("private_key (%zu bytes) recuperada\n", priv_len);
printf("public_key (%zu bytes) recuperada\n", pub_len);
}
// --- deletar ---
ssm_secret_delete(h, "bob", "minha-chave");
ssm_destroy(h);
return 0;
}#include <ssm/ssm.h>
#include <stdio.h>
int main(void) {
ssm_handle* h = NULL;
ssm_init(&h, ":memory:", NULL, 0);
ssm_user_register(h, "carol", "mypass");
// armazena alguns segredos...
unsigned char k1[] = "segredo-importante-1";
unsigned char k2[] = "segredo-importante-2";
ssm_secret_store(h, "carol", k1, sizeof(k1), NULL, 0, "key1", NULL);
ssm_secret_store(h, "carol", k2, sizeof(k2), NULL, 0, "key2", NULL);
// força rotação do KEK (normalmente automática a cada 90 dias)
if (ssm_kek_rotate(h, "carol") == SSM_OK)
printf("KEK rotacionado com sucesso\n");
// segundos continuam acessíveis com o novo KEK
unsigned char out[64];
size_t len = sizeof(out);
ssm_secret_get(h, "carol", "key1", out, &len, NULL, NULL);
ssm_destroy(h);
return 0;
}#include <ssm/ssm.h>
#include <stdio.h>
int main(void) {
ssm_handle* h = NULL;
ssm_init(&h, ":memory:", NULL, 0);
ssm_user_register(h, "dave", "old-password");
// troca a senha
if (ssm_user_change_password(h, "dave", "old-password", "new-password") == SSM_OK)
printf("senha alterada\n");
// segredos continuam acessíveis com a nova senha
int valido = 0;
ssm_user_authenticate(h, "dave", "new-password", &valido);
printf("autenticado: %s\n", valido ? "sim" : "não");
ssm_destroy(h);
return 0;
}#include <ssm/ssm.h>
#include <stdio.h>
void list_cb(const char* name, const char* desc, const char* updated,
size_t pub_len, void* user_data) {
int* count = (int*)user_data;
printf(" %d. %s — %s (pub: %zu bytes, atualizado: %s)\n",
++(*count), name, desc ? desc : "(sem descrição)",
pub_len, updated);
}
int main(void) {
ssm_handle* h = NULL;
ssm_init(&h, ":memory:", NULL, 0);
ssm_user_register(h, "eve", "pass");
ssm_secret_store(h, "eve", (const unsigned char*)"k1", 2, NULL, 0, "key1", "primeira");
ssm_secret_store(h, "eve", (const unsigned char*)"k2", 2, NULL, 0, "key2", "segunda");
int count = 0;
printf("Segredos de eve:\n");
ssm_secret_list(h, "eve", list_cb, &count);
printf("Total: %d\n", count);
ssm_destroy(h);
return 0;
}#include <ssm/ssm.h>
#include <stdio.h>
int main(void) {
ssm_handle* h = NULL;
ssm_init(&h, "vaultine.db", NULL, 0);
ssm_user_register(h, "alice", "p@ss");
// backup key de 32 bytes
unsigned char key[32] = "meu-backup-key-32bytes-aqui!";
if (ssm_backup_create(h, "vaultine.bak", key, sizeof(key)) == SSM_OK)
printf("backup criado\n");
ssm_destroy(h);
// restore em outro handle
ssm_handle* h2 = NULL;
ssm_init(&h2, "vaultine-restored.db", NULL, 0);
ssm_backup_restore(h2, "vaultine.bak", key, sizeof(key));
int valido = 0;
ssm_user_authenticate(h2, "alice", "p@ss", &valido);
printf("alice acessível após restore: %s\n", valido ? "sim" : "não");
ssm_destroy(h2);
return 0;
}void export_cb(const char* chunk, size_t len, void* user) {
fwrite(chunk, 1, len, (FILE*)user);
}
int main(void) {
ssm_handle* h = NULL;
ssm_init(&h, ":memory:", NULL, 0);
ssm_user_register(h, "bob", "pass");
// JSON para stdout (PII preservado)
ssm_export(h, SSM_EXPORT_JSON, 0, export_cb, stdout);
// CSV para stdout (PII redactado)
// ssm_export(h, SSM_EXPORT_CSV, 1, export_cb, stdout);
ssm_destroy(h);
return 0;
}#include <ssm/ssm.h>
#include <stdio.h>
int main(void) {
ssm_handle* h = NULL;
ssm_init(&h, ":memory:", NULL, 0);
ssm_user_register(h, "frank", "p@ss");
// deleta conta (requer senha)
if (ssm_user_delete(h, "frank", "p@ss") == SSM_OK)
printf("usuário removido (KEK + segredos também)\n");
ssm_destroy(h);
return 0;
}ssm_status status = ssm_secret_list(h, "unknown-user", list_cb, NULL);
switch (status) {
case SSM_OK: break;
case SSM_ERR_AUTH: printf("usuário não encontrado ou senha incorreta\n"); break;
case SSM_ERR_NOT_FOUND: printf("segredo não existe\n"); break;
case SSM_ERR_EXPIRED: printf("KEK expirado — rode ssm_kek_rotate\n"); break;
case SSM_ERR_INTEGRITY: printf("dados corrompidos ou KEK incorreto\n"); break;
case SSM_ERR_INTERNAL: printf("erro interno\n"); break;
}
// Converter enum para string legível:
printf("status: %s\n", ssm_status_to_string(status)); // "SSM_ERR_AUTH"Senha do Usuário
│
▼
┌─────────────────────────────────────┐
│ Argon2id (crypto_pwhash_str) │
│ → password_hash (string ~128B) │ → usado para autenticação
└─────────────────────────────────────┘
│
│ password_hash + salt (16B)
▼
┌─────────────────────────────────────┐
│ Argon2id (crypto_pwhash, raw 32B) │
│ → wrapping_key │
└─────────────────────────────────────┘
│
│ AES-KW-256 (wrap)
▼
┌─────────────────────────────────────┐
│ KEK (256-bit aleatório) │
│ → wrapped_kek armazenado no DB │ → descriptografado só em RAM
└─────────────────────────────────────┘
│
│ AES-GCM-256 (encrypt)
▼
┌─────────────────────────────────────┐
│ Segredos do usuário │
│ (private_key criptografada, │
│ public_key plaintext) │
└─────────────────────────────────────┘
| Etapa | Algoritmo | Detalhe |
|---|---|---|
| Hash de senha | Argon2id crypto_pwhash_str |
~128 bytes, parâmetros + salt embutidos |
| Derivação wrapping key | Argon2id crypto_pwhash |
auth_hash + salt → 32 bytes, opslimit=MODERATE |
| Wrap KEK | AES-KW-256 | EVP_aes_256_wrap, padding de 8 bytes |
| Criptografia de segredos | AES-GCM-256 | AEAD: nonce 12B, tag 16B |
| Geração aleatória | randombytes_buf (libsodium) |
KEK, salts, nonces |
stateDiagram-v2
[*] --> Válida : usuário registrado
Válida --> Expirada : 90 dias sem rotação
Válida --> Rotacionando : ssm_kek_rotate
Rotacionando --> Válida : wrap + re-encrypt + commit
Expirada --> Rotacionando : ssm_kek_rotate (forçado)
Rotacionando --> Expirada : rollback
- Padrão: 90 dias (
KEK_DEFAULT_DAYS = 90), configurável por tenant. - Toda operação (
ssm_secret_store,ssm_secret_get) verificakek_is_expired. - Se expirado, retorna
SSM_ERR_EXPIRED. O usuário deve forçarssm_kek_rotate.
sequenceDiagram
participant App
participant Vaultine
participant SQLite
App->>Vaultine: ssm_kek_rotate(user)
Vaultine->>SQLite: BEGIN IMMEDIATE (lock exclusivo)
Vaultine->>SQLite: SELECT wrapped_kek, salt
Vaultine->>Vaultine: derivar wrapping_key (auth_hash + salt_velho)
Vaultine->>Vaultine: AES-KW unwrap → KEK atual
Vaultine->>SQLite: SELECT * FROM secrets WHERE user_id = ?
loop Para cada segredo
Vaultine->>Vaultine: AES-GCM decrypt (KEK velho)
Vaultine->>Vaultine: gerar novo nonce
Vaultine->>Vaultine: AES-GCM encrypt (KEK novo + nonce)
Vaultine->>SQLite: UPDATE secrets
end
Vaultine->>Vaultine: gerar novo salt
Vaultine->>Vaultine: derivar wrapping_key (auth_hash + salt_novo)
Vaultine->>Vaultine: AES-KW wrap (KEK novo)
Vaultine->>Vaultine: calcular expires_at (now + 90d)
Vaultine->>SQLite: UPDATE kek_metadata
Vaultine->>SQLite: COMMIT
alt Qualquer passo falha
Vaultine->>SQLite: ROLLBACK
Vaultine-->>App: SSM_ERR_INTERNAL
else Sucesso
Vaultine-->>App: SSM_OK
end
- KEK nunca persiste em disco sem wrap.
- Wrapping_key derivada de KDF lento (Argon2id MODERATE) — resistente a brute-force mesmo se o
password_hashvazar. memset_s/secure_eraseem buffers sensíveis após uso.- Transação ACID única garante atomicidade da rotação.
kek_version(incrementado a cada rotação) protege contra rollback dekek_metadata.
SQLITE_OPEN_FULLMUTEX: SQLite serializa acesso interno.std::shared_mutexnossm_handle: protege operações multi-step (especialmente rotação).- Todas as funções da API pública adquirem
unique_lockno início.
ssm_status ssm_set_password_validator(ssm_password_validator validator, void* user_data);Registre um callback para validar senhas antes de ssm_user_register e ssm_user_change_password:
// Validador customizado: exige pelo menos 8 caracteres e um '!'
ssm_status my_validator(const char* pw, void*) {
return (strlen(pw) >= 8 && strchr(pw, '!')) ? SSM_OK : SSM_ERR_INTERNAL;
}
ssm_set_password_validator(my_validator, NULL);- Default: mínimo 4 caracteres
NULLrestaura o validador default
void* secure_alloc(size_t size); // malloc + mlock
void secure_free(void* ptr, size_t size); // secure_erase + munlock + free
template <typename T> class secure_buffer; // RAII wrapper
template <typename T> class secure_vector; // RAII vectorPrevine que chaves criptográficas sejam swappadas para disco:
// KEK nunca será paginada para swap
auto buf = secure_buffer<unsigned char>(32); // mlocado
crypto_function(buf.data(), buf.size());
// destructor: secure_erase + munlock + freetypedef struct {
size_t total_entries; // SSM_CACHE_MAX = 256
size_t valid_entries;
size_t hit_count;
size_t miss_count;
} ssm_cache_stats;
ssm_status ssm_cache_get_stats(ssm_handle* h, ssm_cache_stats* out);ssm-cli cache-stats
# Cache Statistics:
# Total slots: 256
# Valid entries: 1
# Hits: 5
# Misses: 1
# Hit rate: 83.3%Cache LRU de 256 entradas no ssm_handle que armazena chaves de wrapping
(derivadas via Argon2id — a etapa mais cara de cada operação) para evitar
o KDF em operações repetidas no mesmo tenant:
| Operação | Sem cache | Com cache |
|---|---|---|
ssm_secret_store (primeira chamada) |
~150ms (KDF + AES-KW + AES-GCM) | ~150ms |
ssm_secret_store (mesmo tenant, após primeira) |
~150ms | ~1µs (só AES-KW + AES-GCM) |
- Cache é invalidado em
ssm_kek_rotate(salt muda → wrapping key muda). - Cache é invalidado em
ssm_user_change_password(auth_hash muda → wrapping key muda). - Cache é zerado com
secure_eraseemssm_destroy. - NÃO armazena o KEK — apenas a wrapping key derivada.
Tabela audit_log registra todas as operações da API com timestamp, user_id,
operação, resultado, alvo e detalhes. Consultável diretamente via SQLite para auditoria forense.
| Coluna | Tipo | Descrição |
|---|---|---|
id |
INTEGER | PK AUTOINCREMENT |
user_id |
INTEGER | FK → users(id) (0 se não autenticado) |
username |
TEXT | Nome do usuário no momento da operação |
operation |
TEXT | Nome da operação (ex: user_register, secret_get) |
operation_target |
TEXT | Alvo da operação (ex: nome do secret) |
details |
TEXT | Detalhes contextuais ("password mismatch", "KEK expired") |
result |
TEXT | Resultado (SSM_OK, SSM_ERR_AUTH, etc.) |
timestamp |
TEXT | ISO 8601 UTC |
| Coluna | Tipo | Restrição |
|---|---|---|
id |
INTEGER | PK AUTOINCREMENT |
username |
TEXT | UNIQUE NOT NULL |
password_hash |
BLOB | NOT NULL (Argon2id string) |
created_at |
TEXT | DEFAULT CURRENT_TIMESTAMP |
| Coluna | Tipo | Restrição |
|---|---|---|
id |
INTEGER | PK AUTOINCREMENT |
user_id |
INTEGER | FK → users(id) ON DELETE CASCADE |
wrapped_kek |
BLOB | NOT NULL (AES-KW-256, 40 bytes) |
salt |
BLOB | NOT NULL (16 bytes) |
expires_at |
TEXT | NOT NULL (ISO 8601 UTC) |
kek_version |
INTEGER | NOT NULL DEFAULT 1 (incrementado na rotação) |
UNIQUE(user_id) |
| Coluna | Tipo | Restrição |
|---|---|---|
id |
INTEGER | PK AUTOINCREMENT |
user_id |
INTEGER | FK → users(id) ON DELETE CASCADE |
name |
TEXT | |
private_key |
BLOB | NOT NULL (AES-GCM-256) |
public_key |
BLOB | NULLABLE (plaintext) |
nonce |
BLOB | NOT NULL (12 bytes) |
tag |
BLOB | NOT NULL (16 bytes, GCM auth tag) |
description |
TEXT | NULLABLE |
updated_at |
TEXT | NOT NULL DEFAULT CURRENT_TIMESTAMP |
A API pública do Vaultine é escrita em C puro com extern "C" e SSM_EXPORT,
o que permite chamar a biblioteca de praticamente qualquer linguagem via FFI
(Foreign Function Interface).
./build/libssm.so
│
├── Python (ctypes / cffi)
├── Rust (extern "C")
├── Go (cgo)
├── C# (DllImport + LibraryImport)
├── Java (JNI / JNR-FFI)
├── Node.js (node-ffi / napi-rs)
└── Zig / Nim / Julia / ... (qualquer FFI C)
| Aspecto | Detalhe |
|---|---|
| Linkage | extern "C" — sem name mangling C++ |
| Handle | ssm_handle* — ponteiro opaco (armazenar como void* ou usize) |
| Thread safety | std::shared_mutex interno — chamadas FFI seguras sem locks extra |
| Strings | const char* UTF-8 (cópia imediata se precisar retain) |
| Buffers | unsigned char* + size_t — caller aloca, Vaultine preenche |
| Callbacks | ssm_secret_list_cb — void(*)(const char*,..., void*) |
| Erros | Retorno ssm_status + ssm_status_to_string() |
| Quem aloca | Quem libera | O que |
|---|---|---|
| Caller | Caller | Buffer de saída de ssm_secret_get (private_key_out, public_key_out) |
| Caller | Caller | Struct ssm_handle* recebido de ssm_init (via ssm_destroy) |
| Vaultine | Vaultine | String retornada por ssm_status_to_string (memória estática — não fazer free) |
| Caller | — | Strings de entrada (username, password, name) — Vaultine copia internamente |
ssm_secret_get: Passe private_key_len_out com a capacidade do buffer. Se o buffer
for pequeno, a API retorna SSM_ERR_INTERNAL e preenche o tamanho necessário.
Estratégia recomendada: alocar 4096 bytes e tratar SSM_ERR_INTERNAL como "buffer pequeno".
Módulo completo em bindings/python/ssm.py:
import ctypes
lib = ctypes.CDLL("./build/libssm.so")
# Configurar tipos
lib.ssm_init.argtypes = [ctypes.POINTER(ctypes.c_void_p), ctypes.c_char_p,
ctypes.POINTER(ctypes.c_ubyte), ctypes.c_size_t]
lib.ssm_init.restype = ctypes.c_int
lib.ssm_destroy.argtypes = [ctypes.c_void_p]
lib.ssm_destroy.restype = ctypes.c_int
# Uso
handle = ctypes.c_void_p()
status = lib.ssm_init(ctypes.byref(handle), b":memory:", None, 0)
if status != 0:
raise RuntimeError(f"ssm_init falhou: {status}")
status = lib.ssm_user_register(handle, b"alice", b"p@ssw0rd")
# ...
lib.ssm_destroy(handle)Callback (ssm_secret_list):
CALLBACK = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_char_p,
ctypes.c_char_p, ctypes.c_size_t, ctypes.c_void_p)
def list_secrets(handle, username):
secrets = []
def cb(name, desc, updated, pub_len, user_data):
secrets.append((name.decode(), desc.decode() if desc else None))
cb_ptr = CALLBACK(cb)
lib.ssm_secret_list(handle, username.encode(), cb_ptr, None)
return secretsCrate completa em bindings/rust/vaultine-rs/:
use vaultine::Vaultine;
let v = Vaultine::new(":memory:", None)?;
v.user_register("alice", "p@ss")?;
let ok = v.user_authenticate("alice", "p@ss")?;
v.secret_store("alice", b"my-key", None, "k1", Some("test key"))?;
let list = v.secret_list("alice")?;
v.kek_rotate("alice")?;
// automatic ssm_destroy via DropVeja examples/basic.rs e src/lib.rs para API completa com SecretEntry, AuditEntry, CacheStats, export_metadata, db_version, backup_create/restore.
Build:
RUSTFLAGS="-L /path/to/build/src" cargo build
LD_LIBRARY_PATH="/path/to/build/src" cargo testPackage completo em bindings/go/vaultine/:
import "github.com/anomalyco/vaultine"
v, err := vaultine.New(":memory:", nil)
v.UserRegister("alice", "p@ss")
ok, _ := v.UserAuthenticate("alice", "p@ss")
v.SecretStore("alice", []byte("my-key"), nil, "k1", "test key")
priv, pub, _ := v.SecretGet("alice", "k1", 4096)
v.SecretList("alice", func(e vaultine.SecretEntry) { /* ... */ })
v.KEKRotate("alice")
v.Destroy()Veja examples/main.go e vaultine.go para API completa (AuditLogQuery, CacheStats, Export, DBVersion, DBMigrate, BackupCreate, BackupRestore).
Build:
CGO_CFLAGS="-I/path/to/include" CGO_LDFLAGS="-L/path/to/build/src -lssm" go build
LD_LIBRARY_PATH="/path/to/build/src" go testconst ffi = require('ffi-napi');
const ssm = ffi.Library('./build/libssm.so', {
'ssm_init': ['int', ['pointer', 'string', 'pointer', 'size_t']],
'ssm_destroy': ['int', ['pointer']],
'ssm_user_register': ['int', ['pointer', 'string', 'string']],
'ssm_user_authenticate': ['int', ['pointer', 'string', 'string', 'pointer']],
'ssm_secret_store': ['int', ['pointer', 'string', 'pointer', 'size_t',
'pointer', 'size_t', 'string', 'string']],
'ssm_secret_get': ['int', ['pointer', 'string', 'string',
'pointer', 'pointer', 'pointer', 'pointer']],
'ssm_secret_delete': ['int', ['pointer', 'string', 'string']],
'ssm_kek_rotate': ['int', ['pointer', 'string']],
'ssm_status_to_string': ['string', ['int']],
});
const ref = require('ref-napi');
const handle = ref.alloc(ref.refType(ref.types.void));
ssm.ssm_init(handle, ':memory:', null, 0);
const h = ref.deref(handle);
ssm.ssm_user_register(h, 'alice', 'p@ss');
console.log(ssm.ssm_status_to_string(ssm.ssm_secret_store(
h, 'alice', Buffer.from('my-key'), 6, null, 0, 'k1', null)));
ssm.ssm_destroy(h);