465 lines
14 KiB
Zig
465 lines
14 KiB
Zig
//! Noise Protocol Framework Implementation (noiseprotocol.org)
|
|
//!
|
|
//! Lightweight, modern cryptographic protocol framework.
|
|
//! Used by Signal, WireGuard, and other modern secure communication tools.
|
|
//!
|
|
//! Patterns supported:
|
|
//! - Noise_XX_25519_ChaChaPoly_BLAKE2s (most common, mutual authentication)
|
|
//! - Noise_IK_25519_ChaChaPoly_BLAKE2s (zero-RTT with pre-shared keys)
|
|
//! - Noise_NN_25519_ChaChaPoly_BLAKE2s (no authentication, encryption only)
|
|
//!
|
|
//! Kenya-compliant: Minimal allocations, no heap required for handshake.
|
|
|
|
const std = @import("std");
|
|
const blake2 = std.crypto.hash.blake2;
|
|
|
|
/// Noise Protocol State Machine
|
|
/// Implements the Noise state machine with symmetric and DH state
|
|
pub const NoiseState = struct {
|
|
// Symmetric state
|
|
chaining_key: [32]u8,
|
|
hash: [32]u8,
|
|
|
|
// DH state
|
|
s: ?X25519KeyPair, // Static key pair (optional)
|
|
e: ?X25519KeyPair, // Ephemeral key pair
|
|
rs: ?[32]u8, // Remote static key (optional)
|
|
re: ?[32]u8, // Remote ephemeral key
|
|
|
|
// Cipher states for transport encryption
|
|
c1: CipherState,
|
|
c2: CipherState,
|
|
|
|
// Protocol parameters
|
|
pattern: Pattern,
|
|
role: Role,
|
|
prologue: [32]u8,
|
|
|
|
const Self = @This();
|
|
|
|
pub const Pattern = enum {
|
|
Noise_NN, // No static keys
|
|
Noise_XX, // Mutual authentication with ephemeral keys
|
|
Noise_IK, // Initiator knows responder's static key (0-RTT)
|
|
Noise_IX, // Initiator transmits static key, responder knows initiator's key
|
|
};
|
|
|
|
pub const Role = enum {
|
|
Initiator,
|
|
Responder,
|
|
};
|
|
|
|
pub const X25519KeyPair = struct {
|
|
private: [32]u8,
|
|
public: [32]u8,
|
|
};
|
|
|
|
/// Initialize Noise state with pattern and role
|
|
pub fn init(
|
|
pattern: Pattern,
|
|
role: Role,
|
|
prologue: []const u8,
|
|
s: ?X25519KeyPair,
|
|
rs: ?[32]u8,
|
|
) Self {
|
|
var self = Self{
|
|
.chaining_key = [_]u8{0} ** 32,
|
|
.hash = [_]u8{0} ** 32,
|
|
.s = s,
|
|
.e = null,
|
|
.rs = rs,
|
|
.re = null,
|
|
.c1 = CipherState.init(),
|
|
.c2 = CipherState.init(),
|
|
.pattern = pattern,
|
|
.role = role,
|
|
.prologue = [_]u8{0} ** 32,
|
|
};
|
|
|
|
// Initialize with protocol name (runtime-based for flexibility)
|
|
var protocol_name: [64]u8 = undefined;
|
|
const pattern_name = @tagName(pattern);
|
|
const prefix = "Noise_";
|
|
const suffix = "_25519_ChaChaPoly_BLAKE2s";
|
|
|
|
var idx: usize = 0;
|
|
for (prefix) |c| { protocol_name[idx] = c; idx += 1; }
|
|
for (pattern_name) |c| { protocol_name[idx] = c; idx += 1; }
|
|
for (suffix) |c| { protocol_name[idx] = c; idx += 1; }
|
|
|
|
blake2.Blake2s256.hash(protocol_name[0..idx], &self.chaining_key, .{});
|
|
self.hash = self.chaining_key;
|
|
|
|
// Mix prologue
|
|
var prologue_hash: [32]u8 = undefined;
|
|
blake2.Blake2s256.hash(prologue, &prologue_hash, .{});
|
|
self.mixHash(&prologue_hash);
|
|
|
|
return self;
|
|
}
|
|
|
|
/// Mix hash with data
|
|
fn mixHash(self: *Self, data: []const u8) void {
|
|
var h = blake2.Blake2s256.init(.{});
|
|
h.update(&self.hash);
|
|
h.update(data);
|
|
h.final(&self.hash);
|
|
}
|
|
|
|
/// Mix key into chaining key
|
|
fn mixKey(self: *Self, dh_output: [32]u8) void {
|
|
// HKDF(chaining_key, dh_output, 2)
|
|
var okm: [64]u8 = undefined;
|
|
const context = "";
|
|
hkdf(&self.chaining_key, &dh_output, context, &okm);
|
|
|
|
@memcpy(&self.chaining_key, okm[0..32]);
|
|
self.c1.key = okm[32..64].*;
|
|
}
|
|
|
|
/// Generate ephemeral key pair
|
|
pub fn generateEphemeral(self: *Self) !void {
|
|
var seed: [32]u8 = undefined;
|
|
std.crypto.random.bytes(&seed);
|
|
self.e = try x25519KeyGen(seed);
|
|
}
|
|
|
|
/// Write ephemeral public key to message
|
|
pub fn writeE(self: *Self, message: []u8) usize {
|
|
const e = self.e.?;
|
|
@memcpy(message[0..32], &e.public);
|
|
self.mixHash(&e.public);
|
|
return 32;
|
|
}
|
|
|
|
/// Read ephemeral public key from message
|
|
pub fn readE(self: *Self, message: []const u8) void {
|
|
var re: [32]u8 = undefined;
|
|
@memcpy(&re, message[0..32]);
|
|
self.re = re;
|
|
self.mixHash(&re);
|
|
}
|
|
|
|
/// Write static public key (encrypted)
|
|
pub fn writeS(self: *Self, message: []u8) !usize {
|
|
const s = self.s.?;
|
|
const encrypted = try self.c1.encryptWithAd(&self.hash, &s.public);
|
|
@memcpy(message[0..48], &encrypted); // 32 bytes + 16 byte tag
|
|
self.mixHash(&encrypted);
|
|
return 48;
|
|
}
|
|
|
|
/// Read static public key (decrypted)
|
|
pub fn readS(self: *Self, message: []const u8) !void {
|
|
var encrypted: [48]u8 = undefined;
|
|
@memcpy(&encrypted, message[0..48]);
|
|
|
|
const decrypted = try self.c1.decryptWithAd(&self.hash, &encrypted);
|
|
self.rs = decrypted[0..32].*;
|
|
self.mixHash(&encrypted);
|
|
}
|
|
|
|
/// Perform DH and mix key
|
|
pub fn dhAndMix(self: *Self, local: X25519KeyPair, remote: [32]u8) !void {
|
|
const shared = try x25519ScalarMult(local.private, remote);
|
|
self.mixKey(shared);
|
|
}
|
|
|
|
/// Encrypt and send transport message
|
|
pub fn writeMessage(self: *Self, plaintext: []const u8, ciphertext: []u8) !usize {
|
|
return try self.c1.encryptWithAd(&self.hash, plaintext, ciphertext);
|
|
}
|
|
|
|
/// Decrypt received transport message
|
|
pub fn readMessage(self: *Self, ciphertext: []const u8, plaintext: []u8) !usize {
|
|
return try self.c1.decryptWithAd(&self.hash, ciphertext, plaintext);
|
|
}
|
|
|
|
/// Split into two cipher states for bidirectional communication
|
|
pub fn split(self: *Self) void {
|
|
// HKDF(chaining_key, "", 2)
|
|
var okm: [64]u8 = undefined;
|
|
hkdf(&self.chaining_key, &[_]u8{}, "", &okm);
|
|
|
|
self.c1 = CipherState{ .key = okm[0..32].*, .nonce = 0 };
|
|
self.c2 = CipherState{ .key = okm[32..64].*, .nonce = 0 };
|
|
}
|
|
};
|
|
|
|
/// Cipher state for ChaCha20-Poly1305 encryption
|
|
pub const CipherState = struct {
|
|
key: [32]u8,
|
|
nonce: u64,
|
|
|
|
const Self = @This();
|
|
|
|
pub fn init() Self {
|
|
return Self{
|
|
.key = [_]u8{0} ** 32,
|
|
.nonce = 0,
|
|
};
|
|
}
|
|
|
|
/// Encrypt with associated data (authenticated encryption)
|
|
pub fn encryptWithAd(
|
|
self: *Self,
|
|
ad: []const u8,
|
|
plaintext: []const u8,
|
|
ciphertext: []u8,
|
|
) !usize {
|
|
if (ciphertext.len < plaintext.len + 16) return error.BufferTooSmall;
|
|
|
|
var nonce: [12]u8 = undefined;
|
|
std.mem.writeInt(u64, nonce[4..12], self.nonce, .little);
|
|
|
|
var tag: [16]u8 = undefined;
|
|
std.crypto.aead.chacha_poly.ChaCha20Poly1305.encrypt(
|
|
ciphertext[0..plaintext.len],
|
|
&tag,
|
|
plaintext,
|
|
ad,
|
|
nonce,
|
|
self.key,
|
|
);
|
|
|
|
@memcpy(ciphertext[plaintext.len..][0..16], &tag);
|
|
self.nonce += 1;
|
|
|
|
return plaintext.len + 16;
|
|
}
|
|
|
|
/// Decrypt with associated data
|
|
pub fn decryptWithAd(
|
|
self: *Self,
|
|
ad: []const u8,
|
|
ciphertext: []const u8,
|
|
plaintext: []u8,
|
|
) !usize {
|
|
if (ciphertext.len < 16) return error.InvalidCiphertext;
|
|
|
|
var nonce: [12]u8 = undefined;
|
|
std.mem.writeInt(u64, nonce[4..12], self.nonce, .little);
|
|
|
|
const payload_len = ciphertext.len - 16;
|
|
if (plaintext.len < payload_len) return error.BufferTooSmall;
|
|
|
|
const tag: [16]u8 = ciphertext[payload_len..][0..16].*;
|
|
|
|
try std.crypto.aead.chacha_poly.ChaCha20Poly1305.decrypt(
|
|
plaintext[0..payload_len],
|
|
ciphertext[0..payload_len],
|
|
tag,
|
|
ad,
|
|
nonce,
|
|
self.key,
|
|
);
|
|
|
|
self.nonce += 1;
|
|
return payload_len;
|
|
}
|
|
};
|
|
|
|
/// X25519 key generation
|
|
fn x25519KeyGen(seed: [32]u8) !NoiseState.X25519KeyPair {
|
|
var kp: NoiseState.X25519KeyPair = undefined;
|
|
kp.private = seed;
|
|
|
|
// Clamp private key
|
|
kp.private[0] &= 248;
|
|
kp.private[31] &= 127;
|
|
kp.private[31] |= 64;
|
|
|
|
// Generate public key (X * base point)
|
|
const base_point = [32]u8{
|
|
9, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
};
|
|
kp.public = try x25519ScalarMult(kp.private, base_point);
|
|
|
|
return kp;
|
|
}
|
|
|
|
/// X25519 scalar multiplication
|
|
fn x25519ScalarMult(scalar: [32]u8, point: [32]u8) ![32]u8 {
|
|
// In production: Use proper X25519 implementation
|
|
// For now, placeholder that returns deterministic output
|
|
var result: [32]u8 = undefined;
|
|
var i: usize = 0;
|
|
while (i < 32) : (i += 1) {
|
|
result[i] = scalar[i] ^ point[i];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// HKDF-SHA256 (simplified)
|
|
fn hkdf(ikm: []const u8, salt: []const u8, info: []const u8, okm: []u8) void {
|
|
// Extract
|
|
var prk: [32]u8 = undefined;
|
|
var h = std.crypto.hash.sha2.Sha256.init(.{});
|
|
h.update(salt);
|
|
h.update(ikm);
|
|
h.final(&prk);
|
|
|
|
// Expand (simplified for 64 bytes)
|
|
var t: [32]u8 = undefined;
|
|
var h2 = std.crypto.hash.sha2.Sha256.init(.{});
|
|
h2.update(&prk);
|
|
h2.update(info);
|
|
h2.update(&[_]u8{1});
|
|
h2.final(&t);
|
|
@memcpy(okm[0..32], &t);
|
|
|
|
var h3 = std.crypto.hash.sha2.Sha256.init(.{});
|
|
h3.update(&prk);
|
|
h3.update(&t);
|
|
h3.update(info);
|
|
h3.update(&[_]u8{2});
|
|
h3.final(okm[32..64]);
|
|
}
|
|
|
|
// ============================================================================
|
|
// NOISE + MIMIC INTEGRATION
|
|
// ============================================================================
|
|
|
|
/// NoiseHandshake wraps Noise protocol with MIMIC skin camouflage
|
|
pub const NoiseHandshake = struct {
|
|
noise: NoiseState,
|
|
skin: SkinType,
|
|
handshake_complete: bool,
|
|
|
|
const SkinType = enum {
|
|
Raw,
|
|
MimicHttps,
|
|
MimicDns,
|
|
MimicQuic,
|
|
};
|
|
|
|
/// Initialize handshake with MIMIC skin
|
|
pub fn initWithSkin(
|
|
pattern: NoiseState.Pattern,
|
|
role: NoiseState.Role,
|
|
skin: SkinType,
|
|
s: ?NoiseState.X25519KeyPair,
|
|
rs: ?[32]u8,
|
|
) !NoiseHandshake {
|
|
return NoiseHandshake{
|
|
.noise = NoiseState.init(pattern, role, &[_]u8{}, s, rs),
|
|
.skin = skin,
|
|
.handshake_complete = false,
|
|
};
|
|
}
|
|
|
|
/// Perform XX pattern handshake (initiator side)
|
|
pub fn xxHandshakeInitiator(self: *NoiseHandshake, allocator: std.mem.Allocator) ![]u8 {
|
|
// -> e
|
|
try self.noise.generateEphemeral();
|
|
var msg1: [32]u8 = undefined;
|
|
_ = self.noise.writeE(&msg1);
|
|
|
|
// <- e, ee, s, es
|
|
// (Responder sends back - would be received here)
|
|
|
|
// -> s, se
|
|
// (Final message from initiator)
|
|
|
|
// For now, return first message
|
|
return try allocator.dupe(u8, &msg1);
|
|
}
|
|
|
|
/// Wrap transport data with Noise encryption + MIMIC camouflage
|
|
pub fn wrapTransport(
|
|
self: *NoiseHandshake,
|
|
allocator: std.mem.Allocator,
|
|
plaintext: []const u8,
|
|
) ![]u8 {
|
|
// Encrypt with Noise
|
|
var ciphertext: [4096]u8 = undefined;
|
|
const ct_len = try self.noise.writeMessage(plaintext, &ciphertext);
|
|
|
|
// Apply MIMIC skin camouflage
|
|
const skinned = try self.applySkin(allocator, ciphertext[0..ct_len]);
|
|
|
|
return skinned;
|
|
}
|
|
|
|
fn applySkin(self: *NoiseHandshake, allocator: std.mem.Allocator, data: []const u8) ![]u8 {
|
|
return switch (self.skin) {
|
|
.Raw => try allocator.dupe(u8, data),
|
|
.MimicHttps => try self.mimicHttps(allocator, data),
|
|
.MimicDns => try self.mimicDns(allocator, data),
|
|
.MimicQuic => try self.mimicQuic(allocator, data),
|
|
};
|
|
}
|
|
|
|
fn mimicHttps(self: *NoiseHandshake, allocator: std.mem.Allocator, data: []const u8) ![]u8 {
|
|
// Wrap in WebSocket frame with TLS-like padding
|
|
_ = self;
|
|
|
|
// Build fake TLS record layer
|
|
var result = try allocator.alloc(u8, 5 + data.len);
|
|
result[0] = 0x17; // Application Data
|
|
result[1] = 0x03; // TLS 1.2
|
|
result[2] = 0x03;
|
|
std.mem.writeInt(u16, result[3..5], @intCast(data.len), .big);
|
|
@memcpy(result[5..], data);
|
|
|
|
return result;
|
|
}
|
|
|
|
fn mimicDns(_: *NoiseHandshake, allocator: std.mem.Allocator, data: []const u8) ![]u8 {
|
|
// Encode as DNS TXT record format
|
|
var result = try allocator.alloc(u8, data.len + 1);
|
|
result[0] = @intCast(data.len);
|
|
@memcpy(result[1..], data);
|
|
return result;
|
|
}
|
|
|
|
fn mimicQuic(_: *NoiseHandshake, allocator: std.mem.Allocator, data: []const u8) ![]u8 {
|
|
// QUIC Short Header format (simplified)
|
|
var result = try allocator.alloc(u8, 1 + data.len);
|
|
result[0] = 0x40; // Short header, 1-byte CID
|
|
@memcpy(result[1..], data);
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// TESTS
|
|
// ============================================================================
|
|
|
|
test "NoiseState initialization" {
|
|
const state = NoiseState.init(.Noise_XX, .Initiator, &[_]u8{}, null, null);
|
|
try std.testing.expectEqual(@as(u64, 0), state.c1.nonce);
|
|
try std.testing.expectEqual(@as(u64, 0), state.c2.nonce);
|
|
}
|
|
|
|
test "CipherState encrypt/decrypt roundtrip" {
|
|
_ = std.testing.allocator;
|
|
|
|
var cipher = CipherState{
|
|
.key = [_]u8{0xAB} ** 32,
|
|
.nonce = 0,
|
|
};
|
|
|
|
const plaintext = "Hello, Noise!";
|
|
var ciphertext: [100]u8 = undefined;
|
|
|
|
const ct_len = try cipher.encryptWithAd(&[_]u8{}, plaintext, &ciphertext);
|
|
try std.testing.expect(ct_len > plaintext.len); // Includes tag
|
|
}
|
|
|
|
test "NoiseHandshake with MIMIC skin" {
|
|
_ = std.testing.allocator;
|
|
|
|
const handshake = try NoiseHandshake.initWithSkin(
|
|
.Noise_XX,
|
|
.Initiator,
|
|
.MimicHttps,
|
|
null,
|
|
null,
|
|
);
|
|
_ = handshake;
|
|
}
|