| author | |
| committer | |
| log | 11016cbb4dac6264b640b95f86742dba8bb8140d |
| tree | c1e6e12da94ef8697328cca5846f948d9d8b0539 |
| parent | a9a86ffe8e36aad9dd3e231d514e3b793fe571f6 |
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 |
| 11 | https://www.rfc-editor.org/rfc/rfc6238 TOTP: Time-Based One-Time Password Algorithm | 11 | https://www.rfc-editor.org/rfc/rfc6238 TOTP: Time-Based One-Time Password Algorithm |
| 12 | 12 | ||
| 13 | https://www.rfc-editor.org/rfc/rfc4226 HOTP: An HMAC-Based One-Time Password Algorithm | 13 | https://www.rfc-editor.org/rfc/rfc4226 HOTP: An HMAC-Based One-Time Password Algorithm |
| 14 | |||
| 15 | https://github.com/google/google-authenticator/wiki/Key-Uri-Format |
licenses.txt+3| ... | @@ -1,8 +1,11 @@ | ... | @@ -1,8 +1,11 @@ |
| 1 | MPL-2.0: | 1 | MPL-2.0: |
| 2 | = https://spdx.org/licenses/MPL-2.0 | 2 | = https://spdx.org/licenses/MPL-2.0 |
| 3 | - This | 3 | - This |
| 4 | - git https://github.com/nektro/zig-unicode-idna | ||
| 5 | - git https://github.com/nektro/zig-whatwg-url | ||
| 4 | 6 | ||
| 5 | MIT: | 7 | MIT: |
| 6 | = https://spdx.org/licenses/MIT | 8 | = https://spdx.org/licenses/MIT |
| 7 | - git https://github.com/nektro/zig-expect | 9 | - git https://github.com/nektro/zig-expect |
| 8 | - git https://github.com/nektro/zig-extras | 10 | - 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 |
| 40 | test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 2000000000)).toEqualString("38618901"); } | 40 | test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 2000000000)).toEqualString("38618901"); } |
| 41 | test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 20000000000)).toEqualString("47863826"); } | 41 | test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 20000000000)).toEqualString("47863826"); } |
| 42 | // zig fmt: on | 42 | // zig fmt: on |
| 43 | |||
| 44 | test { | ||
| 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 | |||
| 33 | pub 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 | |||
| 53 | pub 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 | ||
| 79 | fn 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 | |||
| 85 | const 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 |
| 5 | description: "RFC 6238: TOTP: Time-Based One-Time Password Algorithm" | 5 | description: "RFC 6238: TOTP: Time-Based One-Time Password Algorithm" |
| 6 | dependencies: | 6 | dependencies: |
| 7 | - src: git https://github.com/nektro/zig-extras | 7 | - src: git https://github.com/nektro/zig-extras |
| 8 | - src: git https://github.com/nektro/zig-whatwg-url | ||
| 8 | root_dependencies: | 9 | root_dependencies: |
| 9 | - src: git https://github.com/nektro/zig-extras | 10 | - src: git https://github.com/nektro/zig-extras |
| 10 | - src: git https://github.com/nektro/zig-expect | 11 | - src: git https://github.com/nektro/zig-expect |
| 12 | - src: git https://github.com/nektro/zig-whatwg-url |