authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-02-21 00:16:54 -08:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-02-21 00:16:54 -08:00
log11016cbb4dac6264b640b95f86742dba8bb8140d
treec1e6e12da94ef8697328cca5846f948d9d8b0539
parenta9a86ffe8e36aad9dd3e231d514e3b793fe571f6

add generateUrl


5 files changed, 106 insertions(+), 0 deletions(-)

README.md+2
...@@ -11,3 +11,5 @@ RFC 6238: TOTP: Time-Based One-Time Password Algorithm...@@ -11,3 +11,5 @@ RFC 6238: TOTP: Time-Based One-Time Password Algorithm
11https://www.rfc-editor.org/rfc/rfc6238 TOTP: Time-Based One-Time Password Algorithm11https://www.rfc-editor.org/rfc/rfc6238 TOTP: Time-Based One-Time Password Algorithm
1212
13https://www.rfc-editor.org/rfc/rfc4226 HOTP: An HMAC-Based One-Time Password Algorithm13https://www.rfc-editor.org/rfc/rfc4226 HOTP: An HMAC-Based One-Time Password Algorithm
14
15https://github.com/google/google-authenticator/wiki/Key-Uri-Format
licenses.txt+3
...@@ -1,8 +1,11 @@...@@ -1,8 +1,11 @@
1MPL-2.0:1MPL-2.0:
2= https://spdx.org/licenses/MPL-2.02= https://spdx.org/licenses/MPL-2.0
3- This3- This
4- git https://github.com/nektro/zig-unicode-idna
5- git https://github.com/nektro/zig-whatwg-url
46
5MIT:7MIT:
6= https://spdx.org/licenses/MIT8= https://spdx.org/licenses/MIT
7- git https://github.com/nektro/zig-expect9- git https://github.com/nektro/zig-expect
8- git https://github.com/nektro/zig-extras10- git https://github.com/nektro/zig-extras
11- git https://github.com/nektro/zig-unicode-ucd
test.zig+7
...@@ -40,3 +40,10 @@ test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 1234567890)).t...@@ -40,3 +40,10 @@ test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 1234567890)).t
40test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 2000000000)).toEqualString("38618901"); }40test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 2000000000)).toEqualString("38618901"); }
41test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 20000000000)).toEqualString("47863826"); }41test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 20000000000)).toEqualString("47863826"); }
42// zig fmt: on42// zig fmt: on
43
44test {
45 const allocator = std.testing.allocator;
46 const url = try totp.generateUrl(allocator, "ACME Co", "john.doe@email.com", &from_hex("3dc6caa4824a6d288767b2331e20b43166cb85d9"), .SHA1, 6, 30);
47 defer allocator.free(url);
48 try expect(url).toEqualString("otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA1&digits=6&period=30");
49}
totp.zig+92
...@@ -29,3 +29,95 @@ fn powci(x: comptime_int, y: comptime_int) comptime_int {...@@ -29,3 +29,95 @@ fn powci(x: comptime_int, y: comptime_int) comptime_int {
29 if (y == 0) return 1;29 if (y == 0) return 1;
30 return x * powci(x, y - 1);30 return x * powci(x, y - 1);
31}31}
32
33pub const Algorithm = enum {
34 SHA1,
35 SHA256,
36 SHA512,
37
38 pub fn ty(algo: Algorithm) type {
39 return switch (algo) {
40 .SHA1 => std.crypto.hash.Sha1,
41 .SHA256 => std.crypto.hash.sha2.Sha256,
42 .SHA512 => std.crypto.hash.sha2.Sha512,
43 };
44 }
45
46 pub fn digest_length(algo: Algorithm) u8 {
47 return switch (algo) {
48 inline else => |tag| tag.ty().digest_length,
49 };
50 }
51};
52
53pub fn generateUrl(allocator: std.mem.Allocator, issuer: []const u8, account: []const u8, secret_raw: []const u8, algo: Algorithm, digits: u8, period: u8) ![]const u8 {
54 std.debug.assert(issuer.len > 0);
55 std.debug.assert(account.len > 0);
56 std.debug.assert(secret_raw.len == algo.digest_length());
57 std.debug.assert(digits == 6 or digits == 7 or digits == 8);
58 std.debug.assert(period == 15 or period == 30 or period == 60);
59 var list: std.ArrayList(u8) = .init(allocator);
60 errdefer list.deinit();
61 try list.appendSlice("otpauth://");
62 try list.appendSlice("totp/");
63 try url.percentEncodeAL(&list, issuer, url.is_path_percent_char);
64 try list.append(':');
65 try url.percentEncodeAL(&list, account, url.is_path_percent_char);
66 try list.appendSlice("?secret=");
67 try encodeBase32(&list, secret_raw);
68 try list.appendSlice("&algorithm=");
69 try list.appendSlice(@tagName(algo));
70 try list.appendSlice("&digits=");
71 try list.writer().print("{d}", .{digits});
72 try list.appendSlice("&period=");
73 try list.writer().print("{d}", .{period});
74 return list.toOwnedSlice();
75}
76
77// RFC3548 base32
78// input.len is gonna be 64 | 32 | 64
79fn encodeBase32(list: *std.ArrayList(u8), input: []const u8) !void {
80 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
81 var iter = BufBiterator.init(input);
82 while (iter.nextInt(u5)) |idx| try list.append(alphabet[idx]);
83}
84
85const BufBiterator = struct {
86 buf: []const u8,
87 bits: std.bit_set.IntegerBitSet(8),
88 idx: u8, // buf index
89 jdx: u8, // bits index
90
91 pub fn init(buf: []const u8) BufBiterator {
92 return .{
93 .buf = buf,
94 .bits = .{ .mask = buf[0] },
95 .idx = 0,
96 .jdx = 0,
97 };
98 }
99
100 pub fn nextInt(self: *BufBiterator, T: type) ?T {
101 const info = @typeInfo(T).int;
102 var result: T = 0;
103 for (0..info.bits) |_| {
104 result <<= 1;
105 const val = self.next() orelse return null;
106 result += val;
107 }
108 return result;
109 }
110
111 pub fn next(self: *BufBiterator) ?u1 {
112 if (self.jdx == 8) {
113 self.jdx = 0;
114 self.idx += 1;
115 if (self.idx == self.buf.len) return null;
116 self.bits.mask = self.buf[self.idx];
117 return self.next();
118 }
119 const result = self.bits.isSet(7 - self.jdx);
120 self.jdx += 1;
121 return @intFromBool(result);
122 }
123};
zigmod.yml+2
...@@ -5,6 +5,8 @@ license: MPL-2.0...@@ -5,6 +5,8 @@ license: MPL-2.0
5description: "RFC 6238: TOTP: Time-Based One-Time Password Algorithm"5description: "RFC 6238: TOTP: Time-Based One-Time Password Algorithm"
6dependencies:6dependencies:
7 - src: git https://github.com/nektro/zig-extras7 - src: git https://github.com/nektro/zig-extras
8 - src: git https://github.com/nektro/zig-whatwg-url
8root_dependencies:9root_dependencies:
9 - src: git https://github.com/nektro/zig-extras10 - src: git https://github.com/nektro/zig-extras
10 - src: git https://github.com/nektro/zig-expect11 - src: git https://github.com/nektro/zig-expect
12 - src: git https://github.com/nektro/zig-whatwg-url