mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-15 19:29:16 +02:00
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:
1 parent
f62271b7fe
commit
c38b1ac43e
3 files changed
+110
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user