mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Chiffrement : la discussion active et le mode Code ne deviennent plus illisibles
Activer le chiffrement de la mémoire chiffrait TOUTES les valeurs du bucket des discussions, y compris des clés que le code lit et écrit en clair (getStr/putStr) : le pointeur de discussion active, le mode Chat|Code de chaque discussion, la puce « passer en mode Code ». Relues comme du charabia, la discussion active devenait introuvable et le mode Code disparaissait. Les critères, lus en clair eux aussi, s'effaçaient. - Ces clés-pointeurs restent en clair (plainStoreKey) : ni rechiffrées, ni comptées comme « chiffrement incomplet ». - Celles qu'une base existante a déjà chiffrées sont remises en clair dès le déverrouillage, avant le rechargement de la discussion active. - Les critères passent par putStoreBytes/getStoreBytes : chiffrés comme le reste du fil, et lisibles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e36b1cf82
commit
9ccdc021cb
4 files changed
+89
-5
No files matched your search
@@ -30,7 +30,7 @@ func critKey(convID string) string { return "crit:" + convID }
|
||||
|
||||
func critList(convID string) []Criterion {
|
||||
var list []Criterion
|
||||
if b := getBytes(bkChat, critKey(convID)); len(b) > 0 {
|
||||
if b, ok := getStoreBytes(bkChat, critKey(convID)); ok && len(b) > 0 {
|
||||
_ = json.Unmarshal(b, &list)
|
||||
}
|
||||
return list
|
||||
@@ -45,7 +45,7 @@ func critSave(convID string, list []Criterion) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = putBytes(bkChat, critKey(convID), b)
|
||||
_ = putStoreBytes(bkChat, critKey(convID), b)
|
||||
}
|
||||
|
||||
func critDrop(convID string) { _ = putBytes(bkChat, critKey(convID), nil) }
|
||||
|
||||
@@ -219,3 +219,45 @@ func TestSnapshotRestore(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Activer le chiffrement ne doit pas rendre illisibles les clés-pointeurs du
|
||||
// bucket des discussions (discussion active, mode Code) ni les critères. Une
|
||||
// version précédente les chiffrait : la discussion active devenait
|
||||
// introuvable. Celles déjà chiffrées sont réparées au déverrouillage.
|
||||
func TestChiffrementGardeLesPointeursLisibles(t *testing.T) {
|
||||
testHome(t)
|
||||
clearMemDEK()
|
||||
id := convEnsureActive()
|
||||
if err := setConvMode(id, "code"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
toolCriteria(map[string]any{"action": "add", "texts": []any{"le build compile"}}, false)
|
||||
if _, err := EnableMemEncryption("motdepasse-fort"); err != nil {
|
||||
t.Fatalf("EnableMemEncryption: %v", err)
|
||||
}
|
||||
if got := getStr(bkChat, ckActive); got != id {
|
||||
t.Fatalf("discussion active illisible après chiffrement : %q", got)
|
||||
}
|
||||
if !convCodeMode() {
|
||||
t.Fatal("mode Code perdu après chiffrement")
|
||||
}
|
||||
if l := critList(id); len(l) != 1 {
|
||||
t.Fatalf("critères perdus après chiffrement : %+v", l)
|
||||
}
|
||||
if !looksEncrypted(getBytes(bkChat, critKey(id))) {
|
||||
t.Fatal("critères en clair alors que les discussions sont chiffrées")
|
||||
}
|
||||
if !bucketFullyEncrypted(bkChat) {
|
||||
t.Fatal("les clés-pointeurs en clair font croire à un chiffrement incomplet")
|
||||
}
|
||||
// Base d'une version précédente : le pointeur a été chiffré.
|
||||
enc, err := encodeMemContent([]byte(id))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = putBytes(bkChat, ckActive, enc)
|
||||
healPlainStoreKeys()
|
||||
if got := getStr(bkChat, ckActive); got != id {
|
||||
t.Fatalf("pointeur chiffré non réparé : %q", got)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ package loki
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// errStoreLocked : écriture chiffrée demandée alors que la DEK n'est pas en RAM.
|
||||
@@ -90,12 +91,46 @@ func getStoreJSON(bucket, key string, dst any) bool {
|
||||
return json.Unmarshal(b, dst) == nil
|
||||
}
|
||||
|
||||
// plainStoreKey : clés d'un bucket chiffré qui restent EN CLAIR, parce que le
|
||||
// code les lit et les écrit avec getStr/putStr — des pointeurs et drapeaux, pas
|
||||
// des données : la discussion active (son id), le mode Chat|Code d'une
|
||||
// discussion, la puce « passer en mode Code » déjà montrée. Chiffrées par
|
||||
// reencryptBucket, elles revenaient à la lecture comme du charabia : la
|
||||
// discussion active devenait introuvable, le mode Code et ses critères
|
||||
// disparaissaient dès l'activation du chiffrement.
|
||||
func plainStoreKey(bucket, key string) bool {
|
||||
if bucket != bkChat {
|
||||
return false
|
||||
}
|
||||
return key == ckActive || strings.HasPrefix(key, "mode:") || strings.HasPrefix(key, "hinted:")
|
||||
}
|
||||
|
||||
// healPlainStoreKeys remet en clair les clés-pointeurs qu'une version
|
||||
// précédente avait chiffrées (voir plainStoreKey). À appeler dès que la DEK est
|
||||
// en RAM, AVANT de recharger la discussion active. Sans DEK, ne fait rien.
|
||||
func healPlainStoreKeys() {
|
||||
if !memUnlocked() {
|
||||
return
|
||||
}
|
||||
for _, bucket := range encryptedBuckets {
|
||||
for k, v := range allKV(bucket) {
|
||||
raw := []byte(v)
|
||||
if !plainStoreKey(bucket, k) || !looksEncrypted(raw) {
|
||||
continue
|
||||
}
|
||||
if plain, err := decodeMemContent(raw); err == nil {
|
||||
_ = putBytes(bucket, k, plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if looksEncrypted(raw) || plainStoreKey(bucket, k) {
|
||||
continue
|
||||
}
|
||||
if err := putStoreBytes(bucket, k, raw); err != nil {
|
||||
@@ -113,7 +148,10 @@ func reencryptBucket(bucket string) error {
|
||||
// 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) {
|
||||
for k, v := range allKV(bucket) {
|
||||
if plainStoreKey(bucket, k) {
|
||||
continue
|
||||
}
|
||||
if v != "" && !looksEncrypted([]byte(v)) {
|
||||
return false
|
||||
}
|
||||
@@ -136,6 +174,7 @@ var encryptedBuckets = []string{bkChat, bkRecall, bkTracker, bkToolRes}
|
||||
|
||||
// reencryptChatStores (re)chiffre les buckets de conversation. Exige la DEK.
|
||||
func reencryptChatStores() error {
|
||||
healPlainStoreKeys()
|
||||
for _, b := range encryptedBuckets {
|
||||
if err := reencryptBucket(b); err != nil {
|
||||
return err
|
||||
|
||||
@@ -757,7 +757,10 @@ func handleMemUnlock(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
setMemDEK(dek)
|
||||
resumeMemMigration() // si une migration attendait le déverrouillage
|
||||
resumeMemMigration() // si une migration attendait le déverrouillage
|
||||
// AVANT le rechargement : la discussion active est désignée par une clé
|
||||
// qu'une version précédente chiffrait par erreur (voir plainStoreKey).
|
||||
healPlainStoreKeys()
|
||||
reloadEncryptedStores() // recharge la discussion chiffrée en RAM
|
||||
// Migration douce : chiffre ce qui serait resté en clair (discussions,
|
||||
// blocs archivés, trackers). Idempotent — n'encode que ce qui ne l'est pas.
|
||||
|
||||
Reference in new issue
Block a user