Inicial: biblioteca zcatp2p para comunicación P2P segura

- Especificación completa del protocolo (PROTOCOL.md)
- Referencia de API (API.md)
- Implementación crypto: SHA256, ChaCha20-Poly1305
- Device ID con Base32 y verificación Luhn32
- Framing de mensajes (HELLO, PING, DATA, etc.)
- Discovery local UDP broadcast
- Estructura de conexiones y estados
- Build system para Zig 0.15.2

Pendiente: TLS 1.3, STUN, Global Discovery HTTPS, Relay

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-15 01:06:30 +01:00
commit 7e5b16ee15
12 changed files with 3181 additions and 0 deletions

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
# Zig build artifacts
zig-out/
.zig-cache/
# Editor files
*.swp
*.swo
*~
.vscode/
.idea/
# OS files
.DS_Store
Thumbs.db
# Test artifacts
/tmp/

476
API.md Normal file
View file

@ -0,0 +1,476 @@
# zcatp2p - API Reference
## 1. Inicialización y Configuración
### 1.1 Tipos Principales
```zig
/// Identificador único de dispositivo (32 bytes)
pub const DeviceId = [32]u8;
/// Representación corta del Device ID (primeros 8 bytes)
pub const ShortId = u64;
/// Configuración del nodo P2P
pub const Config = struct {
/// Nombre del dispositivo (mostrado a otros peers)
device_name: []const u8 = "zcatp2p",
/// Puerto para conexiones entrantes (0 = aleatorio)
listen_port: u16 = 22000,
/// Habilitar discovery local (LAN)
local_discovery: bool = true,
/// Servidores de discovery global (vacío = deshabilitado)
global_discovery_servers: []const []const u8 = &.{},
/// Servidores STUN para NAT traversal
stun_servers: []const []const u8 = &.{
"stun.l.google.com:19302",
"stun.syncthing.net:3478",
},
/// Servidores relay (vacío = deshabilitado)
relay_servers: []const []const u8 = &.{},
/// Directorio para almacenar certificado y configuración
data_dir: []const u8,
/// Compresión LZ4 habilitada
compression: bool = true,
/// Callback cuando se descubre un nuevo dispositivo
on_device_discovered: ?*const fn(DeviceId, []const []const u8) void = null,
/// Callback cuando se recibe un mensaje
on_message_received: ?*const fn(*Connection, Message) void = null,
/// Callback cuando cambia el estado de conexión
on_connection_state_changed: ?*const fn(*Connection, ConnectionState) void = null,
};
/// Estado de una conexión
pub const ConnectionState = enum {
connecting,
connected,
disconnecting,
disconnected,
error,
};
/// Mensaje recibido
pub const Message = struct {
id: u32,
content_type: []const u8,
data: []const u8,
timestamp: i64,
};
/// Información sobre un peer conectado
pub const PeerInfo = struct {
device_id: DeviceId,
device_name: []const u8,
client_name: []const u8,
client_version: []const u8,
addresses: []const []const u8,
connected_at: i64,
is_local: bool,
bytes_sent: u64,
bytes_received: u64,
};
/// Error codes
pub const Error = error{
AlreadyInitialized,
NotInitialized,
InvalidDeviceId,
ConnectionFailed,
ConnectionTimeout,
ConnectionClosed,
PeerNotFound,
CertificateError,
TlsError,
ProtocolError,
CompressionError,
OutOfMemory,
IoError,
InvalidConfig,
};
```
### 1.2 Instancia P2P
```zig
pub const P2P = struct {
/// Inicializa el sistema P2P
/// - Carga o genera certificado
/// - Inicia listeners
/// - Inicia discovery
pub fn init(allocator: std.mem.Allocator, config: Config) Error!*P2P;
/// Libera todos los recursos
pub fn deinit(self: *P2P) void;
/// Obtiene el Device ID local
pub fn getDeviceId(self: *P2P) DeviceId;
/// Obtiene el Device ID como string (formato XXXXXXX-XXXXXXX-...)
pub fn getDeviceIdString(self: *P2P, buf: []u8) []const u8;
/// Parsea un Device ID desde string
pub fn parseDeviceId(str: []const u8) Error!DeviceId;
/// Compara dos Device IDs
pub fn deviceIdEquals(a: DeviceId, b: DeviceId) bool;
/// Obtiene el Short ID (para logging)
pub fn getShortId(device_id: DeviceId) ShortId;
};
```
## 2. Gestión de Conexiones
```zig
pub const P2P = struct {
// ... (continuación)
/// Conecta a un dispositivo por su Device ID
/// Busca automáticamente la dirección usando discovery
pub fn connect(self: *P2P, device_id: DeviceId) Error!*Connection;
/// Conecta a una dirección específica
pub fn connectAddress(self: *P2P, address: []const u8) Error!*Connection;
/// Desconecta de un peer
pub fn disconnect(self: *P2P, device_id: DeviceId) void;
/// Obtiene una conexión existente
pub fn getConnection(self: *P2P, device_id: DeviceId) ?*Connection;
/// Lista todas las conexiones activas
pub fn getConnections(self: *P2P, buf: []*Connection) []const *Connection;
/// Número de conexiones activas
pub fn connectionCount(self: *P2P) usize;
/// Información de un peer
pub fn getPeerInfo(self: *P2P, device_id: DeviceId) ?PeerInfo;
};
pub const Connection = struct {
/// Device ID del peer
pub fn getDeviceId(self: *Connection) DeviceId;
/// Estado actual de la conexión
pub fn getState(self: *Connection) ConnectionState;
/// Información del peer
pub fn getPeerInfo(self: *Connection) PeerInfo;
/// Envía datos al peer
/// Retorna el message_id asignado
pub fn send(
self: *Connection,
content_type: []const u8,
data: []const u8,
) Error!u32;
/// Envía datos y espera confirmación
pub fn sendAndWait(
self: *Connection,
content_type: []const u8,
data: []const u8,
timeout_ms: u32,
) Error!void;
/// Cierra la conexión
pub fn close(self: *Connection) void;
/// Cierra con motivo
pub fn closeWithReason(self: *Connection, reason: []const u8) void;
/// Espera hasta que la conexión esté establecida
pub fn waitConnected(self: *Connection, timeout_ms: u32) Error!void;
/// Verifica si la conexión está activa
pub fn isConnected(self: *Connection) bool;
};
```
## 3. Discovery
```zig
pub const P2P = struct {
// ... (continuación)
/// Busca un dispositivo por su ID
/// Retorna lista de direcciones conocidas
pub fn lookup(
self: *P2P,
device_id: DeviceId,
buf: [][]const u8,
) Error![]const []const u8;
/// Fuerza un anuncio local inmediato
pub fn announceLocal(self: *P2P) void;
/// Fuerza un anuncio global inmediato
pub fn announceGlobal(self: *P2P) Error!void;
/// Obtiene dispositivos descubiertos en la LAN
pub fn getLocalDevices(
self: *P2P,
buf: []DeviceId,
) []const DeviceId;
/// Añade manualmente una dirección conocida para un dispositivo
pub fn addKnownAddress(
self: *P2P,
device_id: DeviceId,
address: []const u8,
) void;
/// Elimina direcciones conocidas de un dispositivo
pub fn clearKnownAddresses(self: *P2P, device_id: DeviceId) void;
};
```
## 4. NAT y Relay
```zig
pub const P2P = struct {
// ... (continuación)
/// Obtiene la IP externa descubierta por STUN
pub fn getExternalAddress(self: *P2P) ?[]const u8;
/// Obtiene el tipo de NAT detectado
pub fn getNatType(self: *P2P) NatType;
/// Verifica si estamos conectados a algún relay
pub fn isRelayConnected(self: *P2P) bool;
/// Obtiene la dirección de relay actual
pub fn getRelayAddress(self: *P2P) ?[]const u8;
};
pub const NatType = enum {
unknown,
none, // Sin NAT (IP pública)
full_cone, // Hole-punchable
restricted, // Hole-punchable
port_restricted,// Hole-punchable
symmetric, // No hole-punchable, necesita relay
blocked, // Sin conectividad UDP
};
```
## 5. Callbacks y Eventos
```zig
/// Callback type para dispositivo descubierto
pub const OnDeviceDiscovered = *const fn(
device_id: DeviceId,
addresses: []const []const u8,
) void;
/// Callback type para mensaje recibido
pub const OnMessageReceived = *const fn(
connection: *Connection,
message: Message,
) void;
/// Callback type para cambio de estado de conexión
pub const OnConnectionStateChanged = *const fn(
connection: *Connection,
old_state: ConnectionState,
new_state: ConnectionState,
) void;
/// Callback type para error
pub const OnError = *const fn(
error_code: Error,
message: []const u8,
) void;
pub const P2P = struct {
// ... (continuación)
/// Registra callback para dispositivo descubierto
pub fn setOnDeviceDiscovered(self: *P2P, cb: ?OnDeviceDiscovered) void;
/// Registra callback para mensaje recibido
pub fn setOnMessageReceived(self: *P2P, cb: ?OnMessageReceived) void;
/// Registra callback para cambio de estado
pub fn setOnConnectionStateChanged(self: *P2P, cb: ?OnConnectionStateChanged) void;
/// Registra callback para errores
pub fn setOnError(self: *P2P, cb: ?OnError) void;
};
```
## 6. Utilidades
```zig
pub const utils = struct {
/// Convierte Device ID a string
pub fn deviceIdToString(id: DeviceId, buf: []u8) []const u8;
/// Parsea Device ID desde string
pub fn stringToDeviceId(str: []const u8) Error!DeviceId;
/// Calcula el dígito de verificación Luhn
pub fn luhn32(str: []const u8) u8;
/// Verifica un Device ID string
pub fn verifyDeviceIdString(str: []const u8) bool;
/// Genera un certificado auto-firmado
pub fn generateCertificate(
allocator: std.mem.Allocator,
common_name: []const u8,
) Error!Certificate;
/// Carga un certificado desde archivo
pub fn loadCertificate(path: []const u8) Error!Certificate;
/// Guarda un certificado a archivo
pub fn saveCertificate(cert: Certificate, path: []const u8) Error!void;
};
```
## 7. Ejemplo de Uso Completo
```zig
const std = @import("std");
const p2p = @import("zcatp2p");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Configuración
const config = p2p.Config{
.device_name = "Simifactu-Empresa-A",
.listen_port = 22000,
.local_discovery = true,
.data_dir = "/home/user/.simifactu/p2p",
.on_message_received = handleMessage,
.on_device_discovered = handleDiscovery,
};
// Inicializar
var node = try p2p.P2P.init(allocator, config);
defer node.deinit();
// Mostrar nuestro Device ID
var id_buf: [64]u8 = undefined;
const my_id = node.getDeviceIdString(&id_buf);
std.debug.print("Mi Device ID: {s}\n", .{my_id});
// Conectar a un peer conocido
const peer_id_str = "ABCDEFG-HIJKLMN-OPQRSTU-VWXYZ23-4567ABC-DEFGHIJ-KLMNOPQ-RSTUVWX";
const peer_id = try p2p.P2P.parseDeviceId(peer_id_str);
var conn = try node.connect(peer_id);
try conn.waitConnected(30000); // 30 segundos timeout
// Enviar una factura
const factura_data = @embedFile("factura.xml");
const msg_id = try conn.send("application/x-simifactu-invoice", factura_data);
std.debug.print("Factura enviada, message_id: {}\n", .{msg_id});
// Esperar respuesta...
// (En producción usaríamos el callback on_message_received)
// Cerrar
conn.close();
}
fn handleMessage(conn: *p2p.Connection, msg: p2p.Message) void {
std.debug.print("Mensaje recibido de {}: type={s}, size={}\n", .{
p2p.P2P.getShortId(conn.getDeviceId()),
msg.content_type,
msg.data.len,
});
// Procesar según content_type
if (std.mem.eql(u8, msg.content_type, "application/x-simifactu-invoice")) {
// Procesar factura recibida
processInvoice(msg.data);
}
}
fn handleDiscovery(device_id: p2p.DeviceId, addresses: []const []const u8) void {
std.debug.print("Dispositivo descubierto: {} en {} direcciones\n", .{
p2p.P2P.getShortId(device_id),
addresses.len,
});
}
fn processInvoice(data: []const u8) void {
// ... procesar factura
_ = data;
}
```
## 8. Integración con Simifactu
### 8.1 Content Types Recomendados
```zig
pub const ContentTypes = struct {
pub const invoice = "application/x-simifactu-invoice";
pub const invoice_ack = "application/x-simifactu-invoice-ack";
pub const certificate = "application/x-simifactu-certificate";
pub const verifactu = "application/x-simifactu-verifactu";
pub const query = "application/x-simifactu-query";
pub const response = "application/x-simifactu-response";
};
```
### 8.2 Flujo Típico
```
Empresa A Empresa B
│ │
│ 1. connect(device_id_B) │
│────────────────────────────────────────────>│
│ │
│ 2. HELLO exchange │
<═══════════════════════════════════════════>│
│ │
│ 3. send(invoice, factura_xml) │
│────────────────────────────────────────────>│
│ │
│ 4. DATA_ACK │
<────────────────────────────────────────────│
│ │
│ 5. send(invoice_ack, confirmacion) │
<────────────────────────────────────────────│
│ │
│ 6. DATA_ACK │
│────────────────────────────────────────────>│
│ │
│ 7. close() │
│────────────────────────────────────────────>│
│ │
```
## 9. Thread Safety
- `P2P.init()` y `P2P.deinit()` deben llamarse desde el mismo thread
- Todas las demás funciones son thread-safe
- Los callbacks pueden ser llamados desde cualquier thread
- Se recomienda no hacer operaciones bloqueantes en callbacks
## 10. Gestión de Memoria
- `P2P.init()` asigna memoria usando el allocator proporcionado
- `P2P.deinit()` libera toda la memoria
- Los strings retornados son válidos hasta la siguiente llamada
- Los mensajes recibidos en callbacks son válidos solo durante el callback
- Para retener datos, copiarlos a memoria propia

87
CLAUDE.md Normal file
View file

@ -0,0 +1,87 @@
# zcatp2p - Protocolo P2P para comunicación directa entre empresas
## Descripción
Librería Zig para comunicación P2P segura entre instancias de Simifactu.
Permite intercambio directo de documentos (facturas, certificados) entre empresas
sin necesidad de email ni servicios cloud.
## Objetivos
1. **Seguridad**: E2E cifrado con TLS 1.3 + ChaCha20-Poly1305
2. **Descentralizado**: Sin servidor central obligatorio
3. **Sin dependencias externas**: Implementación completa en Zig puro
4. **Compatible con NAT**: STUN + relay para atravesar firewalls
## Arquitectura
```
┌─────────────────────────────────────────────────────────┐
│ zcatp2p Library │
├─────────────────────────────────────────────────────────┤
│ API Layer │
│ ├── p2p.init() / p2p.deinit() │
│ ├── p2p.connect(device_id) │
│ ├── p2p.send(data) │
│ └── p2p.receive() -> data │
├─────────────────────────────────────────────────────────┤
│ Protocol Layer │
│ ├── Message framing (Header + Payload) │
│ ├── Compression (LZ4) │
│ └── Encryption (ChaCha20-Poly1305) │
├─────────────────────────────────────────────────────────┤
│ Connection Layer │
│ ├── TLS 1.3 handshake │
│ ├── Certificate-based identity │
│ └── Connection multiplexing │
├─────────────────────────────────────────────────────────┤
│ Discovery Layer │
│ ├── Local: UDP broadcast/multicast │
│ ├── Global: HTTPS announce/query │
│ └── Cache de direcciones conocidas │
├─────────────────────────────────────────────────────────┤
│ NAT Traversal Layer │
│ ├── STUN client │
│ ├── UPnP/NAT-PMP port mapping │
│ └── Relay fallback │
└─────────────────────────────────────────────────────────┘
```
## Estructura de archivos
```
zcatp2p/
├── CLAUDE.md # Este archivo
├── PROTOCOL.md # Especificación detallada del protocolo
├── API.md # Documentación de la API
├── build.zig # Build system
└── src/
├── main.zig # Exports públicos
├── identity.zig # Device ID, certificados
├── crypto.zig # ChaCha20-Poly1305, SHA256, etc.
├── tls.zig # TLS 1.3 implementation
├── protocol.zig # Message framing
├── discovery.zig # Local + global discovery
├── stun.zig # STUN client
├── nat.zig # UPnP/NAT-PMP
├── relay.zig # Relay protocol
└── connection.zig # Connection management
```
## Referencias
- Syncthing BEP (Block Exchange Protocol): `/mnt/cello2/arno/re/recode/referencias/syncthing/`
- RFC 5389 (STUN)
- RFC 8446 (TLS 1.3)
- RFC 8439 (ChaCha20-Poly1305)
## Estado
- [ ] Especificación del protocolo
- [ ] API design
- [ ] Implementación crypto
- [ ] Implementación TLS
- [ ] Implementación discovery
- [ ] Implementación STUN
- [ ] Implementación relay
- [ ] Tests

538
PROTOCOL.md Normal file
View file

@ -0,0 +1,538 @@
# Protocolo zcatp2p - Especificación Técnica v1.0
## 1. Visión General
zcatp2p es un protocolo de comunicación P2P para intercambio seguro de mensajes
entre nodos identificados criptográficamente. Diseñado para comunicación directa
entre empresas (facturas, certificados) sin intermediarios.
### 1.1 Principios de Diseño
1. **Seguridad primero**: Todo tráfico cifrado E2E con TLS 1.3
2. **Identificación por certificado**: Device ID = SHA256(certificado)
3. **Descentralizado**: Funciona sin servidor central (LAN)
4. **NAT-friendly**: STUN + relay para conectividad universal
5. **Sin dependencias**: Implementación completa en Zig puro
### 1.2 Componentes del Sistema
```
┌──────────────────────────────────────────────────────────────┐
│ NODO SIMIFACTU │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Discovery │ │ STUN │ │ Relay Client │ │
│ │ (local+ │ │ Client │ │ (fallback) │ │
│ │ global) │ │ │ │ │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ ┌──────┴────────────────┴────────────────────┴──────────┐ │
│ │ Connection Manager │ │
│ │ - Mantiene conexiones activas │ │
│ │ - Intenta conexión directa primero │ │
│ │ - Fallback a relay si falla │ │
│ └──────────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┴────────────────────────────┐ │
│ │ TLS 1.3 Layer │ │
│ │ - Autenticación mutua por certificado │ │
│ │ - Device ID derivado de certificado │ │
│ └──────────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┴────────────────────────────┐ │
│ │ Protocol Layer │ │
│ │ - Message framing │ │
│ │ - Compresión LZ4 opcional │ │
│ │ - Request/Response pattern │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
```
## 2. Identidad de Dispositivo (Device ID)
### 2.1 Generación
El Device ID es un identificador único de 32 bytes derivado del certificado TLS:
```
DeviceID = SHA256(DER_encoded_certificate)
```
### 2.2 Representación en String
Para facilitar el intercambio, el Device ID se representa como string Base32 con
dígitos de verificación Luhn:
```
Formato: XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX
└──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘
Cada grupo de 7 caracteres incluye 1 dígito de verificación Luhn.
Total: 56 caracteres + 7 guiones = 63 caracteres
```
### 2.3 Algoritmo Luhn para Base32
```
Alfabeto Base32: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
function luhn32(s):
factor = 1
sum = 0
n = 32 // base
for char in reverse(s):
codepoint = base32_to_int(char)
addend = factor * codepoint
factor = (factor == 2) ? 1 : 2
addend = (addend / n) + (addend % n)
sum += addend
remainder = sum % n
check = (n - remainder) % n
return int_to_base32(check)
```
### 2.4 Certificado Auto-firmado
Cada nodo genera un certificado X.509 auto-firmado al inicializarse:
```
- Subject: CN=syncthing (o "zcatp2p" para nuestra implementación)
- Key Algorithm: ECDSA P-256 o Ed25519
- Validity: 20 años
- Serial: Random 128 bits
```
## 3. Protocolo de Transporte
### 3.1 Conexión TLS 1.3
Toda comunicación usa TLS 1.3 con autenticación mutua de certificados:
```
Cliente Servidor
│ │
│──────── ClientHello ────────────────>│
│ + key_share │
│ + supported_versions │
│ │
<─────── ServerHello ─────────────────│
│ + key_share │
│ + EncryptedExtensions │
│ + CertificateRequest │
│ + Certificate │
│ + CertificateVerify │
│ + Finished │
│ │
│──────── Certificate ────────────────>│
│ + CertificateVerify │
│ + Finished │
│ │
<═══════ Application Data ═══════════>│
│ │
```
### 3.2 Cipher Suites Soportadas
En orden de preferencia:
1. TLS_CHACHA20_POLY1305_SHA256
2. TLS_AES_256_GCM_SHA384
3. TLS_AES_128_GCM_SHA256
### 3.3 Verificación de Identidad
Después del handshake TLS, ambos extremos:
1. Calculan SHA256 del certificado del peer
2. Verifican que coincide con el Device ID esperado
3. Si no coincide, cierran la conexión
## 4. Protocolo de Mensajes
### 4.1 Formato de Trama
Cada mensaje tiene el siguiente formato:
```
┌────────────────┬────────────────┬─────────────────────────────┐
│ Header (2B) │ Length (4B) │ Payload (variable) │
└────────────────┴────────────────┴─────────────────────────────┘
│ │ │
│ │ └── Datos del mensaje
│ └── Longitud del payload en big-endian
└── Tipo de mensaje (1B) + Flags (1B)
```
### 4.2 Tipos de Mensaje
```
Valor Nombre Descripción
───── ────── ───────────
0x00 HELLO Intercambio inicial de información
0x01 PING Keepalive
0x02 PONG Respuesta a PING
0x03 DATA Datos de aplicación
0x04 DATA_ACK Confirmación de recepción
0x05 CLOSE Cierre de conexión
0x06 ERROR Notificación de error
```
### 4.3 Flags
```
Bit Nombre Descripción
─── ────── ───────────
0 COMPRESSED Payload comprimido con LZ4
1 ENCRYPTED Payload cifrado (adicional a TLS)
2 REQUEST Espera respuesta
3 RESPONSE Es respuesta a REQUEST
4-7 Reserved Para uso futuro
```
### 4.4 Mensaje HELLO
Intercambiado inmediatamente después del handshake TLS:
```
┌─────────────────────────────────────────────────────────────┐
│ HELLO Message │
├─────────────────────────────────────────────────────────────┤
│ device_name_len: u8 │ Longitud del nombre │
│ device_name: [N]u8 │ Nombre del dispositivo │
│ client_name_len: u8 │ Longitud del cliente │
│ client_name: [N]u8 │ "simifactu" o "zcatp2p" │
│ client_version_len: u8 │ Longitud de versión │
│ client_version: [N]u8 │ Ej: "1.0.0" │
│ timestamp: i64 │ Unix timestamp │
│ capabilities: u32 │ Bitmap de capacidades │
└─────────────────────────────────────────────────────────────┘
```
Capacidades (bitmap):
```
Bit Capacidad
─── ─────────
0 COMPRESSION_LZ4
1 ENCRYPTION_CHACHA20
2 RELAY_SUPPORT
3 IPV6_SUPPORT
```
### 4.5 Mensaje DATA
```
┌─────────────────────────────────────────────────────────────┐
│ DATA Message │
├─────────────────────────────────────────────────────────────┤
│ message_id: u32 │ ID único para correlación │
│ content_type_len: u8 │ Longitud del tipo │
│ content_type: [N]u8 │ MIME type │
│ data_len: u32 │ Longitud de los datos │
│ data: [N]u8 │ Datos de aplicación │
└─────────────────────────────────────────────────────────────┘
```
Content types sugeridos para Simifactu:
- `application/x-simifactu-invoice` - Factura
- `application/x-simifactu-certificate` - Certificado
- `application/x-simifactu-verifactu` - Datos Verifactu
### 4.6 Mensaje CLOSE
```
┌─────────────────────────────────────────────────────────────┐
│ CLOSE Message │
├─────────────────────────────────────────────────────────────┤
│ reason_len: u8 │ Longitud del motivo │
│ reason: [N]u8 │ Motivo del cierre │
└─────────────────────────────────────────────────────────────┘
```
## 5. Discovery Protocol
### 5.1 Local Discovery (LAN)
#### 5.1.1 Formato de Anuncio
Enviado via UDP broadcast (IPv4) o multicast (IPv6):
```
Puerto: 21027
IPv4 Broadcast: 255.255.255.255:21027
IPv6 Multicast: [ff12::8384]:21027
┌─────────────────────────────────────────────────────────────┐
│ Local Announce Packet │
├─────────────────────────────────────────────────────────────┤
│ magic: u32 │ 0x2EA7D90B │
│ device_id: [32]u8 │ SHA256 del certificado │
│ instance_id: i64 │ ID único por ejecución │
│ num_addresses: u8 │ Número de direcciones │
│ addresses: [N]Address │ Lista de direcciones │
└─────────────────────────────────────────────────────────────┘
Address:
│ addr_len: u8 │ Longitud de la URL │
│ addr: [N]u8 │ URL (ej: "tcp://192.168.1.5:22000") │
```
#### 5.1.2 Frecuencia de Anuncios
- Intervalo normal: 30 segundos
- Al detectar nuevo dispositivo: inmediatamente
- Caducidad del cache: 90 segundos
### 5.2 Global Discovery (Internet)
#### 5.2.1 Servidor de Discovery
El servidor de discovery es un servicio HTTPS que:
1. Recibe anuncios de dispositivos
2. Responde consultas sobre dispositivos
Para funcionar en Internet, necesitas al menos un servidor de discovery.
Puedes usar servidores públicos de Syncthing o ejecutar uno propio.
#### 5.2.2 Anuncio (POST)
```
POST https://discovery.example.com/v2/
Content-Type: application/json
Authorization: (certificado TLS cliente)
{
"addresses": [
"tcp://203.0.113.45:22000",
"relay://relay.example.com:22067/?id=XXXXX"
]
}
```
El Device ID se extrae del certificado TLS del cliente.
#### 5.2.3 Consulta (GET)
```
GET https://discovery.example.com/v2/?device=XXXXXXX-XXXXXXX-...
Response:
{
"addresses": [
"tcp://203.0.113.45:22000",
"relay://relay.example.com:22067/?id=XXXXX"
]
}
```
#### 5.2.4 Headers de Control
```
Response Headers:
- Reannounce-After: 1800 // Segundos hasta próximo anuncio
- Retry-After: 60 // En caso de error
Status Codes:
- 200: OK
- 400: Bad Request
- 403: Forbidden (sin certificado)
- 404: Device not found
- 429: Too Many Requests
```
## 6. NAT Traversal
### 6.1 Estrategia de Conexión
Orden de intentos para conectar con un peer:
1. **Conexión directa TCP**: Si tenemos IP directa
2. **Conexión directa con STUN**: Usar IP externa descubierta
3. **Hole punching**: Ambos peers intentan simultáneamente
4. **Relay**: Último recurso si todo falla
### 6.2 STUN (Session Traversal Utilities for NAT)
Usamos STUN para descubrir nuestra IP externa y tipo de NAT.
#### 6.2.1 Servidores STUN públicos
```
stun.l.google.com:19302
stun1.l.google.com:19302
stun.syncthing.net:3478
```
#### 6.2.2 Tipos de NAT detectables
```
Tipo Hole-punchable? Descripción
──── ─────────────── ───────────
Full Cone Sí Puerto externo fijo
Restricted Cone Sí Requiere envío previo
Port Restricted Cone Sí + puerto específico
Symmetric No Puerto diferente por destino
```
### 6.3 Relay Protocol
Cuando no es posible conexión directa, usamos un servidor relay.
#### 6.3.1 Conectarse al Relay
```
1. Cliente se conecta al relay via TLS
2. Envía JoinRelayRequest
3. Relay responde con SessionInvitation
4. Cliente A publica su dirección de relay en discovery
5. Cliente B consulta discovery, ve dirección relay
6. Cliente B conecta al relay, envía ConnectRequest(device_id_A)
7. Relay notifica a A, ambos reciben SessionInvitation
8. Relay hace de proxy transparente
```
#### 6.3.2 Mensajes del Protocolo Relay
```
JoinRelayRequest:
│ token_len: u8 │
│ token: [N]u8 │ Token de autenticación (opcional)
ConnectRequest:
│ device_id: [32]u8 │ ID del dispositivo destino
SessionInvitation:
│ from: [32]u8 │ Device ID del peer
│ key: [32]u8 │ Clave de sesión
│ address: [16]u8 │ IP del relay
│ port: u16 │ Puerto
│ is_server: bool │ ¿Somos el "servidor"?
Response:
│ code: i32 │ 0=OK, 1=NotFound, 2=AlreadyConnected
│ message_len: u8 │
│ message: [N]u8 │
```
## 7. Cifrado Adicional (Opcional)
Además del cifrado TLS, se puede añadir cifrado a nivel de aplicación.
### 7.1 ChaCha20-Poly1305
```
Nonce size: 24 bytes (XChaCha20)
Tag size: 16 bytes
Key size: 32 bytes
Encrypted = Nonce || ChaCha20-Poly1305(Key, Nonce, Plaintext)
```
### 7.2 Derivación de Claves
Para cifrado de datos específicos:
```
FileKey = HKDF-SHA256(
IKM = FolderKey || filename,
salt = "zcatp2p",
info = empty
)
```
## 8. Compresión
### 8.1 LZ4
Usamos LZ4 para compresión de mensajes grandes (>128 bytes):
```
Compressed = u32_be(original_size) || LZ4_compress(data)
```
Solo comprimimos si el resultado es al menos 3% más pequeño.
## 9. Keepalive y Timeouts
```
Intervalo de PING: 90 segundos
Timeout de recepción: 300 segundos (5 min)
Timeout de conexión: 30 segundos
Timeout de close: 10 segundos
```
## 10. Puertos por Defecto
```
Puerto Uso
───── ───
22000 Conexiones P2P directas
21027 Local discovery (UDP)
22067 Relay server
22070 Relay status (HTTP)
443 Global discovery (HTTPS)
```
## 11. URLs de Direcciones
```
Formato Ejemplo
─────── ───────
tcp://host:port tcp://192.168.1.5:22000
tcp4://host:port tcp4://192.168.1.5:22000
tcp6://host:port tcp6://[::1]:22000
relay://host:port/?id=XXX relay://relay.example.com:22067/?id=ABCDEFG
```
## 12. Consideraciones de Seguridad
1. **Certificados**: Generar con entropía suficiente
2. **Device ID**: Verificar siempre después del handshake TLS
3. **Replay attacks**: Usar timestamps y nonces únicos
4. **DoS**: Limitar conexiones por IP, rate limiting
5. **Man-in-the-middle**: Verificar Device ID conocidos
6. **Relay**: El relay ve metadatos pero NO el contenido (TLS end-to-end)
## 13. Ejemplo de Flujo Completo
```
Empresa A quiere enviar factura a Empresa B
1. A tiene el Device ID de B (intercambiado previamente, ej: QR)
2. A busca a B:
a. Busca en cache local
b. Envía broadcast LAN
c. Consulta discovery global
3. A obtiene direcciones de B:
["tcp://203.0.113.45:22000", "relay://relay.example.com:22067/?id=B"]
4. A intenta conectar:
a. Intenta TCP directo → falla (NAT)
b. Intenta relay → éxito
5. Handshake TLS:
- A presenta certificado
- B presenta certificado
- Ambos verifican Device IDs
6. Intercambio HELLO:
- A envía sus capacidades
- B responde con las suyas
7. A envía factura:
DATA {
message_id: 1,
content_type: "application/x-simifactu-invoice",
data: <factura_serializada>
}
8. B confirma:
DATA_ACK { message_id: 1 }
9. Cierre:
A envía CLOSE { reason: "transfer complete" }
B cierra conexión
```

56
build.zig Normal file
View file

@ -0,0 +1,56 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// ===========================================
// Main library module
// ===========================================
const zcatp2p_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// ===========================================
// Tests
// ===========================================
const lib_unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
// ===========================================
// Example
// ===========================================
const example = b.addExecutable(.{
.name = "zcatp2p-example",
.root_module = b.createModule(.{
.root_source_file = b.path("examples/basic.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zcatp2p", .module = zcatp2p_mod },
},
}),
});
b.installArtifact(example);
const run_example = b.addRunArtifact(example);
run_example.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_example.addArgs(args);
}
const run_step = b.step("run", "Run the example");
run_step.dependOn(&run_example.step);
}

38
examples/basic.zig Normal file
View file

@ -0,0 +1,38 @@
//! Ejemplo básico de uso de zcatp2p
const std = @import("std");
const p2p = @import("zcatp2p");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Configuración
const config = p2p.Config{
.device_name = "Simifactu-Ejemplo",
.listen_port = 22000,
.local_discovery = true,
.data_dir = "/tmp/zcatp2p-example",
};
// Inicializar
std.debug.print("Inicializando zcatp2p...\n", .{});
var node = try p2p.P2P.init(allocator, config);
defer node.deinit();
// Mostrar nuestro Device ID
var id_buf: [64]u8 = undefined;
const my_id = node.getDeviceIdString(&id_buf);
std.debug.print("Mi Device ID: {s}\n", .{my_id});
// Mostrar estado de NAT
const nat_type = node.getNatType();
std.debug.print("Tipo de NAT: {}\n", .{nat_type});
// Mostrar conexiones activas
std.debug.print("Conexiones activas: {}\n", .{node.connectionCount()});
std.debug.print("\n¡zcatp2p inicializado correctamente!\n", .{});
std.debug.print("Pendiente: implementación completa de TLS, STUN, y relay.\n", .{});
}

369
src/connection.zig Normal file
View file

@ -0,0 +1,369 @@
//! Módulo de conexión - Gestión de conexiones P2P
//!
//! Maneja conexiones TLS, keepalives, y multiplexación de mensajes.
const std = @import("std");
const identity = @import("identity.zig");
const protocol = @import("protocol.zig");
const discovery = @import("discovery.zig");
pub const DeviceId = identity.DeviceId;
/// Configuración del nodo P2P
pub const Config = struct {
/// Nombre del dispositivo
device_name: []const u8 = "zcatp2p",
/// Puerto para conexiones entrantes
listen_port: u16 = 22000,
/// Habilitar discovery local
local_discovery: bool = true,
/// Servidores de discovery global
global_discovery_servers: []const []const u8 = &.{},
/// Servidores STUN
stun_servers: []const []const u8 = &.{
"stun.l.google.com:19302",
"stun.syncthing.net:3478",
},
/// Servidores relay
relay_servers: []const []const u8 = &.{},
/// Directorio de datos
data_dir: []const u8,
/// Compresión habilitada
compression: bool = true,
/// Callbacks
on_device_discovered: ?*const fn (DeviceId, []const []const u8) void = null,
on_message_received: ?*const fn (*Connection, protocol.Message) void = null,
on_connection_state_changed: ?*const fn (*Connection, ConnectionState) void = null,
};
/// Estado de conexión
pub const ConnectionState = enum {
connecting,
connected,
disconnecting,
disconnected,
@"error",
};
/// Tipo de NAT detectado
pub const NatType = enum {
unknown,
none,
full_cone,
restricted,
port_restricted,
symmetric,
blocked,
};
/// Errores del módulo
pub const Error = error{
AlreadyInitialized,
NotInitialized,
InvalidDeviceId,
ConnectionFailed,
ConnectionTimeout,
ConnectionClosed,
PeerNotFound,
CertificateError,
TlsError,
ProtocolError,
CompressionError,
OutOfMemory,
IoError,
InvalidConfig,
};
/// Información de un peer
pub const PeerInfo = struct {
device_id: DeviceId,
device_name: []const u8,
client_name: []const u8,
client_version: []const u8,
addresses: []const []const u8,
connected_at: i64,
is_local: bool,
bytes_sent: u64,
bytes_received: u64,
};
/// Conexión con un peer
pub const Connection = struct {
allocator: std.mem.Allocator,
device_id: DeviceId,
state: ConnectionState,
peer_info: ?PeerInfo,
socket: ?std.posix.socket_t,
next_message_id: u32,
bytes_sent: u64,
bytes_received: u64,
connected_at: i64,
pub fn init(allocator: std.mem.Allocator, device_id: DeviceId) Connection {
return .{
.allocator = allocator,
.device_id = device_id,
.state = .disconnected,
.peer_info = null,
.socket = null,
.next_message_id = 1,
.bytes_sent = 0,
.bytes_received = 0,
.connected_at = 0,
};
}
pub fn deinit(self: *Connection) void {
if (self.socket) |sock| {
std.posix.close(sock);
}
}
pub fn getDeviceId(self: *Connection) DeviceId {
return self.device_id;
}
pub fn getState(self: *Connection) ConnectionState {
return self.state;
}
pub fn getPeerInfo(self: *Connection) ?PeerInfo {
return self.peer_info;
}
pub fn isConnected(self: *Connection) bool {
return self.state == .connected;
}
/// Envía datos al peer
pub fn send(
self: *Connection,
content_type: []const u8,
data: []const u8,
) Error!u32 {
if (self.state != .connected) return Error.ConnectionClosed;
const msg_id = self.next_message_id;
self.next_message_id += 1;
const msg = protocol.DataMessage{
.message_id = msg_id,
.content_type = content_type,
.data = data,
};
const encoded = msg.encode(self.allocator) catch return Error.OutOfMemory;
defer self.allocator.free(encoded);
// TODO: Enviar por socket con TLS
self.bytes_sent += encoded.len;
return msg_id;
}
/// Cierra la conexión
pub fn close(self: *Connection) void {
self.closeWithReason("closed by user");
}
/// Cierra la conexión con motivo
pub fn closeWithReason(self: *Connection, reason: []const u8) void {
_ = reason;
if (self.socket) |sock| {
std.posix.close(sock);
self.socket = null;
}
self.state = .disconnected;
}
/// Espera hasta que la conexión esté establecida
pub fn waitConnected(self: *Connection, timeout_ms: u32) Error!void {
_ = timeout_ms;
if (self.state == .connected) return;
if (self.state == .@"error" or self.state == .disconnected) {
return Error.ConnectionFailed;
}
// TODO: Implementar espera con timeout
return Error.ConnectionTimeout;
}
};
/// Instancia principal P2P
pub const P2P = struct {
allocator: std.mem.Allocator,
config: Config,
device_id: DeviceId,
connections: std.AutoHashMapUnmanaged(DeviceId, *Connection),
discovery_manager: discovery.DiscoveryManager,
listener_socket: ?std.posix.socket_t,
external_address: ?[]const u8,
nat_type: NatType,
pub fn init(allocator: std.mem.Allocator, config: Config) Error!*P2P {
const self = allocator.create(P2P) catch return Error.OutOfMemory;
errdefer allocator.destroy(self);
// Generar o cargar Device ID
// TODO: Implementar carga de certificado
var device_id: DeviceId = undefined;
std.crypto.random.bytes(&device_id);
self.* = .{
.allocator = allocator,
.config = config,
.device_id = device_id,
.connections = .{},
.discovery_manager = discovery.DiscoveryManager.init(allocator, device_id),
.listener_socket = null,
.external_address = null,
.nat_type = .unknown,
};
return self;
}
pub fn deinit(self: *P2P) void {
// Cerrar todas las conexiones
var iter = self.connections.iterator();
while (iter.next()) |entry| {
entry.value_ptr.*.deinit();
self.allocator.destroy(entry.value_ptr.*);
}
self.connections.deinit(self.allocator);
// Cerrar listener
if (self.listener_socket) |sock| {
std.posix.close(sock);
}
// Limpiar discovery
self.discovery_manager.deinit();
// Limpiar external address
if (self.external_address) |addr| {
self.allocator.free(addr);
}
self.allocator.destroy(self);
}
/// Obtiene el Device ID local
pub fn getDeviceId(self: *P2P) DeviceId {
return self.device_id;
}
/// Obtiene el Device ID como string
pub fn getDeviceIdString(self: *P2P, buf: []u8) []const u8 {
return identity.deviceIdToString(self.device_id, buf);
}
/// Parsea un Device ID desde string
pub fn parseDeviceId(str: []const u8) Error!DeviceId {
return identity.stringToDeviceId(str) catch Error.InvalidDeviceId;
}
/// Compara dos Device IDs
pub fn deviceIdEquals(a: DeviceId, b: DeviceId) bool {
return identity.deviceIdEquals(a, b);
}
/// Obtiene el Short ID
pub fn getShortId(device_id: DeviceId) identity.ShortId {
return identity.getShortId(device_id);
}
/// Conecta a un dispositivo
pub fn connect(self: *P2P, device_id: DeviceId) Error!*Connection {
// Verificar si ya existe conexión
if (self.connections.get(device_id)) |conn| {
if (conn.isConnected()) return conn;
}
// Crear nueva conexión
const conn = self.allocator.create(Connection) catch return Error.OutOfMemory;
conn.* = Connection.init(self.allocator, device_id);
conn.state = .connecting;
self.connections.put(self.allocator, device_id, conn) catch {
self.allocator.destroy(conn);
return Error.OutOfMemory;
};
// Buscar direcciones del peer
const addresses = self.discovery_manager.lookup(device_id) catch null;
if (addresses == null) {
conn.state = .@"error";
return Error.PeerNotFound;
}
// TODO: Intentar conectar a las direcciones
return conn;
}
/// Desconecta de un peer
pub fn disconnect(self: *P2P, device_id: DeviceId) void {
if (self.connections.get(device_id)) |conn| {
conn.close();
}
}
/// Obtiene una conexión existente
pub fn getConnection(self: *P2P, device_id: DeviceId) ?*Connection {
return self.connections.get(device_id);
}
/// Número de conexiones activas
pub fn connectionCount(self: *P2P) usize {
var count: usize = 0;
var iter = self.connections.iterator();
while (iter.next()) |entry| {
if (entry.value_ptr.*.isConnected()) count += 1;
}
return count;
}
/// Obtiene la IP externa
pub fn getExternalAddress(self: *P2P) ?[]const u8 {
return self.external_address;
}
/// Obtiene el tipo de NAT
pub fn getNatType(self: *P2P) NatType {
return self.nat_type;
}
};
// =============================================================================
// Tests
// =============================================================================
test "p2p init/deinit" {
const allocator = std.testing.allocator;
const p2p = try P2P.init(allocator, .{
.data_dir = "/tmp/zcatp2p-test",
});
defer p2p.deinit();
try std.testing.expect(p2p.connectionCount() == 0);
}
test "connection init" {
const allocator = std.testing.allocator;
const device_id = [_]u8{0xab} ** 32;
var conn = Connection.init(allocator, device_id);
defer conn.deinit();
try std.testing.expect(conn.state == .disconnected);
try std.testing.expect(!conn.isConnected());
}

645
src/crypto.zig Normal file
View file

@ -0,0 +1,645 @@
//! Módulo de criptografía - SHA256, ChaCha20-Poly1305
//!
//! Implementación pura en Zig sin dependencias externas.
const std = @import("std");
// =============================================================================
// SHA-256
// =============================================================================
/// Longitud del hash SHA256 en bytes
pub const SHA256_DIGEST_LENGTH: usize = 32;
/// Calcula SHA256 de los datos
pub fn sha256(data: []const u8) [SHA256_DIGEST_LENGTH]u8 {
var hasher = Sha256.init();
hasher.update(data);
return hasher.final();
}
/// Estado del hasher SHA256
pub const Sha256 = struct {
state: [8]u32,
buf: [64]u8,
buf_len: usize,
total_len: u64,
const K: [64]u32 = .{
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
};
pub fn init() Sha256 {
return .{
.state = .{
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
},
.buf = undefined,
.buf_len = 0,
.total_len = 0,
};
}
pub fn update(self: *Sha256, data: []const u8) void {
var input = data;
self.total_len += data.len;
// Si hay datos en el buffer, intentar completar un bloque
if (self.buf_len > 0) {
const space = 64 - self.buf_len;
const to_copy = @min(space, input.len);
@memcpy(self.buf[self.buf_len .. self.buf_len + to_copy], input[0..to_copy]);
self.buf_len += to_copy;
input = input[to_copy..];
if (self.buf_len == 64) {
self.processBlock(&self.buf);
self.buf_len = 0;
}
}
// Procesar bloques completos
while (input.len >= 64) {
self.processBlock(input[0..64]);
input = input[64..];
}
// Guardar resto en buffer
if (input.len > 0) {
@memcpy(self.buf[0..input.len], input);
self.buf_len = input.len;
}
}
pub fn final(self: *Sha256) [SHA256_DIGEST_LENGTH]u8 {
// Padding
const total_bits = self.total_len * 8;
self.buf[self.buf_len] = 0x80;
self.buf_len += 1;
// Si no hay espacio para la longitud, procesar bloque extra
if (self.buf_len > 56) {
@memset(self.buf[self.buf_len..64], 0);
self.processBlock(&self.buf);
self.buf_len = 0;
}
@memset(self.buf[self.buf_len..56], 0);
std.mem.writeInt(u64, self.buf[56..64], total_bits, .big);
self.processBlock(&self.buf);
// Convertir estado a bytes
var result: [32]u8 = undefined;
for (self.state, 0..) |s, i| {
std.mem.writeInt(u32, result[i * 4 ..][0..4], s, .big);
}
return result;
}
fn processBlock(self: *Sha256, block: *const [64]u8) void {
var w: [64]u32 = undefined;
// Preparar schedule
for (0..16) |i| {
w[i] = std.mem.readInt(u32, block[i * 4 ..][0..4], .big);
}
for (16..64) |i| {
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3);
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
w[i] = w[i - 16] +% s0 +% w[i - 7] +% s1;
}
var a = self.state[0];
var b = self.state[1];
var c = self.state[2];
var d = self.state[3];
var e = self.state[4];
var f = self.state[5];
var g = self.state[6];
var h = self.state[7];
for (0..64) |i| {
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
const ch = (e & f) ^ (~e & g);
const temp1 = h +% S1 +% ch +% K[i] +% w[i];
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
const maj = (a & b) ^ (a & c) ^ (b & c);
const temp2 = S0 +% maj;
h = g;
g = f;
f = e;
e = d +% temp1;
d = c;
c = b;
b = a;
a = temp1 +% temp2;
}
self.state[0] +%= a;
self.state[1] +%= b;
self.state[2] +%= c;
self.state[3] +%= d;
self.state[4] +%= e;
self.state[5] +%= f;
self.state[6] +%= g;
self.state[7] +%= h;
}
fn rotr(x: u32, comptime n: u5) u32 {
return std.math.rotr(u32, x, n);
}
};
// =============================================================================
// ChaCha20-Poly1305
// =============================================================================
pub const CHACHA20_KEY_SIZE: usize = 32;
pub const CHACHA20_NONCE_SIZE: usize = 12;
pub const XCHACHA20_NONCE_SIZE: usize = 24;
pub const POLY1305_TAG_SIZE: usize = 16;
/// ChaCha20 block cipher
pub const ChaCha20 = struct {
state: [16]u32,
pub fn init(key: *const [32]u8, nonce: *const [12]u8, counter: u32) ChaCha20 {
return .{
.state = .{
0x61707865, 0x3320646e, 0x79622d32, 0x6b206574,
std.mem.readInt(u32, key[0..4], .little),
std.mem.readInt(u32, key[4..8], .little),
std.mem.readInt(u32, key[8..12], .little),
std.mem.readInt(u32, key[12..16], .little),
std.mem.readInt(u32, key[16..20], .little),
std.mem.readInt(u32, key[20..24], .little),
std.mem.readInt(u32, key[24..28], .little),
std.mem.readInt(u32, key[28..32], .little),
counter,
std.mem.readInt(u32, nonce[0..4], .little),
std.mem.readInt(u32, nonce[4..8], .little),
std.mem.readInt(u32, nonce[8..12], .little),
},
};
}
pub fn xor(self: *ChaCha20, out: []u8, in: []const u8) void {
var remaining = in;
var out_ptr = out;
while (remaining.len > 0) {
const keystream = self.block();
const to_process = @min(remaining.len, 64);
for (0..to_process) |i| {
out_ptr[i] = remaining[i] ^ keystream[i];
}
remaining = remaining[to_process..];
out_ptr = out_ptr[to_process..];
// Incrementar contador
self.state[12] +%= 1;
}
}
fn block(self: *ChaCha20) [64]u8 {
var working = self.state;
// 20 rounds (10 double rounds)
for (0..10) |_| {
quarterRound(&working, 0, 4, 8, 12);
quarterRound(&working, 1, 5, 9, 13);
quarterRound(&working, 2, 6, 10, 14);
quarterRound(&working, 3, 7, 11, 15);
quarterRound(&working, 0, 5, 10, 15);
quarterRound(&working, 1, 6, 11, 12);
quarterRound(&working, 2, 7, 8, 13);
quarterRound(&working, 3, 4, 9, 14);
}
// Add original state
for (&working, self.state) |*w, s| {
w.* +%= s;
}
// Convert to bytes
var result: [64]u8 = undefined;
for (working, 0..) |w, i| {
std.mem.writeInt(u32, result[i * 4 ..][0..4], w, .little);
}
return result;
}
fn quarterRound(state: *[16]u32, a: usize, b: usize, c: usize, d: usize) void {
state[a] +%= state[b];
state[d] ^= state[a];
state[d] = rotl(state[d], 16);
state[c] +%= state[d];
state[b] ^= state[c];
state[b] = rotl(state[b], 12);
state[a] +%= state[b];
state[d] ^= state[a];
state[d] = rotl(state[d], 8);
state[c] +%= state[d];
state[b] ^= state[c];
state[b] = rotl(state[b], 7);
}
fn rotl(x: u32, comptime n: u5) u32 {
return std.math.rotl(u32, x, n);
}
};
/// XChaCha20 - ChaCha20 con nonce extendido de 24 bytes
pub const XChaCha20 = struct {
chacha: ChaCha20,
pub fn init(key: *const [32]u8, nonce: *const [24]u8, counter: u32) XChaCha20 {
// HChaCha20 para derivar subkey
const subkey = hchacha20(key, nonce[0..16]);
var short_nonce: [12]u8 = undefined;
@memset(short_nonce[0..4], 0);
@memcpy(short_nonce[4..12], nonce[16..24]);
return .{
.chacha = ChaCha20.init(&subkey, &short_nonce, counter),
};
}
pub fn xor(self: *XChaCha20, out: []u8, in: []const u8) void {
self.chacha.xor(out, in);
}
};
fn hchacha20(key: *const [32]u8, nonce: *const [16]u8) [32]u8 {
var state: [16]u32 = .{
0x61707865, 0x3320646e, 0x79622d32, 0x6b206574,
std.mem.readInt(u32, key[0..4], .little),
std.mem.readInt(u32, key[4..8], .little),
std.mem.readInt(u32, key[8..12], .little),
std.mem.readInt(u32, key[12..16], .little),
std.mem.readInt(u32, key[16..20], .little),
std.mem.readInt(u32, key[20..24], .little),
std.mem.readInt(u32, key[24..28], .little),
std.mem.readInt(u32, key[28..32], .little),
std.mem.readInt(u32, nonce[0..4], .little),
std.mem.readInt(u32, nonce[4..8], .little),
std.mem.readInt(u32, nonce[8..12], .little),
std.mem.readInt(u32, nonce[12..16], .little),
};
for (0..10) |_| {
ChaCha20.quarterRound(&state, 0, 4, 8, 12);
ChaCha20.quarterRound(&state, 1, 5, 9, 13);
ChaCha20.quarterRound(&state, 2, 6, 10, 14);
ChaCha20.quarterRound(&state, 3, 7, 11, 15);
ChaCha20.quarterRound(&state, 0, 5, 10, 15);
ChaCha20.quarterRound(&state, 1, 6, 11, 12);
ChaCha20.quarterRound(&state, 2, 7, 8, 13);
ChaCha20.quarterRound(&state, 3, 4, 9, 14);
}
var result: [32]u8 = undefined;
std.mem.writeInt(u32, result[0..4], state[0], .little);
std.mem.writeInt(u32, result[4..8], state[1], .little);
std.mem.writeInt(u32, result[8..12], state[2], .little);
std.mem.writeInt(u32, result[12..16], state[3], .little);
std.mem.writeInt(u32, result[16..20], state[12], .little);
std.mem.writeInt(u32, result[20..24], state[13], .little);
std.mem.writeInt(u32, result[24..28], state[14], .little);
std.mem.writeInt(u32, result[28..32], state[15], .little);
return result;
}
// =============================================================================
// Poly1305 MAC
// =============================================================================
/// Poly1305 message authentication code
pub const Poly1305 = struct {
r: [3]u64,
h: [3]u64,
pad: [2]u64,
leftover: usize,
buffer: [16]u8,
pub fn init(key: *const [32]u8) Poly1305 {
// r = key[0..16] con bits específicos enmascarados
var r0 = std.mem.readInt(u64, key[0..8], .little);
var r1 = std.mem.readInt(u64, key[8..16], .little);
// Clamp r
r0 &= 0x0ffffffc0fffffff;
r1 &= 0x0ffffffc0ffffffc;
return .{
.r = .{ r0 & 0xfffffffffff, (r0 >> 44) | ((r1 & 0xffffff) << 20), r1 >> 24 },
.h = .{ 0, 0, 0 },
.pad = .{
std.mem.readInt(u64, key[16..24], .little),
std.mem.readInt(u64, key[24..32], .little),
},
.leftover = 0,
.buffer = undefined,
};
}
pub fn update(self: *Poly1305, data: []const u8) void {
var input = data;
// Procesar datos en buffer primero
if (self.leftover > 0) {
const want = 16 - self.leftover;
const have = @min(want, input.len);
@memcpy(self.buffer[self.leftover .. self.leftover + have], input[0..have]);
input = input[have..];
self.leftover += have;
if (self.leftover == 16) {
self.processBlock(&self.buffer, false);
self.leftover = 0;
}
}
// Procesar bloques completos
while (input.len >= 16) {
self.processBlock(input[0..16], false);
input = input[16..];
}
// Guardar resto
if (input.len > 0) {
@memcpy(self.buffer[0..input.len], input);
self.leftover = input.len;
}
}
pub fn final(self: *Poly1305) [16]u8 {
// Procesar último bloque parcial
if (self.leftover > 0) {
self.buffer[self.leftover] = 1;
@memset(self.buffer[self.leftover + 1 .. 16], 0);
self.processBlock(&self.buffer, true);
}
// Finalizar
var h0 = self.h[0];
var h1 = self.h[1];
var h2 = self.h[2];
// Reducción final
var c: u64 = 0;
c = h0 >> 44;
h0 &= 0xfffffffffff;
h1 += c;
c = h1 >> 44;
h1 &= 0xfffffffffff;
h2 += c;
c = h2 >> 42;
h2 &= 0x3ffffffffff;
h0 += c * 5;
c = h0 >> 44;
h0 &= 0xfffffffffff;
h1 += c;
// Computar h + -p
var g0 = h0 +% 5;
c = g0 >> 44;
g0 &= 0xfffffffffff;
var g1 = h1 +% c;
c = g1 >> 44;
g1 &= 0xfffffffffff;
var g2 = h2 +% c -% (1 << 42);
// Seleccionar h o h + -p
c = (g2 >> 63) -% 1;
g0 &= c;
g1 &= c;
g2 &= c;
c = ~c;
h0 = (h0 & c) | g0;
h1 = (h1 & c) | g1;
h2 = (h2 & c) | g2;
// h = h + pad
const t0 = self.pad[0];
const t1 = self.pad[1];
h0 +%= t0 & 0xfffffffffff;
c = h0 >> 44;
h0 &= 0xfffffffffff;
h1 +%= ((t0 >> 44) | (t1 << 20)) & 0xfffffffffff;
h1 += c;
c = h1 >> 44;
h1 &= 0xfffffffffff;
h2 +%= (t1 >> 24) & 0x3ffffffffff;
h2 += c;
h2 &= 0x3ffffffffff;
// Convertir a bytes
var result: [16]u8 = undefined;
const full = h0 | (h1 << 44) | (h2 << 88);
std.mem.writeInt(u128, &result, full, .little);
return result;
}
fn processBlock(self: *Poly1305, block: *const [16]u8, is_final: bool) void {
const hibit: u64 = if (is_final) 0 else (1 << 40);
// h += m
const t0 = std.mem.readInt(u64, block[0..8], .little);
const t1 = std.mem.readInt(u64, block[8..16], .little);
self.h[0] += t0 & 0xfffffffffff;
self.h[1] += ((t0 >> 44) | (t1 << 20)) & 0xfffffffffff;
self.h[2] += ((t1 >> 24) & 0x3ffffffffff) | hibit;
// h *= r (mod 2^130 - 5)
const r0 = self.r[0];
const r1 = self.r[1];
const r2 = self.r[2];
const s1 = r1 * 5;
const s2 = r2 * 5;
var d0: u128 = @as(u128, self.h[0]) * r0;
d0 += @as(u128, self.h[1]) * s2;
d0 += @as(u128, self.h[2]) * s1;
var d1: u128 = @as(u128, self.h[0]) * r1;
d1 += @as(u128, self.h[1]) * r0;
d1 += @as(u128, self.h[2]) * s2;
var d2: u128 = @as(u128, self.h[0]) * r2;
d2 += @as(u128, self.h[1]) * r1;
d2 += @as(u128, self.h[2]) * r0;
// Reducción parcial
var c: u64 = @truncate(d0 >> 44);
self.h[0] = @truncate(d0 & 0xfffffffffff);
d1 += c;
c = @truncate(d1 >> 44);
self.h[1] = @truncate(d1 & 0xfffffffffff);
d2 += c;
c = @truncate(d2 >> 42);
self.h[2] = @truncate(d2 & 0x3ffffffffff);
self.h[0] += c * 5;
c = self.h[0] >> 44;
self.h[0] &= 0xfffffffffff;
self.h[1] += c;
}
};
// =============================================================================
// ChaCha20-Poly1305 AEAD
// =============================================================================
/// Cifra datos con ChaCha20-Poly1305
/// Retorna: nonce || ciphertext || tag
pub fn chachaPoly1305Encrypt(
key: *const [32]u8,
nonce: *const [12]u8,
plaintext: []const u8,
aad: []const u8,
allocator: std.mem.Allocator,
) ![]u8 {
const result = try allocator.alloc(u8, 12 + plaintext.len + 16);
errdefer allocator.free(result);
@memcpy(result[0..12], nonce);
// Generar keystream para Poly1305
var chacha = ChaCha20.init(key, nonce, 0);
var poly_key: [64]u8 = undefined;
var zeros: [64]u8 = [_]u8{0} ** 64;
chacha.xor(&poly_key, &zeros);
// Cifrar
chacha = ChaCha20.init(key, nonce, 1);
chacha.xor(result[12 .. 12 + plaintext.len], plaintext);
// Calcular tag
var poly = Poly1305.init(poly_key[0..32]);
poly.update(aad);
if (aad.len % 16 != 0) {
var pad: [16]u8 = [_]u8{0} ** 16;
poly.update(pad[0 .. 16 - (aad.len % 16)]);
}
poly.update(result[12 .. 12 + plaintext.len]);
if (plaintext.len % 16 != 0) {
var pad: [16]u8 = [_]u8{0} ** 16;
poly.update(pad[0 .. 16 - (plaintext.len % 16)]);
}
var lens: [16]u8 = undefined;
std.mem.writeInt(u64, lens[0..8], aad.len, .little);
std.mem.writeInt(u64, lens[8..16], plaintext.len, .little);
poly.update(&lens);
const tag = poly.final();
@memcpy(result[12 + plaintext.len ..][0..16], &tag);
return result;
}
/// Descifra datos con ChaCha20-Poly1305
/// Input format: nonce (12) || ciphertext || tag (16)
pub fn chachaPoly1305Decrypt(
key: *const [32]u8,
data: []const u8,
aad: []const u8,
allocator: std.mem.Allocator,
) ![]u8 {
if (data.len < 12 + 16) return error.InvalidData;
const nonce = data[0..12];
const ciphertext = data[12 .. data.len - 16];
const tag = data[data.len - 16 ..][0..16];
// Verificar tag
var chacha = ChaCha20.init(key, nonce, 0);
var poly_key: [64]u8 = undefined;
var zeros: [64]u8 = [_]u8{0} ** 64;
chacha.xor(&poly_key, &zeros);
var poly = Poly1305.init(poly_key[0..32]);
poly.update(aad);
if (aad.len % 16 != 0) {
var pad: [16]u8 = [_]u8{0} ** 16;
poly.update(pad[0 .. 16 - (aad.len % 16)]);
}
poly.update(ciphertext);
if (ciphertext.len % 16 != 0) {
var pad: [16]u8 = [_]u8{0} ** 16;
poly.update(pad[0 .. 16 - (ciphertext.len % 16)]);
}
var lens: [16]u8 = undefined;
std.mem.writeInt(u64, lens[0..8], aad.len, .little);
std.mem.writeInt(u64, lens[8..16], ciphertext.len, .little);
poly.update(&lens);
const computed_tag = poly.final();
if (!std.mem.eql(u8, &computed_tag, tag)) {
return error.AuthenticationFailed;
}
// Descifrar
const result = try allocator.alloc(u8, ciphertext.len);
chacha = ChaCha20.init(key, nonce, 1);
chacha.xor(result, ciphertext);
return result;
}
// =============================================================================
// Tests
// =============================================================================
test "sha256 empty" {
const hash = sha256("");
const expected = [_]u8{
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
};
try std.testing.expectEqualSlices(u8, &expected, &hash);
}
test "sha256 abc" {
const hash = sha256("abc");
const expected = [_]u8{
0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea,
0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c,
0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad,
};
try std.testing.expectEqualSlices(u8, &expected, &hash);
}
test "chacha20 basic" {
const key = [_]u8{0} ** 32;
const nonce = [_]u8{0} ** 12;
var chacha = ChaCha20.init(&key, &nonce, 0);
var out: [64]u8 = undefined;
const zeros = [_]u8{0} ** 64;
chacha.xor(&out, &zeros);
// Verificar que no es todo ceros (el keystream se aplicó)
try std.testing.expect(!std.mem.eql(u8, &out, &zeros));
}

296
src/discovery.zig Normal file
View file

@ -0,0 +1,296 @@
//! Módulo de discovery - Descubrimiento local y global de peers
//!
//! Local: UDP broadcast/multicast en LAN
//! Global: HTTPS API a servidores de discovery
const std = @import("std");
const identity = @import("identity.zig");
const protocol = @import("protocol.zig");
pub const DeviceId = identity.DeviceId;
/// Puerto por defecto para discovery local
pub const LOCAL_DISCOVERY_PORT: u16 = 21027;
/// Intervalo de broadcast local
pub const BROADCAST_INTERVAL_MS: u64 = 30 * 1000;
/// Tiempo de vida del cache
pub const CACHE_LIFETIME_MS: u64 = 90 * 1000;
/// Magic number para paquetes de discovery local
pub const LOCAL_MAGIC: u32 = 0x2EA7D90B;
/// Entrada en el cache de dispositivos
pub const CacheEntry = struct {
addresses: std.ArrayListUnmanaged([]const u8),
instance_id: i64,
when: i64,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) CacheEntry {
return .{
.addresses = .{},
.instance_id = 0,
.when = 0,
.allocator = allocator,
};
}
pub fn deinit(self: *CacheEntry) void {
for (self.addresses.items) |addr| {
self.allocator.free(addr);
}
self.addresses.deinit(self.allocator);
}
pub fn isExpired(self: CacheEntry) bool {
const now = std.time.milliTimestamp();
return now - self.when > CACHE_LIFETIME_MS;
}
};
/// Cliente de discovery local (LAN)
pub const LocalDiscovery = struct {
allocator: std.mem.Allocator,
my_id: DeviceId,
cache: std.AutoHashMapUnmanaged(DeviceId, CacheEntry),
socket: ?std.posix.socket_t,
instance_id: i64,
addresses: std.ArrayListUnmanaged([]const u8),
pub fn init(allocator: std.mem.Allocator, device_id: DeviceId) LocalDiscovery {
return .{
.allocator = allocator,
.my_id = device_id,
.cache = .{},
.socket = null,
.instance_id = std.crypto.random.int(i64),
.addresses = .{},
};
}
pub fn deinit(self: *LocalDiscovery) void {
if (self.socket) |sock| {
std.posix.close(sock);
}
var iter = self.cache.iterator();
while (iter.next()) |entry| {
entry.value_ptr.deinit();
}
self.cache.deinit(self.allocator);
for (self.addresses.items) |addr| {
self.allocator.free(addr);
}
self.addresses.deinit(self.allocator);
}
/// Inicia el listener de discovery local
pub fn start(self: *LocalDiscovery) !void {
// Crear socket UDP
self.socket = try std.posix.socket(
std.posix.AF.INET,
std.posix.SOCK.DGRAM,
0,
);
// Permitir reutilizar dirección
const opt: u32 = 1;
try std.posix.setsockopt(
self.socket.?,
std.posix.SOL.SOCKET,
std.posix.SO.REUSEADDR,
std.mem.asBytes(&opt),
);
// Bind al puerto de discovery
const addr = std.net.Address.initIp4(.{ 0, 0, 0, 0 }, LOCAL_DISCOVERY_PORT);
try std.posix.bind(self.socket.?, &addr.any, addr.getOsSockLen());
}
/// Envía un anuncio de discovery
pub fn sendAnnouncement(self: *LocalDiscovery) !void {
if (self.socket == null) return error.NotStarted;
if (self.addresses.items.len == 0) return;
// Construir paquete
var buf: [1024]u8 = undefined;
var pos: usize = 0;
// Magic
std.mem.writeInt(u32, buf[pos..][0..4], LOCAL_MAGIC, .big);
pos += 4;
// Device ID
@memcpy(buf[pos .. pos + 32], &self.my_id);
pos += 32;
// Instance ID
std.mem.writeInt(i64, buf[pos..][0..8], self.instance_id, .big);
pos += 8;
// Número de direcciones
buf[pos] = @intCast(self.addresses.items.len);
pos += 1;
// Direcciones
for (self.addresses.items) |addr| {
if (pos + 1 + addr.len > buf.len) break;
buf[pos] = @intCast(addr.len);
pos += 1;
@memcpy(buf[pos .. pos + addr.len], addr);
pos += addr.len;
}
// Enviar broadcast
const broadcast_addr = std.net.Address.initIp4(.{ 255, 255, 255, 255 }, LOCAL_DISCOVERY_PORT);
_ = try std.posix.sendto(
self.socket.?,
buf[0..pos],
0,
&broadcast_addr.any,
broadcast_addr.getOsSockLen(),
);
}
/// Busca un dispositivo en el cache
pub fn lookup(self: *LocalDiscovery, device_id: DeviceId) ?[]const []const u8 {
if (self.cache.get(device_id)) |entry| {
if (!entry.isExpired()) {
return entry.addresses.items;
}
}
return null;
}
/// Añade una dirección local
pub fn addAddress(self: *LocalDiscovery, addr: []const u8) !void {
const owned = try self.allocator.dupe(u8, addr);
try self.addresses.append(self.allocator, owned);
}
/// Limpia direcciones locales
pub fn clearAddresses(self: *LocalDiscovery) void {
for (self.addresses.items) |addr| {
self.allocator.free(addr);
}
self.addresses.clearRetainingCapacity();
}
};
/// Cliente de discovery global (HTTPS)
pub const GlobalDiscovery = struct {
allocator: std.mem.Allocator,
servers: std.ArrayListUnmanaged([]const u8),
my_id: DeviceId,
pub fn init(allocator: std.mem.Allocator, device_id: DeviceId) GlobalDiscovery {
return .{
.allocator = allocator,
.servers = .{},
.my_id = device_id,
};
}
pub fn deinit(self: *GlobalDiscovery) void {
for (self.servers.items) |server| {
self.allocator.free(server);
}
self.servers.deinit(self.allocator);
}
/// Añade un servidor de discovery
pub fn addServer(self: *GlobalDiscovery, url: []const u8) !void {
const owned = try self.allocator.dupe(u8, url);
try self.servers.append(self.allocator, owned);
}
/// Busca un dispositivo en los servidores globales
/// TODO: Implementar cliente HTTPS
pub fn lookup(self: *GlobalDiscovery, device_id: DeviceId) !?[]const []const u8 {
_ = self;
_ = device_id;
// Pendiente: implementar cliente HTTPS
return null;
}
/// Anuncia el dispositivo a los servidores globales
/// TODO: Implementar cliente HTTPS
pub fn announce(self: *GlobalDiscovery, addresses: []const []const u8) !void {
_ = self;
_ = addresses;
// Pendiente: implementar cliente HTTPS
}
};
/// Gestor combinado de discovery
pub const DiscoveryManager = struct {
allocator: std.mem.Allocator,
local: LocalDiscovery,
global: GlobalDiscovery,
on_device_discovered: ?*const fn (DeviceId, []const []const u8) void,
pub fn init(allocator: std.mem.Allocator, device_id: DeviceId) DiscoveryManager {
return .{
.allocator = allocator,
.local = LocalDiscovery.init(allocator, device_id),
.global = GlobalDiscovery.init(allocator, device_id),
.on_device_discovered = null,
};
}
pub fn deinit(self: *DiscoveryManager) void {
self.local.deinit();
self.global.deinit();
}
/// Busca un dispositivo (primero local, luego global)
pub fn lookup(self: *DiscoveryManager, device_id: DeviceId) !?[]const []const u8 {
// Primero buscar en cache local
if (self.local.lookup(device_id)) |addrs| {
return addrs;
}
// Luego buscar en servidores globales
return try self.global.lookup(device_id);
}
/// Inicia el discovery
pub fn start(self: *DiscoveryManager) !void {
try self.local.start();
}
/// Registra callback para dispositivos descubiertos
pub fn setOnDeviceDiscovered(
self: *DiscoveryManager,
cb: ?*const fn (DeviceId, []const []const u8) void,
) void {
self.on_device_discovered = cb;
}
};
// =============================================================================
// Tests
// =============================================================================
test "cache entry expiration" {
var entry = CacheEntry{
.addresses = .{},
.instance_id = 123,
.when = std.time.milliTimestamp(),
.allocator = std.testing.allocator,
};
defer entry.deinit();
try std.testing.expect(!entry.isExpired());
}
test "local discovery init" {
const id = [_]u8{0xab} ** 32;
var discovery = LocalDiscovery.init(std.testing.allocator, id);
defer discovery.deinit();
try std.testing.expect(discovery.socket == null);
}

