diff --git a/e2e.go b/e2e.go index 348f6c9..cb46be8 100644 --- a/e2e.go +++ b/e2e.go @@ -38,10 +38,6 @@ var ( e2eOnce sync.Once e2eKey *ecdh.PrivateKey e2eErr error - // rCache : racines R déjà descellées, indexées par kid (évite de resceller/ouvrir - // à chaque requête). Protégé par mu. - e2eMu sync.Mutex - e2eRoots = map[string][]byte{} ) // e2ePrivateKey charge (ou crée puis persiste) la clé privée X25519 de l'agent. @@ -132,20 +128,6 @@ func sealKey(shared, ephPub, agentPub []byte) []byte { return h.Sum(nil) } -// chatKey dérive la clé AES-GCM du chat depuis la racine R. -func chatKey(r []byte) []byte { - h := sha256.New() - h.Write(r) - h.Write([]byte("ajean-chat-v1")) - return h.Sum(nil) -} - -// kidOf identifie une racine R (8 premiers octets de SHA-256(R), hex). -func kidOf(r []byte) string { - h := sha256.Sum256(r) - return hex.EncodeToString(h[:8]) -} - func newGCM(key []byte) (cipher.AEAD, error) { blk, err := aes.NewCipher(key) if err != nil { @@ -154,61 +136,21 @@ func newGCM(key []byte) (cipher.AEAD, error) { return cipher.NewGCM(blk) } -// e2eResolveRoot retourne la racine R pour ce kid, en la descellant si besoin et -// en la mémorisant. sealedR peut être vide si R est déjà connue. -func e2eResolveRoot(kid string, sealed []byte) ([]byte, error) { - e2eMu.Lock() - if r, ok := e2eRoots[kid]; ok { - e2eMu.Unlock() - return r, nil - } - e2eMu.Unlock() - if len(sealed) == 0 { - return nil, fmt.Errorf("racine inconnue et aucun sceau fourni") - } - r, err := e2eOpenSeal(sealed) - if err != nil { - return nil, err - } - if kidOf(r) != kid { - return nil, fmt.Errorf("kid ne correspond pas au sceau") - } - e2eMu.Lock() - e2eRoots[kid] = r - e2eMu.Unlock() - return r, nil -} - -// handleE2EChat : chat chiffré de bout en bout. Reçoit une enveloppe scellée, -// déchiffre la requête avec la clé dérivée de R, exécute le chat, et chiffre -// CHAQUE événement SSE. Le relais ne voit que de l'opaque. +// handleE2EChat : chat chiffré de bout en bout, AUTHENTIFIÉ. L'enveloppe est +// déchiffrée avec la clé du canal authentifié (liée à l'identité appairée de +// l'utilisateur) — un relais ne peut ni la lire ni la forger. Chaque événement SSE +// est chiffré. Le relais ne voit que de l'opaque. func handleE2EChat(w http.ResponseWriter, r *http.Request) { - var env struct{ Kid, SealedR, Iv, Ct string } - if err := json.NewDecoder(r.Body).Decode(&env); err != nil { - http.Error(w, "e2e: enveloppe invalide", 400) - return - } - var sealed []byte - if env.SealedR != "" { - sealed, _ = base64.StdEncoding.DecodeString(env.SealedR) - } - root, err := e2eResolveRoot(env.Kid, sealed) + plain, key, err := e2eAuthOpenReq(r) if err != nil { - http.Error(w, "e2e: "+err.Error(), 400) + http.Error(w, "e2e: "+err.Error(), http.StatusForbidden) return } - gcm, err := newGCM(chatKey(root)) + gcm, err := newGCM(key) if err != nil { http.Error(w, "e2e: clé", 500) return } - iv, _ := base64.StdEncoding.DecodeString(env.Iv) - ct, _ := base64.StdEncoding.DecodeString(env.Ct) - plain, err := gcm.Open(nil, iv, ct, nil) - if err != nil { - http.Error(w, "e2e: déchiffrement", 400) - return - } var body chatReq if err := json.Unmarshal(plain, &body); err != nil { http.Error(w, "e2e: requête", 400) @@ -243,32 +185,16 @@ func handleE2EChat(w http.ResponseWriter, r *http.Request) { // skills, service…) transite par le relais SANS qu'il en voie le contenu. « Zéro // exception » : le tunnel refuse tout /api/* en clair, seuls /api/e2e/* passent. func handleE2EReq(w http.ResponseWriter, r *http.Request, inner http.Handler) { - var env struct{ Kid, SealedR, Iv, Ct string } - if err := json.NewDecoder(r.Body).Decode(&env); err != nil { - http.Error(w, "e2e: enveloppe invalide", 400) - return - } - var sealed []byte - if env.SealedR != "" { - sealed, _ = base64.StdEncoding.DecodeString(env.SealedR) - } - root, err := e2eResolveRoot(env.Kid, sealed) + plain, key, err := e2eAuthOpenReq(r) if err != nil { - http.Error(w, "e2e: "+err.Error(), 400) + http.Error(w, "e2e: "+err.Error(), http.StatusForbidden) return } - gcm, err := newGCM(chatKey(root)) + gcm, err := newGCM(key) if err != nil { http.Error(w, "e2e: clé", 500) return } - iv, _ := base64.StdEncoding.DecodeString(env.Iv) - ct, _ := base64.StdEncoding.DecodeString(env.Ct) - plain, err := gcm.Open(nil, iv, ct, nil) - if err != nil { - http.Error(w, "e2e: déchiffrement", 400) - return - } var req struct { Method string `json:"method"` Path string `json:"path"` diff --git a/link.go b/link.go index f060be5..1ba2747 100644 --- a/link.go +++ b/link.go @@ -115,8 +115,11 @@ func cmdLink(args []string) error { // est chiffré vers CET agent et pas vers un relais qui se ferait passer pour lui. if fp := e2eFingerprint(); fp != "" { fmt.Printf("\n%s empreinte E2E de cette machine :\n %s\n", green("[e2e]"), bold(fp)) - fmt.Printf(" Confirme-la dans le portail (Mon compte → serveur) pour activer la boîte noire.\n\n") + fmt.Printf(" Confirme-la dans le portail (Mon compte → serveur) pour activer la boîte noire.\n") } + // Code d'appairage : à saisir UNE FOIS dans le portail. Authentifie TON navigateur + // auprès de l'agent → même un relais compromis ne peut pas émettre de commandes. + fmt.Printf(" Code d'appairage (à saisir une fois dans le portail) : %s\n\n", bold(currentPairCode())) // On construit le handler une seule fois ; il est servi à travers chaque tunnel. handler := newLinkHandler() @@ -173,6 +176,10 @@ func newLinkHandler() http.Handler { handleE2EReq(w, r, web) // dispatche dans le handler local authentifié return } + if p == "/api/e2e/pair" { + handleE2EPair(w, r) // appairage d'une identité utilisateur (code hors-bande) + return + } web.ServeHTTP(w, r) // /api/e2e/chat est routé par le mux return }