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