zcatp2p/src/crypto.zig
reugenio 414de51120 feat: integración completa de red P2P
- connection.zig: Reescrito con integración completa
  - NAT traversal (STUN + NAT-PMP/UPnP)
  - Discovery (local UDP + global HTTPS)
  - TCP listener para conexiones entrantes
  - Flujo completo de conexión: resolving -> connecting -> handshaking -> connected
  - Envío/recepción de mensajes con protocol framing
  - Gestión de direcciones (local, pública, relay)
  - Port mapping automático

- discovery.zig: Añadidos métodos de DiscoveryManager
  - startLocalDiscovery, stopLocalDiscovery
  - addGlobalServer, announceGlobal
  - addKnownPeer

- crypto.zig: Fix u128 cast para shifts > 64 bits
- http.zig: Fix ArrayListUnmanaged API para Zig 0.15.2

Tests: 44 (todos pasando)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 11:04:11 +01:00

645 lines
20 KiB
Zig

//! 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: u128 = @as(u128, h0) | (@as(u128, h1) << 44) | (@as(u128, 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));
}