279
src/identity.zig Normal file
View file

@ -0,0 +1,279 @@
//! Módulo de identidad - Device ID y certificados
//!
//! El Device ID es un identificador único de 32 bytes derivado del certificado TLS:
//! DeviceID = SHA256(DER_encoded_certificate)
const std = @import("std");
const crypto = @import("crypto.zig");
/// Longitud del Device ID en bytes
pub const DEVICE_ID_LENGTH: usize = 32;
/// Longitud del Short ID en caracteres
pub const SHORT_ID_LENGTH: usize = 7;
/// Identificador único de dispositivo (32 bytes = SHA256 hash)
pub const DeviceId = [DEVICE_ID_LENGTH]u8;
/// Representación corta del Device ID (primeros 8 bytes como u64)
pub const ShortId = u64;
/// Device ID especiales
pub const EMPTY_DEVICE_ID: DeviceId = [_]u8{0} ** DEVICE_ID_LENGTH;
pub const LOCAL_DEVICE_ID: DeviceId = [_]u8{0xff} ** DEVICE_ID_LENGTH;
/// Alfabeto Base32 estándar
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const BASE32_DECODE_TABLE = initDecodeTable();
fn initDecodeTable() [256]u8 {
var table: [256]u8 = [_]u8{0xff} ** 256;
for (BASE32_ALPHABET, 0..) |c, i| {
table[c] = @intCast(i);
// También aceptar minúsculas
if (c >= 'A' and c <= 'Z') {
table[c + 32] = @intCast(i);
}
}
// Correcciones para errores de tipeo comunes
table['0'] = table['O'];
table['1'] = table['I'];
table['8'] = table['B'];
return table;
}
/// Genera un Device ID desde el certificado DER
pub fn newDeviceId(der_certificate: []const u8) DeviceId {
return crypto.sha256(der_certificate);
}
/// Compara dos Device IDs
pub fn deviceIdEquals(a: DeviceId, b: DeviceId) bool {
return std.mem.eql(u8, &a, &b);
}
/// Obtiene el Short ID (primeros 8 bytes como u64 big-endian)
pub fn getShortId(id: DeviceId) ShortId {
return std.mem.readInt(u64, id[0..8], .big);
}
/// Verifica si un Device ID está vacío
pub fn isEmptyDeviceId(id: DeviceId) bool {
return deviceIdEquals(id, EMPTY_DEVICE_ID);
}
/// Convierte Device ID a string con formato XXXXXXX-XXXXXXX-...
/// Buffer debe tener al menos 64 bytes
pub fn deviceIdToString(id: DeviceId, buf: []u8) []const u8 {
if (isEmptyDeviceId(id)) {
return "";
}
// Primero codificar en Base32 (52 caracteres sin padding)
var base32_buf: [52]u8 = undefined;
base32Encode(&id, &base32_buf);
// Añadir dígitos de verificación Luhn (56 caracteres)
var luhn_buf: [56]u8 = undefined;
luhnify(&base32_buf, &luhn_buf);
// Añadir guiones cada 7 caracteres (63 caracteres total)
return chunkify(&luhn_buf, buf);
}
/// Parsea Device ID desde string
pub fn stringToDeviceId(str: []const u8) !DeviceId {
if (str.len == 0) {
return EMPTY_DEVICE_ID;
}
// Eliminar guiones y espacios
var clean_buf: [64]u8 = undefined;
var clean_len: usize = 0;
for (str) |c| {
if (c != '-' and c != ' ') {
if (clean_len >= clean_buf.len) return error.InvalidDeviceId;
clean_buf[clean_len] = std.ascii.toUpper(c);
clean_len += 1;
}
}
const clean = clean_buf[0..clean_len];
// Corregir errores de tipeo comunes
for (clean) |*c| {
if (c.* == '0') c.* = 'O';
if (c.* == '1') c.* = 'I';
if (c.* == '8') c.* = 'B';
}
// Verificar longitud
if (clean.len == 56) {
// Con dígitos Luhn - verificarlos
var without_luhn: [52]u8 = undefined;
try unluhnify(clean[0..56], &without_luhn);
return base32Decode(&without_luhn);
} else if (clean.len == 52) {
// Sin dígitos Luhn (formato antiguo)
return base32Decode(clean[0..52]);
} else {
return error.InvalidDeviceId;
}
}
/// Verifica si un string de Device ID es válido
pub fn verifyDeviceIdString(str: []const u8) bool {
_ = stringToDeviceId(str) catch return false;
return true;
}
/// Calcula el dígito de verificación Luhn para Base32
pub fn luhn32(data: []const u8) u8 {
var factor: u32 = 1;
var sum: u32 = 0;
const n: u32 = 32;
var i: usize = data.len;
while (i > 0) {
i -= 1;
const codepoint = BASE32_DECODE_TABLE[data[i]];
if (codepoint == 0xff) continue; // Ignorar caracteres inválidos
var addend = factor * codepoint;
factor = if (factor == 2) 1 else 2;
addend = (addend / n) + (addend % n);
sum += addend;
}
const remainder = sum % n;
const check = (n - remainder) % n;
return BASE32_ALPHABET[@intCast(check)];
}
// --- Funciones internas ---
fn base32Encode(data: *const [32]u8, out: *[52]u8) void {
var bits: u64 = 0;
var num_bits: u6 = 0;
var out_idx: usize = 0;
for (data) |byte| {
bits = (bits << 8) | byte;
num_bits += 8;
while (num_bits >= 5) {
num_bits -= 5;
const idx: u5 = @truncate(bits >> num_bits);
out[out_idx] = BASE32_ALPHABET[idx];
out_idx += 1;
}
}
// Bits restantes (si los hay)
if (num_bits > 0) {
const idx: u5 = @truncate(bits << (5 - num_bits));
out[out_idx] = BASE32_ALPHABET[idx];
}
}
fn base32Decode(data: *const [52]u8) !DeviceId {
var result: DeviceId = undefined;
var bits: u64 = 0;
var num_bits: u6 = 0;
var out_idx: usize = 0;
for (data) |c| {
const val = BASE32_DECODE_TABLE[c];
if (val == 0xff) return error.InvalidDeviceId;
bits = (bits << 5) | val;
num_bits += 5;
if (num_bits >= 8) {
num_bits -= 8;
result[out_idx] = @truncate(bits >> num_bits);
out_idx += 1;
if (out_idx >= result.len) break;
}
}
return result;
}
fn luhnify(input: *const [52]u8, output: *[56]u8) void {
// Dividir en 4 grupos de 13 caracteres, añadir dígito Luhn a cada uno
for (0..4) |i| {
const start = i * 13;
const out_start = i * 14;
@memcpy(output[out_start .. out_start + 13], input[start .. start + 13]);
output[out_start + 13] = luhn32(input[start .. start + 13]);
}
}
fn unluhnify(input: []const u8, output: *[52]u8) !void {
if (input.len != 56) return error.InvalidDeviceId;
for (0..4) |i| {
const in_start = i * 14;
const out_start = i * 13;
const group = input[in_start .. in_start + 13];
const check = input[in_start + 13];
// Verificar dígito Luhn
if (luhn32(group) != check) {
return error.InvalidDeviceId;
}
@memcpy(output[out_start .. out_start + 13], group);
}
}
fn chunkify(input: *const [56]u8, output: []u8) []const u8 {
if (output.len < 63) @panic("buffer too small");
var out_idx: usize = 0;
for (0..8) |i| {
if (i > 0) {
output[out_idx] = '-';
out_idx += 1;
}
const start = i * 7;
@memcpy(output[out_idx .. out_idx + 7], input[start .. start + 7]);
out_idx += 7;
}
return output[0..63];
}
// --- Tests ---
test "device id round trip" {
// Certificado de prueba (datos aleatorios)
const test_cert = [_]u8{ 0x30, 0x82, 0x01, 0x22 } ++ [_]u8{0xab} ** 28;
const id = newDeviceId(&test_cert);
var buf: [64]u8 = undefined;
const str = deviceIdToString(id, &buf);
const parsed = try stringToDeviceId(str);
try std.testing.expect(deviceIdEquals(id, parsed));
}
test "luhn32 calculation" {
// Test conocido
const check = luhn32("ABCDEFGHIJKLM");
try std.testing.expect(check >= 'A' and check <= 'Z' or check >= '2' and check <= '7');
}
test "empty device id" {
try std.testing.expect(isEmptyDeviceId(EMPTY_DEVICE_ID));
try std.testing.expect(!isEmptyDeviceId(LOCAL_DEVICE_ID));
}
test "parse with errors" {
// Con errores de tipeo comunes
const result = stringToDeviceId("0II88OO-1234567-ABCDEFG-HIJKLMN-OPQRSTU-VWXYZ23-4567ABC-DEFGHIJ");
_ = result catch |err| {
try std.testing.expect(err == error.InvalidDeviceId);
return;
};
}

