fix(crypto): add AAD to AEAD encryption binding ciphertext to context
Previously encryptPayload() used empty AAD, allowing ciphertext to be replayed across different contexts. Now includes header fields as AAD: - ephemeral_pubkey: Binds to sender identity - timestamp: Replay protection (5 min window) - service_type: Context binding (WORLD/FEED/MESSAGE/DIRECT) API changes: - encryptPayload() now requires service_type parameter - decryptPayload() now requires expected_service_type parameter - EncryptedPayload extended with timestamp and service_type fields - New error types: ServiceTypeMismatch, TimestampTooOld, TimestampInFuture Security: Ciphertext is now cryptographically bound to sender, timestamp, and service context. Replay and context confusion attacks are prevented via AAD verification during decryption. Fixes P0 security audit issue: Missing AAD in AEAD Encryption
This commit is contained in:
parent
ac47f8ddf4
commit
bdfb0b2775
|
|
@ -30,12 +30,20 @@ pub const WORLD_PUBLIC_KEY: [32]u8 = [_]u8{
|
||||||
0x6e, 0x65, 0x73, 0x69, 0x73, 0x20, 0x4b, 0x65, // "nesis Ke"
|
0x6e, 0x65, 0x73, 0x69, 0x73, 0x20, 0x4b, 0x65, // "nesis Ke"
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Encrypted payload structure
|
/// Encrypted payload structure with AAD (Additional Authenticated Data)
|
||||||
pub const EncryptedPayload = struct {
|
pub const EncryptedPayload = struct {
|
||||||
ephemeral_pubkey: [32]u8, // Sender's ephemeral public key
|
ephemeral_pubkey: [32]u8, // Sender's ephemeral public key
|
||||||
|
timestamp: u64, // Unix timestamp for replay protection
|
||||||
|
service_type: u8, // Service type for context binding
|
||||||
nonce: [24]u8, // XChaCha20 nonce (never reused)
|
nonce: [24]u8, // XChaCha20 nonce (never reused)
|
||||||
ciphertext: []u8, // Encrypted data + 16-byte auth tag
|
ciphertext: []u8, // Encrypted data + 16-byte auth tag
|
||||||
|
|
||||||
|
/// Service type constants
|
||||||
|
pub const SERVICE_WORLD: u8 = 0;
|
||||||
|
pub const SERVICE_FEED: u8 = 1;
|
||||||
|
pub const SERVICE_MESSAGE: u8 = 2;
|
||||||
|
pub const SERVICE_DIRECT: u8 = 3;
|
||||||
|
|
||||||
/// Free ciphertext memory
|
/// Free ciphertext memory
|
||||||
pub fn deinit(self: *EncryptedPayload, allocator: std.mem.Allocator) void {
|
pub fn deinit(self: *EncryptedPayload, allocator: std.mem.Allocator) void {
|
||||||
allocator.free(self.ciphertext);
|
allocator.free(self.ciphertext);
|
||||||
|
|
@ -43,7 +51,7 @@ pub const EncryptedPayload = struct {
|
||||||
|
|
||||||
/// Total size when serialized
|
/// Total size when serialized
|
||||||
pub fn size(self: *const EncryptedPayload) usize {
|
pub fn size(self: *const EncryptedPayload) usize {
|
||||||
return 32 + 24 + self.ciphertext.len;
|
return 32 + 8 + 1 + 24 + self.ciphertext.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize to bytes
|
/// Serialize to bytes
|
||||||
|
|
@ -52,29 +60,44 @@ pub const EncryptedPayload = struct {
|
||||||
var buffer = try allocator.alloc(u8, total_size);
|
var buffer = try allocator.alloc(u8, total_size);
|
||||||
|
|
||||||
@memcpy(buffer[0..32], &self.ephemeral_pubkey);
|
@memcpy(buffer[0..32], &self.ephemeral_pubkey);
|
||||||
@memcpy(buffer[32..56], &self.nonce);
|
std.mem.writeInt(u64, buffer[32..40], self.timestamp, .big);
|
||||||
@memcpy(buffer[56..], self.ciphertext);
|
buffer[40] = self.service_type;
|
||||||
|
@memcpy(buffer[41..65], &self.nonce);
|
||||||
|
@memcpy(buffer[65..], self.ciphertext);
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserialize from bytes
|
/// Deserialize from bytes
|
||||||
pub fn fromBytes(allocator: std.mem.Allocator, data: []const u8) !EncryptedPayload {
|
pub fn fromBytes(allocator: std.mem.Allocator, data: []const u8) !EncryptedPayload {
|
||||||
if (data.len < 56) {
|
if (data.len < 65) {
|
||||||
return error.PayloadTooSmall;
|
return error.PayloadTooSmall;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ephemeral_pubkey = data[0..32].*;
|
const ephemeral_pubkey = data[0..32].*;
|
||||||
const nonce = data[32..56].*;
|
const timestamp = std.mem.readInt(u64, data[32..40], .big);
|
||||||
const ciphertext = try allocator.alloc(u8, data.len - 56);
|
const service_type = data[40];
|
||||||
@memcpy(ciphertext, data[56..]);
|
const nonce = data[41..65].*;
|
||||||
|
const ciphertext = try allocator.alloc(u8, data.len - 65);
|
||||||
|
@memcpy(ciphertext, data[65..]);
|
||||||
|
|
||||||
return EncryptedPayload{
|
return EncryptedPayload{
|
||||||
.ephemeral_pubkey = ephemeral_pubkey,
|
.ephemeral_pubkey = ephemeral_pubkey,
|
||||||
|
.timestamp = timestamp,
|
||||||
|
.service_type = service_type,
|
||||||
.nonce = nonce,
|
.nonce = nonce,
|
||||||
.ciphertext = ciphertext,
|
.ciphertext = ciphertext,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build AAD (Additional Authenticated Data) from header fields
|
||||||
|
/// Binds ciphertext to: sender (ephemeral_pubkey), time (timestamp), context (service_type)
|
||||||
|
pub fn buildAAD(self: *const EncryptedPayload, buffer: *[41]u8) []const u8 {
|
||||||
|
@memcpy(buffer[0..32], &self.ephemeral_pubkey);
|
||||||
|
std.mem.writeInt(u64, buffer[32..40], self.timestamp, .big);
|
||||||
|
buffer[40] = self.service_type;
|
||||||
|
return buffer[0..41];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Generate a random 24-byte nonce for XChaCha20
|
/// Generate a random 24-byte nonce for XChaCha20
|
||||||
|
|
@ -84,7 +107,7 @@ pub fn generateNonce() [24]u8 {
|
||||||
return nonce;
|
return nonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt payload using X25519-XChaCha20-Poly1305
|
/// Encrypt payload using X25519-XChaCha20-Poly1305 with AAD
|
||||||
///
|
///
|
||||||
/// This is the standard encryption for all Libertaria tiers except MESSAGE
|
/// This is the standard encryption for all Libertaria tiers except MESSAGE
|
||||||
/// (MESSAGE uses PQXDH → Double Ratchet via LatticePost).
|
/// (MESSAGE uses PQXDH → Double Ratchet via LatticePost).
|
||||||
|
|
@ -92,12 +115,14 @@ pub fn generateNonce() [24]u8 {
|
||||||
/// Steps:
|
/// Steps:
|
||||||
/// 1. Generate ephemeral keypair for sender
|
/// 1. Generate ephemeral keypair for sender
|
||||||
/// 2. Perform X25519 key agreement with recipient's public key
|
/// 2. Perform X25519 key agreement with recipient's public key
|
||||||
/// 3. Encrypt plaintext with XChaCha20-Poly1305 using shared secret
|
/// 3. Build AAD from header (ephemeral_pubkey, timestamp, service_type)
|
||||||
/// 4. Return ephemeral pubkey + nonce + ciphertext
|
/// 4. Encrypt plaintext with XChaCha20-Poly1305 using shared secret and AAD
|
||||||
|
/// 5. Return ephemeral pubkey + timestamp + service_type + nonce + ciphertext
|
||||||
pub fn encryptPayload(
|
pub fn encryptPayload(
|
||||||
plaintext: []const u8,
|
plaintext: []const u8,
|
||||||
recipient_pubkey: [32]u8,
|
recipient_pubkey: [32]u8,
|
||||||
sender_private: [32]u8,
|
sender_private: [32]u8,
|
||||||
|
service_type: u8,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
) !EncryptedPayload {
|
) !EncryptedPayload {
|
||||||
// X25519 key agreement
|
// X25519 key agreement
|
||||||
|
|
@ -106,57 +131,101 @@ pub fn encryptPayload(
|
||||||
// Derive ephemeral public key from sender's private key
|
// Derive ephemeral public key from sender's private key
|
||||||
const ephemeral_pubkey = try crypto.dh.X25519.recoverPublicKey(sender_private);
|
const ephemeral_pubkey = try crypto.dh.X25519.recoverPublicKey(sender_private);
|
||||||
|
|
||||||
|
// Get current timestamp for replay protection
|
||||||
|
const timestamp = @as(u64, @intCast(std.time.timestamp()));
|
||||||
|
|
||||||
// Generate random nonce
|
// Generate random nonce
|
||||||
const nonce = generateNonce();
|
const nonce = generateNonce();
|
||||||
|
|
||||||
// Allocate ciphertext buffer (plaintext + 16-byte auth tag)
|
// Allocate ciphertext buffer (plaintext + 16-byte auth tag)
|
||||||
const ciphertext = try allocator.alloc(u8, plaintext.len + 16);
|
const ciphertext = try allocator.alloc(u8, plaintext.len + 16);
|
||||||
|
|
||||||
// XChaCha20-Poly1305 AEAD encryption
|
// Build AAD to bind ciphertext to context
|
||||||
|
var aad_buffer: [41]u8 = undefined;
|
||||||
|
var payload_for_aad = EncryptedPayload{
|
||||||
|
.ephemeral_pubkey = ephemeral_pubkey,
|
||||||
|
.timestamp = timestamp,
|
||||||
|
.service_type = service_type,
|
||||||
|
.nonce = nonce,
|
||||||
|
.ciphertext = &[_]u8{}, // Empty for AAD calculation
|
||||||
|
};
|
||||||
|
const aad = payload_for_aad.buildAAD(&aad_buffer);
|
||||||
|
|
||||||
|
// XChaCha20-Poly1305 AEAD encryption with AAD
|
||||||
crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(
|
crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(
|
||||||
ciphertext[0..plaintext.len],
|
ciphertext[0..plaintext.len],
|
||||||
ciphertext[plaintext.len..][0..16],
|
ciphertext[plaintext.len..][0..16],
|
||||||
plaintext,
|
plaintext,
|
||||||
&[_]u8{}, // No additional authenticated data
|
aad, // AAD binds ciphertext to sender, timestamp, and service type
|
||||||
nonce,
|
nonce,
|
||||||
shared_secret,
|
shared_secret,
|
||||||
);
|
);
|
||||||
|
|
||||||
return EncryptedPayload{
|
return EncryptedPayload{
|
||||||
.ephemeral_pubkey = ephemeral_pubkey,
|
.ephemeral_pubkey = ephemeral_pubkey,
|
||||||
|
.timestamp = timestamp,
|
||||||
|
.service_type = service_type,
|
||||||
.nonce = nonce,
|
.nonce = nonce,
|
||||||
.ciphertext = ciphertext,
|
.ciphertext = ciphertext,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt payload using X25519-XChaCha20-Poly1305
|
/// Decrypt payload using X25519-XChaCha20-Poly1305 with AAD verification
|
||||||
///
|
///
|
||||||
/// Steps:
|
/// Steps:
|
||||||
/// 1. Perform X25519 key agreement using recipient's private key and sender's ephemeral pubkey
|
/// 1. Perform X25519 key agreement using recipient's private key and sender's ephemeral pubkey
|
||||||
/// 2. Decrypt ciphertext with XChaCha20-Poly1305 using shared secret
|
/// 2. Rebuild AAD from header fields
|
||||||
/// 3. Verify authentication tag
|
/// 3. Decrypt ciphertext with XChaCha20-Poly1305 using shared secret and AAD
|
||||||
/// 4. Return plaintext
|
/// 4. Verify authentication tag (fails if AAD doesn't match)
|
||||||
|
/// 5. Return plaintext
|
||||||
pub fn decryptPayload(
|
pub fn decryptPayload(
|
||||||
encrypted: *const EncryptedPayload,
|
encrypted: *const EncryptedPayload,
|
||||||
recipient_private: [32]u8,
|
recipient_private: [32]u8,
|
||||||
|
expected_service_type: u8,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
) ![]u8 {
|
) ![]u8 {
|
||||||
// X25519 key agreement
|
// X25519 key agreement
|
||||||
const shared_secret = try crypto.dh.X25519.scalarmult(recipient_private, encrypted.ephemeral_pubkey);
|
const shared_secret = try crypto.dh.X25519.scalarmult(recipient_private, encrypted.ephemeral_pubkey);
|
||||||
|
|
||||||
|
// Verify service type matches (context binding)
|
||||||
|
if (encrypted.service_type != expected_service_type) {
|
||||||
|
return error.ServiceTypeMismatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for replay attacks (timestamp should be within reasonable window)
|
||||||
|
const current_time = @as(u64, @intCast(std.time.timestamp()));
|
||||||
|
const timestamp = encrypted.timestamp;
|
||||||
|
// Allow 5 minutes of clock skew
|
||||||
|
const max_age = 5 * 60;
|
||||||
|
if (current_time > timestamp + max_age) {
|
||||||
|
return error.TimestampTooOld;
|
||||||
|
}
|
||||||
|
if (timestamp > current_time + 60) { // 1 minute future tolerance
|
||||||
|
return error.TimestampInFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild AAD from header fields
|
||||||
|
var aad_buffer: [41]u8 = undefined;
|
||||||
|
const aad = encrypted.buildAAD(&aad_buffer);
|
||||||
|
|
||||||
// Calculate plaintext length (ciphertext - 16-byte auth tag)
|
// Calculate plaintext length (ciphertext - 16-byte auth tag)
|
||||||
const plaintext_len = encrypted.ciphertext.len - 16;
|
const plaintext_len = encrypted.ciphertext.len - 16;
|
||||||
const plaintext = try allocator.alloc(u8, plaintext_len);
|
const plaintext = try allocator.alloc(u8, plaintext_len);
|
||||||
|
|
||||||
// XChaCha20-Poly1305 AEAD decryption
|
// XChaCha20-Poly1305 AEAD decryption with AAD verification
|
||||||
try crypto.aead.chacha_poly.XChaCha20Poly1305.decrypt(
|
crypto.aead.chacha_poly.XChaCha20Poly1305.decrypt(
|
||||||
plaintext,
|
plaintext,
|
||||||
encrypted.ciphertext[0..plaintext_len],
|
encrypted.ciphertext[0..plaintext_len],
|
||||||
encrypted.ciphertext[plaintext_len..][0..16].*, // Auth tag
|
encrypted.ciphertext[plaintext_len..][0..16].*, // Auth tag
|
||||||
&[_]u8{}, // No additional authenticated data
|
aad, // AAD must match what was used during encryption
|
||||||
encrypted.nonce,
|
encrypted.nonce,
|
||||||
shared_secret,
|
shared_secret,
|
||||||
);
|
) catch |err| {
|
||||||
|
// Clear plaintext buffer on failure to avoid partial data exposure
|
||||||
|
@memset(plaintext, 0);
|
||||||
|
allocator.free(plaintext);
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
return plaintext;
|
return plaintext;
|
||||||
}
|
}
|
||||||
|
|
@ -174,18 +243,32 @@ pub fn encryptWorld(
|
||||||
// Use WORLD_PUBLIC_KEY directly as shared secret (symmetric-like encryption)
|
// Use WORLD_PUBLIC_KEY directly as shared secret (symmetric-like encryption)
|
||||||
const shared_secret = WORLD_PUBLIC_KEY;
|
const shared_secret = WORLD_PUBLIC_KEY;
|
||||||
|
|
||||||
|
// Get current timestamp
|
||||||
|
const timestamp = @as(u64, @intCast(std.time.timestamp()));
|
||||||
|
|
||||||
// Generate random nonce
|
// Generate random nonce
|
||||||
const nonce = generateNonce();
|
const nonce = generateNonce();
|
||||||
|
|
||||||
// Allocate ciphertext buffer (plaintext + 16-byte auth tag)
|
// Allocate ciphertext buffer (plaintext + 16-byte auth tag)
|
||||||
const ciphertext = try allocator.alloc(u8, plaintext.len + 16);
|
const ciphertext = try allocator.alloc(u8, plaintext.len + 16);
|
||||||
|
|
||||||
// XChaCha20-Poly1305 AEAD encryption
|
// Build AAD for World tier
|
||||||
|
var aad_buffer: [41]u8 = undefined;
|
||||||
|
var payload_for_aad = EncryptedPayload{
|
||||||
|
.ephemeral_pubkey = WORLD_PUBLIC_KEY,
|
||||||
|
.timestamp = timestamp,
|
||||||
|
.service_type = EncryptedPayload.SERVICE_WORLD,
|
||||||
|
.nonce = nonce,
|
||||||
|
.ciphertext = &[_]u8{},
|
||||||
|
};
|
||||||
|
const aad = payload_for_aad.buildAAD(&aad_buffer);
|
||||||
|
|
||||||
|
// XChaCha20-Poly1305 AEAD encryption with AAD
|
||||||
crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(
|
crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(
|
||||||
ciphertext[0..plaintext.len],
|
ciphertext[0..plaintext.len],
|
||||||
ciphertext[plaintext.len..][0..16],
|
ciphertext[plaintext.len..][0..16],
|
||||||
plaintext,
|
plaintext,
|
||||||
&[_]u8{}, // No additional authenticated data
|
aad,
|
||||||
nonce,
|
nonce,
|
||||||
shared_secret,
|
shared_secret,
|
||||||
);
|
);
|
||||||
|
|
@ -194,6 +277,8 @@ pub fn encryptWorld(
|
||||||
// This signals that it's world-readable (no ECDH needed)
|
// This signals that it's world-readable (no ECDH needed)
|
||||||
return EncryptedPayload{
|
return EncryptedPayload{
|
||||||
.ephemeral_pubkey = WORLD_PUBLIC_KEY,
|
.ephemeral_pubkey = WORLD_PUBLIC_KEY,
|
||||||
|
.timestamp = timestamp,
|
||||||
|
.service_type = EncryptedPayload.SERVICE_WORLD,
|
||||||
.nonce = nonce,
|
.nonce = nonce,
|
||||||
.ciphertext = ciphertext,
|
.ciphertext = ciphertext,
|
||||||
};
|
};
|
||||||
|
|
@ -211,19 +296,39 @@ pub fn decryptWorld(
|
||||||
// Use WORLD_PUBLIC_KEY directly as shared secret
|
// Use WORLD_PUBLIC_KEY directly as shared secret
|
||||||
const shared_secret = WORLD_PUBLIC_KEY;
|
const shared_secret = WORLD_PUBLIC_KEY;
|
||||||
|
|
||||||
|
// Verify this is actually a WORLD tier payload
|
||||||
|
if (encrypted.service_type != EncryptedPayload.SERVICE_WORLD) {
|
||||||
|
return error.ServiceTypeMismatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check timestamp for replay protection
|
||||||
|
const current_time = @as(u64, @intCast(std.time.timestamp()));
|
||||||
|
const max_age = 5 * 60; // 5 minutes
|
||||||
|
if (current_time > encrypted.timestamp + max_age) {
|
||||||
|
return error.TimestampTooOld;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild AAD
|
||||||
|
var aad_buffer: [41]u8 = undefined;
|
||||||
|
const aad = encrypted.buildAAD(&aad_buffer);
|
||||||
|
|
||||||
// Calculate plaintext length (ciphertext - 16-byte auth tag)
|
// Calculate plaintext length (ciphertext - 16-byte auth tag)
|
||||||
const plaintext_len = encrypted.ciphertext.len - 16;
|
const plaintext_len = encrypted.ciphertext.len - 16;
|
||||||
const plaintext = try allocator.alloc(u8, plaintext_len);
|
const plaintext = try allocator.alloc(u8, plaintext_len);
|
||||||
|
|
||||||
// XChaCha20-Poly1305 AEAD decryption
|
// XChaCha20-Poly1305 AEAD decryption
|
||||||
try crypto.aead.chacha_poly.XChaCha20Poly1305.decrypt(
|
crypto.aead.chacha_poly.XChaCha20Poly1305.decrypt(
|
||||||
plaintext,
|
plaintext,
|
||||||
encrypted.ciphertext[0..plaintext_len],
|
encrypted.ciphertext[0..plaintext_len],
|
||||||
encrypted.ciphertext[plaintext_len..][0..16].*, // Auth tag
|
encrypted.ciphertext[plaintext_len..][0..16].*, // Auth tag
|
||||||
&[_]u8{}, // No additional authenticated data
|
aad,
|
||||||
encrypted.nonce,
|
encrypted.nonce,
|
||||||
shared_secret,
|
shared_secret,
|
||||||
);
|
) catch |err| {
|
||||||
|
@memset(plaintext, 0);
|
||||||
|
allocator.free(plaintext);
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
return plaintext;
|
return plaintext;
|
||||||
}
|
}
|
||||||
|
|
@ -232,7 +337,7 @@ pub fn decryptWorld(
|
||||||
// Tests
|
// Tests
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
test "encryptPayload/decryptPayload roundtrip" {
|
test "encryptPayload/decryptPayload roundtrip with AAD" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
// Generate keypairs
|
// Generate keypairs
|
||||||
|
|
@ -245,19 +350,49 @@ test "encryptPayload/decryptPayload roundtrip" {
|
||||||
|
|
||||||
// Encrypt
|
// Encrypt
|
||||||
const plaintext = "Hello, Libertaria!";
|
const plaintext = "Hello, Libertaria!";
|
||||||
var encrypted = try encryptPayload(plaintext, recipient_public, sender_private, allocator);
|
var encrypted = try encryptPayload(plaintext, recipient_public, sender_private, EncryptedPayload.SERVICE_FEED, allocator);
|
||||||
defer encrypted.deinit(allocator);
|
defer encrypted.deinit(allocator);
|
||||||
|
|
||||||
try std.testing.expect(encrypted.ciphertext.len > plaintext.len); // Has auth tag
|
try std.testing.expect(encrypted.ciphertext.len > plaintext.len); // Has auth tag
|
||||||
|
try std.testing.expectEqual(@as(u8, EncryptedPayload.SERVICE_FEED), encrypted.service_type);
|
||||||
|
|
||||||
// Decrypt
|
// Decrypt
|
||||||
const decrypted = try decryptPayload(&encrypted, recipient_private, allocator);
|
const decrypted = try decryptPayload(&encrypted,
|
||||||
|
recipient_private,
|
||||||
|
EncryptedPayload.SERVICE_FEED, // Correct service type
|
||||||
|
allocator,
|
||||||
|
);
|
||||||
defer allocator.free(decrypted);
|
defer allocator.free(decrypted);
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
try std.testing.expectEqualStrings(plaintext, decrypted);
|
try std.testing.expectEqualStrings(plaintext, decrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "decryptPayload fails with wrong service type" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
// Generate keypairs
|
||||||
|
var sender_private: [32]u8 = undefined;
|
||||||
|
var recipient_private: [32]u8 = undefined;
|
||||||
|
crypto.random.bytes(&sender_private);
|
||||||
|
crypto.random.bytes(&recipient_private);
|
||||||
|
|
||||||
|
const recipient_public = try crypto.dh.X25519.recoverPublicKey(recipient_private);
|
||||||
|
|
||||||
|
// Encrypt for FEED service
|
||||||
|
const plaintext = "Hello, Libertaria!";
|
||||||
|
var encrypted = try encryptPayload(plaintext, recipient_public, sender_private, EncryptedPayload.SERVICE_FEED, allocator);
|
||||||
|
defer encrypted.deinit(allocator);
|
||||||
|
|
||||||
|
// Decrypt with wrong service type should fail
|
||||||
|
const result = decryptPayload(&encrypted,
|
||||||
|
recipient_private,
|
||||||
|
EncryptedPayload.SERVICE_MESSAGE, // Wrong service type
|
||||||
|
allocator,
|
||||||
|
);
|
||||||
|
try std.testing.expectError(error.ServiceTypeMismatch, result);
|
||||||
|
}
|
||||||
|
|
||||||
test "encryptWorld/decryptWorld roundtrip" {
|
test "encryptWorld/decryptWorld roundtrip" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
|
@ -270,6 +405,9 @@ test "encryptWorld/decryptWorld roundtrip" {
|
||||||
var encrypted = try encryptWorld(plaintext, private_key, allocator);
|
var encrypted = try encryptWorld(plaintext, private_key, allocator);
|
||||||
defer encrypted.deinit(allocator);
|
defer encrypted.deinit(allocator);
|
||||||
|
|
||||||
|
// Verify service type
|
||||||
|
try std.testing.expectEqual(@as(u8, EncryptedPayload.SERVICE_WORLD), encrypted.service_type);
|
||||||
|
|
||||||
// Decrypt from World
|
// Decrypt from World
|
||||||
const decrypted = try decryptWorld(&encrypted, private_key, allocator);
|
const decrypted = try decryptWorld(&encrypted, private_key, allocator);
|
||||||
defer allocator.free(decrypted);
|
defer allocator.free(decrypted);
|
||||||
|
|
@ -278,12 +416,14 @@ test "encryptWorld/decryptWorld roundtrip" {
|
||||||
try std.testing.expectEqualStrings(plaintext, decrypted);
|
try std.testing.expectEqualStrings(plaintext, decrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "EncryptedPayload serialization" {
|
test "EncryptedPayload serialization with AAD fields" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
// Create encrypted payload
|
// Create encrypted payload
|
||||||
var encrypted = EncryptedPayload{
|
var encrypted = EncryptedPayload{
|
||||||
.ephemeral_pubkey = [_]u8{0xAA} ** 32,
|
.ephemeral_pubkey = [_]u8{0xAA} ** 32,
|
||||||
|
.timestamp = 1234567890,
|
||||||
|
.service_type = EncryptedPayload.SERVICE_MESSAGE,
|
||||||
.nonce = [_]u8{0xBB} ** 24,
|
.nonce = [_]u8{0xBB} ** 24,
|
||||||
.ciphertext = try allocator.alloc(u8, 48), // 32 bytes + 16 auth tag
|
.ciphertext = try allocator.alloc(u8, 48), // 32 bytes + 16 auth tag
|
||||||
};
|
};
|
||||||
|
|
@ -294,17 +434,49 @@ test "EncryptedPayload serialization" {
|
||||||
const bytes = try encrypted.toBytes(allocator);
|
const bytes = try encrypted.toBytes(allocator);
|
||||||
defer allocator.free(bytes);
|
defer allocator.free(bytes);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(usize, 32 + 24 + 48), bytes.len);
|
try std.testing.expectEqual(@as(usize, 32 + 8 + 1 + 24 + 48), bytes.len);
|
||||||
|
|
||||||
// Deserialize
|
// Deserialize
|
||||||
var deserialized = try EncryptedPayload.fromBytes(allocator, bytes);
|
var deserialized = try EncryptedPayload.fromBytes(allocator, bytes);
|
||||||
defer deserialized.deinit(allocator);
|
defer deserialized.deinit(allocator);
|
||||||
|
|
||||||
try std.testing.expectEqualSlices(u8, &encrypted.ephemeral_pubkey, &deserialized.ephemeral_pubkey);
|
try std.testing.expectEqualSlices(u8, &encrypted.ephemeral_pubkey, &deserialized.ephemeral_pubkey);
|
||||||
|
try std.testing.expectEqual(encrypted.timestamp, deserialized.timestamp);
|
||||||
|
try std.testing.expectEqual(encrypted.service_type, deserialized.service_type);
|
||||||
try std.testing.expectEqualSlices(u8, &encrypted.nonce, &deserialized.nonce);
|
try std.testing.expectEqualSlices(u8, &encrypted.nonce, &deserialized.nonce);
|
||||||
try std.testing.expectEqualSlices(u8, encrypted.ciphertext, deserialized.ciphertext);
|
try std.testing.expectEqualSlices(u8, encrypted.ciphertext, deserialized.ciphertext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "AAD binds to correct context" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
// Generate keypairs
|
||||||
|
var sender_private: [32]u8 = undefined;
|
||||||
|
var recipient_private: [32]u8 = undefined;
|
||||||
|
crypto.random.bytes(&sender_private);
|
||||||
|
crypto.random.bytes(&recipient_private);
|
||||||
|
|
||||||
|
const recipient_public = try crypto.dh.X25519.recoverPublicKey(recipient_private);
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
const plaintext = "Secret message";
|
||||||
|
var encrypted = try encryptPayload(plaintext, recipient_public, sender_private, EncryptedPayload.SERVICE_DIRECT, allocator);
|
||||||
|
defer encrypted.deinit(allocator);
|
||||||
|
|
||||||
|
// Build AAD and verify it contains expected data
|
||||||
|
var aad_buffer: [41]u8 = undefined;
|
||||||
|
const aad = encrypted.buildAAD(&aad_buffer);
|
||||||
|
|
||||||
|
// AAD should be 41 bytes: 32 (pubkey) + 8 (timestamp) + 1 (service_type)
|
||||||
|
try std.testing.expectEqual(@as(usize, 41), aad.len);
|
||||||
|
|
||||||
|
// First 32 bytes should be ephemeral pubkey
|
||||||
|
try std.testing.expectEqualSlices(u8, &encrypted.ephemeral_pubkey, aad[0..32]);
|
||||||
|
|
||||||
|
// Byte 40 should be service type
|
||||||
|
try std.testing.expectEqual(encrypted.service_type, aad[40]);
|
||||||
|
}
|
||||||
|
|
||||||
test "nonce generation is random" {
|
test "nonce generation is random" {
|
||||||
const nonce1 = generateNonce();
|
const nonce1 = generateNonce();
|
||||||
const nonce2 = generateNonce();
|
const nonce2 = generateNonce();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue