API interne de chiffrement pour la conversation tenue par Rust

Les discussions sont chiffrées en base quand la mémoire l'est, avec une
clé de données qui ne vit qu'en mémoire du Go après déverrouillage. Une
conversation tenue par la façade fera sceller et ouvrir ses blobs ici :
la clé ne sort jamais. Mémoire verrouillée : 423, jamais d'écriture en
clair ni de perte — l'appelant attend le déverrouillage, comme le Go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-15 08:49:08 +02:00
1 parent f62271b7fe
commit c38b1ac43e
3 files changed
+110

No files matched your search

+4
View File
@@ -71,6 +71,10 @@ func withInternalAPI(next http.Handler) http.Handler {
handleInternalOAIEnd(w, r)
case "/internal/v1/tool/exec":
handleInternalToolExec(w, r)
case "/internal/v1/crypt/seal":
handleInternalCrypt(w, r, true)
case "/internal/v1/crypt/open":
handleInternalCrypt(w, r, false)
default:
http.NotFound(w, r)
}
+56
View File
@@ -0,0 +1,56 @@
package loki
import (
"errors"
"io"
"net/http"
)
// API interne de chiffrement (lot 6 de la migration Rust).
//
// Les discussions sont chiffrées en base quand la mémoire l'est
// (memEncoderNow / decodeMemContent). La clé de données (DEK) ne vit qu'en
// mémoire de ce process, après déverrouillage : une conversation tenue par la
// façade Rust fait donc sceller et ouvrir ses blobs ICI, la clé ne sort jamais.
//
// POST /internal/v1/crypt/seal corps clair → 200 corps scellé (ou clair si
// le chiffrement est coupé)
// POST /internal/v1/crypt/open corps brut → 200 corps clair (un contenu
// non chiffré passe tel quel)
//
// Mémoire verrouillée : 423 (Locked) — l'appelant ne doit ni écrire en clair
// ni perdre la donnée, il attend le déverrouillage comme le Go le fait.
const internalCryptMaxBody = 256 << 20
func handleInternalCrypt(w http.ResponseWriter, r *http.Request, seal bool) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "POST attendu"})
return
}
in, err := io.ReadAll(io.LimitReader(r.Body, internalCryptMaxBody+1))
if err != nil || len(in) > internalCryptMaxBody {
sendJSON(w, http.StatusRequestEntityTooLarge, map[string]any{"error": "corps trop grand ou illisible"})
return
}
var out []byte
if seal {
var enc func([]byte) ([]byte, error)
if enc, err = memEncoderNow(); err == nil {
out, err = enc(in)
}
} else {
out, err = decodeMemContent(in)
}
if errors.Is(err, errMemLocked) {
sendJSON(w, http.StatusLocked, map[string]any{"error": err.Error()})
return
}
if err != nil {
sendJSON(w, http.StatusUnprocessableEntity, map[string]any{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(out)
}
+50
View File
@@ -0,0 +1,50 @@
package loki
import (
"bytes"
"testing"
)
// Sceller puis ouvrir par l'API interne rend le clair ; la clé ne sort pas.
func TestCryptInterneAllerRetour(t *testing.T) {
testHome(t)
t.Setenv(internalTokenEnv, jetonTest)
// Chiffrement coupé : scellé = clair, et un clair s'ouvre tel quel.
w := appelOAIInterne(t, "POST", "/internal/v1/crypt/seal", "bonjour")
if w.Code != 200 || w.Body.String() != "bonjour" {
t.Fatalf("seal sans chiffrement : %d %q", w.Code, w.Body.String())
}
if w := appelOAIInterne(t, "POST", "/internal/v1/crypt/open", "bonjour"); w.Body.String() != "bonjour" {
t.Fatalf("open d'un clair : %q", w.Body.String())
}
// Chiffrement actif, mémoire verrouillée : 423, jamais d'écriture en clair.
if err := putStr(bkConfig, "MEM_ENCRYPTED", "1"); err != nil {
t.Fatal(err)
}
clearMemDEK()
if w := appelOAIInterne(t, "POST", "/internal/v1/crypt/seal", "secret"); w.Code != 423 {
t.Fatalf("seal verrouillé : %d", w.Code)
}
// Déverrouillée : scellé ≠ clair, ouverture = clair.
dek, _ := randBytes(memDEKLen)
setMemDEK(dek)
t.Cleanup(clearMemDEK)
w = appelOAIInterne(t, "POST", "/internal/v1/crypt/seal", "secret")
sealed := w.Body.Bytes()
if w.Code != 200 || bytes.Contains(sealed, []byte("secret")) || !looksEncrypted(sealed) {
t.Fatalf("seal : %d %q", w.Code, sealed)
}
w = appelOAIInterne(t, "POST", "/internal/v1/crypt/open", string(sealed))
if w.Code != 200 || w.Body.String() != "secret" {
t.Fatalf("open : %d %q", w.Code, w.Body.String())
}
// Reverrouillée : un scellé ne s'ouvre plus.
clearMemDEK()
if w := appelOAIInterne(t, "POST", "/internal/v1/crypt/open", string(sealed)); w.Code != 423 {
t.Fatalf("open verrouillé : %d", w.Code)
}
}