47
src/main.zig Normal file
View file

@ -0,0 +1,47 @@
//! zcatp2p - Protocolo P2P para comunicación directa entre empresas
//!
//! Librería Zig para comunicación P2P segura entre instancias de Simifactu.
//! Permite intercambio directo de documentos (facturas, certificados) entre empresas
//! sin necesidad de email ni servicios cloud.
const std = @import("std");
// Módulos públicos
pub const identity = @import("identity.zig");
pub const crypto = @import("crypto.zig");
pub const protocol = @import("protocol.zig");
pub const discovery = @import("discovery.zig");
pub const connection = @import("connection.zig");
// Re-exports principales
pub const DeviceId = identity.DeviceId;
pub const ShortId = identity.ShortId;
pub const Config = connection.Config;
pub const P2P = connection.P2P;
pub const Connection = connection.Connection;
pub const Message = protocol.Message;
pub const ConnectionState = connection.ConnectionState;
pub const NatType = connection.NatType;
pub const Error = connection.Error;
// Content types para Simifactu
pub const ContentTypes = struct {
pub const invoice = "application/x-simifactu-invoice";
pub const invoice_ack = "application/x-simifactu-invoice-ack";
pub const certificate = "application/x-simifactu-certificate";
pub const verifactu = "application/x-simifactu-verifactu";
pub const query = "application/x-simifactu-query";
pub const response = "application/x-simifactu-response";
};
// Utilidades
pub const utils = struct {
pub const deviceIdToString = identity.deviceIdToString;
pub const stringToDeviceId = identity.stringToDeviceId;
pub const verifyDeviceIdString = identity.verifyDeviceIdString;
pub const luhn32 = identity.luhn32;
};
test {
std.testing.refAllDecls(@This());
}

333
src/protocol.zig Normal file
View file

@ -0,0 +1,333 @@
//! Módulo de protocolo - Message framing y tipos de mensaje
//!
//! Formato de trama:
//!
//! Header (2B) Length (4B) Payload (variable)
//!
const std = @import("std");
/// Magic number para validación de paquetes
pub const MAGIC: u32 = 0x2EA7D90B;
/// Versión del protocolo
pub const PROTOCOL_VERSION: u8 = 1;
/// Tamaño máximo de mensaje (500 MB)
pub const MAX_MESSAGE_LEN: usize = 500 * 1000 * 1000;
/// Umbral para compresión (128 bytes)
pub const COMPRESSION_THRESHOLD: usize = 128;
/// Tipos de mensaje
pub const MessageType = enum(u8) {
hello = 0x00,
ping = 0x01,
pong = 0x02,
data = 0x03,
data_ack = 0x04,
close = 0x05,
@"error" = 0x06,
_,
};
/// Flags de mensaje
pub const MessageFlags = packed struct(u8) {
compressed: bool = false,
encrypted: bool = false,
request: bool = false,
response: bool = false,
_reserved: u4 = 0,
};
/// Header de mensaje
pub const MessageHeader = struct {
msg_type: MessageType,
flags: MessageFlags,
length: u32,
pub const SIZE: usize = 6;
pub fn encode(self: MessageHeader) [SIZE]u8 {
var buf: [SIZE]u8 = undefined;
buf[0] = @intFromEnum(self.msg_type);
buf[1] = @bitCast(self.flags);
std.mem.writeInt(u32, buf[2..6], self.length, .big);
return buf;
}
pub fn decode(buf: *const [SIZE]u8) MessageHeader {
return .{
.msg_type = @enumFromInt(buf[0]),
.flags = @bitCast(buf[1]),
.length = std.mem.readInt(u32, buf[2..6], .big),
};
}
};
/// Mensaje de aplicación
pub const Message = struct {
id: u32,
content_type: []const u8,
data: []const u8,
timestamp: i64,
};
/// Capacidades soportadas
pub const Capabilities = packed struct(u32) {
compression_lz4: bool = false,
encryption_chacha20: bool = false,
relay_support: bool = false,
ipv6_support: bool = false,
_reserved: u28 = 0,
};
/// Mensaje HELLO
pub const HelloMessage = struct {
device_name: []const u8,
client_name: []const u8,
client_version: []const u8,
timestamp: i64,
capabilities: Capabilities,
pub fn encode(self: HelloMessage, allocator: std.mem.Allocator) ![]u8 {
const size = 1 + self.device_name.len +
1 + self.client_name.len +
1 + self.client_version.len +
8 + 4;
const buf = try allocator.alloc(u8, size);
errdefer allocator.free(buf);
var pos: usize = 0;
// device_name
buf[pos] = @intCast(self.device_name.len);
pos += 1;
@memcpy(buf[pos .. pos + self.device_name.len], self.device_name);
pos += self.device_name.len;
// client_name
buf[pos] = @intCast(self.client_name.len);
pos += 1;
@memcpy(buf[pos .. pos + self.client_name.len], self.client_name);
pos += self.client_name.len;
// client_version
buf[pos] = @intCast(self.client_version.len);
pos += 1;
@memcpy(buf[pos .. pos + self.client_version.len], self.client_version);
pos += self.client_version.len;
// timestamp
std.mem.writeInt(i64, buf[pos..][0..8], self.timestamp, .big);
pos += 8;
// capabilities
std.mem.writeInt(u32, buf[pos..][0..4], @bitCast(self.capabilities), .big);
return buf;
}
pub fn decode(data: []const u8, allocator: std.mem.Allocator) !HelloMessage {
if (data.len < 15) return error.InvalidMessage;
var pos: usize = 0;
// device_name
const device_name_len = data[pos];
pos += 1;
if (pos + device_name_len > data.len) return error.InvalidMessage;
const device_name = try allocator.dupe(u8, data[pos .. pos + device_name_len]);
pos += device_name_len;
// client_name
const client_name_len = data[pos];
pos += 1;
if (pos + client_name_len > data.len) return error.InvalidMessage;
const client_name = try allocator.dupe(u8, data[pos .. pos + client_name_len]);
pos += client_name_len;
// client_version
const client_version_len = data[pos];
pos += 1;
if (pos + client_version_len > data.len) return error.InvalidMessage;
const client_version = try allocator.dupe(u8, data[pos .. pos + client_version_len]);
pos += client_version_len;
if (pos + 12 > data.len) return error.InvalidMessage;
// timestamp
const timestamp = std.mem.readInt(i64, data[pos..][0..8], .big);
pos += 8;
// capabilities
const capabilities: Capabilities = @bitCast(std.mem.readInt(u32, data[pos..][0..4], .big));
return .{
.device_name = device_name,
.client_name = client_name,
.client_version = client_version,
.timestamp = timestamp,
.capabilities = capabilities,
};
}
pub fn deinit(self: *HelloMessage, allocator: std.mem.Allocator) void {
allocator.free(self.device_name);
allocator.free(self.client_name);
allocator.free(self.client_version);
}
};
/// Mensaje DATA
pub const DataMessage = struct {
message_id: u32,
content_type: []const u8,
data: []const u8,
pub fn encode(self: DataMessage, allocator: std.mem.Allocator) ![]u8 {
const size = 4 + 1 + self.content_type.len + 4 + self.data.len;
const buf = try allocator.alloc(u8, size);
errdefer allocator.free(buf);
var pos: usize = 0;
// message_id
std.mem.writeInt(u32, buf[pos..][0..4], self.message_id, .big);
pos += 4;
// content_type
buf[pos] = @intCast(self.content_type.len);
pos += 1;
@memcpy(buf[pos .. pos + self.content_type.len], self.content_type);
pos += self.content_type.len;
// data
std.mem.writeInt(u32, buf[pos..][0..4], @intCast(self.data.len), .big);
pos += 4;
@memcpy(buf[pos .. pos + self.data.len], self.data);
return buf;
}
pub fn decode(data: []const u8, allocator: std.mem.Allocator) !DataMessage {
if (data.len < 9) return error.InvalidMessage;
var pos: usize = 0;
// message_id
const message_id = std.mem.readInt(u32, data[pos..][0..4], .big);
pos += 4;
// content_type
const content_type_len = data[pos];
pos += 1;
if (pos + content_type_len > data.len) return error.InvalidMessage;
const content_type = try allocator.dupe(u8, data[pos .. pos + content_type_len]);
pos += content_type_len;
// data
if (pos + 4 > data.len) return error.InvalidMessage;
const data_len = std.mem.readInt(u32, data[pos..][0..4], .big);
pos += 4;
if (pos + data_len > data.len) return error.InvalidMessage;
const payload = try allocator.dupe(u8, data[pos .. pos + data_len]);
return .{
.message_id = message_id,
.content_type = content_type,
.data = payload,
};
}
pub fn deinit(self: *DataMessage, allocator: std.mem.Allocator) void {
allocator.free(self.content_type);
allocator.free(self.data);
}
};
/// Mensaje CLOSE
pub const CloseMessage = struct {
reason: []const u8,
pub fn encode(self: CloseMessage, allocator: std.mem.Allocator) ![]u8 {
const size = 1 + self.reason.len;
const buf = try allocator.alloc(u8, size);
buf[0] = @intCast(self.reason.len);
@memcpy(buf[1..], self.reason);
return buf;
}
pub fn decode(data: []const u8, allocator: std.mem.Allocator) !CloseMessage {
if (data.len < 1) return error.InvalidMessage;
const reason_len = data[0];
if (1 + reason_len > data.len) return error.InvalidMessage;
return .{
.reason = try allocator.dupe(u8, data[1 .. 1 + reason_len]),
};
}
pub fn deinit(self: *CloseMessage, allocator: std.mem.Allocator) void {
allocator.free(self.reason);
}
};
/// Mensaje DATA_ACK
pub const DataAckMessage = struct {
message_id: u32,
pub fn encode(self: DataAckMessage) [4]u8 {
var buf: [4]u8 = undefined;
std.mem.writeInt(u32, &buf, self.message_id, .big);
return buf;
}
pub fn decode(data: []const u8) !DataAckMessage {
if (data.len < 4) return error.InvalidMessage;
return .{
.message_id = std.mem.readInt(u32, data[0..4], .big),
};
}
};
// =============================================================================
// Tests
// =============================================================================
test "message header encode/decode" {
const header = MessageHeader{
.msg_type = .data,
.flags = .{ .compressed = true },
.length = 1234,
};
const encoded = header.encode();
const decoded = MessageHeader.decode(&encoded);
try std.testing.expectEqual(header.msg_type, decoded.msg_type);
try std.testing.expectEqual(header.flags.compressed, decoded.flags.compressed);
try std.testing.expectEqual(header.length, decoded.length);
}
test "hello message encode/decode" {
const allocator = std.testing.allocator;
var hello = HelloMessage{
.device_name = "test-device",
.client_name = "zcatp2p",
.client_version = "1.0.0",
.timestamp = 1234567890,
.capabilities = .{ .compression_lz4 = true },
};
const encoded = try hello.encode(allocator);
defer allocator.free(encoded);
var decoded = try HelloMessage.decode(encoded, allocator);
defer decoded.deinit(allocator);
try std.testing.expectEqualStrings(hello.device_name, decoded.device_name);
try std.testing.expectEqualStrings(hello.client_name, decoded.client_name);
try std.testing.expectEqual(hello.timestamp, decoded.timestamp);
}