
Simple gardien sécurisé de secrets
Keeper est un coffre-fort cryptographique pour Go. Il chiffre des données brutes arbitraires au repos en utilisant la dérivation de clé Argon2id et le chiffrement authentifié XChaCha20-Poly1305 (par défaut), et les stocke dans une base de données bbolt intégrée.
Il se décline en trois composants utilisables indépendamment :
x/keephandler) — montez des endpoints keeper sur n'importe quel
multiplexeur net/http en un seul appel, avec des hooks, des gardes et des encodeurs de réponse
enfichables pour le contrôle d'accès et la journalisation d'audit.cmd/keeper) — une interface terminal avec une session REPL persistante,
une saisie de secrets sans écho et une exposition zéro à l'historique du shell.Keeper a été conçu comme la couche de gestion de secrets fondamentale pour le répartiteur de charge Agbero mais n'a aucune dépendance envers Agbero et fonctionne dans tout projet Go.
Keeper partitionne les secrets en buckets. Chaque bucket possède une
BucketSecurityPolicy immuable qui régit la protection de sa clé de chiffrement de données (DEK).
Quatre niveaux sont disponibles.
Un schéma est un préfixe URI qui regroupe des buckets connexes (vault://, certs://,
space://, ou tout nom que vous enregistrez). Un niveau de sécurité est une propriété de la
politique du bucket définie à la création et immuable par la suite.
Vous pouvez librement mélanger les niveaux de sécurité au sein d'un même schéma. Par exemple,
vault://system pourrait être LevelPasswordOnly (déverrouillé automatiquement au démarrage), tandis que
vault://admin est LevelAdminWrapped (nécessite des identifiants explicites).
La DEK du bucket est dérivée de la clé maîtresse à l'aide de HKDF-SHA256 avec une
chaîne d'information séparée par domaine pour chaque bucket (keeper-bucket-dek-v1:scheme:namespace).
Tous les buckets LevelPasswordOnly sont déverrouillés automatiquement lorsque
UnlockDatabase est appelée avec la bonne phrase de passe maîtresse. Aucun identifiant
par bucket n'est nécessaire à l'exécution. Ce niveau convient aux secrets dont le processus
a besoin au démarrage sans intervention humaine.
Le bucket possède une DEK aléatoire de 32 octets unique à ce bucket. La DEK
n'est jamais stockée en clair. Pour chaque administrateur autorisé, une clé de chiffrement de clé (KEK)
est dérivée de HKDF(masterKey‖adminCred, dekSalt) et utilisée pour envelopper
la DEK via XChaCha20-Poly1305. Le bucket est inaccessible jusqu'à ce qu'un administrateur appelle
UnlockBucket avec ses identifiants. La phrase de passe maîtresse seule ne peut pas
déchiffrer le bucket. La révocation d'un administrateur n'affecte aucune copie enveloppée d'un autre
administrateur.
La DEK du bucket est générée lors de CreateBucket et immédiatement enveloppée par
un HSMProvider fourni par l'appelant. Le fournisseur effectue les opérations d'enveloppement et de
déballage — keeper ne manipule jamais la DEK brute après l'avoir transmise au fournisseur.
UnlockDatabase appelle automatiquement le fournisseur pour déballer et amorcer l'
Enveloppe pour tous les buckets HSM enregistrés. La rotation de la clé maîtresse ne
rechiffre pas ces buckets ; la DEK est contrôlée par le fournisseur.
Une implémentation intégrée SoftHSM basée sur une clé d'enveloppement protégée par memguard
est disponible dans pkg/hsm pour les tests et les environnements CI. Ne l'utilisez pas
en production.
Identique à LevelHSM en termes de comportement de gestion des clés, mais le HSMProvider est
implémenté par pkg/remote.Provider — un adaptateur HTTPS configurable qui
délègue l'enveloppement et le déballage à n'importe quel service KMS distant via TLS. Des
configurations pré-construites pour HashiCorp Vault Transit, AWS KMS et GCP Cloud KMS sont
fournies dans pkg/remote. Pour une utilisation en production, configurez TLSClientCert et
TLSClientKey pour activer l'authentification mutuelle TLS.
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
Un hash de vérification est stocké lors de la première dérivation :```
verifyHash ← Argon2id(masterKey, "verification", t=1, m=64 MiB, p=4) → 32 bytes
Les appels ultérieurs à DeriveMaster recalculent ce hachage et le comparent avec crypto/subtle.ConstantTimeCompare. Une non-correspondance renvoie ErrInvalidPassphrase.
Le sel KDF est stocké non chiffré par conception. Il doit être lisible avant UnlockDatabase pour dériver la clé maîtresse — le chiffrer avec une clé dérivée de la clé maîtresse serait circulaire. Un sel KDF n'est pas un secret ; son but est l'unicité, pas la confidentialité.
Chaque valeur en clair est chiffrée avec XChaCha20-Poly1305 en utilisant la DEK du compartiment :``` nonce ← random 24 bytes ciphertext ← XChaCha20-Poly1305.Seal(nonce, DEK, plaintext)
L'enregistrement stocké est une structure `Secret` encodée en msgpack contenant le texte chiffré, les métadonnées chiffrées et la version du schéma. L'authentification est implicite : un texte chiffré déchiffré avec la mauvaise clé produit une échec d'authentification AEAD avant qu'aucun texte en clair ne soit renvoyé.
### Dérivation 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)
La KEK est dérivée à l'aide de HKDF plutôt que d'un second passage d'Argon2. La clé maîtresse a déjà été produite par une KDF à coût élevé ; une seconde invocation d'Argon2 ajouterait des centaines de millisecondes de latence à chaque appel UnlockBucket sans bénéfice de sécurité. HKDF-SHA256 opère en environ une microseconde.
Défense en profondeur : Un attaquant qui ne compromet que la base de données obtient la DEK enveloppée et le sel HKDF mais ne peut pas dériver la KEK sans la clé maîtresse. Un attaquant qui ne compromet que la clé maîtresse ne peut déchiffrer aucune DEK LevelAdminWrapped sans également connaître l'identifiant administrateur.
Les métadonnées secrètes (heure de création, heure de mise à jour, nombre d'accès, version) sont chiffrées séparément du texte chiffré :``` metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))
Pour `LevelAdminWrapped`, `LevelHSM` et `LevelRemote` buckets, cela signifie que les métadonnées sont inaccessibles sans le credential du bucket, empêchant un attaquant ayant un accès en lecture au fichier de base de données d'apprendre les patterns d'accès ou les horodatages.
**Note sur le canal auxiliaire temporel :** XChaCha20-Poly1305 traite l'intégralité du ciphertext avant de renvoyer une erreur d'authentification. Le chemin de décryptage de secours (nouvelle DEK dérivée → ancienne master-key-as-DEK) prend le même temps horloge mural quelle que soit la clé qui réussit. Aucun canal auxiliaire temporel ne fuit l'état de migration d'un enregistrement.
### Chiffrement des métadonnées — politiques, WAL et audit
Toutes les métadonnées structurelles sont également chiffrées au repos. Deux clés sont dérivées de la clé maîtresse au moment 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 chiffre : les valeurs de BucketSecurityPolicy et le WAL de rotation.
auditEncKey chiffre : les champs Scheme, Namespace et Details de
chaque événement d'audit.
Les deux clés sont effacées de la mémoire lors de Lock(). Le chiffrement utilisé pour le chiffrement des métadonnées est la même interface configurable crypt.Cipher que celle utilisée pour les secrets — le choix de chiffrement de l'utilisateur (AES-256-GCM pour FIPS, XChaCha20-Poly1305 par défaut) s'applique automatiquement.
Format de fil pour tous les blobs de métadonnées chiffrés :``` nonce (cipher.NonceSize() bytes) || AEAD-ciphertext
### Hachage des clés de compartiment de politique
Sur disque, les clés de politique sont des hachages opaques plutôt que des chaînes en texte clair `scheme:namespace`,
empêchant l'énumération hors ligne des noms de compartiments :```
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)
Le schemeRegistry en mémoire continue d'utiliser "scheme:namespace" comme
clé — seule la représentation sur disque change.
Chaque enregistrement de politique porte deux balises d'intégrité écrites atomiquement dans une transaction 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
Avant `UnlockDatabase`, seul le hachage SHA-256 est disponible. Après le déverrouillage,
`loadPolicy` vérifie la balise HMAC. `UnlockDatabase` appelle `upgradePolicyHMACs`
pour remplir rétroactivement les balises HMAC sur les politiques créées avant l'existence de cette fonctionnalité.
### Signature HMAC d'audit```
auditKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-hmac-v1") → 32 bytes
HMAC ← HMAC-SHA256(auditKey, event fields including Seq)
La clé de signature est activée lors de UnlockDatabase et effacée lors de Lock. Lorsque la clé maître est permutée, Rotate ajoute un événement de point de contrôle de rotation de clé à chaque chaîne d'audit active, signé avec l'ancienne clé d'audit comme dernier événement de l'ancienne époque. L'historique n'est jamais réécrit ; le point de contrôle est le pont de confiance entre les époques.
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)
All intermediate keys are zeroed immediately after use. The master key is
never written to disk in any form.
---
## Storage schema
The underlying database is bbolt. All buckets and their contents:
| bbolt bucket | Key | Value |
|---|---|---|
| `__meta__` | `salt` | msgpack — SaltStore (non chiffré ; dépendance circulaire si chiffré) |
| `__meta__` | `verify` | octets bruts — hachage de vérification Argon2id |
| `__meta__` | `rotation_wal` | `nonce‖AEAD(msgpack(RotationWAL))` |
| `__meta__` | `bucket_dek_done` | `"1"` — marqueur d'achèvement de migration DEK |
| `__policies__` | `hex(SHA-256(scheme:ns))[:32]` | `nonce‖AEAD(msgpack(BucketSecurityPolicy))` |
| `__policies__` | `<base>__hash__` | hex SHA-256 des octets de politique chiffrés |
| `__policies__` | `<base>__hmac__` | hex HMAC-SHA256(policyKey, encrypted policy bytes) |
| `__audit__/scheme/namespace` | event UUID | JSON — audit Event |
| `__audit__/scheme/namespace` | `__chain_index__` | JSON — chainIndex |
| `scheme/namespace` | key string | msgpack — Secret struct |
### Secret struct (msgpack)```go
type Secret struct {
Ciphertext []byte `msgpack:"ct"`
EncryptedMeta []byte `msgpack:"em,omitempty"`
SchemaVersion int `msgpack:"sv"` // always 1
}
La structure Event utilise des champs de routage en texte clair séparés (Scheme, Namespace) ainsi que des champs de charge utile chiffrés (EncScheme, EncNamespace, EncDetails). Les sommes de contrôle sont calculées sur les champs de routage en texte clair et les octets chiffrés d'EncDetails, de sorte que l'intégrité de la chaîne peut être vérifiée à trois niveaux sans aucune clé :
| Niveau | Possède | Peut vérifier |
|---|---|---|
| Public | Rien | Chaîne de somme de contrôle SHA-256 (détecte les altérations et insertions) |
| Détenteur de la clé d'audit |
Exemple : Un auditeur de conformité ne reçoit que auditEncKey. Il peut vérifier la chaîne HMAC complète à travers les rotations de clés et lire tous les détails des événements, mais ne peut déchiffrer aucune valeur secrète. Un observateur public disposant uniquement du fichier de base de données peut toujours détecter si un événement a été modifié ou inséré après coup.
Le sel KDF est stocké en tant que SaltStore encodé en msgpack sous la clé de métadonnées salt. Chaque rotation de sel ajoute une nouvelle SaltEntry et avance CurrentVersion. Les anciennes entrées sont conservées comme piste d'audit. Le SaltStore est stocké non chiffré — voir Décisions de sécurité.
Rotate écrit un WAL avant de toucher à un enregistrement. Le WAL contient WrappedOldKey : la clé maître pré-rotation chiffrée avec la nouvelle clé maître. Après un crash, l'ancienne phrase de passe a disparu ; WrappedOldKey est la seule manière correcte de transporter l'ancienne clé à travers la frontière. Lors de UnlockDatabase, lorsqu'un WAL est présent, la nouvelle clé maître déchiffre WrappedOldKey et la rotation reprend à partir du curseur WAL. Le WAL lui-même est chiffré avec policyEncKey.
Chaque opération significative ajoute un événement inviolable à la chaîne d'audit du bucket. L'intégrité de la chaîne repose sur deux mécanismes.
Somme de contrôle. SHA-256 sur prevChecksum, ID, BucketID, Scheme, Namespace, EncDetails, EventType et Timestamp. L'utilisation de Scheme/Namespace en texte clair (toujours conservés aux côtés des formes chiffrées) garantit que la somme de contrôle est stable à travers les chemins de chargement. EncDetails fournit une intégrité sur la charge utile chiffrée.
HMAC. HMAC-SHA256 sur tous les champs, y compris Seq. Un attaquant qui peut écrire dans la base de données mais ne connaît pas la clé d'audit ne peut pas produire un HMAC valide. VerifyIntegrity vérifie les deux couches pour chaque événement.
Limite d'époque de rotation de clé. Lors de Rotate, un événement de point de contrôle est ajouté à chaque chaîne active portant les empreintes des clés d'audit sortante et entrante. Le point de contrôle est signé avec la clé sortante. Les auditeurs détenant une clé d'époque peuvent récupérer les clés d'époque suivantes à partir du champ wrapped_new_key et vérifier la continuité HMAC sur toute la chaîne.
Élagage automatique. Lorsque AuditPruneInterval est défini dans Config, un jack.Scheduler s'exécute périodiquement et appelle PruneEvents sur chaque bucket enregistré. Les buckets LevelHSM et LevelRemote ne sont jamais élagués, indépendamment de ce paramètre.
Jack est une bibliothèque optionnelle de supervision de processus. Lorsqu'un JackConfig est fourni via WithJack, keeper active automatiquement les composants d'arrière-plan :
LevelAdminWrapped après AutoLockInterval. Les buckets LevelPasswordOnly restent déverrouillés afin que les tâches d'arrière-plan se poursuivent sans interruption. Le modèle de verrouillage en écriture unique à l'intérieur de la tâche de la boucle élimine la condition de course RUnlock→Lock présente dans les conceptions antérieures.LevelAdminWrapped.jack.Doctor.PruneEvents périodique sur tous les buckets non HSM.Si JackConfig n'est pas fourni, keeper s'exécute sans ces tâches d'arrière-plan. Keeper n'appelle jamais pool.Shutdown — le cycle de vie du pool appartient à l'appelant.
x/keepcmd fournit des opérations keeper réutilisables découplées de tout framework CLI. Intégrez-le dans votre propre application pour obtenir une gestion de secrets typée et testable sans avoir à inclure le binaire 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` n'appelle jamais `prompter` ni ne lit depuis stdin. La résolution de la phrase de passe est entièrement de la responsabilité de l'appelant — cela maintient le package en sécurité dans les contextes de serveur sans tête.
`NoClose: true` empêche `Commands` d'appeler `store.Close()` après chaque opération. Utilisez ceci dans les contextes REPL / session où un même store est partagé entre de nombreux appels.
---
## x/keephandler
`x/keephandler` monte les points de terminaison HTTP de keeper sur n'importe quel mux `net/http`. Aucune dépendance de routeur externe — il utilise le routage par méthode+patron de Go 1.22+ avec `http.ServeMux` de la stdlib.```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)
}),
)
BeforeFunc retourne (allow bool, err error).
(true, nil) — laisser la requête se poursuivre.(false, nil) — annuler; le hook a déjà écrit une réponse complète.(false, err) — annuler; le framework écrit un 500 en utilisant err.Error().
Le hook ne doit pas avoir écrit quoi que ce soit dans w.Hook.CaptureBody bool contrôle si AfterFunc reçoit le corps de la réponse. false (par défaut) coûte un wrapper léger statusWriter ; true met en mémoire tampon le corps complet dans un bytes.Buffer pour AfterFunc — une allocation par requête.
Les hooks sont exécutés dans l'ordre d'enregistrement. Plusieurs appels WithHooks sont additifs. Seul le premier hook enregistré pour un nom de route donné est utilisé—les enregistrements ultérieurs pour la même route sont ignorés.
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` effectue les opérations suivantes dans l'ordre :
1. Dérive et active la clé de signature HMAC d'audit
2. Dérive et active la clé HMAC de politique
3. Dérive et active `policyEncKey` et `auditEncKey`
4. Efface et recharge `schemeRegistry` (déchiffre tous les blobs de politique)
5. Reprend toute rotation WAL interrompue
6. Met à jour les tags HMAC de politique
7. Insère toutes les DEKs des compartiments `LevelPasswordOnly` dans l'Enveloppe
8. Démarre les tâches en arrière-plan (boucle de migration, verrouillage automatique, patients de santé)
### Compartiment LevelPasswordOnly — cycle de vie complet```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")
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 buckets```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")
// 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)
### Rotation des clés```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"))
err := store.CompareAndSwapNamespacedFull("vault", "system", "counter", []byte("old"), []byte("new")) // ErrCASConflict if current value does not match old
### Sauvegarde```go
f, _ := os.Create("keeper.db.bak")
info, err := store.Backup(f)
// info.Bytes, info.Timestamp, info.DBPath
Toutes les erreurs sentinelles fonctionnent avec errors.Is et errors.As. Les traces d'appel sont capturées au moment de la création via github.com/olekukonko/errors.
ErrAuthFailed unifie tous les échecs de UnlockBucket (CWE-204 / CVSS 5.3). Un ID admin inconnu et un mauvais mot de passe renvoient tous deux ErrAuthFailed. Cela empêche l'énumération des ID admin par timing ou comparaison de chaînes d'erreur. RevokeAdmin conserve ErrAdminNotFound car il s'agit d'une opération administrative sur un magasin déjà déverrouillé. La comparaison en temps constant pour la présence de l'ID admin est intentionnellement omise. Un attaquant capable de mesurer des différences sub-microsecondes dans les recherches de bucket bbolt aurait besoin d'un accès local au système de fichiers — à ce stade, il peut lire directement le bucket de politique. Le modèle de menace suppose que le fichier de base de données peut être compromis ; la défense par timing contre l'énumération distante est la préoccupation principale.
Argon2id domine le timing. Argon2id prend 200 à 500 ms sur du matériel typique. Les différences de comparaison post-dérivation sont quatre ordres de grandeur ou plus inférieures et ne sont pas mesurables à distance. Aucune égalisation artificielle n'est appliquée.
DEK récupéré à l'intérieur de la limite de transaction CAS. CompareAndSwapNamespacedFull récupère le DEK du bucket à l'intérieur de la transaction d'écriture bbolt, éliminant la fenêtre où une rotation concurrente pourrait changer le DEK entre la récupération et l'utilisation.
La phrase de passe n'est jamais stockée sous forme de chaîne Go dans le gestionnaire HTTP. Les trois champs de phrase de passe (passphrase, new_passphrase) sont décodés du JSON directement en []byte via une extraction de carte brute, gardant le tableau de support de chaîne hors du tas à longue durée de vie. La copie []byte est effacée avec wipeBytes après utilisation.
Pas de drapeau --passphrase dans la CLI. Les drapeaux apparaissent dans la sortie ps et l'historique du shell. La CLI n'accepte la phrase de passe que depuis l'environnement KEEPER_PASSPHRASE ou une invite interactive sans écho.
Les valeurs secrètes du REPL ne sont jamais visibles. set <key> dans le REPL sans valeur en ligne utilise term.ReadPassword — elle n'apparaît pas dans le défilement du terminal, l'historique du shell ou ps. Une valeur en ligne (set key value) peut être fournie pour des données non sensibles lorsque c'est pratique.
SaltStore est intentionnellement non chiffré. Le sel KDF doit être lisible avant UnlockDatabase pour dériver la clé maîtresse. policyEncKey (utilisé pour tout autre chiffrement de métadonnées) est lui-même dérivé de la clé maîtresse — chiffrer le sel avec policyEncKey serait circulaire. Un sel KDF fournit l'unicité, pas la confidentialité ; il n'y a aucune valeur de sécurité à le chiffrer.
Les clés du bucket de politique sont hachées, pas en clair. Les clés de politique sur disque sont hex(SHA-256("scheme:namespace"))[:32] — 128 bits d'espace de clé — plutôt que des chaînes lisibles. Un attaquant hors ligne lisant le fichier bbolt ne peut pas énumérer les noms de bucket sans déchiffrer les blobs de politique.
Le chiffrement des métadonnées utilise la même interface de chiffrement que les secrets. Toutes les opérations policyEncKey et auditEncKey passent par s.config.NewCipher(key) — la même interface crypt.Cipher configurée pour les valeurs secrètes. Le choix de chiffrement de l'utilisateur (AES-256-GCM pour FIPS 140, XChaCha20-Poly1305 par défaut) se répercute automatiquement sur le chiffrement de la politique, du WAL et de l'audit. Aucun chemin de code ne code en dur un algorithme spécifique.
Les buckets LevelHSM et LevelRemote sont ignorés lors de la rotation de la clé maîtresse. reencryptAllWithKey et RotateSalt ignorent explicitement ces buckets. Le DEK est contrôlé par le fournisseur ; la rotation du sel maître ne l'affecte pas.
Rotation à sécurité crash avec WrappedOldKey. Rotate écrit un WAL avant de toucher à tout enregistrement. Le WAL contient WrappedOldKey : la clé maîtresse avant rotation chiffrée avec la nouvelle clé maîtresse. Après un crash, UnlockDatabase déchiffre WrappedOldKey en utilisant la nouvelle clé vérifiée et reprend la rotation à partir du curseur.
auditEncKey| Chaîne complète + déchiffrer Scheme/Namespace/Details |
| Opérateur | Phrase de passe maître | Tout |
| Méthode | Chemin | Description |
|---|
POST | {prefix}/unlock | Déverrouiller le coffre avec une phrase de passe |
POST | {prefix}/lock | Verrouiller le coffre |
GET | {prefix}/status | État de verrouillage — sûr à interroger sans authentification |
GET | {prefix}/keys | Lister toutes les clés secrètes |
GET | {prefix}/keys/{key} | Récupérer une valeur secrète |
POST | {prefix}/keys | Stocker un secret (JSON ou multipart) |
DELETE | {prefix}/keys/{key} | Supprimer un secret |
POST | {prefix}/rotate | Faire pivoter la phrase de passe maîtresse |
POST | {prefix}/rotate/salt | Faire pivoter le sel KDF |
GET | {prefix}/backup | Diffuser un instantané de la base de données |
| Erreur | Signification |
|---|
ErrStoreLocked | Opération tentée alors que le magasin est verrouillé |
ErrInvalidPassphrase | Mauvaise phrase de passe principale |
ErrAuthFailed | Tout échec de UnlockBucket — ne distingue pas un mauvais mot de passe d'un ID admin inconnu (CWE-204) |
ErrKeyNotFound | La clé secrète n'existe pas |
ErrBucketLocked | Le bucket n'a pas été déverrouillé |
ErrPolicyImmutable | Deuxième politique pour un bucket existant |
ErrPolicyNotFound | Aucune politique pour le schéma/namespace donné |
ErrAdminNotFound | L'ID admin n'est pas dans la politique — RevokeAdmin uniquement |
ErrHSMProviderNil | Bucket HSM/Remote créé sans fournisseur enregistré |
ErrCheckLatency | La latence de lecture DB a dépassé DBLatencyThreshold |
ErrCASConflict | La valeur actuelle ne correspond pas à celle attendue dans CompareAndSwap |
ErrSecurityDowngrade | Déplacement inter-bucket d'un niveau de sécurité supérieur à inférieur |
ErrAlreadyUnlocked | UnlockDatabase appelé sur un magasin déjà déverrouillé |
ErrMasterRequired | UnlockDatabase appelé avec Master nil ou détruit |
ErrChainBroken | La vérification d'intégrité de la chaîne d'audit a échoué |
ErrMetadataDecrypt | Les métadonnées chiffrées n'ont pas pu être déchiffrées |
ErrPolicySignature | La vérification HMAC de la politique a échoué — l'enregistrement a été falsifié |
| Paquet | Objectif |
|---|
go.etcd.io/bbolt | Magasin de clés-valeurs embarqué |
golang.org/x/crypto | Argon2id, XChaCha20-Poly1305, HKDF, scrypt |
github.com/awnumar/memguard | Enclave de clé sécurisée en mémoire (clé maîtresse, DEKs) |
github.com/vmihailenco/msgpack/v5 | Sérialisation binaire pour les secrets et politiques |
github.com/olekukonko/jack | Supervision des processus (intégration Jack optionnelle) |
github.com/olekukonko/ll | Journalisation structurée |
github.com/olekukonko/errors | Erreurs sentinelles avec traces d'appel |
github.com/olekukonko/zero | Mise à zéro sécurisée de tranches d'octets |
github.com/olekukonko/prompter | Invites de terminal sans écho (CLI uniquement) |
github.com/integrii/flaggy | Analyse des drapeaux CLI (cmd/keeper uniquement) |
golang.org/x/term | Détection TTY et lecture de mot de passe brut (CLI uniquement) |