
Segredo Simples e Seguro
# keeper
O Keeper é um armazenamento criptográfico de segredos para Go. Ele criptografa payloads arbitrários de bytes em repouso usando derivação de chave Argon2id e criptografia autenticada XChaCha20-Poly1305 (padrão), e os armazena em um banco de dados bbolt embutido.
Ele é fornecido como três coisas que você pode usar de forma independente:
- **Uma biblioteca Go** — incorpore um armazenamento de segredos reforçado diretamente no seu processo, com quatro níveis de segurança, isolamento de DEK por bucket e uma cadeia de auditoria à prova de violação.
- **Um manipulador HTTP** (`x/keephandler`) — monte endpoints do keeper em qualquer mux `net/http` em uma chamada, com hooks, guardas e codificadores de resposta plugáveis para controle de acesso e registro de auditoria.
- **Uma CLI** (`cmd/keeper`) — uma interface de terminal com sessão REPL persistente, entrada de segredos sem eco e zero exposição no histórico do shell.
O Keeper foi projetado como a camada fundamental de gerenciamento de segredos para o balanceador de carga [Agbero](https://github.com/agberohq/agbero), mas não possui dependência do Agbero e funciona em qualquer projeto Go.
---
## Conteúdo
- [Modelo de segurança](#security-model)
- [Design criptográfico](#cryptographic-design)
- [Hierarquia de chaves](#key-hierarchy)
- [Esquema de armazenamento](#storage-schema)
- [Cadeia de auditoria](#audit-chain)
- [Integração Jack](#jack-integration)
- [x/keepcmd — operações CLI reutilizáveis](#xkeepcmd)
- [x/keephandler — manipulador HTTP](#xkeephandler)
- [Referência da API](#api-reference)
- [Catálogo de erros](#error-catalogue)
- [Decisões de segurança](#security-decisions)
- [Dependências](#dependencies)
---
## Modelo de segurança
O Keeper particiona segredos em buckets. Cada bucket possui uma `BucketSecurityPolicy` imutável que governa como sua Chave de Criptografia de Dados (DEK) é protegida. Quatro níveis estão disponíveis.
### Esquemas vs. Níveis de Segurança
Um **esquema** é um prefixo URI que agrupa buckets relacionados (`vault://`, `certs://`, `space://`, ou qualquer nome que você registre). Um **nível de segurança** é uma propriedade da política do bucket definida na criação e imutável a partir de então.
Você pode misturar níveis de segurança livremente dentro do mesmo esquema. Por exemplo, `vault://system` pode ser `LevelPasswordOnly` (desbloqueado automaticamente na inicialização), enquanto `vault://admin` é `LevelAdminWrapped` (requer credencial explícita).
### LevelPasswordOnly
A DEK do bucket é derivada da chave mestre usando HKDF-SHA256 com uma string de informação separada por domínio por bucket (`keeper-bucket-dek-v1:scheme:namespace`). Todos os buckets `LevelPasswordOnly` são desbloqueados automaticamente quando `UnlockDatabase` é chamada com a frase-senha mestre correta. Nenhuma credencial por bucket é necessária em tempo de execução. Este nível é apropriado para segredos que o processo precisa na inicialização sem interação humana.
### LevelAdminWrapped
O bucket possui uma DEK de 32 bytes gerada aleatoriamente e única para aquele bucket. A DEK nunca é armazenada em texto claro. Para cada administrador autorizado, uma Chave de Criptografia de Chave (KEK) é derivada de `HKDF(masterKey‖adminCred, dekSalt)` e usada para envolver a DEK via XChaCha20-Poly1305. O bucket fica inacessível até que um administrador chame `UnlockBucket` com sua credencial. A frase-senha mestre sozinha não pode descriptografar o bucket. Revogar um administrador não afeta a cópia envolvida de nenhum outro administrador.
### LevelHSM
A DEK do bucket é gerada no momento do `CreateBucket` e imediatamente envolvida por um `HSMProvider` fornecido pelo chamador. O provedor realiza as operações de wrap e unwrap — o keeper nunca manipula a DEK bruta após entregá-la ao provedor. `UnlockDatabase` chama automaticamente o provedor para desembrulhar e semear o Envelope para todos os buckets HSM registrados. A rotação da chave mestre não recriptografa esses buckets; a DEK é controlada pelo provedor.
Uma implementação interna `SoftHSM` apoiada por uma chave de wrapping protegida por memguard está disponível em `pkg/hsm` para ambientes de teste e CI. Não a use em produção.
### LevelRemote
Idêntico ao `LevelHSM` no comportamento de gerenciamento de chaves, mas o `HSMProvider` é implementado por `pkg/remote.Provider` — um adaptador HTTPS configurável que delega wrap e unwrap para qualquer serviço KMS remoto sobre TLS. Configurações pré-construídas para HashiCorp Vault Transit, AWS KMS e GCP Cloud KMS são fornecidas em `pkg/remote`. Para uso em produção, configure `TLSClientCert` e `TLSClientKey` para habilitar autenticação TLS mútua.
---
## Design criptográfico
### Derivação da chave mestre```
salt ← random 32 bytes, generated once, stored as a versioned SaltStore (unencrypted)
masterKey ← Argon2id(passphrase, salt, t=3, m=64 MiB, p=4) → 32 bytes
```
Um hash de verificação é armazenado na primeira derivação:```
verifyHash ← Argon2id(masterKey, "verification", t=1, m=64 MiB, p=4) → 32 bytes
```
Chamadas subsequentes de `DeriveMaster` recalculam este hash e o comparam com `crypto/subtle.ConstantTimeCompare`. Uma incompatibilidade retorna `ErrInvalidPassphrase`.
O salt do KDF é armazenado **não criptografado** por design. Ele deve ser legível antes de `UnlockDatabase` para derivar a chave mestra — criptografá-lo com uma chave derivada da chave mestra seria circular. Um salt do KDF não é um segredo; seu propósito é exclusividade, não confidencialidade.
### Criptografia dos segredos
Cada valor de texto simples é criptografado com XChaCha20-Poly1305 usando a DEK do bucket:```
nonce ← random 24 bytes
ciphertext ← XChaCha20-Poly1305.Seal(nonce, DEK, plaintext)
```
O registro armazenado é uma struct `Secret` codificada em msgpack contendo o texto cifrado, metadados criptografados e a versão do esquema. A autenticação é implícita: um texto cifrado decifrado com a chave errada produz uma falha de autenticação AEAD antes que qualquer texto simples seja retornado.
### Derivação de KEK — LevelAdminWrapped```
salt ← random 32 bytes, generated at bucket creation, stored in policy
ikm ← masterKey ‖ adminCredential
KEK ← HKDF-SHA256(ikm, salt, info="keeper-kek-v1") → 32 bytes
wrappedDEK ← XChaCha20-Poly1305.Seal(nonce, KEK, DEK)
```
A KEK é derivada usando HKDF em vez de uma segunda passagem Argon2. A chave mestra
já foi produzida por um KDF de alto custo; uma segunda invocação Argon2 adicionaria
centenas de milissegundos de latência a cada chamada `UnlockBucket` sem
benefício de segurança. HKDF-SHA256 opera em aproximadamente um microssegundo.
**Defesa em profundidade:** Um atacante que compromete apenas o banco de dados obtém a
DEK encapsulada e o salt HKDF, mas não consegue derivar a KEK sem a chave mestra.
Um atacante que compromete apenas a chave mestra não consegue desencapsular nenhuma
DEK `LevelAdminWrapped` sem também conhecer a credencial de administrador.
### Criptografia de metadados — segredos
Os metadados secretos (hora de criação, hora de atualização, contagem de acesso, versão) são
criptografados separadamente do texto cifrado:```
metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes
encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))
```
Para os buckets `LevelAdminWrapped`, `LevelHSM` e `LevelRemote`, isso significa que os metadados ficam inacessíveis sem a credencial do bucket, impedindo que um invasor com acesso de leitura ao arquivo do banco de dados descubra padrões de acesso ou timestamps.
**Nota sobre canal lateral de temporização:** O XChaCha20-Poly1305 processa o texto cifrado completo antes de retornar um erro de autenticação. O caminho de descriptografia de fallback (nova DEK derivada → antiga chave-mestre-como-DEK) leva o mesmo tempo de relógio de parede, independentemente de qual chave for bem-sucedida. Nenhum canal lateral de temporização vaza o estado de migração de um registro.
### Criptografia de metadados — políticas, WAL e auditoria
Todos os metadados estruturais também são criptografados em repouso. Duas chaves são derivadas da chave mestre no momento de `UnlockDatabase`:```
policyEncKey ← HKDF-SHA256(masterKey, nil, info="keeper-policy-enc-v1") → 32 bytes
auditEncKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-enc-v1") → 32 bytes
```
`policyEncKey` criptografa: os valores de `BucketSecurityPolicy` e o WAL de rotação.
`auditEncKey` criptografa: os campos `Scheme`, `Namespace` e `Details` de
cada evento de auditoria.
Ambas as chaves são limpas da memória em `Lock()`. A cifra usada para criptografia
de metadados é a mesma interface `crypt.Cipher` configurável usada para segredos —
a escolha de cifra do usuário (AES-256-GCM para FIPS, XChaCha20-Poly1305 por padrão)
flui automaticamente.
Formato wire para todos os blobs de metadados criptografados:```
nonce (cipher.NonceSize() bytes) || AEAD-ciphertext
```
### Hashing da chave do bucket de política
As chaves de política no disco são hashes opacos em vez de strings de texto simples `scheme:namespace`, prevenindo a enumeração offline de nomes de bucket:```
base ← hex(SHA-256("scheme:namespace"))[:32] // 32 hex chars = 128-bit key space
_policies/<base> → encrypted BucketSecurityPolicy
_policies/<base>__hash__ → SHA-256(encrypted policy bytes)
_policies/<base>__hmac__ → HMAC-SHA256(policyKey, encrypted policy bytes)
```
O `schemeRegistry` em memória continua a usar `"scheme:namespace"` como sua
chave — apenas a representação em disco muda.
### Autenticação de política
Cada registro de política carrega duas tags de integridade escritas atomicamente em uma única transação bbolt.```
hash ← SHA-256(encryptedPolicyBytes) — unauthenticated, pre-unlock integrity
policyKey ← HKDF-SHA256(masterKey, nil, info="keeper-policy-hmac-v1") → 32 bytes
hmac ← HMAC-SHA256(policyKey, encryptedPolicyBytes) — authenticated, post-unlock integrity
```
Antes de `UnlockDatabase`, apenas o hash SHA-256 está disponível. Após o desbloqueio, `loadPolicy` verifica a tag HMAC. `UnlockDatabase` chama `upgradePolicyHMACs` para preencher retroativamente as tags HMAC nas políticas criadas antes da existência deste recurso.
### Auditoria de assinatura HMAC```
auditKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-hmac-v1") → 32 bytes
HMAC ← HMAC-SHA256(auditKey, event fields including Seq)
```
A chave de assinatura é ativada em `UnlockDatabase` e limpa em `Lock`. Quando a chave mestra é rotacionada, `Rotate` anexa um evento de checkpoint de rotação de chave a cada cadeia de auditoria ativa, assinado com a chave de auditoria antiga como o evento final da época antiga. O histórico nunca é reescrito; o checkpoint é a ponte de confiança entre as épocas.
---
## Hierarquia de chaves```
passphrase
│
└─ Argon2id(salt) ──→ masterKey (32 bytes, memguard Enclave)
│
├─ HKDF("keeper-audit-hmac-v1") ──→ auditKey (HMAC signing)
├─ HKDF("keeper-audit-enc-v1") ──→ auditEncKey (audit field encryption)
├─ HKDF("keeper-policy-hmac-v1") ──→ policyKey (policy HMAC)
├─ HKDF("keeper-policy-enc-v1") ──→ policyEncKey (policy/WAL encryption)
│
├─ [LevelPasswordOnly]
│ └─ HKDF("keeper-bucket-dek-v1:scheme:ns") ──→ DEK
│ └─ HKDF("keeper-metadata-v1") ──→ metaKey
│
├─ [LevelAdminWrapped]
│ ├─ random 32 bytes ──→ DEK
│ │ └─ HKDF("keeper-metadata-v1") ──→ metaKey
│ │
│ └─ HKDF("keeper-kek-v1", masterKey‖adminCred, dekSalt)
│ └─ KEK
│ └─ XChaCha20-Poly1305(KEK, DEK) ──→ wrappedDEK
│
└─ [LevelHSM / LevelRemote]
├─ random 32 bytes ──→ DEK
│ └─ HKDF("keeper-metadata-v1") ──→ metaKey
│
└─ HSMProvider.WrapDEK(DEK) ──→ wrappedDEK
(stored; provider controls the wrapping key)
```
Todas as chaves intermediárias são zeradas imediatamente após o uso. A chave mestre nunca é gravada no disco de nenhuma forma.
---
## Esquema de armazenamento
O banco de dados subjacente é o bbolt. Todos os buckets e seus conteúdos:
| bbolt bucket | Chave | Valor |
|---|---|---|
| `__meta__` | `salt` | msgpack — SaltStore (não criptografado; dependência circular se criptografado) |
| `__meta__` | `verify` | bytes brutos — hash de verificação Argon2id |
| `__meta__` | `rotation_wal` | `nonce‖AEAD(msgpack(RotationWAL))` |
| `__meta__` | `bucket_dek_done` | `"1"` — marcador de conclusão de migração DEK |
| `__policies__` | `hex(SHA-256(scheme:ns))[:32]` | `nonce‖AEAD(msgpack(BucketSecurityPolicy))` |
| `__policies__` | `<base>__hash__` | hex SHA-256 dos bytes da política criptografada |
| `__policies__` | `<base>__hmac__` | hex HMAC-SHA256(policyKey, bytes da política criptografada) |
| `__audit__/scheme/namespace` | UUID do evento | JSON — Event de auditoria |
| `__audit__/scheme/namespace` | `__chain_index__` | JSON — chainIndex |
| `scheme/namespace` | string de chave | msgpack — struct Secret |
### Estrutura Secret (msgpack)```go
type Secret struct {
Ciphertext []byte `msgpack:"ct"`
EncryptedMeta []byte `msgpack:"em,omitempty"`
SchemaVersion int `msgpack:"sv"` // always 1
}
```
### Campos do Evento de Auditoria
A struct `Event` usa campos de roteamento em texto simples separados (`Scheme`,
`Namespace`) juntamente com campos de payload criptografados (`EncScheme`, `EncNamespace`,
`EncDetails`). Checksums são calculados sobre os campos de roteamento em texto simples e os
bytes criptografados de `EncDetails`, para que a integridade da cadeia possa ser verificada em três níveis
sem qualquer chave:
| Nível | Possui | Pode verificar |
|---|---|---|
| Público | Nada | Cadeia de checksum SHA-256 (detecta adulteração e inserção) |
| Detentor da chave de auditoria | `auditEncKey` | Cadeia completa + descriptografar Scheme/Namespace/Details |
| Operador | Frase secreta principal | Tudo |
**Exemplo:** Um auditor de conformidade recebe apenas `auditEncKey`. Ele pode verificar
toda a cadeia HMAC em rotações de chave e ler todos os detalhes do evento, mas não pode
descriptografar nenhum valor secreto. Um observador público com apenas o arquivo de banco de dados pode
ainda detectar se algum evento foi modificado ou inserido posteriormente.
### Repositório de salt versionado
O salt KDF é armazenado como um `SaltStore` codificado em msgpack sob a chave de metadados `salt`.
Cada rotação de salt acrescenta uma nova `SaltEntry` e avança
`CurrentVersion`. Entradas antigas são retidas como trilha de auditoria. O SaltStore é
armazenado não criptografado — veja [Decisões de segurança](#security-decisions).
### WAL de rotação à prova de falhas
`Rotate` escreve um WAL antes de tocar em qualquer registro. O WAL carrega
`WrappedOldKey`: a chave mestra pré-rotação criptografada com a nova chave mestra.
Após uma falha, a frase secreta antiga se foi; `WrappedOldKey` é a única maneira correta
de transportar a chave antiga através do limite. Em `UnlockDatabase`, quando um WAL
está presente, a nova chave mestra descriptografa `WrappedOldKey` e a rotação retoma
a partir do cursor do WAL. O próprio WAL é criptografado com `policyEncKey`.
---
## Cadeia de auditoria
Cada operação significativa acrescenta um evento à prova de adulteração à cadeia de auditoria do bucket.
A integridade da cadeia depende de dois mecanismos.
**Checksum.** SHA-256 sobre prevChecksum, ID, BucketID, Scheme, Namespace,
EncDetails, EventType e Timestamp. Usar `Scheme`/`Namespace` como texto simples
(sempre preservados junto com as formas criptografadas) garante que o checksum seja estável
em todos os caminhos de carregamento. `EncDetails` fornece integridade sobre o payload criptografado.
**HMAC.** HMAC-SHA256 sobre todos os campos, incluindo Seq. Um atacante que pode escrever
no banco de dados, mas não conhece a chave de auditoria, não pode produzir um HMAC válido.
`VerifyIntegrity` verifica ambas as camadas para cada evento.
**Limite de época de rotação de chave.** Em `Rotate`, um evento de checkpoint é acrescentado
a cada cadeia ativa, carregando fingerprints das chaves de auditoria de saída e de entrada.
O checkpoint é assinado com a chave de saída. Auditores que possuem
qualquer chave de época podem recuperar chaves de época subsequentes do campo `wrapped_new_key`
e verificar a continuidade do HMAC em toda a cadeia.
**Poda automática.** Quando `AuditPruneInterval` está definido em `Config`, um
`jack.Scheduler` é executado periodicamente e chama `PruneEvents` em cada bucket
registrado. Buckets `LevelHSM` e `LevelRemote` nunca são podados, independentemente
desta configuração.
---
## Integração Jack
Jack é uma biblioteca opcional de supervisão de processos. Quando um `JackConfig` é
fornecido via `WithJack`, o keeper ativa componentes em segundo plano automaticamente:
- **Auto-lock Looper:** Verifica periodicamente o timestamp da última atividade e
descarta DEKs de bucket `LevelAdminWrapped` após `AutoLockInterval`. Buckets `LevelPasswordOnly`
permanecem desbloqueados para que tarefas em segundo plano continuem ininterruptas. O
padrão de bloqueio de escrita única dentro da tarefa looper elimina a condição de corrida `RUnlock→Lock`
presente em projetos anteriores.
- **Per-bucket DEK Reaper:** Expiração baseada em TTL para DEKs `LevelAdminWrapped`.
- **Pacientes de monitoramento de saúde:** Verificação de latência de leitura bbolt e verificação de ida e volta de criptografia/descriptografia,
ambos registrados com `jack.Doctor`.
- **Agendador de poda de auditoria:** `PruneEvents` periódico em todos os buckets que não sejam HSM.
- **Pool de eventos assíncrono:** Eventos de auditoria enviados sem bloquear a operação principal.
Se `JackConfig` não for fornecido, o keeper é executado sem essas tarefas em segundo plano.
Keeper nunca chama `pool.Shutdown` — o ciclo de vida do pool pertence ao chamador.
---
## x/keepcmd
`x/keepcmd` fornece operações keeper reutilizáveis desacopladas de qualquer estrutura CLI.
Embuta-o no seu próprio aplicativo para obter gerenciamento de segredos tipado e testável
sem puxar o binário CLI.```go
import "github.com/agberohq/keeper/x/keepcmd"
cmds := &keepcmd.Commands{
Store: func() (*keeper.Keeper, error) {
return security.KeeperOpen(cfg) // your own config
},
Out: keepcmd.PlainOutput{},
NoClose: false, // true in REPL / session contexts
}
cmds.List() // all keys: scheme://namespace/key
cmds.List("vault") // all keys in scheme vault
cmds.List("vault", "system") // all keys in vault://system
cmds.Get("vault://system/jwt_secret")
cmds.Set("vault://system/jwt_secret", "newsecret", keepcmd.SetOptions{})
cmds.Rotate(newPassphraseBytes) // caller resolved the passphrase — no prompter dependency
cmds.RotateSalt(currentPassBytes) // same
```
`keepcmd` nunca chama `prompter` ou lê de stdin. A resolução da frase secreta
é inteiramente responsabilidade do chamador — isso mantém o pacote seguro em
contextos de servidor headless.
`NoClose: true` impede que `Commands` chame `store.Close()` após cada
operação. Use isso em contextos de REPL / sessão onde um único store é compartilhado
entre muitas chamadas.
---
## x/keephandler
`x/keephandler` monta endpoints HTTP do keeper em qualquer mux `net/http`. Sem
dependência de roteador externo — usa roteamento método+padrão do Go 1.22+ com
stdlib `http.ServeMux`.```go
import "github.com/agberohq/keeper/x/keephandler"
keephandler.Mount(mux, store,
keephandler.WithPrefix("/api/keeper"),
keephandler.WithGuard(func(w http.ResponseWriter, r *http.Request, route string) bool {
if !acl.Allow(r.Header.Get("X-Principal"), route) {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return false
}
return true
}),
keephandler.WithHooks(
keephandler.Hook{
Route: keephandler.RouteGet,
CaptureBody: false,
After: func(r *http.Request, status int, _ []byte) {
audit.Log(r.Context(), route, status)
},
},
),
keephandler.WithEncoder(func(w http.ResponseWriter, route string, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]any{
"ok": status < 400,
"route": route,
"data": data,
})
}),
keephandler.WithRoutes(func(m *http.ServeMux) {
m.HandleFunc("POST /api/keeper/totp/{user}", myTOTPHandler)
}),
)
```
### Endpoints
| Method | Path | Descrição |
|---|---|---|
| `POST` | `{prefix}/unlock` | Desbloquear o cofre com uma frase secreta |
| `POST` | `{prefix}/lock` | Bloquear o cofre |
| `GET` | `{prefix}/status` | Estado do bloqueio — seguro consultar sem autenticação |
| `GET` | `{prefix}/keys` | Listar todas as chaves secretas |
| `GET` | `{prefix}/keys/{key}` | Recuperar um valor secreto |
| `POST` | `{prefix}/keys` | Armazenar um segredo (JSON ou multipart) |
| `DELETE` | `{prefix}/keys/{key}` | Excluir um segredo |
| `POST` | `{prefix}/rotate` | Rotacionar a frase secreta mestre |
| `POST` | `{prefix}/rotate/salt` | Rotacionar o salt KDF |
| `GET` | `{prefix}/backup` | Transmitir um instantâneo do banco de dados |
### Contrato do Hook
`BeforeFunc` retorna `(allow bool, err error)`.
- `(true, nil)` — permitir que a requisição prossiga.
- `(false, nil)` — abortar; o hook já escreveu uma resposta completa.
- `(false, err)` — abortar; o framework escreve um `500` usando `err.Error()`. O hook **não** deve ter escrito nada em `w`.
`Hook.CaptureBody bool` controla se `AfterFunc` recebe o corpo da resposta. `false` (padrão) custa um wrapper `statusWriter` leve; `true` armazena o corpo completo em um `bytes.Buffer` para o `AfterFunc` — uma alocação por requisição.
Os hooks são executados na ordem de registro. Múltiplas chamadas `WithHooks` são aditivas. Apenas o primeiro hook registrado para um determinado nome de rota é usado—registros subsequentes para a mesma rota são ignorados.
---
## Referência da API
### Construção e desbloqueio```go
store, err := keeper.New(keeper.Config{
DBPath: "/var/lib/agbero/keeper.db",
AutoLockInterval: 30 * time.Minute,
EnableAudit: true,
AuditPruneInterval: 24 * time.Hour,
AuditPruneKeepLastN: 10_000,
AuditPruneOlderThan: 90 * 24 * time.Hour,
DBLatencyThreshold: 200 * time.Millisecond,
Logger: logger,
}, keeper.WithJack(keeper.JackConfig{
Pool: jackPool,
Shutdown: jackShutdown,
}))
defer store.Close()
// Shorthand (wraps DeriveMaster + UnlockDatabase):
if err := store.Unlock([]byte(os.Getenv("KEEPER_PASSPHRASE"))); err != nil {
log.Fatal(err) // ErrInvalidPassphrase on wrong passphrase
}
```
`UnlockDatabase` executa o seguinte, em ordem:
1. Deriva e ativa a chave de assinatura HMAC de auditoria
2. Deriva e ativa a chave HMAC de política
3. Deriva e ativa `policyEncKey` e `auditEncKey`
4. Limpa e recarrega `schemeRegistry` (descriptografa todos os blobs de política)
5. Retoma qualquer WAL de rotação interrompida
6. Atualiza tags HMAC de política
7. Propaga todos os DEKs de bucket `LevelPasswordOnly` para o Envelope
8. Inicia tarefas em segundo plano (loop de migração, bloqueio automático, pacientes de saúde)
### Bucket `LevelPasswordOnly` — ciclo de vida completo```go
err := store.CreateBucket("vault", "system", keeper.LevelPasswordOnly, "init")
store.Set("vault://system/jwt_secret", []byte("supersecret"))
val, err := store.Get("vault://system/jwt_secret")
// Namespaced convenience wrappers
store.SetNamespaced("admin", "jwt_secret", secretBytes)
val, err = store.GetNamespaced("admin", "jwt_secret")
```
### LevelAdminWrapped bucket — ciclo de vida completo```go
err := store.CreateBucket("finance", "payroll", keeper.LevelAdminWrapped, "ops-team")
err = store.AddAdminToPolicy("finance", "payroll", "alice", []byte("alicepass"))
store.SetNamespacedFull("finance", "payroll", "salary_key", []byte("AES256..."))
store.LockBucket("finance", "payroll")
err = store.UnlockBucket("finance", "payroll", "bob", []byte("bobpass"))
// ErrAuthFailed — does not distinguish wrong password from unknown admin (CWE-204)
err = store.RevokeAdmin("finance", "payroll", "alice")
err = store.RotateAdminWrappedDEK("finance", "payroll", "bob", []byte("bobpass"))
needs, err := store.NeedsAdminRekey("finance", "payroll")
```
### LevelHSM / LevelRemote baldes```go
import (
"github.com/agberohq/keeper/pkg/hsm"
"github.com/agberohq/keeper/pkg/remote"
)
// SoftHSM — testing only
provider, _ := hsm.NewSoftHSM()
store.RegisterHSMProvider("secure", "keys", provider)
store.CreateBucket("secure", "keys", keeper.LevelHSM, "ops")
// Vault Transit
cfg := remote.VaultTransit("https://vault.corp:8200", vaultToken, "my-key")
cfg.TLSClientCert = "/etc/keeper/client.crt"
cfg.TLSClientKey = "/etc/keeper/client.key"
provider, _ = remote.New(cfg)
store.RegisterHSMProvider("tenant", "secrets", provider)
store.CreateBucket("tenant", "secrets", keeper.LevelRemote, "ops")
```
### Exportação de chave de auditoria```go
// Export the audit encryption key to allow a third-party auditor to decrypt
// event details without access to the master passphrase.
auditKey, err := store.ExportAuditKey()
defer zero.Bytes(auditKey)
events, err := auditStore.LoadChain("vault", "system", auditKey)
```
### Rotação de chaves```go
// Rotate passphrase — crash-safe WAL, resumes on next Unlock if interrupted
store.Rotate([]byte("new-passphrase"))
// Rotate KDF salt — re-derives master key, re-encrypts LevelPasswordOnly
store.RotateSalt([]byte("current-passphrase"))
```
### Comparar e trocar```go
err := store.CompareAndSwapNamespacedFull("vault", "system", "counter",
[]byte("old"), []byte("new"))
// ErrCASConflict if current value does not match old
```
### Cópia de segurança```go
f, _ := os.Create("keeper.db.bak")
info, err := store.Backup(f)
// info.Bytes, info.Timestamp, info.DBPath
```
---
## Catálogo de erros
Todos os erros sentinela funcionam com `errors.Is` e `errors.As`. Rastreamentos de pilha são
capturados no ponto de criação via `github.com/olekukonko/errors`.
| Erro | Significado |
|---|---|
| `ErrStoreLocked` | Operação tentada enquanto o armazenamento está bloqueado |
| `ErrInvalidPassphrase` | Frase-passe mestre incorreta |
| `ErrAuthFailed` | Qualquer falha de `UnlockBucket` — não distingue senha errada de ID de administrador desconhecido (CWE-204) |
| `ErrKeyNotFound` | Chave secreta não existe |
| `ErrBucketLocked` | Bucket não foi desbloqueado |
| `ErrPolicyImmutable` | Segunda política para um bucket existente |
| `ErrPolicyNotFound` | Nenhuma política para o esquema/espaço de nomes fornecido |
| `ErrAdminNotFound` | ID de administrador não está na política — apenas `RevokeAdmin` |
| `ErrHSMProviderNil` | Bucket HSM/Remoto criado sem um provedor registrado |
| `ErrCheckLatency` | Latência de leitura do BD excedeu `DBLatencyThreshold` |
| `ErrCASConflict` | Valor atual não corresponde ao esperado em `CompareAndSwap` |
| `ErrSecurityDowngrade` | Movimento entre buckets de nível de segurança superior para inferior |
| `ErrAlreadyUnlocked` | `UnlockDatabase` chamado em um armazenamento já desbloqueado |
| `ErrMasterRequired` | `UnlockDatabase` chamado com `Master` nulo ou destruído |
| `ErrChainBroken` | Verificação de integridade da cadeia de auditoria falhou |
| `ErrMetadataDecrypt` | Metadados criptografados não puderam ser descriptografados |
| `ErrPolicySignature` | Verificação HMAC da política falhou — o registro foi adulterado |
---
## Decisões de segurança
**ErrAuthFailed unifica todas as falhas de UnlockBucket (CWE-204 / CVSS 5.3).** Tanto
um ID de administrador desconhecido quanto uma senha errada retornam `ErrAuthFailed`. Isso impede
enumeração de ID de administrador por tempo ou comparação de string de erro. `RevokeAdmin` mantém
`ErrAdminNotFound` porque é uma operação administrativa em um armazenamento já
desbloqueado. A comparação em tempo constante para presença de ID de administrador é
omitida intencionalmente. Um atacante que consiga medir diferenças de submicrossegundos
em consultas de bucket bbolt precisaria de acesso local ao sistema de arquivos — ponto no qual
pode ler o bucket de políticas diretamente. O modelo de ameaça assume que o arquivo do banco de dados
pode estar comprometido; a defesa de tempo contra enumeração remota é a principal
preocupação.
**Argon2id domina o tempo.** Argon2id leva 200–500 ms em hardware típico.
Diferenças de comparação pós-derivação são quatro ou mais ordens de grandeza
menores e não são mensuráveis remotamente. Nenhuma equalização artificial é aplicada.
**DEK recuperado dentro do limite da transação CAS.** `CompareAndSwapNamespacedFull`
recupera o DEK do bucket dentro da transação de escrita do bbolt, eliminando a
janela onde um `Rotate` concorrente poderia alterar o DEK entre a recuperação e o
uso.
**Frase-passe nunca armazenada como string Go no manipulador HTTP.** Todos os três
campos de frase-passe (`passphrase`, `new_passphrase`) são decodificados de JSON
diretamente para `[]byte` via extração de mapa bruto, mantendo o array de suporte da string
fora do heap de longa duração. A cópia `[]byte` é zerada com `wipeBytes` após o uso.
**Nenhuma flag `--passphrase` na CLI.** Flags aparecem na saída do `ps` e no histórico do shell.
A CLI aceita a frase-passe apenas da variável de ambiente `KEEPER_PASSPHRASE` ou
de um prompt interativo sem eco.
**Valores secretos do REPL nunca são visíveis.** `set <key>` no REPL sem um
valor inline usa `term.ReadPassword` — não aparece no histórico de rolagem do terminal,
histórico do shell ou `ps`. Um valor inline (`set key valor`) pode ser
fornecido para dados não sensíveis quando conveniente.
**SaltStore é intencionalmente não criptografado.** O sal do KDF deve ser legível
antes de `UnlockDatabase` para derivar a chave mestre. `policyEncKey` (usada para toda
outra criptografia de metadados) é ela própria derivada da chave mestre — criptografar
o sal com `policyEncKey` seria circular. Um sal de KDF fornece exclusividade,
não confidencialidade; não há valor de segurança em criptografá-lo.
**Chaves do bucket de políticas são hash, não texto simples.** As chaves de políticas em disco são
`hex(SHA-256("scheme:namespace"))[:32]` — 128 bits de espaço de chave — em vez de
strings legíveis. Um atacante offline que leia o arquivo bbolt não pode enumerar
nomes de bucket sem descriptografar os blobs de políticas.
**Criptografia de metadados usa a mesma interface de cifra que segredos.** Todas as
operações `policyEncKey` e `auditEncKey` passam por `s.config.NewCipher(key)`
— a mesma interface `crypt.Cipher` configurada para valores secretos. A escolha de
cifra do usuário (AES-256-GCM para FIPS 140, XChaCha20-Poly1305 por padrão) flui
para criptografia de políticas, WAL e auditoria automaticamente. Nenhum caminho de código
define um algoritmo específico.
**Buckets LevelHSM e LevelRemote ignorados durante rotação da chave mestre.**
`reencryptAllWithKey` e `RotateSalt` ignoram explicitamente esses buckets. O DEK
é controlado pelo provedor; a rotação do sal mestre não o afeta.
**Rotação segura contra falhas com WrappedOldKey.** `Rotate` escreve um WAL antes
de tocar em qualquer registro. O WAL carrega `WrappedOldKey`: a chave mestre pré-rotação
criptografada com a nova chave mestre. Após uma falha, `UnlockDatabase` descriptografa
`WrappedOldKey` usando a nova chave verificada e retoma a rotação a partir do cursor.
---
## Dependências
| Pacote | Propósito |
|---|---|
| `go.etcd.io/bbolt` | Armazenamento chave-valor embutido |
| `golang.org/x/crypto` | Argon2id, XChaCha20-Poly1305, HKDF, scrypt |
| `github.com/awnumar/memguard` | Enclave de chave seguro na memória (chave mestre, DEKs) |
| `github.com/vmihailenco/msgpack/v5` | Serialização binária para segredos e políticas |
| `github.com/olekukonko/jack` | Supervisão de processos (integração opcional com Jack) |
| `github.com/olekukonko/ll` | Registro estruturado |
| `github.com/olekukonko/errors` | Erros sentinela com rastreamento de pilha |
| `github.com/olekukonko/zero` | Zeramento seguro de fatias de bytes |
| `github.com/olekukonko/prompter` | Prompts de terminal sem eco (apenas CLI) |
| `github.com/integrii/flaggy` | Análise de flags de CLI (apenas cmd/keeper) |
| `golang.org/x/term` | Detecção de TTY e leitura de senha bruta (apenas CLI) |