mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
Reprise d'AJEAN (mem_crypto/mem_vault/mem_store/mem_io/mem_migrate/ mem_snapshots/mem_health/mem_fsutil + backup_bundle), adaptée à Loki. Chiffrement à enveloppe : une DEK tirée une fois chiffre les données en AES-256-GCM ; elle est enfermée dans un coffre par une KEK dérivée en Argon2id. Ce qui ouvre le coffre : la clé de pilotage de l'appareil (le serveur n'en a que l'empreinte, il ne peut pas ouvrir seul) ou la clé de récupération donnée une fois. La DEK ne vit qu'en RAM. Périmètre chiffré, choisi pour Loki : les pages mémoire, les discussions (journal ET index, qui porte les titres), les blocs archivés au compactage — du verbatim de conversation — et les trackers. Pas les buckets de réglages : le coffre lui-même y vit, les chiffrer serait une boucle. Verrouillé, rien n'est écrasé : putStoreBytes REFUSE d'écrire du clair par-dessus du chiffré, la liste des discussions se lit vide et les pages s'affichent « 🔒 chiffré ». Le déverrouillage recharge la discussion et rattrape ce qui serait resté en clair. Une migration interrompue reprend au démarrage, un snapshot est pris avant chaque bascule, et aucune donnée n'est supprimée avant relecture vérifiée de son remplaçant. Piège corrigé au passage : chiffrer À L'INTÉRIEUR d'une transaction bbolt se bloquait sur le verrou de la base (memEncActive relit la config, donc la base). L'état du chiffrement est désormais résolu AVANT la transaction (memEncoderNow), qui ne reçoit plus qu'un encodeur pur. Sauvegarde : le paquet chiffré {mémoire, presets, réglages} s'exporte et s'importe en FICHIER (/api/backup/export, /api/backup/import). La sauvegarde vers le relais ajean.link n'est pas reprise — c'est le service de l'auteur amont ; ici le fichier reste chez soi. Le dossier mémoire n'était déjà plus joignable qu'aux outils mem_* : c'était la condition de ce chiffrement (un `cat memory/…` aurait rendu du binaire). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
134 lines
4.4 KiB
Go
134 lines
4.4 KiB
Go
package loki
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
// La case « chiffré » (health.Fully) ne doit être vraie QUE si TOUT est chiffré.
|
|
func TestFullyReflectsRealState(t *testing.T) {
|
|
testHome(t)
|
|
clearMemDEK()
|
|
if err := MemAdd("a.md", "# A\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = putBytes(bkChat, convKey("c1"), []byte(`{"id":"c1","log":[]}`))
|
|
if _, err := EnableMemEncryption("pw"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !memHealth().Fully {
|
|
t.Fatal("après chiffrement complet, Fully devrait être vrai")
|
|
}
|
|
// Une page en clair réapparue → Fully faux.
|
|
p, _ := safeMemPath("a.md")
|
|
if err := os.WriteFile(p, []byte("# clair"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if memHealth().Fully {
|
|
t.Fatal("une page en clair aurait dû rendre Fully faux")
|
|
}
|
|
// Remets la page chiffrée, salis une discussion → Fully faux aussi.
|
|
if err := writeMemFile("a.md", []byte("# A\n")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = putBytes(bkChat, convKey("c2"), []byte("journal en clair"))
|
|
if memHealth().Fully {
|
|
t.Fatal("une discussion en clair aurait dû rendre Fully faux")
|
|
}
|
|
}
|
|
|
|
// Les discussions (journal + index) et les blocs archivés doivent être chiffrés
|
|
// à l'activation, illisibles verrouillé, et intacts après déchiffrement.
|
|
func TestConversationEncryptionLifecycle(t *testing.T) {
|
|
testHome(t)
|
|
clearMemDEK()
|
|
|
|
// Une discussion avec son journal et son entrée d'index.
|
|
_ = putStoreBytes(bkChat, convKey("c1"), []byte(`{"id":"c1","log":[{"seq":1}],"messages":[]}`))
|
|
convIndexSave([]convMeta{{ID: "c1", Title: "Client confidentiel", Created: 1, Updated: 2}})
|
|
// Une page mémoire, pour activer le chiffrement (il chiffre tout).
|
|
if err := MemAdd("note.md", "# Note\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := EnableMemEncryption("pw"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Sur disque, journal ET index doivent porter le magic.
|
|
if raw := getBytes(bkChat, convKey("c1")); !looksEncrypted(raw) {
|
|
t.Fatal("journal de discussion non chiffré sur disque")
|
|
}
|
|
if raw := getBytes(bkChat, ckIndex); !looksEncrypted(raw) {
|
|
t.Fatal("index des discussions non chiffré sur disque")
|
|
}
|
|
|
|
// Déverrouillé : lisible.
|
|
if idx := convIndex(); len(idx) != 1 || idx[0].Title != "Client confidentiel" {
|
|
t.Fatalf("index illisible alors que déverrouillé: %+v", idx)
|
|
}
|
|
|
|
// Verrouillé : plus rien de lisible, mais rien de perdu.
|
|
clearMemDEK()
|
|
if idx := convIndex(); len(idx) != 0 {
|
|
t.Fatal("les discussions devraient être masquées quand verrouillé")
|
|
}
|
|
if b := storeBytes(bkChat, convKey("c1")); len(b) != 0 {
|
|
t.Fatal("le journal devrait être illisible quand verrouillé")
|
|
}
|
|
// putStoreBytes doit REFUSER d'écrire du clair quand verrouillé.
|
|
if err := putStoreBytes(bkChat, convKey("c1"), []byte("clair")); err != errStoreLocked {
|
|
t.Fatalf("écriture clair aurait dû être refusée, obtenu %v", err)
|
|
}
|
|
if raw := getBytes(bkChat, convKey("c1")); !looksEncrypted(raw) {
|
|
t.Fatal("le blob chiffré a été écrasé alors que verrouillé")
|
|
}
|
|
|
|
// Re-déverrouille et déchiffre : tout revient en clair et intact.
|
|
v, _ := loadVault()
|
|
dek, _, err := v.unlockWith("pw")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
setMemDEK(dek)
|
|
if err := DisableMemEncryption(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if raw := getBytes(bkChat, convKey("c1")); looksEncrypted(raw) {
|
|
t.Fatal("journal encore chiffré après déchiffrement")
|
|
}
|
|
if idx := convIndex(); len(idx) != 1 || idx[0].Title != "Client confidentiel" {
|
|
t.Fatal("index perdu après déchiffrement")
|
|
}
|
|
}
|
|
|
|
// Un bloc archivé au compactage est du verbatim de conversation : chiffré comme
|
|
// le reste, et hors de portée tant que la mémoire est verrouillée.
|
|
func TestRecallBlocksFollowEncryption(t *testing.T) {
|
|
testHome(t)
|
|
clearMemDEK()
|
|
if err := MemAdd("note.md", "# Note\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := EnableMemEncryption("pw"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, err := archiveRecallBlock("tool: lecture", "tool", "contenu très bavard")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if blk, ok := recallGet(id); !ok || blk.Content != "contenu très bavard" {
|
|
t.Fatal("bloc illisible alors que déverrouillé")
|
|
}
|
|
if hits := recallSearch("bavard", 5); len(hits) != 1 {
|
|
t.Fatalf("la recherche devrait trouver le bloc déverrouillé, obtenu %d", len(hits))
|
|
}
|
|
clearMemDEK()
|
|
if _, ok := recallGet(id); ok {
|
|
t.Fatal("bloc lisible alors que verrouillé")
|
|
}
|
|
if hits := recallSearch("bavard", 5); len(hits) != 0 {
|
|
t.Fatal("la recherche ne devrait rien trouver quand verrouillé")
|
|
}
|
|
}
|