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
143 lines
4.8 KiB
Go
143 lines
4.8 KiB
Go
package loki
|
|
|
|
// mem_io.go — couche d'accès transparente aux pages mémoire. C'est le SEUL
|
|
// endroit qui sait si une page est chiffrée ou non : chat_memory.go passe par
|
|
// ici et n'a jamais à s'en soucier.
|
|
//
|
|
// Choix d'implémentation : le NOM du fichier ne change pas (toujours foo.md).
|
|
// C'est le CONTENU qui est chiffré en place, précédé d'un magic (voir
|
|
// mem_crypto.go) qui rend chaque fichier auto-descriptif. Avantage : rien à
|
|
// changer dans le listing, la recherche ou la validation de chemins — la
|
|
// détection se fait sur le contenu, pas sur l'extension.
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// errMemLocked : page chiffrée alors que la mémoire est verrouillée (DEK absente).
|
|
var errMemLocked = errors.New("page chiffrée, mémoire verrouillée")
|
|
|
|
// memEncActive indique si le chiffrement de la mémoire est activé (réglage
|
|
// MEM_ENCRYPTED). Indépendant de l'état verrouillé/déverrouillé.
|
|
func memEncActive() bool {
|
|
switch strings.ToLower(strings.TrimSpace(ReadConfig()["MEM_ENCRYPTED"])) {
|
|
case "1", "on", "true", "yes", "oui":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// decodeMemContent transforme le contenu BRUT d'un fichier en texte lisible :
|
|
// - pas chiffré (pas de magic) : renvoyé tel quel.
|
|
// - chiffré + DEK en RAM : déchiffré.
|
|
// - chiffré + verrouillé : errMemLocked.
|
|
func decodeMemContent(raw []byte) ([]byte, error) {
|
|
if !looksEncrypted(raw) {
|
|
return raw, nil
|
|
}
|
|
dek, err := currentDEK()
|
|
if err != nil {
|
|
return nil, errMemLocked
|
|
}
|
|
return decPage(dek, raw)
|
|
}
|
|
|
|
// memReadPage lit une page par son nom et renvoie son texte clair. Erreur si le
|
|
// fichier est absent, ou chiffré alors que la mémoire est verrouillée.
|
|
func memReadPage(name string) ([]byte, error) {
|
|
p, err := safeMemPath(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
raw, err := os.ReadFile(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return decodeMemContent(raw)
|
|
}
|
|
|
|
// memPageText lit une page et renvoie (texte, lisible). Ne renvoie jamais
|
|
// d'erreur : une page chiffrée non déchiffrable donne readable=false (utile pour
|
|
// lister/chercher sans planter quand la mémoire est verrouillée).
|
|
func memPageText(name string) (string, bool) {
|
|
b, err := memReadPage(name)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
return string(b), true
|
|
}
|
|
|
|
// encodeMemContent prépare le contenu à écrire sur disque selon l'état du
|
|
// chiffrement : chiffré si activé ET déverrouillé, clair sinon. Renvoie une
|
|
// erreur si le chiffrement est activé mais la mémoire verrouillée (on refuse
|
|
// alors d'écrire, plutôt que d'écrire en clair une donnée censée être chiffrée).
|
|
func encodeMemContent(plain []byte) ([]byte, error) {
|
|
if !memEncActive() {
|
|
return plain, nil
|
|
}
|
|
dek, err := currentDEK()
|
|
if err != nil {
|
|
return nil, errMemLocked
|
|
}
|
|
return encPage(dek, plain)
|
|
}
|
|
|
|
// memEncoderNow résout l'état du chiffrement (drapeau + DEK) AVANT toute
|
|
// transaction, et renvoie un encodeur PUR. C'est la seule façon de chiffrer une
|
|
// valeur écrite À L'INTÉRIEUR d'une transaction bbolt : la base n'est pas
|
|
// rentrante — memEncActive lit la configuration, donc rouvrirait la base et se
|
|
// bloquerait sur son propre verrou (vécu sur l'archivage des blocs recall).
|
|
func memEncoderNow() (func([]byte) ([]byte, error), error) {
|
|
if !memEncActive() {
|
|
return func(p []byte) ([]byte, error) { return p, nil }, nil
|
|
}
|
|
dek, err := currentDEK()
|
|
if err != nil {
|
|
return nil, errMemLocked
|
|
}
|
|
return func(p []byte) ([]byte, error) { return encPage(dek, p) }, nil
|
|
}
|
|
|
|
// writeMemFile écrit (crée ou remplace) une page de façon sûre : encode selon
|
|
// l'état du chiffrement, garde un .bak de la version précédente, écrit de façon
|
|
// atomique et vérifiée. Un échec laisse l'ancienne version intacte.
|
|
func writeMemFile(name string, plain []byte) error {
|
|
p, err := safeMemPath(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := encodeMemContent(plain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(memoryDir(), 0o755); err != nil {
|
|
return err
|
|
}
|
|
// .bak de la version précédente (best-effort) : filet en cas de pépin juste
|
|
// après le rename.
|
|
if old, err := os.ReadFile(p); err == nil {
|
|
_ = memWriteFileAtomic(p+".bak", old, 0o600)
|
|
}
|
|
return memWriteFileVerified(p, out, 0o600)
|
|
}
|
|
|
|
// writeMemPlain écrit une page EN CLAIR quel que soit l'état du chiffrement.
|
|
// Réservé au déchiffrement (DisableMemEncryption) : il faut pouvoir poser du
|
|
// clair pendant que le drapeau MEM_ENCRYPTED est encore levé, sinon writeMemFile
|
|
// re-chiffrerait ce qu'on cherche justement à déchiffrer.
|
|
func writeMemPlain(name string, plain []byte) error {
|
|
p, err := safeMemPath(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(memoryDir(), 0o755); err != nil {
|
|
return err
|
|
}
|
|
if old, err := os.ReadFile(p); err == nil {
|
|
_ = memWriteFileAtomic(p+".bak", old, 0o600)
|
|
}
|
|
return memWriteFileVerified(p, plain, 0o600)
|
|
}
|