Files
Loki/internal/nodewire/crypto.go
T
Loki de6551a153 Rebaptise AJEAN en Loki (fork, lignée conservée)
- module github.com/R0m1k3/Loki, cmd/loki, internal/loki (package loki)
- LOKI_HOME, LOKI_MODEL_DIRS, LOKI_SERVICE, LOKI_DL_CONNS ; /etc/loki ;
  units loki-engine / loki-ui ; binaire et aide CLI
- updateRepo pointe sur R0m1k3/Loki (l'auto-update ne tirera plus les
  binaires AJEAN amont)

Conservé à l'identique : le domaine ajean.link (service de tunnel amont),
les littéraux de migration 0.7.x (migrate_07.go), RELEASE_NOTES.md et
LICENSE (historique et licence de l'amont).

go build/vet/test : verts.
2026-08-14 21:33:15 +00:00

202 lines
5.8 KiB
Go

package nodewire
// crypto.go — chiffrement de bout en bout du canal poste↔agent, pour que le
// relais ajean.link ne voie que de l'opaque (« relais aveugle »), exactement
// comme le canal navigateur↔agent.
//
// Deux briques, toutes en stdlib (crypto/ecdh X25519 + AES-GCM) :
// - un SCEAU anonyme vers la clé publique de l'agent (enrôlement) : format
// IDENTIQUE à e2eSeal/e2eOpenSeal de l'agent (ephPub32 || nonce12 || ct,
// clé = SHA256(shared || ephPub || agentPub)) — donc l'agent ouvre le sceau
// du poste avec son code existant, sans dupliquer la crypto ;
// - un CANAL authentifié à clés statiques : K = SHA256(ECDH(postePriv,
// agentPub) || "loki-node-chan-v1"). Le poste et l'agent le calculent tous
// les deux ; le relais, qui n'a aucune des deux clés privées, non. Chaque
// message est scellé AES-GCM avec un nonce à compteur (anti-rejeu / anti-
// réordonnancement à l'intérieur d'une connexion).
import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdh"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
)
// GenKeyPair génère une paire X25519 et la renvoie en hex (priv, pub).
func GenKeyPair() (privHex, pubHex string, err error) {
k, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return "", "", err
}
return hex.EncodeToString(k.Bytes()), hex.EncodeToString(k.PublicKey().Bytes()), nil
}
// PubFromPriv recalcule la clé publique hex à partir de la privée hex.
func PubFromPriv(privHex string) (string, error) {
raw, err := hex.DecodeString(privHex)
if err != nil {
return "", err
}
k, err := ecdh.X25519().NewPrivateKey(raw)
if err != nil {
return "", err
}
return hex.EncodeToString(k.PublicKey().Bytes()), nil
}
func newGCM(key []byte) (cipher.AEAD, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(block)
}
// SealTo scelle plaintext vers recipientPubHex (clé publique de l'agent). Le
// format est celui qu'ouvre e2eOpenSeal de l'agent : ephPub32 || nonce12 || ct.
func SealTo(recipientPubHex string, plaintext []byte) ([]byte, error) {
recBytes, err := hex.DecodeString(recipientPubHex)
if err != nil {
return nil, err
}
recPub, err := ecdh.X25519().NewPublicKey(recBytes)
if err != nil {
return nil, err
}
eph, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
shared, err := eph.ECDH(recPub)
if err != nil {
return nil, err
}
key := sealKey(shared, eph.PublicKey().Bytes(), recBytes)
gcm, err := newGCM(key)
if err != nil {
return nil, err
}
nonce := make([]byte, 12)
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
ct := gcm.Seal(nil, nonce, plaintext, nil)
out := make([]byte, 0, 32+12+len(ct))
out = append(out, eph.PublicKey().Bytes()...)
out = append(out, nonce...)
out = append(out, ct...)
return out, nil
}
// sealKey — identique à sealKey de l'agent (relay_e2e.go). Ne pas modifier sans
// changer les deux : c'est ce qui garantit que l'agent ouvre le sceau du poste.
func sealKey(shared, ephPub, recipientPub []byte) []byte {
h := sha256.New()
h.Write(shared)
h.Write(ephPub)
h.Write(recipientPub)
return h.Sum(nil)
}
// ChannelKey dérive la clé du canal poste↔agent à partir de la clé PRIVÉE (hex)
// d'un bout et de la clé PUBLIQUE (hex) de l'autre. Symétrique : les deux bouts
// obtiennent le même secret.
func ChannelKey(privHex, peerPubHex string) ([]byte, error) {
pr, err := hex.DecodeString(privHex)
if err != nil {
return nil, err
}
priv, err := ecdh.X25519().NewPrivateKey(pr)
if err != nil {
return nil, err
}
pb, err := hex.DecodeString(peerPubHex)
if err != nil {
return nil, err
}
peer, err := ecdh.X25519().NewPublicKey(pb)
if err != nil {
return nil, err
}
ss, err := priv.ECDH(peer)
if err != nil {
return nil, err
}
h := sha256.New()
h.Write(ss)
h.Write([]byte("loki-node-chan-v1"))
return h.Sum(nil), nil
}
// Frame est un message chiffré du canal : nonce (side||compteur) + ciphertext.
type Frame struct {
N []byte `json:"n"`
C []byte `json:"c"`
}
// Chan chiffre/déchiffre les messages d'un canal, avec des nonces à compteur
// pour interdire rejeu et réordonnancement DANS la connexion. Chaque bout a un
// « côté » (1 octet) distinct → les nonces des deux sens ne se croisent jamais.
type Chan struct {
gcm cipher.AEAD
sendSide byte
recvSide byte
sendCtr uint64
recvNext uint64
}
// NewChan construit un canal à partir de la clé partagée. initiator=true pour le
// POSTE (côté d'envoi 1), false pour l'AGENT (côté d'envoi 2). Les côtés doivent
// être opposés des deux bouts, sinon les nonces entreraient en collision.
func NewChan(key []byte, initiator bool) (*Chan, error) {
gcm, err := newGCM(key)
if err != nil {
return nil, err
}
c := &Chan{gcm: gcm}
if initiator {
c.sendSide, c.recvSide = 1, 2
} else {
c.sendSide, c.recvSide = 2, 1
}
return c, nil
}
func (c *Chan) nonce(side byte, ctr uint64) []byte {
n := make([]byte, 12)
n[0] = side
binary.BigEndian.PutUint64(n[1:9], ctr)
return n
}
// Seal chiffre un message sortant.
func (c *Chan) Seal(plaintext []byte) Frame {
n := c.nonce(c.sendSide, c.sendCtr)
ct := c.gcm.Seal(nil, n, plaintext, nil)
c.sendCtr++
return Frame{N: n, C: ct}
}
// Open déchiffre un message entrant et vérifie l'ordre (compteur strictement
// croissant, bon côté) — un frame rejoué ou réordonné par le relais est rejeté.
func (c *Chan) Open(f Frame) ([]byte, error) {
if len(f.N) != 12 || f.N[0] != c.recvSide {
return nil, errors.New("nonce invalide")
}
ctr := binary.BigEndian.Uint64(f.N[1:9])
if ctr < c.recvNext {
return nil, fmt.Errorf("rejeu détecté (compteur %d < %d)", ctr, c.recvNext)
}
plain, err := c.gcm.Open(nil, f.N, f.C, nil)
if err != nil {
return nil, errors.New("authentification échouée")
}
c.recvNext = ctr + 1
return plain, nil
}