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:
MichaelandClaude Opus 5.5 committed 2026-10-02 23:27:43 +02:00
1 parent 5e36b1cf82
commit 9ccdc021cb
4 files changed
+89 -5

No files matched your search

+2 -2
View File
@@ -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) }
+42
View File
@@ -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)
}
}
+41 -2
View File
@@ -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
+4 -1
View File
@@ -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.