mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
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>
175 lines
5.6 KiB
Go
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
|
|
}
|