Skip to content

Commit ba4995f

Browse files
committed
kdbx decryption test successful
1 parent 0889da9 commit ba4995f

File tree

3 files changed

+554
-0
lines changed

3 files changed

+554
-0
lines changed

kdbx/chacha20.zig

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
const std = @import("std");
2+
const assert = std.debug.assert;
3+
const ChaCha20IETF = std.crypto.stream.chacha.ChaCha20IETF;
4+
const Sha512 = std.crypto.hash.sha2.Sha512;
5+
const Base64Decoder = std.base64.standard.Decoder;
6+
7+
pub const Nonce = "\xe8\x30\x09\x4b\x97\x20\x5d\x2a";
8+
9+
pub const ChaCha20 = struct {
10+
pub const nonce_length = 12;
11+
pub const key_length = 32;
12+
13+
pub fn encrypt(c: []u8, m: []const u8, npub: [nonce_length]u8, k: [key_length]u8, index: u32) void {
14+
assert(c.len == m.len);
15+
assert(m.len <= 64 * (@as(u39, 1 << 32) - 1));
16+
17+
ChaCha20IETF.xor(c[0..m.len], m, index, k, npub);
18+
}
19+
20+
pub fn decrypt(m: []u8, c: []const u8, npub: [nonce_length]u8, k: [key_length]u8, index: u32) void {
21+
assert(c.len == m.len);
22+
23+
ChaCha20IETF.xor(m[0..c.len], c, index, k, npub);
24+
}
25+
};
26+
27+
test "decrypt protected data #1" {
28+
const stream_key = "\x43\x8a\xe9\x40\x78\xc8\xe6\xe5\xac\xdf\xcc\xc8\x9c\xcd\xde\x07\x85\x49\xd3\xe2\x63\x2b\x5f\xcd\x31\x37\x3c\x9b\x73\xd1\x34\xff\xc3\x0c\xc7\x18\x16\x68\x21\xae\x23\x04\xdf\xe1\x47\x67\x85\x30\x23\xb7\xbd\x44\x75\x34\x56\x7f\x4b\xb6\xab\x00\xeb\x42\x6f\x9e";
29+
const password_enc = "\x92\x4d\x6b\xe5";
30+
var password: [4]u8 = .{0} ** 4;
31+
var digest: [64]u8 = undefined;
32+
33+
Sha512.hash(stream_key, &digest, .{});
34+
ChaCha20.decrypt(&password, password_enc, digest[32..44].*, digest[0..32].*, 0);
35+
36+
try std.testing.expectEqualStrings("1234", &password);
37+
}
38+
39+
test "decrypt protected data #2" {
40+
const stream_key = "\x4b\xb4\x0b\xf1\x38\x54\x75\x45\x6f\x89\x99\xbf\x83\xfb\x45\xb7\xf4\xae\xd6\x15\xa3\x79\x85\x9c\x25\x89\xd6\x01\x8f\xdd\x6e\x5c\x80\xad\x19\xe2\xd0\x4e\x05\xcd\xc7\x8e\x83\xaf\xa4\xf5\x5d\x71\xb1\x5b\x63\xe4\xa2\x35\x34\x1c\xdf\x41\x81\x19\x6f\x9c\xe0\xd3";
41+
const test_vector: [3][2][]const u8 = .{
42+
.{ "YeM3ssVT06nJ1g==", "helloworld" },
43+
.{ "m2NgXg==", "1234" },
44+
.{ "q1akNQ==", "9876" },
45+
};
46+
47+
var digest: [64]u8 = undefined;
48+
Sha512.hash(stream_key, &digest, .{});
49+
50+
var buffer: [1024]u8 = undefined;
51+
var i: usize = 0;
52+
53+
for (test_vector) |e| {
54+
const size = try Base64Decoder.calcSizeForSlice(e[0]);
55+
const m = try std.testing.allocator.alloc(u8, size);
56+
defer std.testing.allocator.free(m);
57+
try Base64Decoder.decode(m, e[0]);
58+
59+
@memcpy(buffer[i .. i + size], m);
60+
i += size;
61+
}
62+
63+
const pw = try std.testing.allocator.alloc(u8, i);
64+
defer std.testing.allocator.free(pw);
65+
66+
ChaCha20.decrypt(pw, buffer[0..i], digest[32..44].*, digest[0..32].*, 0);
67+
try std.testing.expectEqualStrings("helloworld12349876", pw);
68+
}
69+
70+
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
71+
72+
pub const ChaCha20Context = struct {
73+
keystream32: [16]u32,
74+
position: usize,
75+
key: [32]u8,
76+
nonce: [12]u8,
77+
counter: u64,
78+
79+
state: [16]u32,
80+
81+
pub fn init(key: [32]u8, nonce: [12]u8, counter: u64) @This() {
82+
var ctx: @This() = undefined;
83+
chacha20_init_block(&ctx, key, nonce);
84+
chacha20_block_set_counter(&ctx, counter);
85+
ctx.counter = counter;
86+
ctx.position = 64;
87+
return ctx;
88+
}
89+
90+
pub fn xor(ctx: *@This(), bytes: []u8) void {
91+
const keystream8 = @as([*c]u8, @ptrCast(ctx.keystream32[0..].ptr));
92+
for (bytes) |*b| {
93+
if (ctx.position >= 64) {
94+
chacha20_block_next(ctx);
95+
ctx.position = 0;
96+
}
97+
b.* ^= keystream8[@as(usize, @intCast(ctx.position))];
98+
ctx.position += 1;
99+
}
100+
}
101+
};
102+
103+
fn rotl32(x: u32, n: u5) u32 {
104+
return (x << n) | (x >> (31 - n + 1)); // the +1 is a workaround to satisfy the compiler (instead of 32)
105+
}
106+
107+
fn pack4(a: []const u8) u32 {
108+
var res: u32 = 0;
109+
res |= @as(u32, @intCast(a[0])) << 0 * 8;
110+
res |= @as(u32, @intCast(a[1])) << 1 * 8;
111+
res |= @as(u32, @intCast(a[2])) << 2 * 8;
112+
res |= @as(u32, @intCast(a[3])) << 3 * 8;
113+
return res;
114+
}
115+
116+
fn unpack4(src: u32, dst: []const u8) void {
117+
dst[0] = @as(u8, @intCast((src >> 0 * 8) & 0xff));
118+
dst[1] = @as(u8, @intCast((src >> 1 * 8) & 0xff));
119+
dst[2] = @as(u8, @intCast((src >> 2 * 8) & 0xff));
120+
dst[3] = @as(u8, @intCast((src >> 3 * 8) & 0xff));
121+
}
122+
123+
fn chacha20_init_block(ctx: *ChaCha20Context, key: [32]u8, nonce: [12]u8) void {
124+
ctx.key = key;
125+
ctx.nonce = nonce;
126+
127+
const magic_constant: []const u8 = "expand 32-byte k";
128+
129+
ctx.state[0] = pack4(magic_constant[0..4]);
130+
ctx.state[1] = pack4(magic_constant[4..8]);
131+
ctx.state[2] = pack4(magic_constant[8..12]);
132+
ctx.state[3] = pack4(magic_constant[12..16]);
133+
ctx.state[4] = pack4(key[0..4]);
134+
ctx.state[5] = pack4(key[4..8]);
135+
ctx.state[6] = pack4(key[8..12]);
136+
ctx.state[7] = pack4(key[12..16]);
137+
ctx.state[8] = pack4(key[16..20]);
138+
ctx.state[9] = pack4(key[20..24]);
139+
ctx.state[10] = pack4(key[24..28]);
140+
ctx.state[11] = pack4(key[28..32]);
141+
// 64 bit counter initialized to zero by default.
142+
ctx.state[12] = 0;
143+
ctx.state[13] = pack4(nonce[0..4]);
144+
ctx.state[14] = pack4(nonce[4..8]);
145+
ctx.state[15] = pack4(nonce[8..12]);
146+
147+
ctx.nonce = nonce;
148+
}
149+
150+
fn chacha20_block_set_counter(ctx: *ChaCha20Context, counter: u64) void {
151+
ctx.state[12] = @as(u32, @intCast(counter));
152+
ctx.state[13] = pack4(ctx.nonce[0..4]) + @as(u32, @intCast(counter >> 32));
153+
}
154+
155+
inline fn quarterround(x: []u32, a: usize, b: usize, c: usize, d: usize) void {
156+
x[a] +%= x[b];
157+
x[d] = rotl32(x[d] ^ x[a], 16);
158+
x[c] +%= x[d];
159+
x[b] = rotl32(x[b] ^ x[c], 12);
160+
x[a] +%= x[b];
161+
x[d] = rotl32(x[d] ^ x[a], 8);
162+
x[c] +%= x[d];
163+
x[b] = rotl32(x[b] ^ x[c], 7);
164+
}
165+
166+
fn chacha20_block_next(ctx: *ChaCha20Context) void {
167+
for (ctx.keystream32[0..], ctx.state[0..]) |*k, s| k.* = s;
168+
169+
for (0..10) |_| {
170+
quarterround(ctx.keystream32[0..], 0, 4, 8, 12);
171+
quarterround(ctx.keystream32[0..], 1, 5, 9, 13);
172+
quarterround(ctx.keystream32[0..], 2, 6, 10, 14);
173+
quarterround(ctx.keystream32[0..], 3, 7, 11, 15);
174+
quarterround(ctx.keystream32[0..], 0, 5, 10, 15);
175+
quarterround(ctx.keystream32[0..], 1, 6, 11, 12);
176+
quarterround(ctx.keystream32[0..], 2, 7, 8, 13);
177+
quarterround(ctx.keystream32[0..], 3, 4, 9, 14);
178+
}
179+
180+
for (ctx.keystream32[0..], ctx.state[0..]) |*k, s| k.* +%= s;
181+
ctx.state[12] +%= 1;
182+
if (0 == ctx.state[12]) {
183+
// wrap around occured, increment higher 32 bits of counter
184+
ctx.state[13] +%= 1;
185+
// Limited to 2^64 blocks of 64 bytes each.
186+
std.debug.assert(0 != ctx.state[13]);
187+
}
188+
}
189+
190+
test "decrypt protected with custom impl" {
191+
const stream_key = "\x4b\xb4\x0b\xf1\x38\x54\x75\x45\x6f\x89\x99\xbf\x83\xfb\x45\xb7\xf4\xae\xd6\x15\xa3\x79\x85\x9c\x25\x89\xd6\x01\x8f\xdd\x6e\x5c\x80\xad\x19\xe2\xd0\x4e\x05\xcd\xc7\x8e\x83\xaf\xa4\xf5\x5d\x71\xb1\x5b\x63\xe4\xa2\x35\x34\x1c\xdf\x41\x81\x19\x6f\x9c\xe0\xd3";
192+
const test_vector: [3][2][]const u8 = .{
193+
.{ "YeM3ssVT06nJ1g==", "helloworld" },
194+
.{ "m2NgXg==", "1234" },
195+
.{ "q1akNQ==", "9876" },
196+
};
197+
198+
var digest: [64]u8 = undefined;
199+
Sha512.hash(stream_key, &digest, .{});
200+
201+
var ctx = ChaCha20Context.init(digest[0..32].*, digest[32..44].*, 0);
202+
203+
for (test_vector) |e| {
204+
const size = try Base64Decoder.calcSizeForSlice(e[0]);
205+
const m = try std.testing.allocator.alloc(u8, size);
206+
defer std.testing.allocator.free(m);
207+
try Base64Decoder.decode(m, e[0]);
208+
209+
ctx.xor(m);
210+
try std.testing.expectEqualStrings(e[1], m);
211+
}
212+
}

0 commit comments

Comments
 (0)