Files
Loki/internal/loki/mem_store.go
T
MichaelandClaude Opus 5.5 58c2da1302 Chiffrement : les résultats d'outils et les images du fil suivent la mémoire
Les deux reprises d'AJEAN arrivées en parallèle du chiffrement écrivaient
en clair à côté de discussions chiffrées.

- Résultats complets des outils (« voir plus ») : bucket toolres ajouté à
  encryptedBuckets, écrits et relus par putStoreBytes / getStoreBytesErr.
  Mémoire verrouillée : rien n'est écrit, le flux porte le résultat entier.
  Le nettoyage des orphelins ne tourne plus quand la mémoire est
  verrouillée : l'index des discussions revenait vide et TOUT passait pour
  orphelin.
- Images du fil (chatimg/) : même enveloppe que les pages mémoire ;
  verrouillée, l'image reste en base64 dans le message. Elles n'étaient
  jamais effacées : comme un fil rechargé perd ses images (stripImageParts),
  celles de plus de 24 h partent au démarrage et à chaque bascule de
  discussion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:10:56 +02:00

175 lines
5.6 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), et les résultats complets des
// outils gardés pour « voir plus » (bkToolRes — des extraits du fil).
//
// 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, bkToolRes}
// 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
}