Skip to content

Commit 4774c56

Browse files
committed
chore: automated sync of local changes
1 parent cb75a75 commit 4774c56

4 files changed

Lines changed: 338 additions & 0 deletions

File tree

lithoglyph/core-zig/build.zig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ pub fn build(b: *std.Build) void {
5555
}),
5656
});
5757

58+
// Unit tests for crypto
59+
const _crypto_tests = b.addTest(.{
60+
.name = "crypto-tests",
61+
.root_module = b.createModule(.{
62+
.root_source_file = b.path("src/crypto_test.zig"),
63+
.target = target,
64+
.optimize = optimize,
65+
}),
66+
});
67+
5868
const run_blocks_tests = b.addRunArtifact(blocks_tests);
5969

6070
const test_step = b.step("test", "Run unit tests");

lithoglyph/core-zig/src/blocks.zig

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
const std = @import("std");
1010
const builtin = @import("builtin");
11+
const crypto = @import("crypto.zig");
1112

1213
// ============================================================
1314
// Constants (must match Forth specification)
@@ -231,6 +232,28 @@ pub const Block = struct {
231232

232233
return block;
233234
}
235+
236+
/// Encrypt block payload using AES-256-GCM
237+
/// Requires 32-byte encryption key
238+
pub fn encrypt(self: *Block, key: [crypto.AES256_KEY_SIZE]u8) !void {
239+
try crypto.encryptBlockPayload(self, key);
240+
}
241+
242+
/// Decrypt block payload using AES-256-GCM
243+
/// Requires 32-byte encryption key
244+
pub fn decrypt(self: *Block, key: [crypto.AES256_KEY_SIZE]u8) !void {
245+
try crypto.decryptBlockPayload(self, key);
246+
}
247+
248+
/// Check if block payload is encrypted
249+
pub fn isEncrypted(self: *const Block) bool {
250+
return self.header.encrypted;
251+
}
252+
253+
/// Derive encryption key from master key
254+
pub fn deriveKey(master_key: []const u8, block_type: BlockType, block_id: u64) ![crypto.AES256_KEY_SIZE]u8 {
255+
return crypto.deriveBlockKey(master_key, block_type, block_id);
256+
}
234257
};
235258

236259
// ============================================================

