mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +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
93 lines
3.0 KiB
Go
93 lines
3.0 KiB
Go
package loki
|
|
|
|
// mem_health.go — état de santé de la mémoire pour l'UI : chiffrement actif ?
|
|
// verrouillé ? toutes les pages déchiffrables ? combien de copies du keyvault ?
|
|
// dernier snapshot ? C'est le tableau de bord qui rassure d'un coup d'œil.
|
|
|
|
import (
|
|
"os"
|
|
)
|
|
|
|
// MemHealth résume l'état de la mémoire (sérialisé tel quel vers l'UI).
|
|
type MemHealth struct {
|
|
Encrypted bool `json:"encrypted"` // chiffrement activé (drapeau)
|
|
Fully bool `json:"fully"` // TOUT est réellement chiffré au repos (pages + conversations)
|
|
Locked bool `json:"locked"` // chiffré mais DEK absente
|
|
Pages int `json:"pages"` // nb de pages
|
|
Readable int `json:"readable"` // pages déchiffrables (ou claires)
|
|
Unreadable int `json:"unreadable"` // pages illisibles (chiffrées + verrouillé, ou corrompues)
|
|
Mixed bool `json:"mixed"` // mélange clair/chiffré (migration en cours ?)
|
|
VaultCopies int `json:"vault_copies"` // nb de copies du keyvault présentes (sur 3)
|
|
HasRecovery bool `json:"has_recovery"` // une clé de récupération est enregistrée
|
|
Snapshots int `json:"snapshots"` // nb de snapshots locaux
|
|
LastSnapshot string `json:"last_snapshot"` // ID du snapshot le plus récent
|
|
MigrationBusy bool `json:"migration_busy"` // une migration est en cours (journal présent)
|
|
}
|
|
|
|
// memHealth calcule l'état courant.
|
|
func memHealth() MemHealth {
|
|
h := MemHealth{Encrypted: memEncActive(), Locked: memEncActive() && !memUnlocked()}
|
|
|
|
clair, chiffre := 0, 0
|
|
for _, name := range mdPages() {
|
|
h.Pages++
|
|
p, _ := safeMemPath(name)
|
|
raw, err := os.ReadFile(p)
|
|
if err != nil {
|
|
h.Unreadable++
|
|
continue
|
|
}
|
|
if looksEncrypted(raw) {
|
|
chiffre++
|
|
if _, err := decodeMemContent(raw); err == nil {
|
|
h.Readable++
|
|
} else {
|
|
h.Unreadable++
|
|
}
|
|
} else {
|
|
clair++
|
|
h.Readable++
|
|
}
|
|
}
|
|
h.Mixed = clair > 0 && chiffre > 0
|
|
|
|
// Fully : la case « activer le chiffrement » ne doit être cochée QUE si TOUT est
|
|
// réellement chiffré au repos — aucune page en clair ET aucune valeur de
|
|
// conversation en clair. Vérifiable même verrouillé (looksEncrypted lit le magic
|
|
// sans la DEK). Un chiffrement partiel/raté laisse donc la case DÉCOCHÉE.
|
|
if h.Encrypted {
|
|
full := clair == 0 && h.Pages == chiffre // toutes les pages chiffrées (0 en clair)
|
|
if full {
|
|
for _, b := range encryptedBuckets {
|
|
if !bucketFullyEncrypted(b) {
|
|
full = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
h.Fully = full
|
|
}
|
|
|
|
// Copies du keyvault présentes.
|
|
if _, err := os.Stat(vaultPathPrimary()); err == nil {
|
|
h.VaultCopies++
|
|
}
|
|
if _, err := os.Stat(vaultPathBackup()); err == nil {
|
|
h.VaultCopies++
|
|
}
|
|
if len(getBytes(bkState, vaultDBKey)) > 0 {
|
|
h.VaultCopies++
|
|
}
|
|
if v, _ := loadVault(); v != nil {
|
|
h.HasRecovery = v.hasKind(wrapRecovery)
|
|
}
|
|
|
|
snaps := listSnapshots()
|
|
h.Snapshots = len(snaps)
|
|
if len(snaps) > 0 {
|
|
h.LastSnapshot = snaps[0].ID
|
|
}
|
|
_, h.MigrationBusy = readMigrationJournal()
|
|
return h
|
|
}
|