feat(cybersecurity): auto-commit con 48 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
package cybersecurity
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// --- GenerateIdentity ---
|
||||
|
||||
func TestGenerateIdentity(t *testing.T) {
|
||||
t.Run("genera keypairs con longitudes correctas", func(t *testing.T) {
|
||||
id, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateIdentity() error = %v", err)
|
||||
}
|
||||
if len(id.SignPub) != 32 {
|
||||
t.Errorf("SignPub len = %d, want 32", len(id.SignPub))
|
||||
}
|
||||
if len(id.SignPriv) != 64 {
|
||||
t.Errorf("SignPriv len = %d, want 64", len(id.SignPriv))
|
||||
}
|
||||
if len(id.KexPub) != 32 {
|
||||
t.Errorf("KexPub len = %d, want 32", len(id.KexPub))
|
||||
}
|
||||
if len(id.KexPriv) != 32 {
|
||||
t.Errorf("KexPriv len = %d, want 32", len(id.KexPriv))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("dos llamadas producen identidades distintas", func(t *testing.T) {
|
||||
id1, err1 := GenerateIdentity()
|
||||
id2, err2 := GenerateIdentity()
|
||||
if err1 != nil || err2 != nil {
|
||||
t.Fatal("GenerateIdentity() error inesperado")
|
||||
}
|
||||
if bytes.Equal(id1.SignPub, id2.SignPub) {
|
||||
t.Error("SignPub idénticos en dos identidades distintas")
|
||||
}
|
||||
if bytes.Equal(id1.KexPub, id2.KexPub) {
|
||||
t.Error("KexPub idénticos en dos identidades distintas")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- SealAEAD / OpenAEAD ---
|
||||
|
||||
func TestSealOpenAEAD(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
for i := range key {
|
||||
key[i] = byte(i + 1)
|
||||
}
|
||||
plaintext := []byte("mensaje secreto del bus de mensajería")
|
||||
aad := []byte("room:sala-general")
|
||||
|
||||
t.Run("round-trip con aad", func(t *testing.T) {
|
||||
nonce, ct, err := SealAEAD(key, plaintext, aad)
|
||||
if err != nil {
|
||||
t.Fatalf("SealAEAD error = %v", err)
|
||||
}
|
||||
got, err := OpenAEAD(key, nonce, ct, aad)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenAEAD error = %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, plaintext) {
|
||||
t.Errorf("got %q, want %q", got, plaintext)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("round-trip sin aad (nil)", func(t *testing.T) {
|
||||
nonce, ct, err := SealAEAD(key, plaintext, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SealAEAD error = %v", err)
|
||||
}
|
||||
got, err := OpenAEAD(key, nonce, ct, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenAEAD error = %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, plaintext) {
|
||||
t.Errorf("got %q, want %q", got, plaintext)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error con clave de longitud incorrecta", func(t *testing.T) {
|
||||
_, _, err := SealAEAD(key[:16], plaintext, nil)
|
||||
if err == nil {
|
||||
t.Error("esperaba error con clave de 16 bytes, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error de autenticacion con ciphertext modificado", func(t *testing.T) {
|
||||
nonce, ct, err := SealAEAD(key, plaintext, aad)
|
||||
if err != nil {
|
||||
t.Fatalf("SealAEAD error = %v", err)
|
||||
}
|
||||
ct[0] ^= 0xFF // corromper el primer byte
|
||||
_, err = OpenAEAD(key, nonce, ct, aad)
|
||||
if err == nil {
|
||||
t.Error("esperaba error de autenticación con ciphertext corrupto, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error de autenticacion con aad distinto", func(t *testing.T) {
|
||||
nonce, ct, err := SealAEAD(key, plaintext, aad)
|
||||
if err != nil {
|
||||
t.Fatalf("SealAEAD error = %v", err)
|
||||
}
|
||||
_, err = OpenAEAD(key, nonce, ct, []byte("room:otra-sala"))
|
||||
if err == nil {
|
||||
t.Error("esperaba error de autenticación con aad distinto, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nonces distintos en llamadas sucesivas", func(t *testing.T) {
|
||||
n1, _, err1 := SealAEAD(key, plaintext, nil)
|
||||
n2, _, err2 := SealAEAD(key, plaintext, nil)
|
||||
if err1 != nil || err2 != nil {
|
||||
t.Fatal("SealAEAD error inesperado")
|
||||
}
|
||||
if bytes.Equal(n1, n2) {
|
||||
t.Error("nonces iguales en dos llamadas sucesivas (no aleatorios)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- SealKeyBox / OpenKeyBox ---
|
||||
|
||||
func TestSealOpenKeyBox(t *testing.T) {
|
||||
t.Run("round-trip con identidad generada", func(t *testing.T) {
|
||||
id, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateIdentity error = %v", err)
|
||||
}
|
||||
roomKey := make([]byte, 32)
|
||||
for i := range roomKey {
|
||||
roomKey[i] = byte(i + 42)
|
||||
}
|
||||
|
||||
sealed, err := SealKeyBox(id.KexPub, roomKey)
|
||||
if err != nil {
|
||||
t.Fatalf("SealKeyBox error = %v", err)
|
||||
}
|
||||
|
||||
opened, err := OpenKeyBox(id.KexPub, id.KexPriv, sealed)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenKeyBox error = %v", err)
|
||||
}
|
||||
if !bytes.Equal(opened, roomKey) {
|
||||
t.Errorf("got %x, want %x", opened, roomKey)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error con recipientKexPub de longitud incorrecta", func(t *testing.T) {
|
||||
_, err := SealKeyBox(make([]byte, 16), []byte("secret"))
|
||||
if err == nil {
|
||||
t.Error("esperaba error con kexPub de 16 bytes, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error al abrir con clave equivocada", func(t *testing.T) {
|
||||
id, _ := GenerateIdentity()
|
||||
other, _ := GenerateIdentity()
|
||||
sealed, err := SealKeyBox(id.KexPub, []byte("roomkey"))
|
||||
if err != nil {
|
||||
t.Fatalf("SealKeyBox error = %v", err)
|
||||
}
|
||||
_, err = OpenKeyBox(other.KexPub, other.KexPriv, sealed)
|
||||
if err == nil {
|
||||
t.Error("esperaba error al abrir con keypair distinto, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error con mensaje truncado", func(t *testing.T) {
|
||||
id, _ := GenerateIdentity()
|
||||
_, err := OpenKeyBox(id.KexPub, id.KexPriv, []byte("corto"))
|
||||
if err == nil {
|
||||
t.Error("esperaba error con sealedMsg truncado, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- SignEd25519 / VerifyEd25519 ---
|
||||
|
||||
func TestSignVerifyEd25519(t *testing.T) {
|
||||
t.Run("firma y verificacion exitosa", func(t *testing.T) {
|
||||
id, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateIdentity error = %v", err)
|
||||
}
|
||||
msg := []byte("evento:room_key_rotation:v2")
|
||||
sig := SignEd25519(id.SignPriv, msg)
|
||||
if len(sig) != 64 {
|
||||
t.Errorf("sig len = %d, want 64", len(sig))
|
||||
}
|
||||
if !VerifyEd25519(id.SignPub, msg, sig) {
|
||||
t.Error("VerifyEd25519 devolvió false para firma válida")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("firma es determinista (misma entrada, misma firma)", func(t *testing.T) {
|
||||
id, _ := GenerateIdentity()
|
||||
msg := []byte("determinismo criptografico")
|
||||
sig1 := SignEd25519(id.SignPriv, msg)
|
||||
sig2 := SignEd25519(id.SignPriv, msg)
|
||||
if !bytes.Equal(sig1, sig2) {
|
||||
t.Error("Ed25519 debe ser determinista: mismas entradas deben producir misma firma")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falla con mensaje modificado", func(t *testing.T) {
|
||||
id, _ := GenerateIdentity()
|
||||
msg := []byte("mensaje original")
|
||||
sig := SignEd25519(id.SignPriv, msg)
|
||||
modified := []byte("mensaje modificado")
|
||||
if VerifyEd25519(id.SignPub, modified, sig) {
|
||||
t.Error("VerifyEd25519 devolvió true para mensaje modificado")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falla con clave publica incorrecta", func(t *testing.T) {
|
||||
id1, _ := GenerateIdentity()
|
||||
id2, _ := GenerateIdentity()
|
||||
msg := []byte("autenticidad del remitente")
|
||||
sig := SignEd25519(id1.SignPriv, msg)
|
||||
if VerifyEd25519(id2.SignPub, msg, sig) {
|
||||
t.Error("VerifyEd25519 devolvió true con clave pública de otra identidad")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falla con firma corrupta", func(t *testing.T) {
|
||||
id, _ := GenerateIdentity()
|
||||
msg := []byte("integridad")
|
||||
sig := SignEd25519(id.SignPriv, msg)
|
||||
sig[0] ^= 0xFF
|
||||
if VerifyEd25519(id.SignPub, msg, sig) {
|
||||
t.Error("VerifyEd25519 devolvió true con firma corrupta")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- Integración: flujo completo megolm-reducido ---
|
||||
|
||||
func TestE2EMessagingFlow(t *testing.T) {
|
||||
t.Run("flujo completo: generar identidad, distribuir clave de sala, cifrar y firmar mensaje", func(t *testing.T) {
|
||||
// Servidor genera identidad
|
||||
server, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateIdentity server: %v", err)
|
||||
}
|
||||
// Usuario genera identidad
|
||||
user, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateIdentity user: %v", err)
|
||||
}
|
||||
|
||||
// Servidor genera clave de sala y la distribuye al usuario cifrada con su KexPub
|
||||
roomKey := make([]byte, 32)
|
||||
for i := range roomKey {
|
||||
roomKey[i] = byte(i)
|
||||
}
|
||||
sealedKey, err := SealKeyBox(user.KexPub, roomKey)
|
||||
if err != nil {
|
||||
t.Fatalf("SealKeyBox: %v", err)
|
||||
}
|
||||
|
||||
// Usuario desella la clave de sala
|
||||
gotRoomKey, err := OpenKeyBox(user.KexPub, user.KexPriv, sealedKey)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenKeyBox: %v", err)
|
||||
}
|
||||
if !bytes.Equal(gotRoomKey, roomKey) {
|
||||
t.Fatal("clave de sala distribuida no coincide")
|
||||
}
|
||||
|
||||
// Usuario cifra un mensaje con la clave de sala
|
||||
plainMsg := []byte("hola sala, este es mi primer mensaje cifrado e2e")
|
||||
aad := []byte("room:sala-secreta:seq:1")
|
||||
nonce, ct, err := SealAEAD(gotRoomKey, plainMsg, aad)
|
||||
if err != nil {
|
||||
t.Fatalf("SealAEAD: %v", err)
|
||||
}
|
||||
|
||||
// Usuario firma el ciphertext para autenticación del remitente
|
||||
sig := SignEd25519(user.SignPriv, ct)
|
||||
|
||||
// Receptor verifica firma del remitente
|
||||
if !VerifyEd25519(user.SignPub, ct, sig) {
|
||||
t.Fatal("verificación de firma del remitente falló")
|
||||
}
|
||||
|
||||
// Receptor descifra el mensaje
|
||||
decrypted, err := OpenAEAD(gotRoomKey, nonce, ct, aad)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenAEAD: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decrypted, plainMsg) {
|
||||
t.Errorf("mensaje descifrado %q != original %q", decrypted, plainMsg)
|
||||
}
|
||||
|
||||
// Servidor tiene distinta identidad que el usuario (las claves no se confunden)
|
||||
if bytes.Equal(server.SignPub, user.SignPub) {
|
||||
t.Error("server y user tienen la misma clave pública de firma")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cybersecurity
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
// Identity holds a dual keypair for a messaging participant:
|
||||
// an Ed25519 keypair for signing and a X25519 keypair for key exchange.
|
||||
type Identity struct {
|
||||
SignPub []byte // Ed25519 public key (32 bytes)
|
||||
SignPriv []byte // Ed25519 private key (64 bytes)
|
||||
KexPub []byte // X25519 public key (32 bytes)
|
||||
KexPriv []byte // X25519 private key (32 bytes)
|
||||
}
|
||||
|
||||
// GenerateIdentity creates a new Identity with freshly generated Ed25519 and X25519 keypairs.
|
||||
// Ed25519 keys are used for signing; X25519 keys for key exchange (sealed box).
|
||||
func GenerateIdentity() (Identity, error) {
|
||||
// Ed25519 keypair for message signing
|
||||
signPub, signPriv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return Identity{}, err
|
||||
}
|
||||
|
||||
// X25519 keypair for key exchange (nacl/box uses Curve25519 internally)
|
||||
kexPub, kexPriv, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return Identity{}, err
|
||||
}
|
||||
|
||||
return Identity{
|
||||
SignPub: []byte(signPub),
|
||||
SignPriv: []byte(signPriv),
|
||||
KexPub: kexPub[:],
|
||||
KexPriv: kexPriv[:],
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: generate_identity
|
||||
kind: function
|
||||
lang: go
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func GenerateIdentity() (Identity, error)"
|
||||
description: "Genera una identidad criptográfica dual con un par Ed25519 (firma) y un par X25519 (intercambio de claves). Punto de entrada obligatorio para cualquier participante en el bus de mensajería cifrado."
|
||||
tags: [messaging, e2e-crypto, crypto, identity, ed25519, x25519, keygen, e2e-messaging]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports:
|
||||
- crypto/ed25519
|
||||
- crypto/rand
|
||||
- golang.org/x/crypto/nacl/box
|
||||
params:
|
||||
- name: "(ninguno)"
|
||||
desc: "Sin parámetros. Usa crypto/rand como fuente de entropía del sistema."
|
||||
output: "Identity{SignPub []byte, SignPriv []byte, KexPub []byte, KexPriv []byte} o error si falla el RNG del sistema."
|
||||
tested: true
|
||||
tests:
|
||||
- "genera keypairs con longitudes correctas"
|
||||
- "dos llamadas producen identidades distintas"
|
||||
test_file_path: "functions/cybersecurity/e2e_messaging_crypto_test.go"
|
||||
file_path: "functions/cybersecurity/generate_identity.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
id, err := cybersecurity.GenerateIdentity()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// id.SignPub / id.SignPriv — par Ed25519 para firmar mensajes
|
||||
// id.KexPub / id.KexPriv — par X25519 para recibir claves de sala cifradas
|
||||
fmt.Printf("identity pub(sign)=%x pub(kex)=%x\n", id.SignPub, id.KexPub)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Al registrar un nuevo participante en el bus de mensajería: llama GenerateIdentity una sola vez por dispositivo/sesión, persiste los bytes de las cuatro claves de forma segura, y publica `SignPub` + `KexPub` en el directorio de participantes.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- La función depende de `crypto/rand`; en entornos con entropía insuficiente (contenedores recién arrancados) puede bloquearse brevemente.
|
||||
- `SignPriv` tiene 64 bytes (no 32): Ed25519 concatena seed (32) + clave pública (32) internamente. No truncar.
|
||||
- `KexPub`/`KexPriv` son exactamente 32 bytes (Curve25519). Pasar exactamente esos slices a `SealKeyBox`/`OpenKeyBox`.
|
||||
- Nunca reutilizar una identidad entre dispositivos distintos del mismo usuario sin un protocolo de clonado seguro.
|
||||
@@ -0,0 +1,29 @@
|
||||
package cybersecurity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
// OpenAEAD decrypts a ciphertext produced by SealAEAD using ChaCha20-Poly1305.
|
||||
// key must be exactly 32 bytes. nonce must match the one returned by SealAEAD.
|
||||
// aad must match what was passed to SealAEAD (can be nil).
|
||||
// Returns an error if authentication fails (tampered ciphertext, wrong key, or wrong aad).
|
||||
func OpenAEAD(key, nonce, ciphertext, aad []byte) ([]byte, error) {
|
||||
if len(key) != chacha20poly1305.KeySize {
|
||||
return nil, fmt.Errorf("open_aead: key must be %d bytes, got %d", chacha20poly1305.KeySize, len(key))
|
||||
}
|
||||
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open_aead: create cipher: %w", err)
|
||||
}
|
||||
|
||||
plaintext, err := aead.Open(nil, nonce, ciphertext, aad)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open_aead: authentication failed: %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: open_aead
|
||||
kind: function
|
||||
lang: go
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func OpenAEAD(key, nonce, ciphertext, aad []byte) ([]byte, error)"
|
||||
description: "Descifra y autentica un ciphertext producido por SealAEAD usando ChaCha20-Poly1305. Devuelve error explícito si la autenticación falla (ciphertext alterado, clave incorrecta o AAD distinto)."
|
||||
tags: [messaging, e2e-crypto, crypto, aead, chacha20poly1305, symmetric, e2e-messaging]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports:
|
||||
- golang.org/x/crypto/chacha20poly1305
|
||||
params:
|
||||
- name: key
|
||||
desc: "Clave simétrica de exactamente 32 bytes. Debe ser la misma usada en SealAEAD."
|
||||
- name: nonce
|
||||
desc: "Nonce de 12 bytes devuelto por SealAEAD. Debe transmitirse junto al ciphertext."
|
||||
- name: ciphertext
|
||||
desc: "Ciphertext producido por SealAEAD (incluye los 16 bytes del tag Poly1305)."
|
||||
- name: aad
|
||||
desc: "Datos autenticados adicionales. Debe ser idéntico al aad usado en SealAEAD, o nil si se pasó nil."
|
||||
output: "Plaintext descifrado, o error si la autenticación falla o la clave tiene longitud incorrecta."
|
||||
tested: true
|
||||
tests:
|
||||
- "round-trip con aad"
|
||||
- "round-trip sin aad (nil)"
|
||||
- "error con clave de longitud incorrecta"
|
||||
- "error de autenticacion con ciphertext modificado"
|
||||
- "error de autenticacion con aad distinto"
|
||||
- "nonces distintos en llamadas sucesivas"
|
||||
test_file_path: "functions/cybersecurity/e2e_messaging_crypto_test.go"
|
||||
file_path: "functions/cybersecurity/open_aead.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// nonce y ct vienen de SealAEAD; aad debe reconstruirse igual
|
||||
aad := []byte("room:sala-general:seq:42")
|
||||
plaintext, err := cybersecurity.OpenAEAD(key, nonce, ct, aad)
|
||||
if err != nil {
|
||||
// mensaje alterado, clave incorrecta o aad distinto — descartar
|
||||
log.Printf("autenticación fallida: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("mensaje: %s\n", plaintext)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Al recibir un mensaje del bus: después de resolver la room key con OpenKeyBox, llama OpenAEAD para descifrar y verificar integridad. Si devuelve error, el mensaje llegó corrupto o fue alterado en tránsito — descartar siempre, nunca procesar plaintext parcial.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El error no distingue entre "clave incorrecta", "nonce incorrecto" y "ciphertext alterado": todos devuelven el mismo error de autenticación por diseño (evita oráculos de padding).
|
||||
- Si el ciphertext tiene menos de 16 bytes, la función devuelve error antes de intentar descifrar.
|
||||
- El aad debe ser reconstructible por el receptor de forma independiente (no viaja en el mensaje cifrado).
|
||||
@@ -0,0 +1,32 @@
|
||||
package cybersecurity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
// OpenKeyBox decrypts a sealed box produced by SealKeyBox using the recipient's X25519 keypair.
|
||||
// kexPub and kexPriv must each be exactly 32 bytes and correspond to the public key
|
||||
// passed to SealKeyBox as recipientKexPub.
|
||||
// Returns an error if decryption or authentication fails.
|
||||
func OpenKeyBox(kexPub, kexPriv, sealedMsg []byte) ([]byte, error) {
|
||||
if len(kexPub) != 32 {
|
||||
return nil, fmt.Errorf("open_key_box: kexPub must be 32 bytes, got %d", len(kexPub))
|
||||
}
|
||||
if len(kexPriv) != 32 {
|
||||
return nil, fmt.Errorf("open_key_box: kexPriv must be 32 bytes, got %d", len(kexPriv))
|
||||
}
|
||||
|
||||
var pub [32]byte
|
||||
var priv [32]byte
|
||||
copy(pub[:], kexPub)
|
||||
copy(priv[:], kexPriv)
|
||||
|
||||
plaintext, ok := box.OpenAnonymous(nil, sealedMsg, &pub, &priv)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("open_key_box: decryption failed (authentication error or corrupted message)")
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: open_key_box
|
||||
kind: function
|
||||
lang: go
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func OpenKeyBox(kexPub, kexPriv, sealedMsg []byte) ([]byte, error)"
|
||||
description: "Descifra un sealed box anónimo producido por SealKeyBox usando el par X25519 del destinatario. Devuelve error si la autenticación falla o el mensaje está corrupto."
|
||||
tags: [messaging, e2e-crypto, crypto, nacl, x25519, sealed-box, key-distribution, e2e-messaging]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports:
|
||||
- golang.org/x/crypto/nacl/box
|
||||
params:
|
||||
- name: kexPub
|
||||
desc: "Clave pública X25519 del destinatario (exactamente 32 bytes). Debe coincidir con la usada en SealKeyBox."
|
||||
- name: kexPriv
|
||||
desc: "Clave privada X25519 del destinatario (exactamente 32 bytes). Viene del campo KexPriv de su Identity."
|
||||
- name: sealedMsg
|
||||
desc: "Sealed box producido por SealKeyBox. Mínimo 48 bytes (32 overhead ephemeral + 16 tag)."
|
||||
output: "Secreto descifrado (ej. room key de 32 bytes), o error si la autenticación falla, el par de claves no coincide, o el mensaje está truncado."
|
||||
tested: true
|
||||
tests:
|
||||
- "round-trip con identidad generada"
|
||||
- "error con recipientKexPub de longitud incorrecta"
|
||||
- "error al abrir con clave equivocada"
|
||||
- "error con mensaje truncado"
|
||||
test_file_path: "functions/cybersecurity/e2e_messaging_crypto_test.go"
|
||||
file_path: "functions/cybersecurity/open_key_box.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Receptor obtiene su Identity del almacén seguro
|
||||
id, _ := loadIdentityFromSecureStorage()
|
||||
roomKey, err := cybersecurity.OpenKeyBox(id.KexPub, id.KexPriv, sealedMsgFromServer)
|
||||
if err != nil {
|
||||
log.Printf("no se pudo abrir la room key: %v", err)
|
||||
return
|
||||
}
|
||||
// roomKey lista para usar en SealAEAD / OpenAEAD
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Al recibir una distribución de clave de sala del servidor: llama OpenKeyBox con el par X25519 propio para recuperar la room key simétrica. Después de obtenerla, úsala en OpenAEAD para descifrar los mensajes de esa sala.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El error no distingue entre "clave incorrecta" y "mensaje corrupto" por diseño de seguridad.
|
||||
- Si `sealedMsg` tiene menos de 48 bytes (overhead mínimo del sealed box), la función devuelve error sin intentar descifrar.
|
||||
- `kexPub` y `kexPriv` deben ser el par correspondiente: pasar la pubkey de otro usuario con la privkey propia siempre falla.
|
||||
- La room key recuperada es sensible: no logearla ni incluirla en mensajes de error.
|
||||
@@ -0,0 +1,31 @@
|
||||
package cybersecurity
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
// SealAEAD encrypts plaintext with ChaCha20-Poly1305, returning a random nonce and ciphertext.
|
||||
// key must be exactly 32 bytes. aad (additional authenticated data) may be nil.
|
||||
// The returned nonce must be stored alongside the ciphertext and passed to OpenAEAD.
|
||||
func SealAEAD(key, plaintext, aad []byte) (nonce, ciphertext []byte, err error) {
|
||||
if len(key) != chacha20poly1305.KeySize {
|
||||
return nil, nil, fmt.Errorf("seal_aead: key must be %d bytes, got %d", chacha20poly1305.KeySize, len(key))
|
||||
}
|
||||
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("seal_aead: create cipher: %w", err)
|
||||
}
|
||||
|
||||
nonce = make([]byte, aead.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, nil, fmt.Errorf("seal_aead: generate nonce: %w", err)
|
||||
}
|
||||
|
||||
ciphertext = aead.Seal(nil, nonce, plaintext, aad)
|
||||
return nonce, ciphertext, nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: seal_aead
|
||||
kind: function
|
||||
lang: go
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func SealAEAD(key, plaintext, aad []byte) (nonce, ciphertext []byte, err error)"
|
||||
description: "Cifra plaintext con ChaCha20-Poly1305 usando una clave simétrica de 32 bytes. Genera un nonce aleatorio por llamada. Admite datos autenticados adicionales (AAD) para vincular contexto al cifrado sin cifrarlo."
|
||||
tags: [messaging, e2e-crypto, crypto, aead, chacha20poly1305, symmetric, e2e-messaging]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports:
|
||||
- crypto/rand
|
||||
- golang.org/x/crypto/chacha20poly1305
|
||||
params:
|
||||
- name: key
|
||||
desc: "Clave simétrica de exactamente 32 bytes (256 bits). Típicamente la room key distribuida con SealKeyBox."
|
||||
- name: plaintext
|
||||
desc: "Bytes a cifrar. Puede ser vacío."
|
||||
- name: aad
|
||||
desc: "Datos autenticados adicionales (AAD): se autentican pero no se cifran. Útil para room ID, número de secuencia, etc. Puede ser nil."
|
||||
output: "nonce (12 bytes aleatorios), ciphertext (plaintext cifrado + 16 bytes de tag Poly1305), o error si la clave tiene longitud incorrecta o falla el RNG."
|
||||
tested: true
|
||||
tests:
|
||||
- "round-trip con aad"
|
||||
- "round-trip sin aad (nil)"
|
||||
- "error con clave de longitud incorrecta"
|
||||
- "error de autenticacion con ciphertext modificado"
|
||||
- "error de autenticacion con aad distinto"
|
||||
- "nonces distintos en llamadas sucesivas"
|
||||
test_file_path: "functions/cybersecurity/e2e_messaging_crypto_test.go"
|
||||
file_path: "functions/cybersecurity/seal_aead.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
key := make([]byte, 32) // en producción: room key distribuida con SealKeyBox
|
||||
aad := []byte("room:sala-general:seq:42")
|
||||
nonce, ct, err := cybersecurity.SealAEAD(key, []byte("hola sala"), aad)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Almacenar nonce junto al ciphertext para descifrar después
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Al cifrar cada mensaje en una sala del bus: usa la room key de la sala como `key`, incluye el ID de sala y número de secuencia en `aad` para prevenir replay attacks entre salas, y transmite `nonce + ciphertext` juntos al destinatario.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El nonce es aleatorio (12 bytes): con una misma key, la probabilidad de colisión de nonces es despreciable para <2^32 mensajes, pero en escenarios de alto volumen considera rotar la room key periódicamente.
|
||||
- El ciphertext es 16 bytes más largo que el plaintext (tag Poly1305).
|
||||
- `aad` no viaja cifrado: el destinatario debe reconstruirlo independientemente para verificar. Si aad difiere aunque sea 1 bit, OpenAEAD falla con error de autenticación.
|
||||
- Nunca reutilizar `(key, nonce)` para dos plaintexts distintos: rompe la confidencialidad de ChaCha20.
|
||||
@@ -0,0 +1,28 @@
|
||||
package cybersecurity
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
// SealKeyBox encrypts secret for a recipient identified by their X25519 public key,
|
||||
// using an anonymous sealed box (ephemeral sender keypair, no sender authentication).
|
||||
// Intended for distributing a symmetric room key to a participant.
|
||||
// recipientKexPub must be exactly 32 bytes.
|
||||
func SealKeyBox(recipientKexPub, secret []byte) ([]byte, error) {
|
||||
if len(recipientKexPub) != 32 {
|
||||
return nil, fmt.Errorf("seal_key_box: recipientKexPub must be 32 bytes, got %d", len(recipientKexPub))
|
||||
}
|
||||
|
||||
var recipientKey [32]byte
|
||||
copy(recipientKey[:], recipientKexPub)
|
||||
|
||||
sealed, err := box.SealAnonymous(nil, secret, &recipientKey, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal_key_box: %w", err)
|
||||
}
|
||||
|
||||
return sealed, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: seal_key_box
|
||||
kind: function
|
||||
lang: go
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func SealKeyBox(recipientKexPub, secret []byte) ([]byte, error)"
|
||||
description: "Cifra un secreto (típicamente una room key simétrica) para un destinatario identificado por su clave pública X25519, usando un sealed box anónimo (nacl/box). El emisor no se autentica; usar SignEd25519 por separado si se necesita autenticación del remitente."
|
||||
tags: [messaging, e2e-crypto, crypto, nacl, x25519, sealed-box, key-distribution, e2e-messaging]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports:
|
||||
- crypto/rand
|
||||
- golang.org/x/crypto/nacl/box
|
||||
params:
|
||||
- name: recipientKexPub
|
||||
desc: "Clave pública X25519 del destinatario (exactamente 32 bytes). Viene del campo KexPub de su Identity."
|
||||
- name: secret
|
||||
desc: "Bytes a cifrar. Típicamente una room key de 32 bytes, pero puede ser cualquier secreto."
|
||||
output: "Sealed box cifrado (overhead: 32 bytes de ephemeral pubkey + 16 bytes de tag Poly1305), o error si recipientKexPub no tiene 32 bytes o falla el RNG."
|
||||
tested: true
|
||||
tests:
|
||||
- "round-trip con identidad generada"
|
||||
- "error con recipientKexPub de longitud incorrecta"
|
||||
- "error al abrir con clave equivocada"
|
||||
- "error con mensaje truncado"
|
||||
test_file_path: "functions/cybersecurity/e2e_messaging_crypto_test.go"
|
||||
file_path: "functions/cybersecurity/seal_key_box.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Distribuir room key al usuario al unirse a la sala
|
||||
roomKey := make([]byte, 32) // generada por el servidor de sala
|
||||
sealed, err := cybersecurity.SealKeyBox(user.KexPub, roomKey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Enviar sealed al usuario; solo él puede abrirlo con OpenKeyBox
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Al distribuir una clave simétrica de sala a un nuevo participante: cifra la room key con la KexPub del destinatario antes de transmitirla. El destinatario usa OpenKeyBox para recuperarla. Combinar con SignEd25519 sobre el sealed box si se necesita autenticar que el servidor distribuyó la clave.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El sealed box es anónimo: el receptor no puede verificar quién lo generó. Firmar el sealed box con SignEd25519 si la autenticación del emisor importa.
|
||||
- Overhead fijo: 48 bytes adicionales sobre el secreto (32 ephemeral pubkey + 16 tag).
|
||||
- El sealed box no puede abrirse sin la clave privada X25519 correspondiente: si el usuario pierde KexPriv, la room key es irrecuperable.
|
||||
- `recipientKexPub` debe tener exactamente 32 bytes; la función valida y devuelve error claro si no.
|
||||
@@ -0,0 +1,10 @@
|
||||
package cybersecurity
|
||||
|
||||
import "crypto/ed25519"
|
||||
|
||||
// SignEd25519 signs msg with an Ed25519 private key and returns the 64-byte signature.
|
||||
// priv must be a valid Ed25519 private key (64 bytes as returned by GenerateIdentity or ed25519.GenerateKey).
|
||||
// This function is pure: same inputs always produce the same output (ed25519 is deterministic).
|
||||
func SignEd25519(priv, msg []byte) []byte {
|
||||
return ed25519.Sign(ed25519.PrivateKey(priv), msg)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: sign_ed25519
|
||||
kind: function
|
||||
lang: go
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "func SignEd25519(priv, msg []byte) []byte"
|
||||
description: "Firma un mensaje con una clave privada Ed25519 y devuelve la firma de 64 bytes. Determinista: mismas entradas producen siempre la misma firma. Sin efectos secundarios ni I/O."
|
||||
tags: [messaging, e2e-crypto, crypto, ed25519, signing, pure, e2e-messaging]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports:
|
||||
- crypto/ed25519
|
||||
params:
|
||||
- name: priv
|
||||
desc: "Clave privada Ed25519 de 64 bytes. Viene del campo SignPriv de Identity."
|
||||
- name: msg
|
||||
desc: "Bytes a firmar. Puede ser cualquier dato: ciphertext, evento, room key distribuida, etc."
|
||||
output: "Firma Ed25519 de exactamente 64 bytes. Siempre determinista para la misma (priv, msg)."
|
||||
tested: true
|
||||
tests:
|
||||
- "firma y verificacion exitosa"
|
||||
- "firma es determinista (misma entrada, misma firma)"
|
||||
- "falla con mensaje modificado"
|
||||
- "falla con clave publica incorrecta"
|
||||
- "falla con firma corrupta"
|
||||
test_file_path: "functions/cybersecurity/e2e_messaging_crypto_test.go"
|
||||
file_path: "functions/cybersecurity/sign_ed25519.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Firmar el ciphertext de un mensaje antes de transmitirlo
|
||||
sig := cybersecurity.SignEd25519(id.SignPriv, ciphertext)
|
||||
// Transmitir ciphertext + sig; el receptor verifica con VerifyEd25519
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Después de cifrar un mensaje con SealAEAD: firma el ciphertext (no el plaintext) con tu SignPriv para que el receptor pueda verificar la autoría con VerifyEd25519. También útil para firmar eventos de control del bus (rotación de clave, join/leave de sala).
|
||||
@@ -0,0 +1,10 @@
|
||||
package cybersecurity
|
||||
|
||||
import "crypto/ed25519"
|
||||
|
||||
// VerifyEd25519 reports whether sig is a valid Ed25519 signature of msg under pub.
|
||||
// pub must be a valid Ed25519 public key (32 bytes as returned by GenerateIdentity).
|
||||
// Returns true only if the signature is authentic; false on any mismatch or invalid input.
|
||||
func VerifyEd25519(pub, msg, sig []byte) bool {
|
||||
return ed25519.Verify(ed25519.PublicKey(pub), msg, sig)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: verify_ed25519
|
||||
kind: function
|
||||
lang: go
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "func VerifyEd25519(pub, msg, sig []byte) bool"
|
||||
description: "Verifica una firma Ed25519 sobre un mensaje usando la clave pública del firmante. Devuelve true solo si la firma es auténtica. Sin efectos secundarios ni I/O."
|
||||
tags: [messaging, e2e-crypto, crypto, ed25519, signing, pure, e2e-messaging]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports:
|
||||
- crypto/ed25519
|
||||
params:
|
||||
- name: pub
|
||||
desc: "Clave pública Ed25519 de 32 bytes del firmante. Viene del campo SignPub de su Identity."
|
||||
- name: msg
|
||||
desc: "Mensaje original que fue firmado. Debe ser idéntico al pasado a SignEd25519."
|
||||
- name: sig
|
||||
desc: "Firma de 64 bytes producida por SignEd25519."
|
||||
output: "true si la firma es válida para (pub, msg). false en cualquier otro caso: firma incorrecta, pub equivocada, msg alterado, o sig corrupta."
|
||||
tested: true
|
||||
tests:
|
||||
- "firma y verificacion exitosa"
|
||||
- "firma es determinista (misma entrada, misma firma)"
|
||||
- "falla con mensaje modificado"
|
||||
- "falla con clave publica incorrecta"
|
||||
- "falla con firma corrupta"
|
||||
test_file_path: "functions/cybersecurity/e2e_messaging_crypto_test.go"
|
||||
file_path: "functions/cybersecurity/verify_ed25519.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Receptor verifica autoría antes de descifrar
|
||||
if !cybersecurity.VerifyEd25519(sender.SignPub, ciphertext, sig) {
|
||||
log.Println("firma inválida: mensaje descartado")
|
||||
return
|
||||
}
|
||||
// Solo si la firma es válida, descifrar con OpenAEAD
|
||||
plaintext, err := cybersecurity.OpenAEAD(roomKey, nonce, ciphertext, aad)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Al recibir un mensaje del bus: verifica la firma del remitente sobre el ciphertext antes de intentar descifrar. Devuelve false para cualquier fallo de autenticación — nunca procesar un mensaje con firma inválida.
|
||||
Reference in New Issue
Block a user