lithoglyph/core-zig/src/crypto.zig

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Lithoglyph Cryptography Module
3+
//
4+
// Provides AES-256-GCM encryption for block payloads
5+
// Designed to integrate with Svalinn Vault's existing crypto system
6+
7+
const std = @import("std");
8+
const builtin = @import("builtin");
9+
const blocks = @import("blocks.zig");
10+
11+
// ============================================================
12+
// Constants
13+
// ============================================================
14+
15+
pub const AES256_KEY_SIZE: usize = 32; // 256 bits
16+
pub const AES_GCM_NONCE_SIZE: usize = 12; // 96 bits for GCM
17+
pub const AES_GCM_TAG_SIZE: usize = 16; // 128 bits authentication tag
18+
19+
// ============================================================
20+
// Error Types
21+
// ============================================================
22+
23+
pub const CryptoError = error{
24+
InvalidKeySize,
25+
EncryptionFailed,
26+
DecryptionFailed,
27+
AuthenticationFailed,
28+
BufferTooSmall,
29+
};
30+
31+
// ============================================================
32+
// AES-256-GCM Implementation
33+
// ============================================================
34+
35+
// Encrypt block payload using AES-256-GCM
36+
// Uses block_id as part of nonce for uniqueness
37+
pub fn encryptBlockPayload(
38+
block: *blocks.Block,
39+
key: [AES256_KEY_SIZE]u8,
40+
) !void {
41+
if (block.header.encrypted) {
42+
return; // Already encrypted
43+
}
44+
45+
// Create nonce: block_id (8 bytes) + counter (4 bytes)
46+
var nonce = [AES_GCM_NONCE_SIZE]u8 = undefined;
47+
@memcpy(&nonce[0..8], &block.header.block_id, 8);
48+
@memcpy(&nonce[8..12], &block.header.sequence, 4);
49+
// Last 4 bytes zero for now
50+
51+
// Encrypt payload in-place
52+
const payload_len = block.header.payload_len;
53+
if (payload_len == 0) {
54+
return; // Nothing to encrypt
55+
}
56+
57+
// Make space for authentication tag at end
58+
if (payload_len + AES_GCM_TAG_SIZE > blocks.PAYLOAD_SIZE) {
59+
return CryptoError.BufferTooSmall;
60+
}
61+
62+
// Encrypt using AES-GCM
63+
// Note: In real implementation, we'd use a proper crypto library
64+
// This is a placeholder showing the structure
65+
try aes256GcmEncryptInPlace(
66+
&block.payload[0..payload_len],
67+
&key,
68+
&nonce,
69+
&block.payload[payload_len..payload_len + AES_GCM_TAG_SIZE]
70+
);
71+
72+
// Update header
73+
block.header.payload_len = @intCast(payload_len + AES_GCM_TAG_SIZE);
74+
block.header.encrypted = true;
75+
block.header.flags |= @as(u32, @bitCast(@intFromEnum(blocks.BlockFlags.encrypted)));
76+
77+
// Recalculate checksum of encrypted data
78+
block.header.checksum = blocks.crc32c(&block.payload, block.header.payload_len);
79+
}
80+
81+
// Decrypt block payload using AES-256-GCM
82+
pub fn decryptBlockPayload(
83+
block: *blocks.Block,
84+
key: [AES256_KEY_SIZE]u8,
85+
) !void {
86+
if (!block.header.encrypted) {
87+
return; // Not encrypted
88+
}
89+
90+
const total_len = block.header.payload_len;
91+
if (total_len < AES_GCM_TAG_SIZE) {
92+
return CryptoError.AuthenticationFailed;
93+
}
94+
95+
const payload_len = total_len - AES_GCM_TAG_SIZE;
96+
97+
// Create nonce (same as encryption)
98+
var nonce = [AES_GCM_NONCE_SIZE]u8 = undefined;
99+
@memcpy(&nonce[0..8], &block.header.block_id, 8);
100+
@memcpy(&nonce[8..12], &block.header.sequence, 4);
101+
102+
// Decrypt in-place
103+
try aes256GcmDecryptInPlace(
104+
&block.payload[0..payload_len],
105+
&key,
106+
&nonce,
107+
&block.payload[payload_len..total_len]
108+
);
109+
110+
// Update header
111+
block.header.payload_len = @intCast(payload_len);
112+
block.header.encrypted = false;
113+
block.header.flags &= ~@as(u32, @bitCast(@intFromEnum(blocks.BlockFlags.encrypted)));
114+
115+
// Recalculate checksum of decrypted data
116+
block.header.checksum = blocks.crc32c(&block.payload, block.header.payload_len);
117+
}
118+
119+
// ============================================================
120+
// Journal Encryption
121+
// ============================================================
122+
123+
// Journal entries need special handling because they contain
124+
// both the operation and its inverse
125+
pub fn encryptJournalEntry(
126+
entry_data: []u8,
127+
key: [AES256_KEY_SIZE]u8,
128+
entry_id: u64,
129+
) ![]u8 {
130+
// Similar to block encryption but with different nonce
131+
var nonce = [AES_GCM_NONCE_SIZE]u8 = undefined;
132+
@memcpy(&nonce[0..8], &entry_id, 8);
133+
// Use fixed pattern for journal entries
134+
nonce[8] = 'J';
135+
nonce[9] = 'N';
136+
nonce[10] = 'L';
137+
nonce[11] = 0;
138+
139+
// Allocate buffer for encrypted data + tag
140+
var buffer: [entry_data.len + AES_GCM_TAG_SIZE]u8 = undefined;
141+
@memcpy(&buffer[0..entry_data.len], entry_data);
142+
143+
// Encrypt
144+
try aes256GcmEncryptInPlace(
145+
&buffer[0..entry_data.len],
146+
&key,
147+
&nonce,
148+
&buffer[entry_data.len..entry_data.len + AES_GCM_TAG_SIZE]
149+
);
150+
151+
return &buffer;
152+
}
153+
154+
// ============================================================
155+
// Key Derivation
156+
// ============================================================
157+
158+
// Derive encryption key from master key and block type
159+
pub fn deriveBlockKey(
160+
master_key: []const u8,
161+
block_type: blocks.BlockType,
162+
block_id: u64,
163+
) ![AES256_KEY_SIZE]u8 {
164+
// Use BLAKE3 for key derivation (matches vault's crypto)
165+
// In real implementation, use proper KDF
166+
var key = [AES256_KEY_SIZE]u8 = undefined;
167+
168+
// Simple XOR-based derivation for example
169+
// Real implementation would use HKDF or similar
170+
for (0..AES256_KEY_SIZE) |i| {
171+
if (i < master_key.len) {
172+
key[i] = master_key[i] ^ @as(u8, @truncate(block_type)) ^ @as(u8, @truncate(block_id >> (i % 8)));
173+
} else {
174+
key[i] = @as(u8, @truncate(block_type)) ^ @as(u8, @truncate(block_id >> (i % 8)));
175+
}
176+
}
177+
178+
return key;
179+
}
180+
181+
// ============================================================
182+
// Placeholder Crypto Functions
183+
// (In real implementation, use libsodium or similar)
184+
// ============================================================
185+
186+
fn aes256GcmEncryptInPlace(
187+
data: []u8,
188+
key: anytype,
189+
nonce: anytype,
190+
tag_out: []u8,
191+
) !void {
192+
// This is a placeholder - real implementation would use
193+
// a proper crypto library like libsodium or OpenSSL
194+
195+
// For now, just XOR with key (INSECURE - for structure only!)
196+
var key_bytes = @ptrCast([*]const u8, &key);
197+
for (0..data.len) |i| {
198+
data[i] ^= key_bytes[i % @intCast(key_bytes.len)];
199+
}
200+
201+
// Fill tag with dummy data
202+
for (0..tag_out.len) |i| {
203+
tag_out[i] = @as(u8, @truncate(i));
204+
}
205+
}
206+
207+
fn aes256GcmDecryptInPlace(
208+
data: []u8,
209+
key: anytype,
210+
nonce: anytype,
211+
tag: []const u8,
212+
) !void {
213+
// Verify tag (dummy check)
214+
for (0..tag.len) |i| {
215+
if (tag[i] != @as(u8, @truncate(i))) {
216+
return CryptoError.AuthenticationFailed;
217+
}
218+
}
219+
220+
// Decrypt (same as encrypt for XOR)
221+
var key_bytes = @ptrCast([*]const u8, &key);
222+
for (0..data.len) |i| {
223+
data[i] ^= key_bytes[i % @intCast(key_bytes.len)];
224+
}
225+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Test suite for Lithoglyph cryptography module
3+
4+
const std = @import("std");
5+
const crypto = @import("crypto.zig");
6+
const blocks = @import("blocks.zig");
7+
8+
test "block encryption/decryption roundtrip" {
9+
// Create a test block
10+
var block = blocks.Block.init(blocks.BlockType.document, 12345, 1);
11+
12+
// Set some test data
13+
const test_data = "Hello, Svalinn Vault! This is a secret credential.";
14+
try block.setPayload(test_data);
15+
16+
// Verify not encrypted initially
17+
try std.testing.expect(!block.isEncrypted());
18+
19+
// Create encryption key
20+
const key = [crypto.AES256_KEY_SIZE]u8{
21+
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
22+
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
23+
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
24+
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
25+
};
26+
27+
// Encrypt the block
28+
try block.encrypt(key);
29+
30+
// Verify encrypted
31+
try std.testing.expect(block.isEncrypted());
32+
33+
// Verify payload changed (encrypted)
34+
const encrypted_payload = block.getPayload();
35+
try std.testing.expect(!std.mem.eql(u8, test_data, encrypted_payload));
36+
37+
// Decrypt the block
38+
try block.decrypt(key);
39+
40+
// Verify decrypted
41+
try std.testing.expect(!block.isEncrypted());
42+
43+
// Verify payload restored
44+
const decrypted_payload = block.getPayload();
45+
try std.testing.expect(std.mem.eql(u8, test_data, decrypted_payload));
46+
}
47+
48+
test "key derivation" {
49+
const master_key = "master-secret-key-1234567890";
50+
const key1 = try blocks.Block.deriveKey(master_key, blocks.BlockType.document, 123);
51+
const key2 = try blocks.Block.deriveKey(master_key, blocks.BlockType.document, 123);
52+
53+
// Same parameters should give same key
54+
try std.testing.expect(std.mem.eql(u8, &key1, &key2));
55+
56+
const key3 = try blocks.Block.deriveKey(master_key, blocks.BlockType.document, 456);
57+
58+
// Different block ID should give different key
59+
try std.testing.expect(!std.mem.eql(u8, &key1, &key3));
60+
}
61+
62+
test "double encryption detection" {
63+
var block = blocks.Block.init(blocks.BlockType.document, 999, 1);
64+
const test_data = "test";
65+
try block.setPayload(test_data);
66+
67+
const key = [crypto.AES256_KEY_SIZE]u8{0};
68+
69+
// First encryption
70+
try block.encrypt(key);
71+
try std.testing.expect(block.isEncrypted());
72+
73+
// Second encryption should be no-op
74+
try block.encrypt(key);
75+
try std.testing.expect(block.isEncrypted());
76+
77+
// Should still decrypt correctly
78+
try block.decrypt(key);
79+
try std.testing.expect(std.mem.eql(u8, test_data, block.getPayload()));
80+
}

0 commit comments

Comments
 (0)