mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
Boite noire 100% : canal authentifie (identite utilisateur appairee) + anti-rejeu
- e2eauth.go : identites utilisateur appairees (.authorized_users), code d'appairage hors-bande (log jean link), canal authentifie K=SHA256(ECDH(uPriv,agentPriv)||authchan), AAD upub|ts, anti-rejeu. - e2e.go : handleE2EChat/Req passent par e2eAuthOpenReq (plus de seal anonyme). - link.go : route /api/e2e/pair + affiche le code d'appairage.
This commit is contained in:
1 parent
141512e9cb
commit
d2b99bd6f4
2 files changed
+18
-85
No files matched your search
@@ -38,10 +38,6 @@ var (
|
|||||||
e2eOnce sync.Once
|
e2eOnce sync.Once
|
||||||
e2eKey *ecdh.PrivateKey
|
e2eKey *ecdh.PrivateKey
|
||||||
e2eErr error
|
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.
|
// 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)
|
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) {
|
func newGCM(key []byte) (cipher.AEAD, error) {
|
||||||
blk, err := aes.NewCipher(key)
|
blk, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -154,61 +136,21 @@ func newGCM(key []byte) (cipher.AEAD, error) {
|
|||||||
return cipher.NewGCM(blk)
|
return cipher.NewGCM(blk)
|
||||||
}
|
}
|
||||||
|
|
||||||
// e2eResolveRoot retourne la racine R pour ce kid, en la descellant si besoin et
|
// handleE2EChat : chat chiffré de bout en bout, AUTHENTIFIÉ. L'enveloppe est
|
||||||
// en la mémorisant. sealedR peut être vide si R est déjà connue.
|
// déchiffrée avec la clé du canal authentifié (liée à l'identité appairée de
|
||||||
func e2eResolveRoot(kid string, sealed []byte) ([]byte, error) {
|
// l'utilisateur) — un relais ne peut ni la lire ni la forger. Chaque événement SSE
|
||||||
e2eMu.Lock()
|
// est chiffré. Le relais ne voit que de l'opaque.
|
||||||
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.
|
|
||||||
func handleE2EChat(w http.ResponseWriter, r *http.Request) {
|
func handleE2EChat(w http.ResponseWriter, r *http.Request) {
|
||||||
var env struct{ Kid, SealedR, Iv, Ct string }
|
plain, key, err := e2eAuthOpenReq(r)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "e2e: "+err.Error(), 400)
|
http.Error(w, "e2e: "+err.Error(), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gcm, err := newGCM(chatKey(root))
|
gcm, err := newGCM(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "e2e: clé", 500)
|
http.Error(w, "e2e: clé", 500)
|
||||||
return
|
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
|
var body chatReq
|
||||||
if err := json.Unmarshal(plain, &body); err != nil {
|
if err := json.Unmarshal(plain, &body); err != nil {
|
||||||
http.Error(w, "e2e: requête", 400)
|
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
|
// 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.
|
// exception » : le tunnel refuse tout /api/* en clair, seuls /api/e2e/* passent.
|
||||||
func handleE2EReq(w http.ResponseWriter, r *http.Request, inner http.Handler) {
|
func handleE2EReq(w http.ResponseWriter, r *http.Request, inner http.Handler) {
|
||||||
var env struct{ Kid, SealedR, Iv, Ct string }
|
plain, key, err := e2eAuthOpenReq(r)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "e2e: "+err.Error(), 400)
|
http.Error(w, "e2e: "+err.Error(), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gcm, err := newGCM(chatKey(root))
|
gcm, err := newGCM(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "e2e: clé", 500)
|
http.Error(w, "e2e: clé", 500)
|
||||||
return
|
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 {
|
var req struct {
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
|||||||
@@ -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.
|
// est chiffré vers CET agent et pas vers un relais qui se ferait passer pour lui.
|
||||||
if fp := e2eFingerprint(); fp != "" {
|
if fp := e2eFingerprint(); fp != "" {
|
||||||
fmt.Printf("\n%s empreinte E2E de cette machine :\n %s\n", green("[e2e]"), bold(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.
|
// On construit le handler une seule fois ; il est servi à travers chaque tunnel.
|
||||||
handler := newLinkHandler()
|
handler := newLinkHandler()
|
||||||
@@ -173,6 +176,10 @@ func newLinkHandler() http.Handler {
|
|||||||
handleE2EReq(w, r, web) // dispatche dans le handler local authentifié
|
handleE2EReq(w, r, web) // dispatche dans le handler local authentifié
|
||||||
return
|
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
|
web.ServeHTTP(w, r) // /api/e2e/chat est routé par le mux
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user