Inicial: biblioteca zcatp2p para comunicación P2P segura
- Especificación completa del protocolo (PROTOCOL.md) - Referencia de API (API.md) - Implementación crypto: SHA256, ChaCha20-Poly1305 - Device ID con Base32 y verificación Luhn32 - Framing de mensajes (HELLO, PING, DATA, etc.) - Discovery local UDP broadcast - Estructura de conexiones y estados - Build system para Zig 0.15.2 Pendiente: TLS 1.3, STUN, Global Discovery HTTPS, Relay 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
7e5b16ee15
12 changed files with 3181 additions and 0 deletions
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Zig build artifacts
|
||||||
|
zig-out/
|
||||||
|
.zig-cache/
|
||||||
|
|
||||||
|
# Editor files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
/tmp/
|
||||||
476
API.md
Normal file
476
API.md
Normal file
|
|
@ -0,0 +1,476 @@
|
||||||
|
# zcatp2p - API Reference
|
||||||
|
|
||||||
|
## 1. Inicialización y Configuración
|
||||||
|
|
||||||
|
### 1.1 Tipos Principales
|
||||||
|
|
||||||
|
```zig
|
||||||
|
/// Identificador único de dispositivo (32 bytes)
|
||||||
|
pub const DeviceId = [32]u8;
|
||||||
|
|
||||||
|
/// Representación corta del Device ID (primeros 8 bytes)
|
||||||
|
pub const ShortId = u64;
|
||||||
|
|
||||||
|
/// Configuración del nodo P2P
|
||||||
|
pub const Config = struct {
|
||||||
|
/// Nombre del dispositivo (mostrado a otros peers)
|
||||||
|
device_name: []const u8 = "zcatp2p",
|
||||||
|
|
||||||
|
/// Puerto para conexiones entrantes (0 = aleatorio)
|
||||||
|
listen_port: u16 = 22000,
|
||||||
|
|
||||||
|
/// Habilitar discovery local (LAN)
|
||||||
|
local_discovery: bool = true,
|
||||||
|
|
||||||
|
/// Servidores de discovery global (vacío = deshabilitado)
|
||||||
|
global_discovery_servers: []const []const u8 = &.{},
|
||||||
|
|
||||||
|
/// Servidores STUN para NAT traversal
|
||||||
|
stun_servers: []const []const u8 = &.{
|
||||||
|
"stun.l.google.com:19302",
|
||||||
|
"stun.syncthing.net:3478",
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Servidores relay (vacío = deshabilitado)
|
||||||
|
relay_servers: []const []const u8 = &.{},
|
||||||
|
|
||||||
|
/// Directorio para almacenar certificado y configuración
|
||||||
|
data_dir: []const u8,
|
||||||
|
|
||||||
|
/// Compresión LZ4 habilitada
|
||||||
|
compression: bool = true,
|
||||||
|
|
||||||
|
/// Callback cuando se descubre un nuevo dispositivo
|
||||||
|
on_device_discovered: ?*const fn(DeviceId, []const []const u8) void = null,
|
||||||
|
|
||||||
|
/// Callback cuando se recibe un mensaje
|
||||||
|
on_message_received: ?*const fn(*Connection, Message) void = null,
|
||||||
|
|
||||||
|
/// Callback cuando cambia el estado de conexión
|
||||||
|
on_connection_state_changed: ?*const fn(*Connection, ConnectionState) void = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Estado de una conexión
|
||||||
|
pub const ConnectionState = enum {
|
||||||
|
connecting,
|
||||||
|
connected,
|
||||||
|
disconnecting,
|
||||||
|
disconnected,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Mensaje recibido
|
||||||
|
pub const Message = struct {
|
||||||
|
id: u32,
|
||||||
|
content_type: []const u8,
|
||||||
|
data: []const u8,
|
||||||
|
timestamp: i64,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Información sobre un peer conectado
|
||||||
|
pub const PeerInfo = struct {
|
||||||
|
device_id: DeviceId,
|
||||||
|
device_name: []const u8,
|
||||||
|
client_name: []const u8,
|
||||||
|
client_version: []const u8,
|
||||||
|
addresses: []const []const u8,
|
||||||
|
connected_at: i64,
|
||||||
|
is_local: bool,
|
||||||
|
bytes_sent: u64,
|
||||||
|
bytes_received: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Error codes
|
||||||
|
pub const Error = error{
|
||||||
|
AlreadyInitialized,
|
||||||
|
NotInitialized,
|
||||||
|
InvalidDeviceId,
|
||||||
|
ConnectionFailed,
|
||||||
|
ConnectionTimeout,
|
||||||
|
ConnectionClosed,
|
||||||
|
PeerNotFound,
|
||||||
|
CertificateError,
|
||||||
|
TlsError,
|
||||||
|
ProtocolError,
|
||||||
|
CompressionError,
|
||||||
|
OutOfMemory,
|
||||||
|
IoError,
|
||||||
|
InvalidConfig,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Instancia P2P
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub const P2P = struct {
|
||||||
|
/// Inicializa el sistema P2P
|
||||||
|
/// - Carga o genera certificado
|
||||||
|
/// - Inicia listeners
|
||||||
|
/// - Inicia discovery
|
||||||
|
pub fn init(allocator: std.mem.Allocator, config: Config) Error!*P2P;
|
||||||
|
|
||||||
|
/// Libera todos los recursos
|
||||||
|
pub fn deinit(self: *P2P) void;
|
||||||
|
|
||||||
|
/// Obtiene el Device ID local
|
||||||
|
pub fn getDeviceId(self: *P2P) DeviceId;
|
||||||
|
|
||||||
|
/// Obtiene el Device ID como string (formato XXXXXXX-XXXXXXX-...)
|
||||||
|
pub fn getDeviceIdString(self: *P2P, buf: []u8) []const u8;
|
||||||
|
|
||||||
|
/// Parsea un Device ID desde string
|
||||||
|
pub fn parseDeviceId(str: []const u8) Error!DeviceId;
|
||||||
|
|
||||||
|
/// Compara dos Device IDs
|
||||||
|
pub fn deviceIdEquals(a: DeviceId, b: DeviceId) bool;
|
||||||
|
|
||||||
|
/// Obtiene el Short ID (para logging)
|
||||||
|
pub fn getShortId(device_id: DeviceId) ShortId;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Gestión de Conexiones
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub const P2P = struct {
|
||||||
|
// ... (continuación)
|
||||||
|
|
||||||
|
/// Conecta a un dispositivo por su Device ID
|
||||||
|
/// Busca automáticamente la dirección usando discovery
|
||||||
|
pub fn connect(self: *P2P, device_id: DeviceId) Error!*Connection;
|
||||||
|
|
||||||
|
/// Conecta a una dirección específica
|
||||||
|
pub fn connectAddress(self: *P2P, address: []const u8) Error!*Connection;
|
||||||
|
|
||||||
|
/// Desconecta de un peer
|
||||||
|
pub fn disconnect(self: *P2P, device_id: DeviceId) void;
|
||||||
|
|
||||||
|
/// Obtiene una conexión existente
|
||||||
|
pub fn getConnection(self: *P2P, device_id: DeviceId) ?*Connection;
|
||||||
|
|
||||||
|
/// Lista todas las conexiones activas
|
||||||
|
pub fn getConnections(self: *P2P, buf: []*Connection) []const *Connection;
|
||||||
|
|
||||||
|
/// Número de conexiones activas
|
||||||
|
pub fn connectionCount(self: *P2P) usize;
|
||||||
|
|
||||||
|
/// Información de un peer
|
||||||
|
pub fn getPeerInfo(self: *P2P, device_id: DeviceId) ?PeerInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Connection = struct {
|
||||||
|
/// Device ID del peer
|
||||||
|
pub fn getDeviceId(self: *Connection) DeviceId;
|
||||||
|
|
||||||
|
/// Estado actual de la conexión
|
||||||
|
pub fn getState(self: *Connection) ConnectionState;
|
||||||
|
|
||||||
|
/// Información del peer
|
||||||
|
pub fn getPeerInfo(self: *Connection) PeerInfo;
|
||||||
|
|
||||||
|
/// Envía datos al peer
|
||||||
|
/// Retorna el message_id asignado
|
||||||
|
pub fn send(
|
||||||
|
self: *Connection,
|
||||||
|
content_type: []const u8,
|
||||||
|
data: []const u8,
|
||||||
|
) Error!u32;
|
||||||
|
|
||||||
|
/// Envía datos y espera confirmación
|
||||||
|
pub fn sendAndWait(
|
||||||
|
self: *Connection,
|
||||||
|
content_type: []const u8,
|
||||||
|
data: []const u8,
|
||||||
|
timeout_ms: u32,
|
||||||
|
) Error!void;
|
||||||
|
|
||||||
|
/// Cierra la conexión
|
||||||
|
pub fn close(self: *Connection) void;
|
||||||
|
|
||||||
|
/// Cierra con motivo
|
||||||
|
pub fn closeWithReason(self: *Connection, reason: []const u8) void;
|
||||||
|
|
||||||
|
/// Espera hasta que la conexión esté establecida
|
||||||
|
pub fn waitConnected(self: *Connection, timeout_ms: u32) Error!void;
|
||||||
|
|
||||||
|
/// Verifica si la conexión está activa
|
||||||
|
pub fn isConnected(self: *Connection) bool;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Discovery
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub const P2P = struct {
|
||||||
|
// ... (continuación)
|
||||||
|
|
||||||
|
/// Busca un dispositivo por su ID
|
||||||
|
/// Retorna lista de direcciones conocidas
|
||||||
|
pub fn lookup(
|
||||||
|
self: *P2P,
|
||||||
|
device_id: DeviceId,
|
||||||
|
buf: [][]const u8,
|
||||||
|
) Error![]const []const u8;
|
||||||
|
|
||||||
|
/// Fuerza un anuncio local inmediato
|
||||||
|
pub fn announceLocal(self: *P2P) void;
|
||||||
|
|
||||||
|
/// Fuerza un anuncio global inmediato
|
||||||
|
pub fn announceGlobal(self: *P2P) Error!void;
|
||||||
|
|
||||||
|
/// Obtiene dispositivos descubiertos en la LAN
|
||||||
|
pub fn getLocalDevices(
|
||||||
|
self: *P2P,
|
||||||
|
buf: []DeviceId,
|
||||||
|
) []const DeviceId;
|
||||||
|
|
||||||
|
/// Añade manualmente una dirección conocida para un dispositivo
|
||||||
|
pub fn addKnownAddress(
|
||||||
|
self: *P2P,
|
||||||
|
device_id: DeviceId,
|
||||||
|
address: []const u8,
|
||||||
|
) void;
|
||||||
|
|
||||||
|
/// Elimina direcciones conocidas de un dispositivo
|
||||||
|
pub fn clearKnownAddresses(self: *P2P, device_id: DeviceId) void;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. NAT y Relay
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub const P2P = struct {
|
||||||
|
// ... (continuación)
|
||||||
|
|
||||||
|
/// Obtiene la IP externa descubierta por STUN
|
||||||
|
pub fn getExternalAddress(self: *P2P) ?[]const u8;
|
||||||
|
|
||||||
|
/// Obtiene el tipo de NAT detectado
|
||||||
|
pub fn getNatType(self: *P2P) NatType;
|
||||||
|
|
||||||
|
/// Verifica si estamos conectados a algún relay
|
||||||
|
pub fn isRelayConnected(self: *P2P) bool;
|
||||||
|
|
||||||
|
/// Obtiene la dirección de relay actual
|
||||||
|
pub fn getRelayAddress(self: *P2P) ?[]const u8;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const NatType = enum {
|
||||||
|
unknown,
|
||||||
|
none, // Sin NAT (IP pública)
|
||||||
|
full_cone, // Hole-punchable
|
||||||
|
restricted, // Hole-punchable
|
||||||
|
port_restricted,// Hole-punchable
|
||||||
|
symmetric, // No hole-punchable, necesita relay
|
||||||
|
blocked, // Sin conectividad UDP
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Callbacks y Eventos
|
||||||
|
|
||||||
|
```zig
|
||||||
|
/// Callback type para dispositivo descubierto
|
||||||
|
pub const OnDeviceDiscovered = *const fn(
|
||||||
|
device_id: DeviceId,
|
||||||
|
addresses: []const []const u8,
|
||||||
|
) void;
|
||||||
|
|
||||||
|
/// Callback type para mensaje recibido
|
||||||
|
pub const OnMessageReceived = *const fn(
|
||||||
|
connection: *Connection,
|
||||||
|
message: Message,
|
||||||
|
) void;
|
||||||
|
|
||||||
|
/// Callback type para cambio de estado de conexión
|
||||||
|
pub const OnConnectionStateChanged = *const fn(
|
||||||
|
connection: *Connection,
|
||||||
|
old_state: ConnectionState,
|
||||||
|
new_state: ConnectionState,
|
||||||
|
) void;
|
||||||
|
|
||||||
|
/// Callback type para error
|
||||||
|
pub const OnError = *const fn(
|
||||||
|
error_code: Error,
|
||||||
|
message: []const u8,
|
||||||
|
) void;
|
||||||
|
|
||||||
|
pub const P2P = struct {
|
||||||
|
// ... (continuación)
|
||||||
|
|
||||||
|
/// Registra callback para dispositivo descubierto
|
||||||
|
pub fn setOnDeviceDiscovered(self: *P2P, cb: ?OnDeviceDiscovered) void;
|
||||||
|
|
||||||
|
/// Registra callback para mensaje recibido
|
||||||
|
pub fn setOnMessageReceived(self: *P2P, cb: ?OnMessageReceived) void;
|
||||||
|
|
||||||
|
/// Registra callback para cambio de estado
|
||||||
|
pub fn setOnConnectionStateChanged(self: *P2P, cb: ?OnConnectionStateChanged) void;
|
||||||
|
|
||||||
|
/// Registra callback para errores
|
||||||
|
pub fn setOnError(self: *P2P, cb: ?OnError) void;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Utilidades
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub const utils = struct {
|
||||||
|
/// Convierte Device ID a string
|
||||||
|
pub fn deviceIdToString(id: DeviceId, buf: []u8) []const u8;
|
||||||
|
|
||||||
|
/// Parsea Device ID desde string
|
||||||
|
pub fn stringToDeviceId(str: []const u8) Error!DeviceId;
|
||||||
|
|
||||||
|
/// Calcula el dígito de verificación Luhn
|
||||||
|
pub fn luhn32(str: []const u8) u8;
|
||||||
|
|
||||||
|
/// Verifica un Device ID string
|
||||||
|
pub fn verifyDeviceIdString(str: []const u8) bool;
|
||||||
|
|
||||||
|
/// Genera un certificado auto-firmado
|
||||||
|
pub fn generateCertificate(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
common_name: []const u8,
|
||||||
|
) Error!Certificate;
|
||||||
|
|
||||||
|
/// Carga un certificado desde archivo
|
||||||
|
pub fn loadCertificate(path: []const u8) Error!Certificate;
|
||||||
|
|
||||||
|
/// Guarda un certificado a archivo
|
||||||
|
pub fn saveCertificate(cert: Certificate, path: []const u8) Error!void;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Ejemplo de Uso Completo
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const std = @import("std");
|
||||||
|
const p2p = @import("zcatp2p");
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
// Configuración
|
||||||
|
const config = p2p.Config{
|
||||||
|
.device_name = "Simifactu-Empresa-A",
|
||||||
|
.listen_port = 22000,
|
||||||
|
.local_discovery = true,
|
||||||
|
.data_dir = "/home/user/.simifactu/p2p",
|
||||||
|
.on_message_received = handleMessage,
|
||||||
|
.on_device_discovered = handleDiscovery,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inicializar
|
||||||
|
var node = try p2p.P2P.init(allocator, config);
|
||||||
|
defer node.deinit();
|
||||||
|
|
||||||
|
// Mostrar nuestro Device ID
|
||||||
|
var id_buf: [64]u8 = undefined;
|
||||||
|
const my_id = node.getDeviceIdString(&id_buf);
|
||||||
|
std.debug.print("Mi Device ID: {s}\n", .{my_id});
|
||||||
|
|
||||||
|
// Conectar a un peer conocido
|
||||||
|
const peer_id_str = "ABCDEFG-HIJKLMN-OPQRSTU-VWXYZ23-4567ABC-DEFGHIJ-KLMNOPQ-RSTUVWX";
|
||||||
|
const peer_id = try p2p.P2P.parseDeviceId(peer_id_str);
|
||||||
|
|
||||||
|
var conn = try node.connect(peer_id);
|
||||||
|
try conn.waitConnected(30000); // 30 segundos timeout
|
||||||
|
|
||||||
|
// Enviar una factura
|
||||||
|
const factura_data = @embedFile("factura.xml");
|
||||||
|
const msg_id = try conn.send("application/x-simifactu-invoice", factura_data);
|
||||||
|
std.debug.print("Factura enviada, message_id: {}\n", .{msg_id});
|
||||||
|
|
||||||
|
// Esperar respuesta...
|
||||||
|
// (En producción usaríamos el callback on_message_received)
|
||||||
|
|
||||||
|
// Cerrar
|
||||||
|
conn.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleMessage(conn: *p2p.Connection, msg: p2p.Message) void {
|
||||||
|
std.debug.print("Mensaje recibido de {}: type={s}, size={}\n", .{
|
||||||
|
p2p.P2P.getShortId(conn.getDeviceId()),
|
||||||
|
msg.content_type,
|
||||||
|
msg.data.len,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Procesar según content_type
|
||||||
|
if (std.mem.eql(u8, msg.content_type, "application/x-simifactu-invoice")) {
|
||||||
|
// Procesar factura recibida
|
||||||
|
processInvoice(msg.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleDiscovery(device_id: p2p.DeviceId, addresses: []const []const u8) void {
|
||||||
|
std.debug.print("Dispositivo descubierto: {} en {} direcciones\n", .{
|
||||||
|
p2p.P2P.getShortId(device_id),
|
||||||
|
addresses.len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processInvoice(data: []const u8) void {
|
||||||
|
// ... procesar factura
|
||||||
|
_ = data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Integración con Simifactu
|
||||||
|
|
||||||
|
### 8.1 Content Types Recomendados
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub const ContentTypes = struct {
|
||||||
|
pub const invoice = "application/x-simifactu-invoice";
|
||||||
|
pub const invoice_ack = "application/x-simifactu-invoice-ack";
|
||||||
|
pub const certificate = "application/x-simifactu-certificate";
|
||||||
|
pub const verifactu = "application/x-simifactu-verifactu";
|
||||||
|
pub const query = "application/x-simifactu-query";
|
||||||
|
pub const response = "application/x-simifactu-response";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Flujo Típico
|
||||||
|
|
||||||
|
```
|
||||||
|
Empresa A Empresa B
|
||||||
|
│ │
|
||||||
|
│ 1. connect(device_id_B) │
|
||||||
|
│────────────────────────────────────────────>│
|
||||||
|
│ │
|
||||||
|
│ 2. HELLO exchange │
|
||||||
|
│<═══════════════════════════════════════════>│
|
||||||
|
│ │
|
||||||
|
│ 3. send(invoice, factura_xml) │
|
||||||
|
│────────────────────────────────────────────>│
|
||||||
|
│ │
|
||||||
|
│ 4. DATA_ACK │
|
||||||
|
│<────────────────────────────────────────────│
|
||||||
|
│ │
|
||||||
|
│ 5. send(invoice_ack, confirmacion) │
|
||||||
|
│<────────────────────────────────────────────│
|
||||||
|
│ │
|
||||||
|
│ 6. DATA_ACK │
|
||||||
|
│────────────────────────────────────────────>│
|
||||||
|
│ │
|
||||||
|
│ 7. close() │
|
||||||
|
│────────────────────────────────────────────>│
|
||||||
|
│ │
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Thread Safety
|
||||||
|
|
||||||
|
- `P2P.init()` y `P2P.deinit()` deben llamarse desde el mismo thread
|
||||||
|
- Todas las demás funciones son thread-safe
|
||||||
|
- Los callbacks pueden ser llamados desde cualquier thread
|
||||||
|
- Se recomienda no hacer operaciones bloqueantes en callbacks
|
||||||
|
|
||||||
|
## 10. Gestión de Memoria
|
||||||
|
|
||||||
|
- `P2P.init()` asigna memoria usando el allocator proporcionado
|
||||||
|
- `P2P.deinit()` libera toda la memoria
|
||||||
|
- Los strings retornados son válidos hasta la siguiente llamada
|
||||||
|
- Los mensajes recibidos en callbacks son válidos solo durante el callback
|
||||||
|
- Para retener datos, copiarlos a memoria propia
|
||||||
87
CLAUDE.md
Normal file
87
CLAUDE.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# zcatp2p - Protocolo P2P para comunicación directa entre empresas
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Librería Zig para comunicación P2P segura entre instancias de Simifactu.
|
||||||
|
Permite intercambio directo de documentos (facturas, certificados) entre empresas
|
||||||
|
sin necesidad de email ni servicios cloud.
|
||||||
|
|
||||||
|
## Objetivos
|
||||||
|
|
||||||
|
1. **Seguridad**: E2E cifrado con TLS 1.3 + ChaCha20-Poly1305
|
||||||
|
2. **Descentralizado**: Sin servidor central obligatorio
|
||||||
|
3. **Sin dependencias externas**: Implementación completa en Zig puro
|
||||||
|
4. **Compatible con NAT**: STUN + relay para atravesar firewalls
|
||||||
|
|
||||||
|
## Arquitectura
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ zcatp2p Library │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ API Layer │
|
||||||
|
│ ├── p2p.init() / p2p.deinit() │
|
||||||
|
│ ├── p2p.connect(device_id) │
|
||||||
|
│ ├── p2p.send(data) │
|
||||||
|
│ └── p2p.receive() -> data │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Protocol Layer │
|
||||||
|
│ ├── Message framing (Header + Payload) │
|
||||||
|
│ ├── Compression (LZ4) │
|
||||||
|
│ └── Encryption (ChaCha20-Poly1305) │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Connection Layer │
|
||||||
|
│ ├── TLS 1.3 handshake │
|
||||||
|
│ ├── Certificate-based identity │
|
||||||
|
│ └── Connection multiplexing │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Discovery Layer │
|
||||||
|
│ ├── Local: UDP broadcast/multicast │
|
||||||
|
│ ├── Global: HTTPS announce/query │
|
||||||
|
│ └── Cache de direcciones conocidas │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ NAT Traversal Layer │
|
||||||
|
│ ├── STUN client │
|
||||||
|
│ ├── UPnP/NAT-PMP port mapping │
|
||||||
|
│ └── Relay fallback │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Estructura de archivos
|
||||||
|
|
||||||
|
```
|
||||||
|
zcatp2p/
|
||||||
|
├── CLAUDE.md # Este archivo
|
||||||
|
├── PROTOCOL.md # Especificación detallada del protocolo
|
||||||
|
├── API.md # Documentación de la API
|
||||||
|
├── build.zig # Build system
|
||||||
|
└── src/
|
||||||
|
├── main.zig # Exports públicos
|
||||||
|
├── identity.zig # Device ID, certificados
|
||||||
|
├── crypto.zig # ChaCha20-Poly1305, SHA256, etc.
|
||||||
|
├── tls.zig # TLS 1.3 implementation
|
||||||
|
├── protocol.zig # Message framing
|
||||||
|
├── discovery.zig # Local + global discovery
|
||||||
|
├── stun.zig # STUN client
|
||||||
|
├── nat.zig # UPnP/NAT-PMP
|
||||||
|
├── relay.zig # Relay protocol
|
||||||
|
└── connection.zig # Connection management
|
||||||
|
```
|
||||||
|
|
||||||
|
## Referencias
|
||||||
|
|
||||||
|
- Syncthing BEP (Block Exchange Protocol): `/mnt/cello2/arno/re/recode/referencias/syncthing/`
|
||||||
|
- RFC 5389 (STUN)
|
||||||
|
- RFC 8446 (TLS 1.3)
|
||||||
|
- RFC 8439 (ChaCha20-Poly1305)
|
||||||
|
|
||||||
|
## Estado
|
||||||
|
|
||||||
|
- [ ] Especificación del protocolo
|
||||||
|
- [ ] API design
|
||||||
|
- [ ] Implementación crypto
|
||||||
|
- [ ] Implementación TLS
|
||||||
|
- [ ] Implementación discovery
|
||||||
|
- [ ] Implementación STUN
|
||||||
|
- [ ] Implementación relay
|
||||||
|
- [ ] Tests
|
||||||
538
PROTOCOL.md
Normal file
538
PROTOCOL.md
Normal file
|
|
@ -0,0 +1,538 @@
|
||||||
|
# Protocolo zcatp2p - Especificación Técnica v1.0
|
||||||
|
|
||||||
|
## 1. Visión General
|
||||||
|
|
||||||
|
zcatp2p es un protocolo de comunicación P2P para intercambio seguro de mensajes
|
||||||
|
entre nodos identificados criptográficamente. Diseñado para comunicación directa
|
||||||
|
entre empresas (facturas, certificados) sin intermediarios.
|
||||||
|
|
||||||
|
### 1.1 Principios de Diseño
|
||||||
|
|
||||||
|
1. **Seguridad primero**: Todo tráfico cifrado E2E con TLS 1.3
|
||||||
|
2. **Identificación por certificado**: Device ID = SHA256(certificado)
|
||||||
|
3. **Descentralizado**: Funciona sin servidor central (LAN)
|
||||||
|
4. **NAT-friendly**: STUN + relay para conectividad universal
|
||||||
|
5. **Sin dependencias**: Implementación completa en Zig puro
|
||||||
|
|
||||||
|
### 1.2 Componentes del Sistema
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ NODO SIMIFACTU │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ Discovery │ │ STUN │ │ Relay Client │ │
|
||||||
|
│ │ (local+ │ │ Client │ │ (fallback) │ │
|
||||||
|
│ │ global) │ │ │ │ │ │
|
||||||
|
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ┌──────┴────────────────┴────────────────────┴──────────┐ │
|
||||||
|
│ │ Connection Manager │ │
|
||||||
|
│ │ - Mantiene conexiones activas │ │
|
||||||
|
│ │ - Intenta conexión directa primero │ │
|
||||||
|
│ │ - Fallback a relay si falla │ │
|
||||||
|
│ └──────────────────────────┬────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────────────────┴────────────────────────────┐ │
|
||||||
|
│ │ TLS 1.3 Layer │ │
|
||||||
|
│ │ - Autenticación mutua por certificado │ │
|
||||||
|
│ │ - Device ID derivado de certificado │ │
|
||||||
|
│ └──────────────────────────┬────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────────────────┴────────────────────────────┐ │
|
||||||
|
│ │ Protocol Layer │ │
|
||||||
|
│ │ - Message framing │ │
|
||||||
|
│ │ - Compresión LZ4 opcional │ │
|
||||||
|
│ │ - Request/Response pattern │ │
|
||||||
|
│ └───────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Identidad de Dispositivo (Device ID)
|
||||||
|
|
||||||
|
### 2.1 Generación
|
||||||
|
|
||||||
|
El Device ID es un identificador único de 32 bytes derivado del certificado TLS:
|
||||||
|
|
||||||
|
```
|
||||||
|
DeviceID = SHA256(DER_encoded_certificate)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Representación en String
|
||||||
|
|
||||||
|
Para facilitar el intercambio, el Device ID se representa como string Base32 con
|
||||||
|
dígitos de verificación Luhn:
|
||||||
|
|
||||||
|
```
|
||||||
|
Formato: XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX
|
||||||
|
└──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘
|
||||||
|
|
||||||
|
Cada grupo de 7 caracteres incluye 1 dígito de verificación Luhn.
|
||||||
|
Total: 56 caracteres + 7 guiones = 63 caracteres
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Algoritmo Luhn para Base32
|
||||||
|
|
||||||
|
```
|
||||||
|
Alfabeto Base32: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
|
||||||
|
|
||||||
|
function luhn32(s):
|
||||||
|
factor = 1
|
||||||
|
sum = 0
|
||||||
|
n = 32 // base
|
||||||
|
|
||||||
|
for char in reverse(s):
|
||||||
|
codepoint = base32_to_int(char)
|
||||||
|
addend = factor * codepoint
|
||||||
|
factor = (factor == 2) ? 1 : 2
|
||||||
|
addend = (addend / n) + (addend % n)
|
||||||
|
sum += addend
|
||||||
|
|
||||||
|
remainder = sum % n
|
||||||
|
check = (n - remainder) % n
|
||||||
|
return int_to_base32(check)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Certificado Auto-firmado
|
||||||
|
|
||||||
|
Cada nodo genera un certificado X.509 auto-firmado al inicializarse:
|
||||||
|
|
||||||
|
```
|
||||||
|
- Subject: CN=syncthing (o "zcatp2p" para nuestra implementación)
|
||||||
|
- Key Algorithm: ECDSA P-256 o Ed25519
|
||||||
|
- Validity: 20 años
|
||||||
|
- Serial: Random 128 bits
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Protocolo de Transporte
|
||||||
|
|
||||||
|
### 3.1 Conexión TLS 1.3
|
||||||
|
|
||||||
|
Toda comunicación usa TLS 1.3 con autenticación mutua de certificados:
|
||||||
|
|
||||||
|
```
|
||||||
|
Cliente Servidor
|
||||||
|
│ │
|
||||||
|
│──────── ClientHello ────────────────>│
|
||||||
|
│ + key_share │
|
||||||
|
│ + supported_versions │
|
||||||
|
│ │
|
||||||
|
│<─────── ServerHello ─────────────────│
|
||||||
|
│ + key_share │
|
||||||
|
│ + EncryptedExtensions │
|
||||||
|
│ + CertificateRequest │
|
||||||
|
│ + Certificate │
|
||||||
|
│ + CertificateVerify │
|
||||||
|
│ + Finished │
|
||||||
|
│ │
|
||||||
|
│──────── Certificate ────────────────>│
|
||||||
|
│ + CertificateVerify │
|
||||||
|
│ + Finished │
|
||||||
|
│ │
|
||||||
|
│<═══════ Application Data ═══════════>│
|
||||||
|
│ │
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Cipher Suites Soportadas
|
||||||
|
|
||||||
|
En orden de preferencia:
|
||||||
|
1. TLS_CHACHA20_POLY1305_SHA256
|
||||||
|
2. TLS_AES_256_GCM_SHA384
|
||||||
|
3. TLS_AES_128_GCM_SHA256
|
||||||
|
|
||||||
|
### 3.3 Verificación de Identidad
|
||||||
|
|
||||||
|
Después del handshake TLS, ambos extremos:
|
||||||
|
1. Calculan SHA256 del certificado del peer
|
||||||
|
2. Verifican que coincide con el Device ID esperado
|
||||||
|
3. Si no coincide, cierran la conexión
|
||||||
|
|
||||||
|
## 4. Protocolo de Mensajes
|
||||||
|
|
||||||
|
### 4.1 Formato de Trama
|
||||||
|
|
||||||
|
Cada mensaje tiene el siguiente formato:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────┬────────────────┬─────────────────────────────┐
|
||||||
|
│ Header (2B) │ Length (4B) │ Payload (variable) │
|
||||||
|
└────────────────┴────────────────┴─────────────────────────────┘
|
||||||
|
│ │ │
|
||||||
|
│ │ └── Datos del mensaje
|
||||||
|
│ └── Longitud del payload en big-endian
|
||||||
|
└── Tipo de mensaje (1B) + Flags (1B)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Tipos de Mensaje
|
||||||
|
|
||||||
|
```
|
||||||
|
Valor Nombre Descripción
|
||||||
|
───── ────── ───────────
|
||||||
|
0x00 HELLO Intercambio inicial de información
|
||||||
|
0x01 PING Keepalive
|
||||||
|
0x02 PONG Respuesta a PING
|
||||||
|
0x03 DATA Datos de aplicación
|
||||||
|
0x04 DATA_ACK Confirmación de recepción
|
||||||
|
0x05 CLOSE Cierre de conexión
|
||||||
|
0x06 ERROR Notificación de error
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Flags
|
||||||
|
|
||||||
|
```
|
||||||
|
Bit Nombre Descripción
|
||||||
|
─── ────── ───────────
|
||||||
|
0 COMPRESSED Payload comprimido con LZ4
|
||||||
|
1 ENCRYPTED Payload cifrado (adicional a TLS)
|
||||||
|
2 REQUEST Espera respuesta
|
||||||
|
3 RESPONSE Es respuesta a REQUEST
|
||||||
|
4-7 Reserved Para uso futuro
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Mensaje HELLO
|
||||||
|
|
||||||
|
Intercambiado inmediatamente después del handshake TLS:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HELLO Message │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ device_name_len: u8 │ Longitud del nombre │
|
||||||
|
│ device_name: [N]u8 │ Nombre del dispositivo │
|
||||||
|
│ client_name_len: u8 │ Longitud del cliente │
|
||||||
|
│ client_name: [N]u8 │ "simifactu" o "zcatp2p" │
|
||||||
|
│ client_version_len: u8 │ Longitud de versión │
|
||||||
|
│ client_version: [N]u8 │ Ej: "1.0.0" │
|
||||||
|
│ timestamp: i64 │ Unix timestamp │
|
||||||
|
│ capabilities: u32 │ Bitmap de capacidades │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Capacidades (bitmap):
|
||||||
|
```
|
||||||
|
Bit Capacidad
|
||||||
|
─── ─────────
|
||||||
|
0 COMPRESSION_LZ4
|
||||||
|
1 ENCRYPTION_CHACHA20
|
||||||
|
2 RELAY_SUPPORT
|
||||||
|
3 IPV6_SUPPORT
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 Mensaje DATA
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ DATA Message │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ message_id: u32 │ ID único para correlación │
|
||||||
|
│ content_type_len: u8 │ Longitud del tipo │
|
||||||
|
│ content_type: [N]u8 │ MIME type │
|
||||||
|
│ data_len: u32 │ Longitud de los datos │
|
||||||
|
│ data: [N]u8 │ Datos de aplicación │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Content types sugeridos para Simifactu:
|
||||||
|
- `application/x-simifactu-invoice` - Factura
|
||||||
|
- `application/x-simifactu-certificate` - Certificado
|
||||||
|
- `application/x-simifactu-verifactu` - Datos Verifactu
|
||||||
|
|
||||||
|
### 4.6 Mensaje CLOSE
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ CLOSE Message │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ reason_len: u8 │ Longitud del motivo │
|
||||||
|
│ reason: [N]u8 │ Motivo del cierre │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Discovery Protocol
|
||||||
|
|
||||||
|
### 5.1 Local Discovery (LAN)
|
||||||
|
|
||||||
|
#### 5.1.1 Formato de Anuncio
|
||||||
|
|
||||||
|
Enviado via UDP broadcast (IPv4) o multicast (IPv6):
|
||||||
|
|
||||||
|
```
|
||||||
|
Puerto: 21027
|
||||||
|
IPv4 Broadcast: 255.255.255.255:21027
|
||||||
|
IPv6 Multicast: [ff12::8384]:21027
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Local Announce Packet │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ magic: u32 │ 0x2EA7D90B │
|
||||||
|
│ device_id: [32]u8 │ SHA256 del certificado │
|
||||||
|
│ instance_id: i64 │ ID único por ejecución │
|
||||||
|
│ num_addresses: u8 │ Número de direcciones │
|
||||||
|
│ addresses: [N]Address │ Lista de direcciones │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Address:
|
||||||
|
│ addr_len: u8 │ Longitud de la URL │
|
||||||
|
│ addr: [N]u8 │ URL (ej: "tcp://192.168.1.5:22000") │
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.1.2 Frecuencia de Anuncios
|
||||||
|
|
||||||
|
- Intervalo normal: 30 segundos
|
||||||
|
- Al detectar nuevo dispositivo: inmediatamente
|
||||||
|
- Caducidad del cache: 90 segundos
|
||||||
|
|
||||||
|
### 5.2 Global Discovery (Internet)
|
||||||
|
|
||||||
|
#### 5.2.1 Servidor de Discovery
|
||||||
|
|
||||||
|
El servidor de discovery es un servicio HTTPS que:
|
||||||
|
1. Recibe anuncios de dispositivos
|
||||||
|
2. Responde consultas sobre dispositivos
|
||||||
|
|
||||||
|
Para funcionar en Internet, necesitas al menos un servidor de discovery.
|
||||||
|
Puedes usar servidores públicos de Syncthing o ejecutar uno propio.
|
||||||
|
|
||||||
|
#### 5.2.2 Anuncio (POST)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST https://discovery.example.com/v2/
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: (certificado TLS cliente)
|
||||||
|
|
||||||
|
{
|
||||||
|
"addresses": [
|
||||||
|
"tcp://203.0.113.45:22000",
|
||||||
|
"relay://relay.example.com:22067/?id=XXXXX"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
El Device ID se extrae del certificado TLS del cliente.
|
||||||
|
|
||||||
|
#### 5.2.3 Consulta (GET)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://discovery.example.com/v2/?device=XXXXXXX-XXXXXXX-...
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"addresses": [
|
||||||
|
"tcp://203.0.113.45:22000",
|
||||||
|
"relay://relay.example.com:22067/?id=XXXXX"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2.4 Headers de Control
|
||||||
|
|
||||||
|
```
|
||||||
|
Response Headers:
|
||||||
|
- Reannounce-After: 1800 // Segundos hasta próximo anuncio
|
||||||
|
- Retry-After: 60 // En caso de error
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
- 200: OK
|
||||||
|
- 400: Bad Request
|
||||||
|
- 403: Forbidden (sin certificado)
|
||||||
|
- 404: Device not found
|
||||||
|
- 429: Too Many Requests
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. NAT Traversal
|
||||||
|
|
||||||
|
### 6.1 Estrategia de Conexión
|
||||||
|
|
||||||
|
Orden de intentos para conectar con un peer:
|
||||||
|
|
||||||
|
1. **Conexión directa TCP**: Si tenemos IP directa
|
||||||
|
2. **Conexión directa con STUN**: Usar IP externa descubierta
|
||||||
|
3. **Hole punching**: Ambos peers intentan simultáneamente
|
||||||
|
4. **Relay**: Último recurso si todo falla
|
||||||
|
|
||||||
|
### 6.2 STUN (Session Traversal Utilities for NAT)
|
||||||
|
|
||||||
|
Usamos STUN para descubrir nuestra IP externa y tipo de NAT.
|
||||||
|
|
||||||
|
#### 6.2.1 Servidores STUN públicos
|
||||||
|
|
||||||
|
```
|
||||||
|
stun.l.google.com:19302
|
||||||
|
stun1.l.google.com:19302
|
||||||
|
stun.syncthing.net:3478
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2.2 Tipos de NAT detectables
|
||||||
|
|
||||||
|
```
|
||||||
|
Tipo Hole-punchable? Descripción
|
||||||
|
──── ─────────────── ───────────
|
||||||
|
Full Cone Sí Puerto externo fijo
|
||||||
|
Restricted Cone Sí Requiere envío previo
|
||||||
|
Port Restricted Cone Sí + puerto específico
|
||||||
|
Symmetric No Puerto diferente por destino
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Relay Protocol
|
||||||
|
|
||||||
|
Cuando no es posible conexión directa, usamos un servidor relay.
|
||||||
|
|
||||||
|
#### 6.3.1 Conectarse al Relay
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Cliente se conecta al relay via TLS
|
||||||
|
2. Envía JoinRelayRequest
|
||||||
|
3. Relay responde con SessionInvitation
|
||||||
|
4. Cliente A publica su dirección de relay en discovery
|
||||||
|
5. Cliente B consulta discovery, ve dirección relay
|
||||||
|
6. Cliente B conecta al relay, envía ConnectRequest(device_id_A)
|
||||||
|
7. Relay notifica a A, ambos reciben SessionInvitation
|
||||||
|
8. Relay hace de proxy transparente
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3.2 Mensajes del Protocolo Relay
|
||||||
|
|
||||||
|
```
|
||||||
|
JoinRelayRequest:
|
||||||
|
│ token_len: u8 │
|
||||||
|
│ token: [N]u8 │ Token de autenticación (opcional)
|
||||||
|
|
||||||
|
ConnectRequest:
|
||||||
|
│ device_id: [32]u8 │ ID del dispositivo destino
|
||||||
|
|
||||||
|
SessionInvitation:
|
||||||
|
│ from: [32]u8 │ Device ID del peer
|
||||||
|
│ key: [32]u8 │ Clave de sesión
|
||||||
|
│ address: [16]u8 │ IP del relay
|
||||||
|
│ port: u16 │ Puerto
|
||||||
|
│ is_server: bool │ ¿Somos el "servidor"?
|
||||||
|
|
||||||
|
Response:
|
||||||
|
│ code: i32 │ 0=OK, 1=NotFound, 2=AlreadyConnected
|
||||||
|
│ message_len: u8 │
|
||||||
|
│ message: [N]u8 │
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Cifrado Adicional (Opcional)
|
||||||
|
|
||||||
|
Además del cifrado TLS, se puede añadir cifrado a nivel de aplicación.
|
||||||
|
|
||||||
|
### 7.1 ChaCha20-Poly1305
|
||||||
|
|
||||||
|
```
|
||||||
|
Nonce size: 24 bytes (XChaCha20)
|
||||||
|
Tag size: 16 bytes
|
||||||
|
Key size: 32 bytes
|
||||||
|
|
||||||
|
Encrypted = Nonce || ChaCha20-Poly1305(Key, Nonce, Plaintext)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Derivación de Claves
|
||||||
|
|
||||||
|
Para cifrado de datos específicos:
|
||||||
|
|
||||||
|
```
|
||||||
|
FileKey = HKDF-SHA256(
|
||||||
|
IKM = FolderKey || filename,
|
||||||
|
salt = "zcatp2p",
|
||||||
|
info = empty
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Compresión
|
||||||
|
|
||||||
|
### 8.1 LZ4
|
||||||
|
|
||||||
|
Usamos LZ4 para compresión de mensajes grandes (>128 bytes):
|
||||||
|
|
||||||
|
```
|
||||||
|
Compressed = u32_be(original_size) || LZ4_compress(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
Solo comprimimos si el resultado es al menos 3% más pequeño.
|
||||||
|
|
||||||
|
## 9. Keepalive y Timeouts
|
||||||
|
|
||||||
|
```
|
||||||
|
Intervalo de PING: 90 segundos
|
||||||
|
Timeout de recepción: 300 segundos (5 min)
|
||||||
|
Timeout de conexión: 30 segundos
|
||||||
|
Timeout de close: 10 segundos
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Puertos por Defecto
|
||||||
|
|
||||||
|
```
|
||||||
|
Puerto Uso
|
||||||
|
───── ───
|
||||||
|
22000 Conexiones P2P directas
|
||||||
|
21027 Local discovery (UDP)
|
||||||
|
22067 Relay server
|
||||||
|
22070 Relay status (HTTP)
|
||||||
|
443 Global discovery (HTTPS)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. URLs de Direcciones
|
||||||
|
|
||||||
|
```
|
||||||
|
Formato Ejemplo
|
||||||
|
─────── ───────
|
||||||
|
tcp://host:port tcp://192.168.1.5:22000
|
||||||
|
tcp4://host:port tcp4://192.168.1.5:22000
|
||||||
|
tcp6://host:port tcp6://[::1]:22000
|
||||||
|
relay://host:port/?id=XXX relay://relay.example.com:22067/?id=ABCDEFG
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. Consideraciones de Seguridad
|
||||||
|
|
||||||
|
1. **Certificados**: Generar con entropía suficiente
|
||||||
|
2. **Device ID**: Verificar siempre después del handshake TLS
|
||||||
|
3. **Replay attacks**: Usar timestamps y nonces únicos
|
||||||
|
4. **DoS**: Limitar conexiones por IP, rate limiting
|
||||||
|
5. **Man-in-the-middle**: Verificar Device ID conocidos
|
||||||
|
6. **Relay**: El relay ve metadatos pero NO el contenido (TLS end-to-end)
|
||||||
|
|
||||||
|
## 13. Ejemplo de Flujo Completo
|
||||||
|
|
||||||
|
```
|
||||||
|
Empresa A quiere enviar factura a Empresa B
|
||||||
|
|
||||||
|
1. A tiene el Device ID de B (intercambiado previamente, ej: QR)
|
||||||
|
|
||||||
|
2. A busca a B:
|
||||||
|
a. Busca en cache local
|
||||||
|
b. Envía broadcast LAN
|
||||||
|
c. Consulta discovery global
|
||||||
|
|
||||||
|
3. A obtiene direcciones de B:
|
||||||
|
["tcp://203.0.113.45:22000", "relay://relay.example.com:22067/?id=B"]
|
||||||
|
|
||||||
|
4. A intenta conectar:
|
||||||
|
a. Intenta TCP directo → falla (NAT)
|
||||||
|
b. Intenta relay → éxito
|
||||||
|
|
||||||
|
5. Handshake TLS:
|
||||||
|
- A presenta certificado
|
||||||
|
- B presenta certificado
|
||||||
|
- Ambos verifican Device IDs
|
||||||
|
|
||||||
|
6. Intercambio HELLO:
|
||||||
|
- A envía sus capacidades
|
||||||
|
- B responde con las suyas
|
||||||
|
|
||||||
|
7. A envía factura:
|
||||||
|
DATA {
|
||||||
|
message_id: 1,
|
||||||
|
content_type: "application/x-simifactu-invoice",
|
||||||
|
data: <factura_serializada>
|
||||||
|
}
|
||||||
|
|
||||||
|
8. B confirma:
|
||||||
|
DATA_ACK { message_id: 1 }
|
||||||
|
|
||||||
|
9. Cierre:
|
||||||
|
A envía CLOSE { reason: "transfer complete" }
|
||||||
|
B cierra conexión
|
||||||
|
```
|
||||||
56
build.zig
Normal file
56
build.zig
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Main library module
|
||||||
|
// ===========================================
|
||||||
|
const zcatp2p_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Tests
|
||||||
|
// ===========================================
|
||||||
|
const lib_unit_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||||
|
|
||||||
|
const test_step = b.step("test", "Run unit tests");
|
||||||
|
test_step.dependOn(&run_lib_unit_tests.step);
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Example
|
||||||
|
// ===========================================
|
||||||
|
const example = b.addExecutable(.{
|
||||||
|
.name = "zcatp2p-example",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("examples/basic.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "zcatp2p", .module = zcatp2p_mod },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
b.installArtifact(example);
|
||||||
|
|
||||||
|
const run_example = b.addRunArtifact(example);
|
||||||
|
run_example.step.dependOn(b.getInstallStep());
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_example.addArgs(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
const run_step = b.step("run", "Run the example");
|
||||||
|
run_step.dependOn(&run_example.step);
|
||||||
|
}
|
||||||
38
examples/basic.zig
Normal file
38
examples/basic.zig
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
//! Ejemplo básico de uso de zcatp2p
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const p2p = @import("zcatp2p");
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
// Configuración
|
||||||
|
const config = p2p.Config{
|
||||||
|
.device_name = "Simifactu-Ejemplo",
|
||||||
|
.listen_port = 22000,
|
||||||
|
.local_discovery = true,
|
||||||
|
.data_dir = "/tmp/zcatp2p-example",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inicializar
|
||||||
|
std.debug.print("Inicializando zcatp2p...\n", .{});
|
||||||
|
var node = try p2p.P2P.init(allocator, config);
|
||||||
|
defer node.deinit();
|
||||||
|
|
||||||
|
// Mostrar nuestro Device ID
|
||||||
|
var id_buf: [64]u8 = undefined;
|
||||||
|
const my_id = node.getDeviceIdString(&id_buf);
|
||||||
|
std.debug.print("Mi Device ID: {s}\n", .{my_id});
|
||||||
|
|
||||||
|
// Mostrar estado de NAT
|
||||||
|
const nat_type = node.getNatType();
|
||||||
|
std.debug.print("Tipo de NAT: {}\n", .{nat_type});
|
||||||
|
|
||||||
|
// Mostrar conexiones activas
|
||||||
|
std.debug.print("Conexiones activas: {}\n", .{node.connectionCount()});
|
||||||
|
|
||||||
|
std.debug.print("\n¡zcatp2p inicializado correctamente!\n", .{});
|
||||||
|
std.debug.print("Pendiente: implementación completa de TLS, STUN, y relay.\n", .{});
|
||||||
|
}
|
||||||
369
src/connection.zig
Normal file
369
src/connection.zig
Normal file
|
|
@ -0,0 +1,369 @@
|
||||||
|
//! Módulo de conexión - Gestión de conexiones P2P
|
||||||
|
//!
|
||||||
|
//! Maneja conexiones TLS, keepalives, y multiplexación de mensajes.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const identity = @import("identity.zig");
|
||||||
|
const protocol = @import("protocol.zig");
|
||||||
|
const discovery = @import("discovery.zig");
|
||||||
|
|
||||||
|
pub const DeviceId = identity.DeviceId;
|
||||||
|
|
||||||
|
/// Configuración del nodo P2P
|
||||||
|
pub const Config = struct {
|
||||||
|
/// Nombre del dispositivo
|
||||||
|
device_name: []const u8 = "zcatp2p",
|
||||||
|
|
||||||
|
/// Puerto para conexiones entrantes
|
||||||
|
listen_port: u16 = 22000,
|
||||||
|
|
||||||
|
/// Habilitar discovery local
|
||||||
|
local_discovery: bool = true,
|
||||||
|
|
||||||
|
/// Servidores de discovery global
|
||||||
|
global_discovery_servers: []const []const u8 = &.{},
|
||||||
|
|
||||||
|
/// Servidores STUN
|
||||||
|
stun_servers: []const []const u8 = &.{
|
||||||
|
"stun.l.google.com:19302",
|
||||||
|
"stun.syncthing.net:3478",
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Servidores relay
|
||||||
|
relay_servers: []const []const u8 = &.{},
|
||||||
|
|
||||||
|
/// Directorio de datos
|
||||||
|
data_dir: []const u8,
|
||||||
|
|
||||||
|
/// Compresión habilitada
|
||||||
|
compression: bool = true,
|
||||||
|
|
||||||
|
/// Callbacks
|
||||||
|
on_device_discovered: ?*const fn (DeviceId, []const []const u8) void = null,
|
||||||
|
on_message_received: ?*const fn (*Connection, protocol.Message) void = null,
|
||||||
|
on_connection_state_changed: ?*const fn (*Connection, ConnectionState) void = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Estado de conexión
|
||||||
|
pub const ConnectionState = enum {
|
||||||
|
connecting,
|
||||||
|
connected,
|
||||||
|
disconnecting,
|
||||||
|
disconnected,
|
||||||
|
@"error",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Tipo de NAT detectado
|
||||||
|
pub const NatType = enum {
|
||||||
|
unknown,
|
||||||
|
none,
|
||||||
|
full_cone,
|
||||||
|
restricted,
|
||||||
|
port_restricted,
|
||||||
|
symmetric,
|
||||||
|
blocked,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Errores del módulo
|
||||||
|
pub const Error = error{
|
||||||
|
AlreadyInitialized,
|
||||||
|
NotInitialized,
|
||||||
|
InvalidDeviceId,
|
||||||
|
ConnectionFailed,
|
||||||
|
ConnectionTimeout,
|
||||||
|
ConnectionClosed,
|
||||||
|
PeerNotFound,
|
||||||
|
CertificateError,
|
||||||
|
TlsError,
|
||||||
|
ProtocolError,
|
||||||
|
CompressionError,
|
||||||
|
OutOfMemory,
|
||||||
|
IoError,
|
||||||
|
InvalidConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Información de un peer
|
||||||
|
pub const PeerInfo = struct {
|
||||||
|
device_id: DeviceId,
|
||||||
|
device_name: []const u8,
|
||||||
|
client_name: []const u8,
|
||||||
|
client_version: []const u8,
|
||||||
|
addresses: []const []const u8,
|
||||||
|
connected_at: i64,
|
||||||
|
is_local: bool,
|
||||||
|
bytes_sent: u64,
|
||||||
|
bytes_received: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Conexión con un peer
|
||||||
|
pub const Connection = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
device_id: DeviceId,
|
||||||
|
state: ConnectionState,
|
||||||
|
peer_info: ?PeerInfo,
|
||||||
|
socket: ?std.posix.socket_t,
|
||||||
|
next_message_id: u32,
|
||||||
|
bytes_sent: u64,
|
||||||
|
bytes_received: u64,
|
||||||
|
connected_at: i64,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, device_id: DeviceId) Connection {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.device_id = device_id,
|
||||||
|
.state = .disconnected,
|
||||||
|
.peer_info = null,
|
||||||
|
.socket = null,
|
||||||
|
.next_message_id = 1,
|
||||||
|
.bytes_sent = 0,
|
||||||
|
.bytes_received = 0,
|
||||||
|
.connected_at = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Connection) void {
|
||||||
|
if (self.socket) |sock| {
|
||||||
|
std.posix.close(sock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getDeviceId(self: *Connection) DeviceId {
|
||||||
|
return self.device_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getState(self: *Connection) ConnectionState {
|
||||||
|
return self.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getPeerInfo(self: *Connection) ?PeerInfo {
|
||||||
|
return self.peer_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isConnected(self: *Connection) bool {
|
||||||
|
return self.state == .connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Envía datos al peer
|
||||||
|
pub fn send(
|
||||||
|
self: *Connection,
|
||||||
|
content_type: []const u8,
|
||||||
|
data: []const u8,
|
||||||
|
) Error!u32 {
|
||||||
|
if (self.state != .connected) return Error.ConnectionClosed;
|
||||||
|
|
||||||
|
const msg_id = self.next_message_id;
|
||||||
|
self.next_message_id += 1;
|
||||||
|
|
||||||
|
const msg = protocol.DataMessage{
|
||||||
|
.message_id = msg_id,
|
||||||
|
.content_type = content_type,
|
||||||
|
.data = data,
|
||||||
|
};
|
||||||
|
|
||||||
|
const encoded = msg.encode(self.allocator) catch return Error.OutOfMemory;
|
||||||
|
defer self.allocator.free(encoded);
|
||||||
|
|
||||||
|
// TODO: Enviar por socket con TLS
|
||||||
|
self.bytes_sent += encoded.len;
|
||||||
|
|
||||||
|
return msg_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cierra la conexión
|
||||||
|
pub fn close(self: *Connection) void {
|
||||||
|
self.closeWithReason("closed by user");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cierra la conexión con motivo
|
||||||
|
pub fn closeWithReason(self: *Connection, reason: []const u8) void {
|
||||||
|
_ = reason;
|
||||||
|
if (self.socket) |sock| {
|
||||||
|
std.posix.close(sock);
|
||||||
|
self.socket = null;
|
||||||
|
}
|
||||||
|
self.state = .disconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Espera hasta que la conexión esté establecida
|
||||||
|
pub fn waitConnected(self: *Connection, timeout_ms: u32) Error!void {
|
||||||
|
_ = timeout_ms;
|
||||||
|
if (self.state == .connected) return;
|
||||||
|
if (self.state == .@"error" or self.state == .disconnected) {
|
||||||
|
return Error.ConnectionFailed;
|
||||||
|
}
|
||||||
|
// TODO: Implementar espera con timeout
|
||||||
|
return Error.ConnectionTimeout;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Instancia principal P2P
|
||||||
|
pub const P2P = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
config: Config,
|
||||||
|
device_id: DeviceId,
|
||||||
|
connections: std.AutoHashMapUnmanaged(DeviceId, *Connection),
|
||||||
|
discovery_manager: discovery.DiscoveryManager,
|
||||||
|
listener_socket: ?std.posix.socket_t,
|
||||||
|
external_address: ?[]const u8,
|
||||||
|
nat_type: NatType,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, config: Config) Error!*P2P {
|
||||||
|
const self = allocator.create(P2P) catch return Error.OutOfMemory;
|
||||||
|
errdefer allocator.destroy(self);
|
||||||
|
|
||||||
|
// Generar o cargar Device ID
|
||||||
|
// TODO: Implementar carga de certificado
|
||||||
|
var device_id: DeviceId = undefined;
|
||||||
|
std.crypto.random.bytes(&device_id);
|
||||||
|
|
||||||
|
self.* = .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.config = config,
|
||||||
|
.device_id = device_id,
|
||||||
|
.connections = .{},
|
||||||
|
.discovery_manager = discovery.DiscoveryManager.init(allocator, device_id),
|
||||||
|
.listener_socket = null,
|
||||||
|
.external_address = null,
|
||||||
|
.nat_type = .unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *P2P) void {
|
||||||
|
// Cerrar todas las conexiones
|
||||||
|
var iter = self.connections.iterator();
|
||||||
|
while (iter.next()) |entry| {
|
||||||
|
entry.value_ptr.*.deinit();
|
||||||
|
self.allocator.destroy(entry.value_ptr.*);
|
||||||
|
}
|
||||||
|
self.connections.deinit(self.allocator);
|
||||||
|
|
||||||
|
// Cerrar listener
|
||||||
|
if (self.listener_socket) |sock| {
|
||||||
|
std.posix.close(sock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpiar discovery
|
||||||
|
self.discovery_manager.deinit();
|
||||||
|
|
||||||
|
// Limpiar external address
|
||||||
|
if (self.external_address) |addr| {
|
||||||
|
self.allocator.free(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.allocator.destroy(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene el Device ID local
|
||||||
|
pub fn getDeviceId(self: *P2P) DeviceId {
|
||||||
|
return self.device_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene el Device ID como string
|
||||||
|
pub fn getDeviceIdString(self: *P2P, buf: []u8) []const u8 {
|
||||||
|
return identity.deviceIdToString(self.device_id, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsea un Device ID desde string
|
||||||
|
pub fn parseDeviceId(str: []const u8) Error!DeviceId {
|
||||||
|
return identity.stringToDeviceId(str) catch Error.InvalidDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compara dos Device IDs
|
||||||
|
pub fn deviceIdEquals(a: DeviceId, b: DeviceId) bool {
|
||||||
|
return identity.deviceIdEquals(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene el Short ID
|
||||||
|
pub fn getShortId(device_id: DeviceId) identity.ShortId {
|
||||||
|
return identity.getShortId(device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Conecta a un dispositivo
|
||||||
|
pub fn connect(self: *P2P, device_id: DeviceId) Error!*Connection {
|
||||||
|
// Verificar si ya existe conexión
|
||||||
|
if (self.connections.get(device_id)) |conn| {
|
||||||
|
if (conn.isConnected()) return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear nueva conexión
|
||||||
|
const conn = self.allocator.create(Connection) catch return Error.OutOfMemory;
|
||||||
|
conn.* = Connection.init(self.allocator, device_id);
|
||||||
|
conn.state = .connecting;
|
||||||
|
|
||||||
|
self.connections.put(self.allocator, device_id, conn) catch {
|
||||||
|
self.allocator.destroy(conn);
|
||||||
|
return Error.OutOfMemory;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Buscar direcciones del peer
|
||||||
|
const addresses = self.discovery_manager.lookup(device_id) catch null;
|
||||||
|
if (addresses == null) {
|
||||||
|
conn.state = .@"error";
|
||||||
|
return Error.PeerNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Intentar conectar a las direcciones
|
||||||
|
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Desconecta de un peer
|
||||||
|
pub fn disconnect(self: *P2P, device_id: DeviceId) void {
|
||||||
|
if (self.connections.get(device_id)) |conn| {
|
||||||
|
conn.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene una conexión existente
|
||||||
|
pub fn getConnection(self: *P2P, device_id: DeviceId) ?*Connection {
|
||||||
|
return self.connections.get(device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Número de conexiones activas
|
||||||
|
pub fn connectionCount(self: *P2P) usize {
|
||||||
|
var count: usize = 0;
|
||||||
|
var iter = self.connections.iterator();
|
||||||
|
while (iter.next()) |entry| {
|
||||||
|
if (entry.value_ptr.*.isConnected()) count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene la IP externa
|
||||||
|
pub fn getExternalAddress(self: *P2P) ?[]const u8 {
|
||||||
|
return self.external_address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene el tipo de NAT
|
||||||
|
pub fn getNatType(self: *P2P) NatType {
|
||||||
|
return self.nat_type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test "p2p init/deinit" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
const p2p = try P2P.init(allocator, .{
|
||||||
|
.data_dir = "/tmp/zcatp2p-test",
|
||||||
|
});
|
||||||
|
defer p2p.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect(p2p.connectionCount() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "connection init" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
const device_id = [_]u8{0xab} ** 32;
|
||||||
|
|
||||||
|
var conn = Connection.init(allocator, device_id);
|
||||||
|
defer conn.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect(conn.state == .disconnected);
|
||||||
|
try std.testing.expect(!conn.isConnected());
|
||||||
|
}
|
||||||
645
src/crypto.zig
Normal file
645
src/crypto.zig
Normal file
|
|
@ -0,0 +1,645 @@
|
||||||
|
//! Módulo de criptografía - SHA256, ChaCha20-Poly1305
|
||||||
|
//!
|
||||||
|
//! Implementación pura en Zig sin dependencias externas.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SHA-256
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Longitud del hash SHA256 en bytes
|
||||||
|
pub const SHA256_DIGEST_LENGTH: usize = 32;
|
||||||
|
|
||||||
|
/// Calcula SHA256 de los datos
|
||||||
|
pub fn sha256(data: []const u8) [SHA256_DIGEST_LENGTH]u8 {
|
||||||
|
var hasher = Sha256.init();
|
||||||
|
hasher.update(data);
|
||||||
|
return hasher.final();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estado del hasher SHA256
|
||||||
|
pub const Sha256 = struct {
|
||||||
|
state: [8]u32,
|
||||||
|
buf: [64]u8,
|
||||||
|
buf_len: usize,
|
||||||
|
total_len: u64,
|
||||||
|
|
||||||
|
const K: [64]u32 = .{
|
||||||
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
||||||
|
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||||
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
||||||
|
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||||
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
||||||
|
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||||
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
||||||
|
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||||
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
||||||
|
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||||
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
||||||
|
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||||
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
||||||
|
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||||
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
||||||
|
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init() Sha256 {
|
||||||
|
return .{
|
||||||
|
.state = .{
|
||||||
|
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
||||||
|
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
||||||
|
},
|
||||||
|
.buf = undefined,
|
||||||
|
.buf_len = 0,
|
||||||
|
.total_len = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(self: *Sha256, data: []const u8) void {
|
||||||
|
var input = data;
|
||||||
|
self.total_len += data.len;
|
||||||
|
|
||||||
|
// Si hay datos en el buffer, intentar completar un bloque
|
||||||
|
if (self.buf_len > 0) {
|
||||||
|
const space = 64 - self.buf_len;
|
||||||
|
const to_copy = @min(space, input.len);
|
||||||
|
@memcpy(self.buf[self.buf_len .. self.buf_len + to_copy], input[0..to_copy]);
|
||||||
|
self.buf_len += to_copy;
|
||||||
|
input = input[to_copy..];
|
||||||
|
|
||||||
|
if (self.buf_len == 64) {
|
||||||
|
self.processBlock(&self.buf);
|
||||||
|
self.buf_len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procesar bloques completos
|
||||||
|
while (input.len >= 64) {
|
||||||
|
self.processBlock(input[0..64]);
|
||||||
|
input = input[64..];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar resto en buffer
|
||||||
|
if (input.len > 0) {
|
||||||
|
@memcpy(self.buf[0..input.len], input);
|
||||||
|
self.buf_len = input.len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn final(self: *Sha256) [SHA256_DIGEST_LENGTH]u8 {
|
||||||
|
// Padding
|
||||||
|
const total_bits = self.total_len * 8;
|
||||||
|
self.buf[self.buf_len] = 0x80;
|
||||||
|
self.buf_len += 1;
|
||||||
|
|
||||||
|
// Si no hay espacio para la longitud, procesar bloque extra
|
||||||
|
if (self.buf_len > 56) {
|
||||||
|
@memset(self.buf[self.buf_len..64], 0);
|
||||||
|
self.processBlock(&self.buf);
|
||||||
|
self.buf_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@memset(self.buf[self.buf_len..56], 0);
|
||||||
|
std.mem.writeInt(u64, self.buf[56..64], total_bits, .big);
|
||||||
|
self.processBlock(&self.buf);
|
||||||
|
|
||||||
|
// Convertir estado a bytes
|
||||||
|
var result: [32]u8 = undefined;
|
||||||
|
for (self.state, 0..) |s, i| {
|
||||||
|
std.mem.writeInt(u32, result[i * 4 ..][0..4], s, .big);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processBlock(self: *Sha256, block: *const [64]u8) void {
|
||||||
|
var w: [64]u32 = undefined;
|
||||||
|
|
||||||
|
// Preparar schedule
|
||||||
|
for (0..16) |i| {
|
||||||
|
w[i] = std.mem.readInt(u32, block[i * 4 ..][0..4], .big);
|
||||||
|
}
|
||||||
|
for (16..64) |i| {
|
||||||
|
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3);
|
||||||
|
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
|
||||||
|
w[i] = w[i - 16] +% s0 +% w[i - 7] +% s1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var a = self.state[0];
|
||||||
|
var b = self.state[1];
|
||||||
|
var c = self.state[2];
|
||||||
|
var d = self.state[3];
|
||||||
|
var e = self.state[4];
|
||||||
|
var f = self.state[5];
|
||||||
|
var g = self.state[6];
|
||||||
|
var h = self.state[7];
|
||||||
|
|
||||||
|
for (0..64) |i| {
|
||||||
|
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
||||||
|
const ch = (e & f) ^ (~e & g);
|
||||||
|
const temp1 = h +% S1 +% ch +% K[i] +% w[i];
|
||||||
|
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
||||||
|
const maj = (a & b) ^ (a & c) ^ (b & c);
|
||||||
|
const temp2 = S0 +% maj;
|
||||||
|
|
||||||
|
h = g;
|
||||||
|
g = f;
|
||||||
|
f = e;
|
||||||
|
e = d +% temp1;
|
||||||
|
d = c;
|
||||||
|
c = b;
|
||||||
|
b = a;
|
||||||
|
a = temp1 +% temp2;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state[0] +%= a;
|
||||||
|
self.state[1] +%= b;
|
||||||
|
self.state[2] +%= c;
|
||||||
|
self.state[3] +%= d;
|
||||||
|
self.state[4] +%= e;
|
||||||
|
self.state[5] +%= f;
|
||||||
|
self.state[6] +%= g;
|
||||||
|
self.state[7] +%= h;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotr(x: u32, comptime n: u5) u32 {
|
||||||
|
return std.math.rotr(u32, x, n);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ChaCha20-Poly1305
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub const CHACHA20_KEY_SIZE: usize = 32;
|
||||||
|
pub const CHACHA20_NONCE_SIZE: usize = 12;
|
||||||
|
pub const XCHACHA20_NONCE_SIZE: usize = 24;
|
||||||
|
pub const POLY1305_TAG_SIZE: usize = 16;
|
||||||
|
|
||||||
|
/// ChaCha20 block cipher
|
||||||
|
pub const ChaCha20 = struct {
|
||||||
|
state: [16]u32,
|
||||||
|
|
||||||
|
pub fn init(key: *const [32]u8, nonce: *const [12]u8, counter: u32) ChaCha20 {
|
||||||
|
return .{
|
||||||
|
.state = .{
|
||||||
|
0x61707865, 0x3320646e, 0x79622d32, 0x6b206574,
|
||||||
|
std.mem.readInt(u32, key[0..4], .little),
|
||||||
|
std.mem.readInt(u32, key[4..8], .little),
|
||||||
|
std.mem.readInt(u32, key[8..12], .little),
|
||||||
|
std.mem.readInt(u32, key[12..16], .little),
|
||||||
|
std.mem.readInt(u32, key[16..20], .little),
|
||||||
|
std.mem.readInt(u32, key[20..24], .little),
|
||||||
|
std.mem.readInt(u32, key[24..28], .little),
|
||||||
|
std.mem.readInt(u32, key[28..32], .little),
|
||||||
|
counter,
|
||||||
|
std.mem.readInt(u32, nonce[0..4], .little),
|
||||||
|
std.mem.readInt(u32, nonce[4..8], .little),
|
||||||
|
std.mem.readInt(u32, nonce[8..12], .little),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xor(self: *ChaCha20, out: []u8, in: []const u8) void {
|
||||||
|
var remaining = in;
|
||||||
|
var out_ptr = out;
|
||||||
|
|
||||||
|
while (remaining.len > 0) {
|
||||||
|
const keystream = self.block();
|
||||||
|
|
||||||
|
const to_process = @min(remaining.len, 64);
|
||||||
|
for (0..to_process) |i| {
|
||||||
|
out_ptr[i] = remaining[i] ^ keystream[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining = remaining[to_process..];
|
||||||
|
out_ptr = out_ptr[to_process..];
|
||||||
|
|
||||||
|
// Incrementar contador
|
||||||
|
self.state[12] +%= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block(self: *ChaCha20) [64]u8 {
|
||||||
|
var working = self.state;
|
||||||
|
|
||||||
|
// 20 rounds (10 double rounds)
|
||||||
|
for (0..10) |_| {
|
||||||
|
quarterRound(&working, 0, 4, 8, 12);
|
||||||
|
quarterRound(&working, 1, 5, 9, 13);
|
||||||
|
quarterRound(&working, 2, 6, 10, 14);
|
||||||
|
quarterRound(&working, 3, 7, 11, 15);
|
||||||
|
quarterRound(&working, 0, 5, 10, 15);
|
||||||
|
quarterRound(&working, 1, 6, 11, 12);
|
||||||
|
quarterRound(&working, 2, 7, 8, 13);
|
||||||
|
quarterRound(&working, 3, 4, 9, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add original state
|
||||||
|
for (&working, self.state) |*w, s| {
|
||||||
|
w.* +%= s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to bytes
|
||||||
|
var result: [64]u8 = undefined;
|
||||||
|
for (working, 0..) |w, i| {
|
||||||
|
std.mem.writeInt(u32, result[i * 4 ..][0..4], w, .little);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quarterRound(state: *[16]u32, a: usize, b: usize, c: usize, d: usize) void {
|
||||||
|
state[a] +%= state[b];
|
||||||
|
state[d] ^= state[a];
|
||||||
|
state[d] = rotl(state[d], 16);
|
||||||
|
state[c] +%= state[d];
|
||||||
|
state[b] ^= state[c];
|
||||||
|
state[b] = rotl(state[b], 12);
|
||||||
|
state[a] +%= state[b];
|
||||||
|
state[d] ^= state[a];
|
||||||
|
state[d] = rotl(state[d], 8);
|
||||||
|
state[c] +%= state[d];
|
||||||
|
state[b] ^= state[c];
|
||||||
|
state[b] = rotl(state[b], 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotl(x: u32, comptime n: u5) u32 {
|
||||||
|
return std.math.rotl(u32, x, n);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// XChaCha20 - ChaCha20 con nonce extendido de 24 bytes
|
||||||
|
pub const XChaCha20 = struct {
|
||||||
|
chacha: ChaCha20,
|
||||||
|
|
||||||
|
pub fn init(key: *const [32]u8, nonce: *const [24]u8, counter: u32) XChaCha20 {
|
||||||
|
// HChaCha20 para derivar subkey
|
||||||
|
const subkey = hchacha20(key, nonce[0..16]);
|
||||||
|
var short_nonce: [12]u8 = undefined;
|
||||||
|
@memset(short_nonce[0..4], 0);
|
||||||
|
@memcpy(short_nonce[4..12], nonce[16..24]);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.chacha = ChaCha20.init(&subkey, &short_nonce, counter),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xor(self: *XChaCha20, out: []u8, in: []const u8) void {
|
||||||
|
self.chacha.xor(out, in);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn hchacha20(key: *const [32]u8, nonce: *const [16]u8) [32]u8 {
|
||||||
|
var state: [16]u32 = .{
|
||||||
|
0x61707865, 0x3320646e, 0x79622d32, 0x6b206574,
|
||||||
|
std.mem.readInt(u32, key[0..4], .little),
|
||||||
|
std.mem.readInt(u32, key[4..8], .little),
|
||||||
|
std.mem.readInt(u32, key[8..12], .little),
|
||||||
|
std.mem.readInt(u32, key[12..16], .little),
|
||||||
|
std.mem.readInt(u32, key[16..20], .little),
|
||||||
|
std.mem.readInt(u32, key[20..24], .little),
|
||||||
|
std.mem.readInt(u32, key[24..28], .little),
|
||||||
|
std.mem.readInt(u32, key[28..32], .little),
|
||||||
|
std.mem.readInt(u32, nonce[0..4], .little),
|
||||||
|
std.mem.readInt(u32, nonce[4..8], .little),
|
||||||
|
std.mem.readInt(u32, nonce[8..12], .little),
|
||||||
|
std.mem.readInt(u32, nonce[12..16], .little),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (0..10) |_| {
|
||||||
|
ChaCha20.quarterRound(&state, 0, 4, 8, 12);
|
||||||
|
ChaCha20.quarterRound(&state, 1, 5, 9, 13);
|
||||||
|
ChaCha20.quarterRound(&state, 2, 6, 10, 14);
|
||||||
|
ChaCha20.quarterRound(&state, 3, 7, 11, 15);
|
||||||
|
ChaCha20.quarterRound(&state, 0, 5, 10, 15);
|
||||||
|
ChaCha20.quarterRound(&state, 1, 6, 11, 12);
|
||||||
|
ChaCha20.quarterRound(&state, 2, 7, 8, 13);
|
||||||
|
ChaCha20.quarterRound(&state, 3, 4, 9, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result: [32]u8 = undefined;
|
||||||
|
std.mem.writeInt(u32, result[0..4], state[0], .little);
|
||||||
|
std.mem.writeInt(u32, result[4..8], state[1], .little);
|
||||||
|
std.mem.writeInt(u32, result[8..12], state[2], .little);
|
||||||
|
std.mem.writeInt(u32, result[12..16], state[3], .little);
|
||||||
|
std.mem.writeInt(u32, result[16..20], state[12], .little);
|
||||||
|
std.mem.writeInt(u32, result[20..24], state[13], .little);
|
||||||
|
std.mem.writeInt(u32, result[24..28], state[14], .little);
|
||||||
|
std.mem.writeInt(u32, result[28..32], state[15], .little);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Poly1305 MAC
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Poly1305 message authentication code
|
||||||
|
pub const Poly1305 = struct {
|
||||||
|
r: [3]u64,
|
||||||
|
h: [3]u64,
|
||||||
|
pad: [2]u64,
|
||||||
|
leftover: usize,
|
||||||
|
buffer: [16]u8,
|
||||||
|
|
||||||
|
pub fn init(key: *const [32]u8) Poly1305 {
|
||||||
|
// r = key[0..16] con bits específicos enmascarados
|
||||||
|
var r0 = std.mem.readInt(u64, key[0..8], .little);
|
||||||
|
var r1 = std.mem.readInt(u64, key[8..16], .little);
|
||||||
|
|
||||||
|
// Clamp r
|
||||||
|
r0 &= 0x0ffffffc0fffffff;
|
||||||
|
r1 &= 0x0ffffffc0ffffffc;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.r = .{ r0 & 0xfffffffffff, (r0 >> 44) | ((r1 & 0xffffff) << 20), r1 >> 24 },
|
||||||
|
.h = .{ 0, 0, 0 },
|
||||||
|
.pad = .{
|
||||||
|
std.mem.readInt(u64, key[16..24], .little),
|
||||||
|
std.mem.readInt(u64, key[24..32], .little),
|
||||||
|
},
|
||||||
|
.leftover = 0,
|
||||||
|
.buffer = undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(self: *Poly1305, data: []const u8) void {
|
||||||
|
var input = data;
|
||||||
|
|
||||||
|
// Procesar datos en buffer primero
|
||||||
|
if (self.leftover > 0) {
|
||||||
|
const want = 16 - self.leftover;
|
||||||
|
const have = @min(want, input.len);
|
||||||
|
@memcpy(self.buffer[self.leftover .. self.leftover + have], input[0..have]);
|
||||||
|
input = input[have..];
|
||||||
|
self.leftover += have;
|
||||||
|
|
||||||
|
if (self.leftover == 16) {
|
||||||
|
self.processBlock(&self.buffer, false);
|
||||||
|
self.leftover = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procesar bloques completos
|
||||||
|
while (input.len >= 16) {
|
||||||
|
self.processBlock(input[0..16], false);
|
||||||
|
input = input[16..];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar resto
|
||||||
|
if (input.len > 0) {
|
||||||
|
@memcpy(self.buffer[0..input.len], input);
|
||||||
|
self.leftover = input.len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn final(self: *Poly1305) [16]u8 {
|
||||||
|
// Procesar último bloque parcial
|
||||||
|
if (self.leftover > 0) {
|
||||||
|
self.buffer[self.leftover] = 1;
|
||||||
|
@memset(self.buffer[self.leftover + 1 .. 16], 0);
|
||||||
|
self.processBlock(&self.buffer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalizar
|
||||||
|
var h0 = self.h[0];
|
||||||
|
var h1 = self.h[1];
|
||||||
|
var h2 = self.h[2];
|
||||||
|
|
||||||
|
// Reducción final
|
||||||
|
var c: u64 = 0;
|
||||||
|
c = h0 >> 44;
|
||||||
|
h0 &= 0xfffffffffff;
|
||||||
|
h1 += c;
|
||||||
|
c = h1 >> 44;
|
||||||
|
h1 &= 0xfffffffffff;
|
||||||
|
h2 += c;
|
||||||
|
c = h2 >> 42;
|
||||||
|
h2 &= 0x3ffffffffff;
|
||||||
|
h0 += c * 5;
|
||||||
|
c = h0 >> 44;
|
||||||
|
h0 &= 0xfffffffffff;
|
||||||
|
h1 += c;
|
||||||
|
|
||||||
|
// Computar h + -p
|
||||||
|
var g0 = h0 +% 5;
|
||||||
|
c = g0 >> 44;
|
||||||
|
g0 &= 0xfffffffffff;
|
||||||
|
var g1 = h1 +% c;
|
||||||
|
c = g1 >> 44;
|
||||||
|
g1 &= 0xfffffffffff;
|
||||||
|
var g2 = h2 +% c -% (1 << 42);
|
||||||
|
|
||||||
|
// Seleccionar h o h + -p
|
||||||
|
c = (g2 >> 63) -% 1;
|
||||||
|
g0 &= c;
|
||||||
|
g1 &= c;
|
||||||
|
g2 &= c;
|
||||||
|
c = ~c;
|
||||||
|
h0 = (h0 & c) | g0;
|
||||||
|
h1 = (h1 & c) | g1;
|
||||||
|
h2 = (h2 & c) | g2;
|
||||||
|
|
||||||
|
// h = h + pad
|
||||||
|
const t0 = self.pad[0];
|
||||||
|
const t1 = self.pad[1];
|
||||||
|
|
||||||
|
h0 +%= t0 & 0xfffffffffff;
|
||||||
|
c = h0 >> 44;
|
||||||
|
h0 &= 0xfffffffffff;
|
||||||
|
h1 +%= ((t0 >> 44) | (t1 << 20)) & 0xfffffffffff;
|
||||||
|
h1 += c;
|
||||||
|
c = h1 >> 44;
|
||||||
|
h1 &= 0xfffffffffff;
|
||||||
|
h2 +%= (t1 >> 24) & 0x3ffffffffff;
|
||||||
|
h2 += c;
|
||||||
|
h2 &= 0x3ffffffffff;
|
||||||
|
|
||||||
|
// Convertir a bytes
|
||||||
|
var result: [16]u8 = undefined;
|
||||||
|
const full = h0 | (h1 << 44) | (h2 << 88);
|
||||||
|
std.mem.writeInt(u128, &result, full, .little);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processBlock(self: *Poly1305, block: *const [16]u8, is_final: bool) void {
|
||||||
|
const hibit: u64 = if (is_final) 0 else (1 << 40);
|
||||||
|
|
||||||
|
// h += m
|
||||||
|
const t0 = std.mem.readInt(u64, block[0..8], .little);
|
||||||
|
const t1 = std.mem.readInt(u64, block[8..16], .little);
|
||||||
|
|
||||||
|
self.h[0] += t0 & 0xfffffffffff;
|
||||||
|
self.h[1] += ((t0 >> 44) | (t1 << 20)) & 0xfffffffffff;
|
||||||
|
self.h[2] += ((t1 >> 24) & 0x3ffffffffff) | hibit;
|
||||||
|
|
||||||
|
// h *= r (mod 2^130 - 5)
|
||||||
|
const r0 = self.r[0];
|
||||||
|
const r1 = self.r[1];
|
||||||
|
const r2 = self.r[2];
|
||||||
|
const s1 = r1 * 5;
|
||||||
|
const s2 = r2 * 5;
|
||||||
|
|
||||||
|
var d0: u128 = @as(u128, self.h[0]) * r0;
|
||||||
|
d0 += @as(u128, self.h[1]) * s2;
|
||||||
|
d0 += @as(u128, self.h[2]) * s1;
|
||||||
|
var d1: u128 = @as(u128, self.h[0]) * r1;
|
||||||
|
d1 += @as(u128, self.h[1]) * r0;
|
||||||
|
d1 += @as(u128, self.h[2]) * s2;
|
||||||
|
var d2: u128 = @as(u128, self.h[0]) * r2;
|
||||||
|
d2 += @as(u128, self.h[1]) * r1;
|
||||||
|
d2 += @as(u128, self.h[2]) * r0;
|
||||||
|
|
||||||
|
// Reducción parcial
|
||||||
|
var c: u64 = @truncate(d0 >> 44);
|
||||||
|
self.h[0] = @truncate(d0 & 0xfffffffffff);
|
||||||
|
d1 += c;
|
||||||
|
c = @truncate(d1 >> 44);
|
||||||
|
self.h[1] = @truncate(d1 & 0xfffffffffff);
|
||||||
|
d2 += c;
|
||||||
|
c = @truncate(d2 >> 42);
|
||||||
|
self.h[2] = @truncate(d2 & 0x3ffffffffff);
|
||||||
|
self.h[0] += c * 5;
|
||||||
|
c = self.h[0] >> 44;
|
||||||
|
self.h[0] &= 0xfffffffffff;
|
||||||
|
self.h[1] += c;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ChaCha20-Poly1305 AEAD
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Cifra datos con ChaCha20-Poly1305
|
||||||
|
/// Retorna: nonce || ciphertext || tag
|
||||||
|
pub fn chachaPoly1305Encrypt(
|
||||||
|
key: *const [32]u8,
|
||||||
|
nonce: *const [12]u8,
|
||||||
|
plaintext: []const u8,
|
||||||
|
aad: []const u8,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
) ![]u8 {
|
||||||
|
const result = try allocator.alloc(u8, 12 + plaintext.len + 16);
|
||||||
|
errdefer allocator.free(result);
|
||||||
|
|
||||||
|
@memcpy(result[0..12], nonce);
|
||||||
|
|
||||||
|
// Generar keystream para Poly1305
|
||||||
|
var chacha = ChaCha20.init(key, nonce, 0);
|
||||||
|
var poly_key: [64]u8 = undefined;
|
||||||
|
var zeros: [64]u8 = [_]u8{0} ** 64;
|
||||||
|
chacha.xor(&poly_key, &zeros);
|
||||||
|
|
||||||
|
// Cifrar
|
||||||
|
chacha = ChaCha20.init(key, nonce, 1);
|
||||||
|
chacha.xor(result[12 .. 12 + plaintext.len], plaintext);
|
||||||
|
|
||||||
|
// Calcular tag
|
||||||
|
var poly = Poly1305.init(poly_key[0..32]);
|
||||||
|
poly.update(aad);
|
||||||
|
if (aad.len % 16 != 0) {
|
||||||
|
var pad: [16]u8 = [_]u8{0} ** 16;
|
||||||
|
poly.update(pad[0 .. 16 - (aad.len % 16)]);
|
||||||
|
}
|
||||||
|
poly.update(result[12 .. 12 + plaintext.len]);
|
||||||
|
if (plaintext.len % 16 != 0) {
|
||||||
|
var pad: [16]u8 = [_]u8{0} ** 16;
|
||||||
|
poly.update(pad[0 .. 16 - (plaintext.len % 16)]);
|
||||||
|
}
|
||||||
|
var lens: [16]u8 = undefined;
|
||||||
|
std.mem.writeInt(u64, lens[0..8], aad.len, .little);
|
||||||
|
std.mem.writeInt(u64, lens[8..16], plaintext.len, .little);
|
||||||
|
poly.update(&lens);
|
||||||
|
|
||||||
|
const tag = poly.final();
|
||||||
|
@memcpy(result[12 + plaintext.len ..][0..16], &tag);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Descifra datos con ChaCha20-Poly1305
|
||||||
|
/// Input format: nonce (12) || ciphertext || tag (16)
|
||||||
|
pub fn chachaPoly1305Decrypt(
|
||||||
|
key: *const [32]u8,
|
||||||
|
data: []const u8,
|
||||||
|
aad: []const u8,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
) ![]u8 {
|
||||||
|
if (data.len < 12 + 16) return error.InvalidData;
|
||||||
|
|
||||||
|
const nonce = data[0..12];
|
||||||
|
const ciphertext = data[12 .. data.len - 16];
|
||||||
|
const tag = data[data.len - 16 ..][0..16];
|
||||||
|
|
||||||
|
// Verificar tag
|
||||||
|
var chacha = ChaCha20.init(key, nonce, 0);
|
||||||
|
var poly_key: [64]u8 = undefined;
|
||||||
|
var zeros: [64]u8 = [_]u8{0} ** 64;
|
||||||
|
chacha.xor(&poly_key, &zeros);
|
||||||
|
|
||||||
|
var poly = Poly1305.init(poly_key[0..32]);
|
||||||
|
poly.update(aad);
|
||||||
|
if (aad.len % 16 != 0) {
|
||||||
|
var pad: [16]u8 = [_]u8{0} ** 16;
|
||||||
|
poly.update(pad[0 .. 16 - (aad.len % 16)]);
|
||||||
|
}
|
||||||
|
poly.update(ciphertext);
|
||||||
|
if (ciphertext.len % 16 != 0) {
|
||||||
|
var pad: [16]u8 = [_]u8{0} ** 16;
|
||||||
|
poly.update(pad[0 .. 16 - (ciphertext.len % 16)]);
|
||||||
|
}
|
||||||
|
var lens: [16]u8 = undefined;
|
||||||
|
std.mem.writeInt(u64, lens[0..8], aad.len, .little);
|
||||||
|
std.mem.writeInt(u64, lens[8..16], ciphertext.len, .little);
|
||||||
|
poly.update(&lens);
|
||||||
|
|
||||||
|
const computed_tag = poly.final();
|
||||||
|
if (!std.mem.eql(u8, &computed_tag, tag)) {
|
||||||
|
return error.AuthenticationFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descifrar
|
||||||
|
const result = try allocator.alloc(u8, ciphertext.len);
|
||||||
|
chacha = ChaCha20.init(key, nonce, 1);
|
||||||
|
chacha.xor(result, ciphertext);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test "sha256 empty" {
|
||||||
|
const hash = sha256("");
|
||||||
|
const expected = [_]u8{
|
||||||
|
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
|
||||||
|
0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
|
||||||
|
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
|
||||||
|
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
|
||||||
|
};
|
||||||
|
try std.testing.expectEqualSlices(u8, &expected, &hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "sha256 abc" {
|
||||||
|
const hash = sha256("abc");
|
||||||
|
const expected = [_]u8{
|
||||||
|
0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea,
|
||||||
|
0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
|
||||||
|
0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c,
|
||||||
|
0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad,
|
||||||
|
};
|
||||||
|
try std.testing.expectEqualSlices(u8, &expected, &hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "chacha20 basic" {
|
||||||
|
const key = [_]u8{0} ** 32;
|
||||||
|
const nonce = [_]u8{0} ** 12;
|
||||||
|
var chacha = ChaCha20.init(&key, &nonce, 0);
|
||||||
|
|
||||||
|
var out: [64]u8 = undefined;
|
||||||
|
const zeros = [_]u8{0} ** 64;
|
||||||
|
chacha.xor(&out, &zeros);
|
||||||
|
|
||||||
|
// Verificar que no es todo ceros (el keystream se aplicó)
|
||||||
|
try std.testing.expect(!std.mem.eql(u8, &out, &zeros));
|
||||||
|
}
|
||||||
296
src/discovery.zig
Normal file
296
src/discovery.zig
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
//! Módulo de discovery - Descubrimiento local y global de peers
|
||||||
|
//!
|
||||||
|
//! Local: UDP broadcast/multicast en LAN
|
||||||
|
//! Global: HTTPS API a servidores de discovery
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const identity = @import("identity.zig");
|
||||||
|
const protocol = @import("protocol.zig");
|
||||||
|
|
||||||
|
pub const DeviceId = identity.DeviceId;
|
||||||
|
|
||||||
|
/// Puerto por defecto para discovery local
|
||||||
|
pub const LOCAL_DISCOVERY_PORT: u16 = 21027;
|
||||||
|
|
||||||
|
/// Intervalo de broadcast local
|
||||||
|
pub const BROADCAST_INTERVAL_MS: u64 = 30 * 1000;
|
||||||
|
|
||||||
|
/// Tiempo de vida del cache
|
||||||
|
pub const CACHE_LIFETIME_MS: u64 = 90 * 1000;
|
||||||
|
|
||||||
|
/// Magic number para paquetes de discovery local
|
||||||
|
pub const LOCAL_MAGIC: u32 = 0x2EA7D90B;
|
||||||
|
|
||||||
|
/// Entrada en el cache de dispositivos
|
||||||
|
pub const CacheEntry = struct {
|
||||||
|
addresses: std.ArrayListUnmanaged([]const u8),
|
||||||
|
instance_id: i64,
|
||||||
|
when: i64,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) CacheEntry {
|
||||||
|
return .{
|
||||||
|
.addresses = .{},
|
||||||
|
.instance_id = 0,
|
||||||
|
.when = 0,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *CacheEntry) void {
|
||||||
|
for (self.addresses.items) |addr| {
|
||||||
|
self.allocator.free(addr);
|
||||||
|
}
|
||||||
|
self.addresses.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isExpired(self: CacheEntry) bool {
|
||||||
|
const now = std.time.milliTimestamp();
|
||||||
|
return now - self.when > CACHE_LIFETIME_MS;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Cliente de discovery local (LAN)
|
||||||
|
pub const LocalDiscovery = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
my_id: DeviceId,
|
||||||
|
cache: std.AutoHashMapUnmanaged(DeviceId, CacheEntry),
|
||||||
|
socket: ?std.posix.socket_t,
|
||||||
|
instance_id: i64,
|
||||||
|
addresses: std.ArrayListUnmanaged([]const u8),
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, device_id: DeviceId) LocalDiscovery {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.my_id = device_id,
|
||||||
|
.cache = .{},
|
||||||
|
.socket = null,
|
||||||
|
.instance_id = std.crypto.random.int(i64),
|
||||||
|
.addresses = .{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *LocalDiscovery) void {
|
||||||
|
if (self.socket) |sock| {
|
||||||
|
std.posix.close(sock);
|
||||||
|
}
|
||||||
|
|
||||||
|
var iter = self.cache.iterator();
|
||||||
|
while (iter.next()) |entry| {
|
||||||
|
entry.value_ptr.deinit();
|
||||||
|
}
|
||||||
|
self.cache.deinit(self.allocator);
|
||||||
|
|
||||||
|
for (self.addresses.items) |addr| {
|
||||||
|
self.allocator.free(addr);
|
||||||
|
}
|
||||||
|
self.addresses.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inicia el listener de discovery local
|
||||||
|
pub fn start(self: *LocalDiscovery) !void {
|
||||||
|
// Crear socket UDP
|
||||||
|
self.socket = try std.posix.socket(
|
||||||
|
std.posix.AF.INET,
|
||||||
|
std.posix.SOCK.DGRAM,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Permitir reutilizar dirección
|
||||||
|
const opt: u32 = 1;
|
||||||
|
try std.posix.setsockopt(
|
||||||
|
self.socket.?,
|
||||||
|
std.posix.SOL.SOCKET,
|
||||||
|
std.posix.SO.REUSEADDR,
|
||||||
|
std.mem.asBytes(&opt),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bind al puerto de discovery
|
||||||
|
const addr = std.net.Address.initIp4(.{ 0, 0, 0, 0 }, LOCAL_DISCOVERY_PORT);
|
||||||
|
try std.posix.bind(self.socket.?, &addr.any, addr.getOsSockLen());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Envía un anuncio de discovery
|
||||||
|
pub fn sendAnnouncement(self: *LocalDiscovery) !void {
|
||||||
|
if (self.socket == null) return error.NotStarted;
|
||||||
|
if (self.addresses.items.len == 0) return;
|
||||||
|
|
||||||
|
// Construir paquete
|
||||||
|
var buf: [1024]u8 = undefined;
|
||||||
|
var pos: usize = 0;
|
||||||
|
|
||||||
|
// Magic
|
||||||
|
std.mem.writeInt(u32, buf[pos..][0..4], LOCAL_MAGIC, .big);
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
// Device ID
|
||||||
|
@memcpy(buf[pos .. pos + 32], &self.my_id);
|
||||||
|
pos += 32;
|
||||||
|
|
||||||
|
// Instance ID
|
||||||
|
std.mem.writeInt(i64, buf[pos..][0..8], self.instance_id, .big);
|
||||||
|
pos += 8;
|
||||||
|
|
||||||
|
// Número de direcciones
|
||||||
|
buf[pos] = @intCast(self.addresses.items.len);
|
||||||
|
pos += 1;
|
||||||
|
|
||||||
|
// Direcciones
|
||||||
|
for (self.addresses.items) |addr| {
|
||||||
|
if (pos + 1 + addr.len > buf.len) break;
|
||||||
|
buf[pos] = @intCast(addr.len);
|
||||||
|
pos += 1;
|
||||||
|
@memcpy(buf[pos .. pos + addr.len], addr);
|
||||||
|
pos += addr.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar broadcast
|
||||||
|
const broadcast_addr = std.net.Address.initIp4(.{ 255, 255, 255, 255 }, LOCAL_DISCOVERY_PORT);
|
||||||
|
_ = try std.posix.sendto(
|
||||||
|
self.socket.?,
|
||||||
|
buf[0..pos],
|
||||||
|
0,
|
||||||
|
&broadcast_addr.any,
|
||||||
|
broadcast_addr.getOsSockLen(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Busca un dispositivo en el cache
|
||||||
|
pub fn lookup(self: *LocalDiscovery, device_id: DeviceId) ?[]const []const u8 {
|
||||||
|
if (self.cache.get(device_id)) |entry| {
|
||||||
|
if (!entry.isExpired()) {
|
||||||
|
return entry.addresses.items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade una dirección local
|
||||||
|
pub fn addAddress(self: *LocalDiscovery, addr: []const u8) !void {
|
||||||
|
const owned = try self.allocator.dupe(u8, addr);
|
||||||
|
try self.addresses.append(self.allocator, owned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Limpia direcciones locales
|
||||||
|
pub fn clearAddresses(self: *LocalDiscovery) void {
|
||||||
|
for (self.addresses.items) |addr| {
|
||||||
|
self.allocator.free(addr);
|
||||||
|
}
|
||||||
|
self.addresses.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Cliente de discovery global (HTTPS)
|
||||||
|
pub const GlobalDiscovery = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
servers: std.ArrayListUnmanaged([]const u8),
|
||||||
|
my_id: DeviceId,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, device_id: DeviceId) GlobalDiscovery {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.servers = .{},
|
||||||
|
.my_id = device_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *GlobalDiscovery) void {
|
||||||
|
for (self.servers.items) |server| {
|
||||||
|
self.allocator.free(server);
|
||||||
|
}
|
||||||
|
self.servers.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un servidor de discovery
|
||||||
|
pub fn addServer(self: *GlobalDiscovery, url: []const u8) !void {
|
||||||
|
const owned = try self.allocator.dupe(u8, url);
|
||||||
|
try self.servers.append(self.allocator, owned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Busca un dispositivo en los servidores globales
|
||||||
|
/// TODO: Implementar cliente HTTPS
|
||||||
|
pub fn lookup(self: *GlobalDiscovery, device_id: DeviceId) !?[]const []const u8 {
|
||||||
|
_ = self;
|
||||||
|
_ = device_id;
|
||||||
|
// Pendiente: implementar cliente HTTPS
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Anuncia el dispositivo a los servidores globales
|
||||||
|
/// TODO: Implementar cliente HTTPS
|
||||||
|
pub fn announce(self: *GlobalDiscovery, addresses: []const []const u8) !void {
|
||||||
|
_ = self;
|
||||||
|
_ = addresses;
|
||||||
|
// Pendiente: implementar cliente HTTPS
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Gestor combinado de discovery
|
||||||
|
pub const DiscoveryManager = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
local: LocalDiscovery,
|
||||||
|
global: GlobalDiscovery,
|
||||||
|
on_device_discovered: ?*const fn (DeviceId, []const []const u8) void,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, device_id: DeviceId) DiscoveryManager {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.local = LocalDiscovery.init(allocator, device_id),
|
||||||
|
.global = GlobalDiscovery.init(allocator, device_id),
|
||||||
|
.on_device_discovered = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *DiscoveryManager) void {
|
||||||
|
self.local.deinit();
|
||||||
|
self.global.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Busca un dispositivo (primero local, luego global)
|
||||||
|
pub fn lookup(self: *DiscoveryManager, device_id: DeviceId) !?[]const []const u8 {
|
||||||
|
// Primero buscar en cache local
|
||||||
|
if (self.local.lookup(device_id)) |addrs| {
|
||||||
|
return addrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Luego buscar en servidores globales
|
||||||
|
return try self.global.lookup(device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inicia el discovery
|
||||||
|
pub fn start(self: *DiscoveryManager) !void {
|
||||||
|
try self.local.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registra callback para dispositivos descubiertos
|
||||||
|
pub fn setOnDeviceDiscovered(
|
||||||
|
self: *DiscoveryManager,
|
||||||
|
cb: ?*const fn (DeviceId, []const []const u8) void,
|
||||||
|
) void {
|
||||||
|
self.on_device_discovered = cb;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test "cache entry expiration" {
|
||||||
|
var entry = CacheEntry{
|
||||||
|
.addresses = .{},
|
||||||
|
.instance_id = 123,
|
||||||
|
.when = std.time.milliTimestamp(),
|
||||||
|
.allocator = std.testing.allocator,
|
||||||
|
};
|
||||||
|
defer entry.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect(!entry.isExpired());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "local discovery init" {
|
||||||
|
const id = [_]u8{0xab} ** 32;
|
||||||
|
var discovery = LocalDiscovery.init(std.testing.allocator, id);
|
||||||
|
defer discovery.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect(discovery.socket == null);
|
||||||
|
}
|
||||||
279
src/identity.zig
Normal file
279
src/identity.zig
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
//! Módulo de identidad - Device ID y certificados
|
||||||
|
//!
|
||||||
|
//! El Device ID es un identificador único de 32 bytes derivado del certificado TLS:
|
||||||
|
//! DeviceID = SHA256(DER_encoded_certificate)
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const crypto = @import("crypto.zig");
|
||||||
|
|
||||||
|
/// Longitud del Device ID en bytes
|
||||||
|
pub const DEVICE_ID_LENGTH: usize = 32;
|
||||||
|
|
||||||
|
/// Longitud del Short ID en caracteres
|
||||||
|
pub const SHORT_ID_LENGTH: usize = 7;
|
||||||
|
|
||||||
|
/// Identificador único de dispositivo (32 bytes = SHA256 hash)
|
||||||
|
pub const DeviceId = [DEVICE_ID_LENGTH]u8;
|
||||||
|
|
||||||
|
/// Representación corta del Device ID (primeros 8 bytes como u64)
|
||||||
|
pub const ShortId = u64;
|
||||||
|
|
||||||
|
/// Device ID especiales
|
||||||
|
pub const EMPTY_DEVICE_ID: DeviceId = [_]u8{0} ** DEVICE_ID_LENGTH;
|
||||||
|
pub const LOCAL_DEVICE_ID: DeviceId = [_]u8{0xff} ** DEVICE_ID_LENGTH;
|
||||||
|
|
||||||
|
/// Alfabeto Base32 estándar
|
||||||
|
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||||
|
const BASE32_DECODE_TABLE = initDecodeTable();
|
||||||
|
|
||||||
|
fn initDecodeTable() [256]u8 {
|
||||||
|
var table: [256]u8 = [_]u8{0xff} ** 256;
|
||||||
|
for (BASE32_ALPHABET, 0..) |c, i| {
|
||||||
|
table[c] = @intCast(i);
|
||||||
|
// También aceptar minúsculas
|
||||||
|
if (c >= 'A' and c <= 'Z') {
|
||||||
|
table[c + 32] = @intCast(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Correcciones para errores de tipeo comunes
|
||||||
|
table['0'] = table['O'];
|
||||||
|
table['1'] = table['I'];
|
||||||
|
table['8'] = table['B'];
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Genera un Device ID desde el certificado DER
|
||||||
|
pub fn newDeviceId(der_certificate: []const u8) DeviceId {
|
||||||
|
return crypto.sha256(der_certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compara dos Device IDs
|
||||||
|
pub fn deviceIdEquals(a: DeviceId, b: DeviceId) bool {
|
||||||
|
return std.mem.eql(u8, &a, &b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene el Short ID (primeros 8 bytes como u64 big-endian)
|
||||||
|
pub fn getShortId(id: DeviceId) ShortId {
|
||||||
|
return std.mem.readInt(u64, id[0..8], .big);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica si un Device ID está vacío
|
||||||
|
pub fn isEmptyDeviceId(id: DeviceId) bool {
|
||||||
|
return deviceIdEquals(id, EMPTY_DEVICE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convierte Device ID a string con formato XXXXXXX-XXXXXXX-...
|
||||||
|
/// Buffer debe tener al menos 64 bytes
|
||||||
|
pub fn deviceIdToString(id: DeviceId, buf: []u8) []const u8 {
|
||||||
|
if (isEmptyDeviceId(id)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primero codificar en Base32 (52 caracteres sin padding)
|
||||||
|
var base32_buf: [52]u8 = undefined;
|
||||||
|
base32Encode(&id, &base32_buf);
|
||||||
|
|
||||||
|
// Añadir dígitos de verificación Luhn (56 caracteres)
|
||||||
|
var luhn_buf: [56]u8 = undefined;
|
||||||
|
luhnify(&base32_buf, &luhn_buf);
|
||||||
|
|
||||||
|
// Añadir guiones cada 7 caracteres (63 caracteres total)
|
||||||
|
return chunkify(&luhn_buf, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsea Device ID desde string
|
||||||
|
pub fn stringToDeviceId(str: []const u8) !DeviceId {
|
||||||
|
if (str.len == 0) {
|
||||||
|
return EMPTY_DEVICE_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar guiones y espacios
|
||||||
|
var clean_buf: [64]u8 = undefined;
|
||||||
|
var clean_len: usize = 0;
|
||||||
|
for (str) |c| {
|
||||||
|
if (c != '-' and c != ' ') {
|
||||||
|
if (clean_len >= clean_buf.len) return error.InvalidDeviceId;
|
||||||
|
clean_buf[clean_len] = std.ascii.toUpper(c);
|
||||||
|
clean_len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const clean = clean_buf[0..clean_len];
|
||||||
|
|
||||||
|
// Corregir errores de tipeo comunes
|
||||||
|
for (clean) |*c| {
|
||||||
|
if (c.* == '0') c.* = 'O';
|
||||||
|
if (c.* == '1') c.* = 'I';
|
||||||
|
if (c.* == '8') c.* = 'B';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar longitud
|
||||||
|
if (clean.len == 56) {
|
||||||
|
// Con dígitos Luhn - verificarlos
|
||||||
|
var without_luhn: [52]u8 = undefined;
|
||||||
|
try unluhnify(clean[0..56], &without_luhn);
|
||||||
|
return base32Decode(&without_luhn);
|
||||||
|
} else if (clean.len == 52) {
|
||||||
|
// Sin dígitos Luhn (formato antiguo)
|
||||||
|
return base32Decode(clean[0..52]);
|
||||||
|
} else {
|
||||||
|
return error.InvalidDeviceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica si un string de Device ID es válido
|
||||||
|
pub fn verifyDeviceIdString(str: []const u8) bool {
|
||||||
|
_ = stringToDeviceId(str) catch return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calcula el dígito de verificación Luhn para Base32
|
||||||
|
pub fn luhn32(data: []const u8) u8 {
|
||||||
|
var factor: u32 = 1;
|
||||||
|
var sum: u32 = 0;
|
||||||
|
const n: u32 = 32;
|
||||||
|
|
||||||
|
var i: usize = data.len;
|
||||||
|
while (i > 0) {
|
||||||
|
i -= 1;
|
||||||
|
const codepoint = BASE32_DECODE_TABLE[data[i]];
|
||||||
|
if (codepoint == 0xff) continue; // Ignorar caracteres inválidos
|
||||||
|
|
||||||
|
var addend = factor * codepoint;
|
||||||
|
factor = if (factor == 2) 1 else 2;
|
||||||
|
addend = (addend / n) + (addend % n);
|
||||||
|
sum += addend;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainder = sum % n;
|
||||||
|
const check = (n - remainder) % n;
|
||||||
|
return BASE32_ALPHABET[@intCast(check)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Funciones internas ---
|
||||||
|
|
||||||
|
fn base32Encode(data: *const [32]u8, out: *[52]u8) void {
|
||||||
|
var bits: u64 = 0;
|
||||||
|
var num_bits: u6 = 0;
|
||||||
|
var out_idx: usize = 0;
|
||||||
|
|
||||||
|
for (data) |byte| {
|
||||||
|
bits = (bits << 8) | byte;
|
||||||
|
num_bits += 8;
|
||||||
|
|
||||||
|
while (num_bits >= 5) {
|
||||||
|
num_bits -= 5;
|
||||||
|
const idx: u5 = @truncate(bits >> num_bits);
|
||||||
|
out[out_idx] = BASE32_ALPHABET[idx];
|
||||||
|
out_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bits restantes (si los hay)
|
||||||
|
if (num_bits > 0) {
|
||||||
|
const idx: u5 = @truncate(bits << (5 - num_bits));
|
||||||
|
out[out_idx] = BASE32_ALPHABET[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base32Decode(data: *const [52]u8) !DeviceId {
|
||||||
|
var result: DeviceId = undefined;
|
||||||
|
var bits: u64 = 0;
|
||||||
|
var num_bits: u6 = 0;
|
||||||
|
var out_idx: usize = 0;
|
||||||
|
|
||||||
|
for (data) |c| {
|
||||||
|
const val = BASE32_DECODE_TABLE[c];
|
||||||
|
if (val == 0xff) return error.InvalidDeviceId;
|
||||||
|
|
||||||
|
bits = (bits << 5) | val;
|
||||||
|
num_bits += 5;
|
||||||
|
|
||||||
|
if (num_bits >= 8) {
|
||||||
|
num_bits -= 8;
|
||||||
|
result[out_idx] = @truncate(bits >> num_bits);
|
||||||
|
out_idx += 1;
|
||||||
|
if (out_idx >= result.len) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn luhnify(input: *const [52]u8, output: *[56]u8) void {
|
||||||
|
// Dividir en 4 grupos de 13 caracteres, añadir dígito Luhn a cada uno
|
||||||
|
for (0..4) |i| {
|
||||||
|
const start = i * 13;
|
||||||
|
const out_start = i * 14;
|
||||||
|
@memcpy(output[out_start .. out_start + 13], input[start .. start + 13]);
|
||||||
|
output[out_start + 13] = luhn32(input[start .. start + 13]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unluhnify(input: []const u8, output: *[52]u8) !void {
|
||||||
|
if (input.len != 56) return error.InvalidDeviceId;
|
||||||
|
|
||||||
|
for (0..4) |i| {
|
||||||
|
const in_start = i * 14;
|
||||||
|
const out_start = i * 13;
|
||||||
|
const group = input[in_start .. in_start + 13];
|
||||||
|
const check = input[in_start + 13];
|
||||||
|
|
||||||
|
// Verificar dígito Luhn
|
||||||
|
if (luhn32(group) != check) {
|
||||||
|
return error.InvalidDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@memcpy(output[out_start .. out_start + 13], group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chunkify(input: *const [56]u8, output: []u8) []const u8 {
|
||||||
|
if (output.len < 63) @panic("buffer too small");
|
||||||
|
|
||||||
|
var out_idx: usize = 0;
|
||||||
|
for (0..8) |i| {
|
||||||
|
if (i > 0) {
|
||||||
|
output[out_idx] = '-';
|
||||||
|
out_idx += 1;
|
||||||
|
}
|
||||||
|
const start = i * 7;
|
||||||
|
@memcpy(output[out_idx .. out_idx + 7], input[start .. start + 7]);
|
||||||
|
out_idx += 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output[0..63];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
test "device id round trip" {
|
||||||
|
// Certificado de prueba (datos aleatorios)
|
||||||
|
const test_cert = [_]u8{ 0x30, 0x82, 0x01, 0x22 } ++ [_]u8{0xab} ** 28;
|
||||||
|
|
||||||
|
const id = newDeviceId(&test_cert);
|
||||||
|
var buf: [64]u8 = undefined;
|
||||||
|
const str = deviceIdToString(id, &buf);
|
||||||
|
|
||||||
|
const parsed = try stringToDeviceId(str);
|
||||||
|
try std.testing.expect(deviceIdEquals(id, parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "luhn32 calculation" {
|
||||||
|
// Test conocido
|
||||||
|
const check = luhn32("ABCDEFGHIJKLM");
|
||||||
|
try std.testing.expect(check >= 'A' and check <= 'Z' or check >= '2' and check <= '7');
|
||||||
|
}
|
||||||
|
|
||||||
|
test "empty device id" {
|
||||||
|
try std.testing.expect(isEmptyDeviceId(EMPTY_DEVICE_ID));
|
||||||
|
try std.testing.expect(!isEmptyDeviceId(LOCAL_DEVICE_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse with errors" {
|
||||||
|
// Con errores de tipeo comunes
|
||||||
|
const result = stringToDeviceId("0II88OO-1234567-ABCDEFG-HIJKLMN-OPQRSTU-VWXYZ23-4567ABC-DEFGHIJ");
|
||||||
|
_ = result catch |err| {
|
||||||
|
try std.testing.expect(err == error.InvalidDeviceId);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
47
src/main.zig
Normal file
47
src/main.zig
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
//! zcatp2p - Protocolo P2P para comunicación directa entre empresas
|
||||||
|
//!
|
||||||
|
//! Librería Zig para comunicación P2P segura entre instancias de Simifactu.
|
||||||
|
//! Permite intercambio directo de documentos (facturas, certificados) entre empresas
|
||||||
|
//! sin necesidad de email ni servicios cloud.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// Módulos públicos
|
||||||
|
pub const identity = @import("identity.zig");
|
||||||
|
pub const crypto = @import("crypto.zig");
|
||||||
|
pub const protocol = @import("protocol.zig");
|
||||||
|
pub const discovery = @import("discovery.zig");
|
||||||
|
pub const connection = @import("connection.zig");
|
||||||
|
|
||||||
|
// Re-exports principales
|
||||||
|
pub const DeviceId = identity.DeviceId;
|
||||||
|
pub const ShortId = identity.ShortId;
|
||||||
|
pub const Config = connection.Config;
|
||||||
|
pub const P2P = connection.P2P;
|
||||||
|
pub const Connection = connection.Connection;
|
||||||
|
pub const Message = protocol.Message;
|
||||||
|
pub const ConnectionState = connection.ConnectionState;
|
||||||
|
pub const NatType = connection.NatType;
|
||||||
|
pub const Error = connection.Error;
|
||||||
|
|
||||||
|
// Content types para Simifactu
|
||||||
|
pub const ContentTypes = struct {
|
||||||
|
pub const invoice = "application/x-simifactu-invoice";
|
||||||
|
pub const invoice_ack = "application/x-simifactu-invoice-ack";
|
||||||
|
pub const certificate = "application/x-simifactu-certificate";
|
||||||
|
pub const verifactu = "application/x-simifactu-verifactu";
|
||||||
|
pub const query = "application/x-simifactu-query";
|
||||||
|
pub const response = "application/x-simifactu-response";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utilidades
|
||||||
|
pub const utils = struct {
|
||||||
|
pub const deviceIdToString = identity.deviceIdToString;
|
||||||
|
pub const stringToDeviceId = identity.stringToDeviceId;
|
||||||
|
pub const verifyDeviceIdString = identity.verifyDeviceIdString;
|
||||||
|
pub const luhn32 = identity.luhn32;
|
||||||
|
};
|
||||||
|
|
||||||
|
test {
|
||||||
|
std.testing.refAllDecls(@This());
|
||||||
|
}
|
||||||
333
src/protocol.zig
Normal file
333
src/protocol.zig
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
//! Módulo de protocolo - Message framing y tipos de mensaje
|
||||||
|
//!
|
||||||
|
//! Formato de trama:
|
||||||
|
//! ┌────────────────┬────────────────┬─────────────────────────────┐
|
||||||
|
//! │ Header (2B) │ Length (4B) │ Payload (variable) │
|
||||||
|
//! └────────────────┴────────────────┴─────────────────────────────┘
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Magic number para validación de paquetes
|
||||||
|
pub const MAGIC: u32 = 0x2EA7D90B;
|
||||||
|
|
||||||
|
/// Versión del protocolo
|
||||||
|
pub const PROTOCOL_VERSION: u8 = 1;
|
||||||
|
|
||||||
|
/// Tamaño máximo de mensaje (500 MB)
|
||||||
|
pub const MAX_MESSAGE_LEN: usize = 500 * 1000 * 1000;
|
||||||
|
|
||||||
|
/// Umbral para compresión (128 bytes)
|
||||||
|
pub const COMPRESSION_THRESHOLD: usize = 128;
|
||||||
|
|
||||||
|
/// Tipos de mensaje
|
||||||
|
pub const MessageType = enum(u8) {
|
||||||
|
hello = 0x00,
|
||||||
|
ping = 0x01,
|
||||||
|
pong = 0x02,
|
||||||
|
data = 0x03,
|
||||||
|
data_ack = 0x04,
|
||||||
|
close = 0x05,
|
||||||
|
@"error" = 0x06,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Flags de mensaje
|
||||||
|
pub const MessageFlags = packed struct(u8) {
|
||||||
|
compressed: bool = false,
|
||||||
|
encrypted: bool = false,
|
||||||
|
request: bool = false,
|
||||||
|
response: bool = false,
|
||||||
|
_reserved: u4 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Header de mensaje
|
||||||
|
pub const MessageHeader = struct {
|
||||||
|
msg_type: MessageType,
|
||||||
|
flags: MessageFlags,
|
||||||
|
length: u32,
|
||||||
|
|
||||||
|
pub const SIZE: usize = 6;
|
||||||
|
|
||||||
|
pub fn encode(self: MessageHeader) [SIZE]u8 {
|
||||||
|
var buf: [SIZE]u8 = undefined;
|
||||||
|
buf[0] = @intFromEnum(self.msg_type);
|
||||||
|
buf[1] = @bitCast(self.flags);
|
||||||
|
std.mem.writeInt(u32, buf[2..6], self.length, .big);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(buf: *const [SIZE]u8) MessageHeader {
|
||||||
|
return .{
|
||||||
|
.msg_type = @enumFromInt(buf[0]),
|
||||||
|
.flags = @bitCast(buf[1]),
|
||||||
|
.length = std.mem.readInt(u32, buf[2..6], .big),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Mensaje de aplicación
|
||||||
|
pub const Message = struct {
|
||||||
|
id: u32,
|
||||||
|
content_type: []const u8,
|
||||||
|
data: []const u8,
|
||||||
|
timestamp: i64,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Capacidades soportadas
|
||||||
|
pub const Capabilities = packed struct(u32) {
|
||||||
|
compression_lz4: bool = false,
|
||||||
|
encryption_chacha20: bool = false,
|
||||||
|
relay_support: bool = false,
|
||||||
|
ipv6_support: bool = false,
|
||||||
|
_reserved: u28 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Mensaje HELLO
|
||||||
|
pub const HelloMessage = struct {
|
||||||
|
device_name: []const u8,
|
||||||
|
client_name: []const u8,
|
||||||
|
client_version: []const u8,
|
||||||
|
timestamp: i64,
|
||||||
|
capabilities: Capabilities,
|
||||||
|
|
||||||
|
pub fn encode(self: HelloMessage, allocator: std.mem.Allocator) ![]u8 {
|
||||||
|
const size = 1 + self.device_name.len +
|
||||||
|
1 + self.client_name.len +
|
||||||
|
1 + self.client_version.len +
|
||||||
|
8 + 4;
|
||||||
|
|
||||||
|
const buf = try allocator.alloc(u8, size);
|
||||||
|
errdefer allocator.free(buf);
|
||||||
|
|
||||||
|
var pos: usize = 0;
|
||||||
|
|
||||||
|
// device_name
|
||||||
|
buf[pos] = @intCast(self.device_name.len);
|
||||||
|
pos += 1;
|
||||||
|
@memcpy(buf[pos .. pos + self.device_name.len], self.device_name);
|
||||||
|
pos += self.device_name.len;
|
||||||
|
|
||||||
|
// client_name
|
||||||
|
buf[pos] = @intCast(self.client_name.len);
|
||||||
|
pos += 1;
|
||||||
|
@memcpy(buf[pos .. pos + self.client_name.len], self.client_name);
|
||||||
|
pos += self.client_name.len;
|
||||||
|
|
||||||
|
// client_version
|
||||||
|
buf[pos] = @intCast(self.client_version.len);
|
||||||
|
pos += 1;
|
||||||
|
@memcpy(buf[pos .. pos + self.client_version.len], self.client_version);
|
||||||
|
pos += self.client_version.len;
|
||||||
|
|
||||||
|
// timestamp
|
||||||
|
std.mem.writeInt(i64, buf[pos..][0..8], self.timestamp, .big);
|
||||||
|
pos += 8;
|
||||||
|
|
||||||
|
// capabilities
|
||||||
|
std.mem.writeInt(u32, buf[pos..][0..4], @bitCast(self.capabilities), .big);
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(data: []const u8, allocator: std.mem.Allocator) !HelloMessage {
|
||||||
|
if (data.len < 15) return error.InvalidMessage;
|
||||||
|
|
||||||
|
var pos: usize = 0;
|
||||||
|
|
||||||
|
// device_name
|
||||||
|
const device_name_len = data[pos];
|
||||||
|
pos += 1;
|
||||||
|
if (pos + device_name_len > data.len) return error.InvalidMessage;
|
||||||
|
const device_name = try allocator.dupe(u8, data[pos .. pos + device_name_len]);
|
||||||
|
pos += device_name_len;
|
||||||
|
|
||||||
|
// client_name
|
||||||
|
const client_name_len = data[pos];
|
||||||
|
pos += 1;
|
||||||
|
if (pos + client_name_len > data.len) return error.InvalidMessage;
|
||||||
|
const client_name = try allocator.dupe(u8, data[pos .. pos + client_name_len]);
|
||||||
|
pos += client_name_len;
|
||||||
|
|
||||||
|
// client_version
|
||||||
|
const client_version_len = data[pos];
|
||||||
|
pos += 1;
|
||||||
|
if (pos + client_version_len > data.len) return error.InvalidMessage;
|
||||||
|
const client_version = try allocator.dupe(u8, data[pos .. pos + client_version_len]);
|
||||||
|
pos += client_version_len;
|
||||||
|
|
||||||
|
if (pos + 12 > data.len) return error.InvalidMessage;
|
||||||
|
|
||||||
|
// timestamp
|
||||||
|
const timestamp = std.mem.readInt(i64, data[pos..][0..8], .big);
|
||||||
|
pos += 8;
|
||||||
|
|
||||||
|
// capabilities
|
||||||
|
const capabilities: Capabilities = @bitCast(std.mem.readInt(u32, data[pos..][0..4], .big));
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.device_name = device_name,
|
||||||
|
.client_name = client_name,
|
||||||
|
.client_version = client_version,
|
||||||
|
.timestamp = timestamp,
|
||||||
|
.capabilities = capabilities,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *HelloMessage, allocator: std.mem.Allocator) void {
|
||||||
|
allocator.free(self.device_name);
|
||||||
|
allocator.free(self.client_name);
|
||||||
|
allocator.free(self.client_version);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Mensaje DATA
|
||||||
|
pub const DataMessage = struct {
|
||||||
|
message_id: u32,
|
||||||
|
content_type: []const u8,
|
||||||
|
data: []const u8,
|
||||||
|
|
||||||
|
pub fn encode(self: DataMessage, allocator: std.mem.Allocator) ![]u8 {
|
||||||
|
const size = 4 + 1 + self.content_type.len + 4 + self.data.len;
|
||||||
|
const buf = try allocator.alloc(u8, size);
|
||||||
|
errdefer allocator.free(buf);
|
||||||
|
|
||||||
|
var pos: usize = 0;
|
||||||
|
|
||||||
|
// message_id
|
||||||
|
std.mem.writeInt(u32, buf[pos..][0..4], self.message_id, .big);
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
// content_type
|
||||||
|
buf[pos] = @intCast(self.content_type.len);
|
||||||
|
pos += 1;
|
||||||
|
@memcpy(buf[pos .. pos + self.content_type.len], self.content_type);
|
||||||
|
pos += self.content_type.len;
|
||||||
|
|
||||||
|
// data
|
||||||
|
std.mem.writeInt(u32, buf[pos..][0..4], @intCast(self.data.len), .big);
|
||||||
|
pos += 4;
|
||||||
|
@memcpy(buf[pos .. pos + self.data.len], self.data);
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(data: []const u8, allocator: std.mem.Allocator) !DataMessage {
|
||||||
|
if (data.len < 9) return error.InvalidMessage;
|
||||||
|
|
||||||
|
var pos: usize = 0;
|
||||||
|
|
||||||
|
// message_id
|
||||||
|
const message_id = std.mem.readInt(u32, data[pos..][0..4], .big);
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
// content_type
|
||||||
|
const content_type_len = data[pos];
|
||||||
|
pos += 1;
|
||||||
|
if (pos + content_type_len > data.len) return error.InvalidMessage;
|
||||||
|
const content_type = try allocator.dupe(u8, data[pos .. pos + content_type_len]);
|
||||||
|
pos += content_type_len;
|
||||||
|
|
||||||
|
// data
|
||||||
|
if (pos + 4 > data.len) return error.InvalidMessage;
|
||||||
|
const data_len = std.mem.readInt(u32, data[pos..][0..4], .big);
|
||||||
|
pos += 4;
|
||||||
|
if (pos + data_len > data.len) return error.InvalidMessage;
|
||||||
|
const payload = try allocator.dupe(u8, data[pos .. pos + data_len]);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.message_id = message_id,
|
||||||
|
.content_type = content_type,
|
||||||
|
.data = payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *DataMessage, allocator: std.mem.Allocator) void {
|
||||||
|
allocator.free(self.content_type);
|
||||||
|
allocator.free(self.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Mensaje CLOSE
|
||||||
|
pub const CloseMessage = struct {
|
||||||
|
reason: []const u8,
|
||||||
|
|
||||||
|
pub fn encode(self: CloseMessage, allocator: std.mem.Allocator) ![]u8 {
|
||||||
|
const size = 1 + self.reason.len;
|
||||||
|
const buf = try allocator.alloc(u8, size);
|
||||||
|
buf[0] = @intCast(self.reason.len);
|
||||||
|
@memcpy(buf[1..], self.reason);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(data: []const u8, allocator: std.mem.Allocator) !CloseMessage {
|
||||||
|
if (data.len < 1) return error.InvalidMessage;
|
||||||
|
const reason_len = data[0];
|
||||||
|
if (1 + reason_len > data.len) return error.InvalidMessage;
|
||||||
|
return .{
|
||||||
|
.reason = try allocator.dupe(u8, data[1 .. 1 + reason_len]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *CloseMessage, allocator: std.mem.Allocator) void {
|
||||||
|
allocator.free(self.reason);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Mensaje DATA_ACK
|
||||||
|
pub const DataAckMessage = struct {
|
||||||
|
message_id: u32,
|
||||||
|
|
||||||
|
pub fn encode(self: DataAckMessage) [4]u8 {
|
||||||
|
var buf: [4]u8 = undefined;
|
||||||
|
std.mem.writeInt(u32, &buf, self.message_id, .big);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(data: []const u8) !DataAckMessage {
|
||||||
|
if (data.len < 4) return error.InvalidMessage;
|
||||||
|
return .{
|
||||||
|
.message_id = std.mem.readInt(u32, data[0..4], .big),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test "message header encode/decode" {
|
||||||
|
const header = MessageHeader{
|
||||||
|
.msg_type = .data,
|
||||||
|
.flags = .{ .compressed = true },
|
||||||
|
.length = 1234,
|
||||||
|
};
|
||||||
|
|
||||||
|
const encoded = header.encode();
|
||||||
|
const decoded = MessageHeader.decode(&encoded);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(header.msg_type, decoded.msg_type);
|
||||||
|
try std.testing.expectEqual(header.flags.compressed, decoded.flags.compressed);
|
||||||
|
try std.testing.expectEqual(header.length, decoded.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "hello message encode/decode" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
var hello = HelloMessage{
|
||||||
|
.device_name = "test-device",
|
||||||
|
.client_name = "zcatp2p",
|
||||||
|
.client_version = "1.0.0",
|
||||||
|
.timestamp = 1234567890,
|
||||||
|
.capabilities = .{ .compression_lz4 = true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const encoded = try hello.encode(allocator);
|
||||||
|
defer allocator.free(encoded);
|
||||||
|
|
||||||
|
var decoded = try HelloMessage.decode(encoded, allocator);
|
||||||
|
defer decoded.deinit(allocator);
|
||||||
|
|
||||||
|
try std.testing.expectEqualStrings(hello.device_name, decoded.device_name);
|
||||||
|
try std.testing.expectEqualStrings(hello.client_name, decoded.client_name);
|
||||||
|
try std.testing.expectEqual(hello.timestamp, decoded.timestamp);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue