aboutsummaryrefslogtreecommitdiffstats
path: root/src/root.zig
diff options
context:
space:
mode:
authorAlexander Leonhardt <equinox.salexander@gmail.com>
2026-06-27 02:29:17 +0200
committerAlexander Leonhardt <equinox.salexander@gmail.com>
2026-07-01 19:41:49 +0200
commitf3ab0bf0acbd287b45426bb535acea7633b79b2d (patch)
tree00277256e934078516d98b03e4bae7193ae94ce5 /src/root.zig
downloadtotp-master.tar.gz
totp-master.tar.bz2
totp-master.zip
Added implementation of the time-based-one-time-password algorithm and on-device encryptionHEADmaster
Diffstat (limited to 'src/root.zig')
-rw-r--r--src/root.zig138
1 files changed, 138 insertions, 0 deletions
diff --git a/src/root.zig b/src/root.zig
new file mode 100644
index 0000000..e4ff7a8
--- /dev/null
+++ b/src/root.zig
@@ -0,0 +1,138 @@
+const std = @import("std");
+
+// TODO: Must be replaced by each user by his/her own nonce (for example from /dev/urandom).
+pub const nonce = [_]u8{143, 54, 214, 156, 114, 22, 191, 156, 175, 116, 138, 187, 248, 246, 227, 91, 108, 217, 79, 141, 154, 107, 51, 122};
+
+/// input is a zero terminated byte string to be hashed.
+/// output is a buffer of at least 32 bytes
+pub export fn sha256(input: [*c]const u8, output: [*c]u8) void {
+ std.crypto.hash.sha2.Sha256.hash(std.mem.span(input), output[0..32], .{});
+}
+
+
+/// This code never aborts (asserts or throws an exception)
+/// password_hashed is the sha256 hashed password with exactly 32 bytes length
+/// encoded_secret is the base64 encoded encrypted secret for the TOTP
+/// timestamp is the seconds since the unix epoch
+/// length refers to the supposed length of the TOTP key, range 4-10
+/// output must be a buffer of at least 32 bytes, since it is reused throughout this function as buffer
+pub export fn decode_secret_and_generate_2fa(password_hashed: [*c]const u8, encoded_secret: [*c]const u8, timestamp: u64, length: u8, output: [*c]u8) bool {
+ var buffer: [128]u8 = undefined;
+ const result = decode_2fa_key(std.mem.span(encoded_secret),password_hashed[0..32],buffer[0..]);
+ if(result < 0) return false;
+ hotp(buffer[0..@intCast(result)], timestamp/30, length, output[0..32]);
+ return true;
+}
+
+/// This code never aborts (asserts or throws an exception)
+/// encoded_secret is the base64 encoded encrypted secret for TOTP
+/// password is the hashed password of exactly 32 bytes length
+/// output_buffer holds the decrypted TOTP secret
+/// The return value holds the number of bytes written to the output buffer
+pub fn decode_2fa_key(encoded_secret: []const u8, password: []const u8, output_buffer: []u8) i16 {
+ if (password.len != 32) return -1;
+ var buffer: [128]u8 = undefined;
+ const size = std.base64.standard_no_pad.Decoder.calcSizeForSlice(encoded_secret) catch return -2;
+ if (size>buffer.len or size-16 > output_buffer.len) return -5;
+ std.base64.standard_no_pad.Decoder.decode(&buffer, encoded_secret) catch return -3;
+ const decodedb = buffer[0..size];
+
+ std.crypto.aead.chacha_poly.XChaCha20Poly1305.decrypt(output_buffer[0..size-16], decodedb[0..size-16], decodedb[size-16..][0..16].*, "", nonce, password[0..32].*) catch return -4;
+ return @as(i16,@intCast(size))-16;
+}
+
+/// Generates a HOTP key given
+/// key is the plain bytes of the TOTP secret
+/// counter is the counter parameter for HOTP (seconds since epoch/30) for TOTP
+/// length refers to the outputted key length
+/// output_buffer contains the zero terminated code in ASCII format (must have a size of at least 20 bytes)
+pub fn hotp(key: []const u8, counter: u64, length: u8, output_buffer: []u8) void {
+ std.crypto.auth.hmac.HmacSha1.create(output_buffer[0..20], &@as([8]u8,@bitCast(std.mem.nativeToBig(u64,counter))), key);
+ const offset = output_buffer[19] & 0xf;
+ var truncated: [4]u8 = [4]u8{output_buffer[offset],output_buffer[offset+1],output_buffer[offset+2],output_buffer[offset+3]};
+ truncated[0] &= 0x7f;
+
+ var value = std.mem.nativeToBig(u32,@as(u32,@bitCast(truncated))) % (std.math.pow(u32,10,@intCast(length)));
+ for(1..length+1) |i| {
+ const modulo = value % 10;
+ value = value/10;
+ output_buffer[length-i] = @as(u8,@intCast(modulo))+'0';
+ }
+ output_buffer[length] = 0;
+}
+
+
+
+test "Check decode" {
+ var buffer: [128]u8 = undefined;
+ const payload = [_]u8{143, 54, 214, 156, 114, 22};
+ var output_buffer: [128]u8 = undefined;
+ const testpassword = "Test123";
+ var output: [32]u8 = undefined;
+ std.crypto.hash.sha2.Sha256.hash(testpassword, &output, .{});
+ std.crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(output_buffer[0..payload.len], output_buffer[payload.len..][0..16], payload[0..], "", nonce, output);
+ const slice = std.base64.standard_no_pad.Encoder.encode(&buffer, output_buffer[0..payload.len+16]);
+ const dupe = try std.testing.allocator.dupeZ(u8, slice);
+ defer std.testing.allocator.free(dupe);
+
+
+ try std.testing.expect(decode_2fa_key(dupe, output[0..], output_buffer[0..])>0);
+ try std.testing.expectEqualStrings(output_buffer[0..payload.len], &payload);
+}
+
+
+test "Check hotp output" {
+ var output_buffer: [128]u8 = undefined;
+ const hello_world:[:0]const u8 = "Hello, world!";
+ hotp(hello_world, 42, 6, output_buffer[0..]);
+
+ try std.testing.expectEqualStrings(output_buffer[0..6], "439256");
+}
+
+test "Check output size of hotp" {
+ var output_buffer: [128]u8 = undefined;
+ const hello_world:[:0]const u8 = "Hello, world!";
+ for(4..10) |length| {
+ hotp(hello_world, 42, @intCast(length), output_buffer[0..]);
+
+ try std.testing.expectEqual(std.mem.span(@as([*c]u8, @ptrCast(output_buffer[0..]))).len, length);
+ }
+}
+
+test "Check full pipeline" {
+ var buffer: [128]u8 = undefined;
+ const hello_world:[:0]const u8 = "Hello, world!";
+ var output_buffer: [128]u8 = undefined;
+ const testpassword = "Test123";
+ var output: [32]u8 = undefined;
+ std.crypto.hash.sha2.Sha256.hash(testpassword, &output, .{});
+ std.crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(output_buffer[0..hello_world.len], output_buffer[hello_world.len..][0..16], hello_world, "", nonce, output);
+ const slice = std.base64.standard_no_pad.Encoder.encode(&buffer, output_buffer[0..hello_world.len+16]);
+ const dupe = try std.testing.allocator.dupeZ(u8, slice);
+ defer std.testing.allocator.free(dupe);
+
+ try std.testing.expect(decode_secret_and_generate_2fa(output[0..], dupe, 42*30, 6, output_buffer[0..]));
+
+ try std.testing.expectEqualStrings(output_buffer[0..6], "439256");
+
+ try std.testing.expect(decode_secret_and_generate_2fa(output[0..], dupe, 43*30, 6, output_buffer[0..]));
+ try std.testing.expectEqualStrings(output_buffer[0..6], "208805");
+}
+
+
+test "Check fail on wrong password" {
+ var buffer: [128]u8 = undefined;
+ const hello_world:[:0]const u8 = "Hello, world!";
+ var output_buffer: [128]u8 = undefined;
+ const testpassword = "Test123";
+ var output: [32]u8 = undefined;
+ std.crypto.hash.sha2.Sha256.hash(testpassword, &output, .{});
+ std.crypto.aead.chacha_poly.XChaCha20Poly1305.encrypt(output_buffer[0..hello_world.len], output_buffer[hello_world.len..][0..16], hello_world, "", nonce, output);
+ const slice = std.base64.standard_no_pad.Encoder.encode(&buffer, output_buffer[0..hello_world.len+16]);
+ const dupe = try std.testing.allocator.dupeZ(u8, slice);
+ defer std.testing.allocator.free(dupe);
+
+ output[10] = ~output[10];
+
+ try std.testing.expect(!decode_secret_and_generate_2fa(output[0..], dupe, 42*30, 6, output_buffer[0..]));
+}