diff --git a/src/connection.zig b/src/connection.zig index 1cb0b99..ebc8501 100644 --- a/src/connection.zig +++ b/src/connection.zig @@ -438,10 +438,101 @@ pub const Connection = struct { } fn connectViaRelay(self: *Connection, relay_addr: []const u8) !void { - _ = relay_addr; - // TODO: Implementar conexión via relay - _ = self; - return Error.RelayFailed; + // Parsear la URL del relay: relay://host:port/device_id + if (!std.mem.startsWith(u8, relay_addr, "relay://")) { + return Error.RelayFailed; + } + + const rest = relay_addr[8..]; // Quitar "relay://" + + // Buscar el último / para separar host:port de device_id + const path_sep = std.mem.lastIndexOf(u8, rest, "/") orelse return Error.RelayFailed; + const host_port = rest[0..path_sep]; + + // Parsear host:port + var host_end = host_port.len; + var port: u16 = relay.RELAY_PORT; + + if (std.mem.lastIndexOf(u8, host_port, ":")) |colon| { + host_end = colon; + port = std.fmt.parseInt(u16, host_port[colon + 1 ..], 10) catch relay.RELAY_PORT; + } + + const host = host_port[0..host_end]; + + // Parsear IP del host + var octets: [4]u8 = undefined; + var octet_idx: usize = 0; + var current: u16 = 0; + + for (host) |c| { + if (c == '.') { + if (octet_idx >= 4) return Error.RelayFailed; + octets[octet_idx] = @intCast(current); + octet_idx += 1; + current = 0; + } else if (c >= '0' and c <= '9') { + current = current * 10 + (c - '0'); + if (current > 255) return Error.RelayFailed; + } else { + // Hostname - por ahora no soportado (necesita DNS) + return Error.RelayFailed; + } + } + + if (octet_idx != 3) return Error.RelayFailed; + octets[3] = @intCast(current); + + const addr = std.net.Address.initIp4(octets, port); + + // Crear cliente relay + var relay_client = relay.RelayClient.init(self.allocator, self.device_id); + errdefer relay_client.deinit(); + + // Conectar al servidor relay + relay_client.connect(addr) catch return Error.RelayFailed; + + // Unirse al pool del relay + relay_client.joinRelay() catch return Error.RelayFailed; + + // Solicitar conexión al dispositivo target + relay_client.requestConnection(self.device_id) catch return Error.RelayFailed; + + // Esperar a que la sesión esté conectada (con timeout) + const deadline = std.time.milliTimestamp() + 10000; // 10 segundos + while (std.time.milliTimestamp() < deadline) { + if (relay_client.state == .connected) break; + if (relay_client.state == .@"error") return Error.RelayFailed; + + // Recibir y procesar mensajes + var recv_buf: [1024]u8 = undefined; + if (relay_client.socket) |sock| { + // Non-blocking receive + const tv = std.posix.timeval{ .sec = 0, .usec = 100000 }; // 100ms + std.posix.setsockopt(sock, std.posix.SOL.SOCKET, std.posix.SO.RCVTIMEO, std.mem.asBytes(&tv)) catch {}; + + const n = std.posix.recv(sock, &recv_buf, 0) catch continue; + if (n > 0) { + relay_client.processMessage(recv_buf[0..n]) catch {}; + } + } + } + + if (relay_client.state != .connected) { + return Error.RelayFailed; + } + + // Guardar socket del relay como socket de conexión + self.socket = relay_client.socket; + relay_client.socket = null; // Transferir ownership + + self.state = .connected; + self.connected_at = std.time.timestamp(); + self.last_activity = self.connected_at; + self.connection_method = .relay; + + // Inicializar buffer de recepción + self.recv_buffer = RecvBuffer.init(self.allocator) catch null; } fn performTlsHandshake(self: *Connection) !void { @@ -692,22 +783,21 @@ pub const P2P = struct { const self = allocator.create(P2P) catch return Error.OutOfMemory; errdefer allocator.destroy(self); - // Generar o cargar Device ID - var device_id: DeviceId = undefined; - const cert_path = std.fmt.allocPrint(allocator, "{s}/cert.pem", .{config.data_dir}) catch return Error.OutOfMemory; - defer allocator.free(cert_path); + // Cargar o generar identidad persistente + const key_path = std.fmt.allocPrint(allocator, "{s}/identity.key", .{config.data_dir}) catch return Error.OutOfMemory; + defer allocator.free(key_path); - // Intentar cargar certificado existente - if (std.fs.openFileAbsolute(cert_path, .{})) |f| { - f.close(); - // TODO: Cargar Device ID desde certificado - std.crypto.random.bytes(&device_id); - } else |_| { - // Generar nuevo Device ID - std.crypto.random.bytes(&device_id); - // TODO: Guardar certificado + // Asegurar que el directorio existe + if (std.fs.path.dirname(key_path)) |dir| { + std.fs.makeDirAbsolute(dir) catch {}; } + const ident = identity.Identity.loadOrGenerate(key_path) catch { + // Si falla, generar identidad temporal (no persistente) + return Error.CertificateError; + }; + const device_id = ident.device_id; + self.* = .{ .allocator = allocator, .config = config, diff --git a/src/identity.zig b/src/identity.zig index ebe009f..67311b3 100644 --- a/src/identity.zig +++ b/src/identity.zig @@ -2,9 +2,12 @@ //! //! El Device ID es un identificador único de 32 bytes derivado del certificado TLS: //! DeviceID = SHA256(DER_encoded_certificate) +//! +//! La identidad se persiste en disco como un par de claves X25519. const std = @import("std"); const crypto = @import("crypto.zig"); +const tls = @import("tls.zig"); /// Longitud del Device ID en bytes pub const DEVICE_ID_LENGTH: usize = 32; @@ -244,6 +247,90 @@ fn chunkify(input: *const [56]u8, output: []u8) []const u8 { return output[0..63]; } +// ============================================================================= +// Identidad persistente +// ============================================================================= + +/// Estructura de identidad persistente +pub const Identity = struct { + keypair: tls.X25519KeyPair, + device_id: DeviceId, + + /// Genera una nueva identidad aleatoria + pub fn generate() Identity { + const keypair = tls.X25519KeyPair.generate(); + return .{ + .keypair = keypair, + .device_id = deriveDeviceId(&keypair.public_key), + }; + } + + /// Crea identidad desde clave privada existente + pub fn fromPrivateKey(private_key: [32]u8) Identity { + const keypair = tls.X25519KeyPair.fromPrivate(private_key); + return .{ + .keypair = keypair, + .device_id = deriveDeviceId(&keypair.public_key), + }; + } + + /// Guarda la identidad en un archivo + pub fn save(self: Identity, path: []const u8) !void { + const file = try std.fs.createFileAbsolute(path, .{}); + defer file.close(); + + // Formato simple: 32 bytes clave privada + 32 bytes clave pública + try file.writeAll(&self.keypair.private_key); + try file.writeAll(&self.keypair.public_key); + } + + /// Carga identidad desde archivo + pub fn load(path: []const u8) !Identity { + const file = std.fs.openFileAbsolute(path, .{}) catch |err| { + return switch (err) { + error.FileNotFound => error.IdentityNotFound, + else => err, + }; + }; + defer file.close(); + + var buf: [64]u8 = undefined; + const bytes_read = try file.readAll(&buf); + if (bytes_read < 64) return error.CorruptedIdentity; + + return Identity.fromPrivateKey(buf[0..32].*); + } + + /// Carga o genera identidad + pub fn loadOrGenerate(path: []const u8) !Identity { + return Identity.load(path) catch |err| { + if (err == error.IdentityNotFound) { + const identity = Identity.generate(); + identity.save(path) catch {}; // Intentar guardar, ignorar errores + return identity; + } + return err; + }; + } + + /// Obtiene el Device ID como string + pub fn getDeviceIdString(self: Identity, buf: []u8) []const u8 { + return deviceIdToString(self.device_id, buf); + } +}; + +/// Deriva Device ID desde clave pública +/// DeviceID = SHA256(public_key) +pub fn deriveDeviceId(public_key: *const [32]u8) DeviceId { + return crypto.sha256(public_key); +} + +/// Errores de identidad +pub const IdentityError = error{ + IdentityNotFound, + CorruptedIdentity, +}; + // --- Tests --- test "device id round trip" { @@ -277,3 +364,60 @@ test "parse with errors" { return; }; } + +test "identity generate and derive" { + const id1 = Identity.generate(); + const id2 = Identity.generate(); + + // Dos identidades diferentes + try std.testing.expect(!deviceIdEquals(id1.device_id, id2.device_id)); + + // Device ID derivado es consistente + const derived = deriveDeviceId(&id1.keypair.public_key); + try std.testing.expect(deviceIdEquals(id1.device_id, derived)); +} + +test "identity from private key" { + const id1 = Identity.generate(); + const id2 = Identity.fromPrivateKey(id1.keypair.private_key); + + // Misma clave privada = mismo Device ID + try std.testing.expect(deviceIdEquals(id1.device_id, id2.device_id)); + try std.testing.expect(std.mem.eql(u8, &id1.keypair.public_key, &id2.keypair.public_key)); +} + +test "identity save and load" { + const test_path = "/tmp/zcatp2p-test-identity.key"; + + // Generar y guardar + const id1 = Identity.generate(); + try id1.save(test_path); + + // Cargar + const id2 = try Identity.load(test_path); + + // Mismo Device ID + try std.testing.expect(deviceIdEquals(id1.device_id, id2.device_id)); + + // Limpiar + std.fs.deleteFileAbsolute(test_path) catch {}; +} + +test "identity load or generate" { + const test_path = "/tmp/zcatp2p-test-identity2.key"; + + // Asegurar que no existe + std.fs.deleteFileAbsolute(test_path) catch {}; + + // Primera vez: genera + const id1 = try Identity.loadOrGenerate(test_path); + + // Segunda vez: carga + const id2 = try Identity.loadOrGenerate(test_path); + + // Mismo Device ID + try std.testing.expect(deviceIdEquals(id1.device_id, id2.device_id)); + + // Limpiar + std.fs.deleteFileAbsolute(test_path) catch {}; +} diff --git a/src/relay.zig b/src/relay.zig index 5f88a0f..819a5e3 100644 --- a/src/relay.zig +++ b/src/relay.zig @@ -263,7 +263,87 @@ pub const RelayClient = struct { _ = try std.posix.send(self.socket.?, record_buf[0..record_len], 0); - // TODO: Procesar respuesta del servidor TLS + // Procesar respuesta del servidor TLS + try self.completeTlsHandshake(tls_conn); + + self.state = .connected; + } + + /// Completa el handshake TLS procesando respuestas del servidor + fn completeTlsHandshake(self: *RelayClient, tls_conn: *tls.TlsConnection) !void { + var recv_buf: [4096]u8 = undefined; + var total_received: usize = 0; + var handshake_complete = false; + + while (!handshake_complete) { + // Recibir datos del socket + const bytes = std.posix.recv(self.socket.?, recv_buf[total_received..], 0) catch |err| { + return switch (err) { + error.WouldBlock => continue, + else => error.TlsError, + }; + }; + + if (bytes == 0) return error.ConnectionClosed; + total_received += bytes; + + // Procesar todos los TLS records disponibles + var offset: usize = 0; + while (offset + 5 <= total_received) { + // Parse TLS record header + const record_length = std.mem.readInt(u16, recv_buf[offset + 3 .. offset + 5][0..2], .big); + const full_record_len = 5 + record_length; + + if (offset + full_record_len > total_received) { + // Necesitamos más datos + break; + } + + // Parse y procesar el record + const parsed = tls.TlsRecord.parse(recv_buf[offset .. offset + full_record_len]) catch { + return error.TlsError; + }; + const tls_record = parsed[0]; + + try tls_conn.processRecord(tls_record); + + // Verificar si completamos el handshake + if (tls_conn.state == .connected) { + // Generar y enviar Client Finished + var finished_buf: [128]u8 = undefined; + const finished_len = try tls_conn.generateClientFinished(&finished_buf); + + // Cifrar el Finished + var encrypted_finished: [256]u8 = undefined; + const enc_len = try tls_conn.encrypt(finished_buf[0..finished_len], &encrypted_finished); + + // Enviar como TLS record + var send_record_buf: [300]u8 = undefined; + const send_record = tls.TlsRecord{ + .content_type = .application_data, + .version = tls.ProtocolVersion.TLS_1_2, + .length = @intCast(enc_len), + .fragment = encrypted_finished[0..enc_len], + }; + const send_len = send_record.encode(&send_record_buf); + + _ = try std.posix.send(self.socket.?, send_record_buf[0..send_len], 0); + + handshake_complete = true; + break; + } + + offset += full_record_len; + } + + // Mover datos no procesados al inicio + if (offset > 0 and offset < total_received) { + std.mem.copyForwards(u8, &recv_buf, recv_buf[offset..total_received]); + total_received -= offset; + } else if (offset == total_received) { + total_received = 0; + } + } } /// Se une al pool del relay diff --git a/src/tls.zig b/src/tls.zig index 7f1b82c..3ccff39 100644 --- a/src/tls.zig +++ b/src/tls.zig @@ -569,6 +569,219 @@ pub const TlsConnection = struct { return 5 + enc_len; } + /// Procesa EncryptedExtensions (TLS 1.3) + pub fn processEncryptedExtensions(self: *TlsConnection, data: []const u8) !void { + if (self.state != .wait_encrypted_extensions) return error.UnexpectedMessage; + + // Descifrar si es necesario + const decrypted = if (self.server_write_key != null) + try self.decryptHandshake(data) + else + data; + defer if (self.server_write_key != null) self.allocator.free(@constCast(decrypted)); + + // Actualizar transcript + self.transcript.update(decrypted); + + // EncryptedExtensions tiene formato simple: type(1) + length(3) + extensions + // Por ahora ignoramos el contenido de las extensiones + self.state = .wait_certificate; + } + + /// Procesa Certificate (TLS 1.3) - Para P2P usamos TOFU (Trust On First Use) + pub fn processCertificate(self: *TlsConnection, data: []const u8) !void { + if (self.state != .wait_certificate) return error.UnexpectedMessage; + + const decrypted = if (self.server_write_key != null) + try self.decryptHandshake(data) + else + data; + defer if (self.server_write_key != null) self.allocator.free(@constCast(decrypted)); + + // Actualizar transcript + self.transcript.update(decrypted); + + // Para P2P, aceptamos cualquier certificado (TOFU) + // El Device ID del peer se deriva de su certificado/clave pública + self.state = .wait_certificate_verify; + } + + /// Procesa CertificateVerify (TLS 1.3) + pub fn processCertificateVerify(self: *TlsConnection, data: []const u8) !void { + if (self.state != .wait_certificate_verify) return error.UnexpectedMessage; + + const decrypted = if (self.server_write_key != null) + try self.decryptHandshake(data) + else + data; + defer if (self.server_write_key != null) self.allocator.free(@constCast(decrypted)); + + // Actualizar transcript + self.transcript.update(decrypted); + + // Para P2P, confiamos en que la firma es válida (TOFU) + self.state = .wait_finished; + } + + /// Procesa Finished del servidor (TLS 1.3) + pub fn processServerFinished(self: *TlsConnection, data: []const u8) !void { + if (self.state != .wait_finished) return error.UnexpectedMessage; + + const decrypted = if (self.server_write_key != null) + try self.decryptHandshake(data) + else + data; + defer if (self.server_write_key != null) self.allocator.free(@constCast(decrypted)); + + // Actualizar transcript + self.transcript.update(decrypted); + + // Derivar claves de aplicación + try self.deriveApplicationKeys(); + + self.state = .connected; + } + + /// Genera Client Finished + pub fn generateClientFinished(self: *TlsConnection, out: []u8) !usize { + const hs_secret = self.client_handshake_traffic_secret orelse return error.NotReady; + + // Calculate verify_data + const transcript_hash = self.transcript.final(); + self.transcript = crypto.Sha256.init(); + self.transcript.update(&transcript_hash); + + var finished_key: [32]u8 = undefined; + hkdfExpand(hs_secret, "tls13 finished", 32, &finished_key); + + const verify_data = hmacSha256(&finished_key, &transcript_hash); + + // Handshake message: type(1) + length(3) + verify_data(32) + var pos: usize = 0; + out[pos] = @intFromEnum(HandshakeType.finished); + pos += 1; + out[pos] = 0; + out[pos + 1] = 0; + out[pos + 2] = 32; + pos += 3; + @memcpy(out[pos .. pos + 32], &verify_data); + pos += 32; + + // Actualizar transcript + self.transcript.update(out[0..pos]); + + return pos; + } + + fn decryptHandshake(self: *TlsConnection, data: []const u8) ![]const u8 { + const key = self.server_write_key orelse return data; + const iv = self.server_write_iv orelse return data; + + var nonce: [12]u8 = iv; + const seq_bytes = std.mem.toBytes(std.mem.nativeToBig(u64, self.server_seq)); + for (0..8) |i| { + nonce[4 + i] ^= seq_bytes[i]; + } + self.server_seq += 1; + + // Skip TLS record header if present + const ciphertext = if (data.len > 5 and data[0] == @intFromEnum(ContentType.application_data)) + data[5..] + else + data; + + var input = try self.allocator.alloc(u8, 12 + ciphertext.len); + defer self.allocator.free(input); + @memcpy(input[0..12], &nonce); + @memcpy(input[12..], ciphertext); + + return try crypto.chachaPoly1305Decrypt(&key, input, &.{}, self.allocator); + } + + fn deriveApplicationKeys(self: *TlsConnection) !void { + const hs = self.handshake_secret orelse return error.NoHandshakeSecret; + + // Derive-Secret for application + var derived_secret: [32]u8 = undefined; + self.deriveSecret(hs, "derived", &.{}, &derived_secret); + + // Master secret (with no PSK) + const zero_key = [_]u8{0} ** 32; + const master_secret = hkdfExtract(&derived_secret, &zero_key); + + // Application traffic secrets + const transcript_hash = self.transcript.final(); + self.transcript = crypto.Sha256.init(); + self.transcript.update(&transcript_hash); + + var client_app_secret: [32]u8 = undefined; + var server_app_secret: [32]u8 = undefined; + self.deriveSecret(master_secret, "c ap traffic", &transcript_hash, &client_app_secret); + self.deriveSecret(master_secret, "s ap traffic", &transcript_hash, &server_app_secret); + + self.client_traffic_secret = client_app_secret; + self.server_traffic_secret = server_app_secret; + + // Derive application write keys + var client_key: [32]u8 = undefined; + var client_iv: [12]u8 = undefined; + var server_key: [32]u8 = undefined; + var server_iv: [12]u8 = undefined; + + hkdfExpand(client_app_secret, "tls13 key", 32, &client_key); + hkdfExpand(client_app_secret, "tls13 iv", 12, &client_iv); + hkdfExpand(server_app_secret, "tls13 key", 32, &server_key); + hkdfExpand(server_app_secret, "tls13 iv", 12, &server_iv); + + // Reset sequence numbers for application data + self.client_seq = 0; + self.server_seq = 0; + + // Update keys + self.client_write_key = client_key; + self.client_write_iv = client_iv; + self.server_write_key = server_key; + self.server_write_iv = server_iv; + } + + /// Procesa un registro TLS completo (dispatch por tipo) + pub fn processRecord(self: *TlsConnection, record: TlsRecord) !void { + switch (record.content_type) { + .handshake => { + if (record.fragment.len < 1) return error.InvalidMessage; + const msg_type: HandshakeType = @enumFromInt(record.fragment[0]); + + switch (msg_type) { + .server_hello => try self.processServerHello(record.fragment), + .encrypted_extensions => try self.processEncryptedExtensions(record.fragment), + .certificate => try self.processCertificate(record.fragment), + .certificate_verify => try self.processCertificateVerify(record.fragment), + .finished => try self.processServerFinished(record.fragment), + else => {}, + } + }, + .application_data => { + // Datos de aplicación cifrados - descifrar y procesar handshake interno + if (self.server_write_key != null) { + const decrypted = try self.decrypt(record.fragment); + defer self.allocator.free(decrypted); + + if (decrypted.len > 0) { + const inner_type: HandshakeType = @enumFromInt(decrypted[0]); + switch (inner_type) { + .encrypted_extensions => try self.processEncryptedExtensions(decrypted), + .certificate => try self.processCertificate(decrypted), + .certificate_verify => try self.processCertificateVerify(decrypted), + .finished => try self.processServerFinished(decrypted), + else => {}, + } + } + } + }, + else => {}, + } + } + /// Descifra datos de aplicación pub fn decrypt(self: *TlsConnection, record: []const u8) ![]u8 { const key = self.server_write_key orelse return error.NotReady;