diff options
| author | Alexander Leonhardt <equinox.salexander@gmail.com> | |
|---|---|---|
| 2026-06-27 02:29:17 +0200 | ||
| committer | Alexander Leonhardt <equinox.salexander@gmail.com> | |
| 2026-07-01 19:41:49 +0200 | ||
| commit | f3ab0bf0acbd287b45426bb535acea7633b79b2d (patch) | |
| tree | 00277256e934078516d98b03e4bae7193ae94ce5 /src/root.zig | |
| download | totp-f3ab0bf0acbd287b45426bb535acea7633b79b2d.tar.gz totp-f3ab0bf0acbd287b45426bb535acea7633b79b2d.tar.bz2 totp-f3ab0bf0acbd287b45426bb535acea7633b79b2d.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.zig | 138 |
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..])); +} |