Erik-Castro/Vaultine
GitHub: Erik-Castro/Vaultine
Vaultine是一款用于POSIX系统的多租户加密密钥管理库。
Stars: 1 | Forks: 0
# Vaultine — Gestão de Segredos Multi-Tenant
` | 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 ` | Consulta log de auditoria |
| `env exec ` | 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
ssm-cli export --format json --redact-pii
ssm-cli db version
## Config File
O `ssm-cli` carrega automaticamente um arquivo de configuração JSON na inicialização.
As flags CLI têm precedência sobre valores do config.
### Arquivos (primeiro encontrado vence)
| Caminho | Descrição |
|---------|-----------|
| `./vaultine.json` | Config por projeto (junto ao banco de dados) |
| `~/.vaultinerc` | Config global do usuário |
### Formato
{
"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) |
### Exemplo
# 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 config
## Environment Variables
As 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
## Shell Completion
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-cli
Scripts prontos também em [`scripts/completion/`](scripts/completion/).
## REST API Server
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
### Endpoints
| Método | Rota | Descrição |
|--------|------|-----------|
| `GET` | `/v1/health` | Health check |
| `GET` | `/v1/version` | Versão da API |
| `POST` | `/v1/users//register` | Registrar usuário |
| `POST` | `/v1/users//auth` | Autenticar usuário |
| `DELETE` | `/v1/users/` | Deletar usuário |
| `PUT` | `/v1/users//password` | Trocar senha |
| `GET` | `/v1/users//secrets` | Listar segredos |
| `POST` | `/v1/users//secrets` | Armazenar segredo |
| `GET` | `/v1/users//secrets/` | Recuperar segredo |
| `DELETE` | `/v1/users//secrets/` | Deletar segredo |
| `POST` | `/v1/users//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 |
### Exemplo
# 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-chave
### Parâmetros de Query (Audit/Export)
Os 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
## API Pública
#include
### Tipos
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_init / ssm_destroy
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_user_register / ssm_user_authenticate
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=1` se correta.
### ssm_user_delete
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_user_change_password
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`:
1. Verifica senha antiga.
2. Hash da nova senha (Argon2id).
3. UPDATE `users.password_hash`.
4. Unwrap KEK com wrapping key antiga.
5. Re-wrap KEK com nova wrapping key (derivada do novo hash + mesmo salt).
6. COMMIT (ou ROLLBACK em qualquer falha).
O KEK permanece o mesmo — segredos **não** são re-criptografados.
### ssm_secret_store / ssm_secret_get / ssm_secret_delete / ssm_secret_list
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_kek_rotate
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_backup_create / ssm_backup_restore
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
### ssm_export
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=1` substitui usernames por IDs anônimos
- Inclui: usuários, segredos (nomes/tamanhos), metadados KEK
### ssm_db_version / ssm_db_migrate
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_version` do SQLite
- `ssm_db_migrate`: aplica migrações pendentes automaticamente
- Schema v1: schema inicial (4 tabelas). Schema v2: + índice `idx_secrets_user_id`
## Exemplos
### Inicialização e Registro
#include
#include
#include
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;
}
### Armazenar e Recuperar Segredo
#include
#include
#include
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;
}
### Rotação de KEK
#include
#include
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;
}
### Troca de Senha
#include
#include
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;
}
### Listar Segredos
#include
#include
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;
}
### Backup e Restore
#include
#include
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;
}
### Exportar Metadados
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;
}
### Deletar Usuário
#include
#include
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;
}
### Tratamento de Erros
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"
## Hierarquia Criptográfica
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 |
## Ciclo de Vida da KEK
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
### Validade
- Padrão: **90 dias** (`KEK_DEFAULT_DAYS = 90`), configurável por tenant.
- Toda operação (`ssm_secret_store`, `ssm_secret_get`) verifica `kek_is_expired`.
- Se expirado, retorna `SSM_ERR_EXPIRED`. O usuário deve forçar `ssm_kek_rotate`.
### Rotação (transação ACID)
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
### Segurança
- KEK **nunca** persiste em disco sem wrap.
- Wrapping_key derivada de KDF lento (Argon2id MODERATE) — resistente a brute-force mesmo se o `password_hash` vazar.
- `memset_s` / `secure_erase` em 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 de `kek_metadata`.
## Thread Safety
- `SQLITE_OPEN_FULLMUTEX`: SQLite serializa acesso interno.
- `std::shared_mutex` no `ssm_handle`: protege operações multi-step (especialmente rotação).
- Todas as funções da API pública adquirem `unique_lock` no início.
## Password Validation (v0.2.0)
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**
- `NULL` restaura o validador default
## Secure Memory (mlock) (v0.2.0)
void* secure_alloc(size_t size); // malloc + mlock
void secure_free(void* ptr, size_t size); // secure_erase + munlock + free
template class secure_buffer; // RAII wrapper
template class secure_vector; // RAII vector
Previne que chaves criptográficas sejam swappadas para disco:
// KEK nunca será paginada para swap
auto buf = secure_buffer(32); // mlocado
crypto_function(buf.data(), buf.size());
// destructor: secure_erase + munlock + free
## Cache Statistics (v0.2.0)
typedef 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%
## Audit Log
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_erase` em `ssm_destroy`.
- NÃO armazena o KEK — apenas a wrapping key derivada.
## Audit Log
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 |
## Schema SQLite
### `users`
| 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 |
### `kek_metadata`
| 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)` | | |
### `secrets`
| 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 |
## Bindings
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)
### Características FFI
| 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()` |
### Gerenciamento de Memória
| 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".
### Python (ctypes)
Módulo completo em [`bindings/python/ssm.py`](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 secrets
### Rust
Crate completa em [`bindings/rust/vaultine-rs/`](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 Drop
Veja [`examples/basic.rs`](bindings/rust/vaultine-rs/examples/basic.rs) e [`src/lib.rs`](bindings/rust/vaultine-rs/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 test
### Go (cgo)
Package completo em [`bindings/go/vaultine/`](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`](bindings/go/vaultine/examples/main.go) e [`vaultine.go`](bindings/go/vaultine/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 test
### Node.js (node-ffi)
const 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);
标签:AES-GCM-256, Bash脚本, C++, MIT许可, POSIX系统, Python绑定, SQLCipher, YAML, 加密, 原子操作, 命令行界面, 图形用户界面, 安全库, 安全测试工具, 密钥轮换, 开源, 数据擦除, 漏洞扫描器