From 7e5b16ee150b45d4f956df7f202609787cd7a261 Mon Sep 17 00:00:00 2001 From: reugenio Date: Mon, 15 Dec 2025 01:06:30 +0100 Subject: [PATCH] =?UTF-8?q?Inicial:=20biblioteca=20zcatp2p=20para=20comuni?= =?UTF-8?q?caci=C3=B3n=20P2P=20segura?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitignore | 17 ++ API.md | 476 +++++++++++++++++++++++++++++++++ CLAUDE.md | 87 ++++++ PROTOCOL.md | 538 +++++++++++++++++++++++++++++++++++++ build.zig | 56 ++++ examples/basic.zig | 38 +++ src/connection.zig | 369 ++++++++++++++++++++++++++ src/crypto.zig | 645 +++++++++++++++++++++++++++++++++++++++++++++ src/discovery.zig | 296 +++++++++++++++++++++ src/identity.zig | 279 ++++++++++++++++++++ src/main.zig | 47 ++++ src/protocol.zig | 333 +++++++++++++++++++++++ 12 files changed, 3181 insertions(+) create mode 100644 .gitignore create mode 100644 API.md create mode 100644 CLAUDE.md create mode 100644 PROTOCOL.md create mode 100644 build.zig create mode 100644 examples/basic.zig create mode 100644 src/connection.zig create mode 100644 src/crypto.zig create mode 100644 src/discovery.zig create mode 100644 src/identity.zig create mode 100644 src/main.zig create mode 100644 src/protocol.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8818304 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/API.md b/API.md new file mode 100644 index 0000000..baadfe1 --- /dev/null +++ b/API.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..621ffa8 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 0000000..3cf0769 --- /dev/null +++ b/PROTOCOL.md @@ -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: + } + +8. B confirma: + DATA_ACK { message_id: 1 } + +9. Cierre: + A envΓ­a CLOSE { reason: "transfer complete" } + B cierra conexiΓ³n +``` diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..220fc47 --- /dev/null +++ b/build.zig @@ -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); +} diff --git a/examples/basic.zig b/examples/basic.zig new file mode 100644 index 0000000..0d8dc11 --- /dev/null +++ b/examples/basic.zig @@ -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", .{}); +} diff --git a/src/connection.zig b/src/connection.zig new file mode 100644 index 0000000..0c1b18e --- /dev/null +++ b/src/connection.zig @@ -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()); +} diff --git a/src/crypto.zig b/src/crypto.zig new file mode 100644 index 0000000..fb32bad --- /dev/null +++ b/src/crypto.zig @@ -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)); +} diff --git a/src/discovery.zig b/src/discovery.zig new file mode 100644 index 0000000..c50c960 --- /dev/null +++ b/src/discovery.zig @@ -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); +} diff --git a/src/identity.zig b/src/identity.zig new file mode 100644 index 0000000..ebe009f --- /dev/null +++ b/src/identity.zig @@ -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; + }; +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..e5cd0c8 --- /dev/null +++ b/src/main.zig @@ -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()); +} diff --git a/src/protocol.zig b/src/protocol.zig new file mode 100644 index 0000000..93c5845 --- /dev/null +++ b/src/protocol.zig @@ -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); +}