mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-14 02:38:20 +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
174 lines
5.5 KiB
Go
174 lines
5.5 KiB
Go
package loki
|
|
|
|
// mem_store.go — chiffrement transparent de VALEURS de la base (bbolt), pour
|
|
// étendre le chiffrement au-delà des pages mémoire : conversations et archives
|
|
// contiennent aussi des infos sensibles.
|
|
//
|
|
// Même enveloppe que les pages (encPage/decPage + magic) : une valeur est
|
|
// chiffrée quand le chiffrement est actif ET la mémoire déverrouillée, en clair
|
|
// sinon. La détection se fait sur le magic, donc clair et chiffré cohabitent
|
|
// (utile pendant une migration ou un état verrouillé).
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
)
|
|
|
|
// errStoreLocked : écriture chiffrée demandée alors que la DEK n'est pas en RAM.
|
|
var errStoreLocked = errors.New("valeur chiffrée, mémoire verrouillée")
|
|
|
|
// putStoreBytes écrit une valeur, chiffrée si le chiffrement est actif ET
|
|
// déverrouillé. Si actif mais verrouillé, renvoie errStoreLocked SANS écrire
|
|
// (on ne remplace jamais une valeur chiffrée par du clair).
|
|
func putStoreBytes(bucket, key string, plain []byte) error {
|
|
out, err := encodeMemContent(plain)
|
|
if err != nil {
|
|
return errStoreLocked
|
|
}
|
|
return putBytes(bucket, key, out)
|
|
}
|
|
|
|
// getStoreBytes lit une valeur et la déchiffre au besoin. Renvoie (nil, false)
|
|
// si absente, ou si chiffrée alors que la mémoire est verrouillée.
|
|
func getStoreBytes(bucket, key string) ([]byte, bool) {
|
|
raw := getBytes(bucket, key)
|
|
if len(raw) == 0 {
|
|
return nil, false
|
|
}
|
|
dec, err := decodeMemContent(raw)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
return dec, true
|
|
}
|
|
|
|
// storeBytes : getStoreBytes réduit à sa valeur, pour les appelants qui
|
|
// traitent déjà « absent » et « illisible » de la même façon (un fil vide).
|
|
func storeBytes(bucket, key string) []byte {
|
|
b, _ := getStoreBytes(bucket, key)
|
|
return b
|
|
}
|
|
|
|
// getStoreBytesErr fait la même lecture que getStoreBytes mais SANS avaler
|
|
// l'erreur d'accès (getBytes, lui, la jette — c'est le piège documenté sur
|
|
// getBytesErr : « je n'ai pas pu lire » et « il n'y a rien » ne veulent pas dire
|
|
// la même chose). Une valeur absente reste (nil, nil) — err n'est non-nil QUE
|
|
// si la base elle-même n'a pas pu être ouverte/lue (verrou bbolt disputé, disque
|
|
// indisponible…), pas pour une clé qui n'existe simplement pas encore.
|
|
func getStoreBytesErr(bucket, key string) ([]byte, error) {
|
|
raw, err := getBytesErr(bucket, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(raw) == 0 {
|
|
return nil, nil
|
|
}
|
|
dec, err := decodeMemContent(raw)
|
|
if err != nil {
|
|
// Chiffré mais verrouillé (ou payload corrompu) : légitime, pas un échec
|
|
// d'ACCÈS — même repli que getStoreBytes.
|
|
return nil, nil
|
|
}
|
|
return dec, nil
|
|
}
|
|
|
|
// putStoreJSON sérialise puis écrit (chiffré si actif+déverrouillé).
|
|
func putStoreJSON(bucket, key string, v any) error {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return putStoreBytes(bucket, key, b)
|
|
}
|
|
|
|
// getStoreJSON lit puis décode (déchiffre au besoin). false si absent/verrouillé.
|
|
func getStoreJSON(bucket, key string, dst any) bool {
|
|
b, ok := getStoreBytes(bucket, key)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return json.Unmarshal(b, dst) == nil
|
|
}
|
|
|
|
// reencryptBucket (re)chiffre toutes les valeurs d'un bucket encore en clair,
|
|
// avec vérification par relecture. Sûr à rejouer. Exige la DEK en RAM.
|
|
func reencryptBucket(bucket string) error {
|
|
for k, v := range allKV(bucket) {
|
|
raw := []byte(v)
|
|
if looksEncrypted(raw) {
|
|
continue
|
|
}
|
|
if err := putStoreBytes(bucket, k, raw); err != nil {
|
|
return err
|
|
}
|
|
back, ok := getStoreBytes(bucket, k)
|
|
if !ok || string(back) != v {
|
|
return errors.New("vérification post-chiffrement échouée pour " + bucket + "/" + k)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bucketFullyEncrypted indique qu'AUCUNE valeur du bucket n'est en clair (toutes
|
|
// portent le magic). Lisible sans la DEK. Utilisé pour savoir si le chiffrement
|
|
// est réellement complet.
|
|
func bucketFullyEncrypted(bucket string) bool {
|
|
for _, v := range allKV(bucket) {
|
|
if v != "" && !looksEncrypted([]byte(v)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// encryptedBuckets : les buckets dont les valeurs sont chiffrées quand le
|
|
// chiffrement est actif. Ici : les discussions (bkChat porte l'index, la
|
|
// discussion active et le journal de chacune), les blocs archivés au compactage
|
|
// (bkRecall — du verbatim de conversation, donc aussi sensible que le fil) et
|
|
// les trackers (bkTracker, 3ᵉ type de mémoire).
|
|
//
|
|
// Les autres buckets (config, prefs, state, tasks) restent EN CLAIR : ils
|
|
// portent des réglages, pas des données personnelles — et surtout le coffre
|
|
// lui-même vit dans bkState. Le chiffrer avec la DEK qu'il protège ferait une
|
|
// boucle : plus moyen de déverrouiller quoi que ce soit.
|
|
var encryptedBuckets = []string{bkChat, bkRecall, bkTracker}
|
|
|
|
// reencryptChatStores (re)chiffre les buckets de conversation. Exige la DEK.
|
|
func reencryptChatStores() error {
|
|
for _, b := range encryptedBuckets {
|
|
if err := reencryptBucket(b); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// decryptChatStores remet en clair les buckets de conversation. Exige la DEK.
|
|
func decryptChatStores() error {
|
|
for _, b := range encryptedBuckets {
|
|
if err := decryptBucket(b); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// decryptBucket remet en clair toutes les valeurs chiffrées d'un bucket. Exige
|
|
// la DEK en RAM. Sûr à rejouer.
|
|
func decryptBucket(bucket string) error {
|
|
for k, v := range allKV(bucket) {
|
|
raw := []byte(v)
|
|
if !looksEncrypted(raw) {
|
|
continue
|
|
}
|
|
plain, err := decodeMemContent(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := putBytes(bucket, k, plain); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|