mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
- 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.
202 lines
5.8 KiB
Go
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
|
|
}
|