diff --git a/README.md b/README.md index 6e70e08ee2cbedd62dd8a438662739e3ffcc0a17..55e59c33f892a5ec4385544956f126658d224564 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,5 @@ RFC 6238: TOTP: Time-Based One-Time Password Algorithm https://www.rfc-editor.org/rfc/rfc6238 TOTP: Time-Based One-Time Password Algorithm https://www.rfc-editor.org/rfc/rfc4226 HOTP: An HMAC-Based One-Time Password Algorithm + +https://github.com/google/google-authenticator/wiki/Key-Uri-Format diff --git a/licenses.txt b/licenses.txt index ef71fa5b846f5a5276733f06dda722f7ac948889..2f640ce67a591d96208faa384a15ee4057289219 100644 --- a/licenses.txt +++ b/licenses.txt @@ -1,8 +1,11 @@ MPL-2.0: = https://spdx.org/licenses/MPL-2.0 - This +- git https://github.com/nektro/zig-unicode-idna +- git https://github.com/nektro/zig-whatwg-url MIT: = https://spdx.org/licenses/MIT - git https://github.com/nektro/zig-expect - git https://github.com/nektro/zig-extras +- git https://github.com/nektro/zig-unicode-ucd diff --git a/test.zig b/test.zig index 80a9a09d9404c96acb953fd3d5313a27a064f29a..4fcbcf8eb43173579dff6e78fc19e7639f753bed 100644 --- a/test.zig +++ b/test.zig @@ -40,3 +40,10 @@ test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 1234567890)).t test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 2000000000)).toEqualString("38618901"); } test { try expect(&totp.totp(8, Sha512, 0, 30, &from_hex(seed64), 20000000000)).toEqualString("47863826"); } // zig fmt: on + +test { + const allocator = std.testing.allocator; + const url = try totp.generateUrl(allocator, "ACME Co", "john.doe@email.com", &from_hex("3dc6caa4824a6d288767b2331e20b43166cb85d9"), .SHA1, 6, 30); + defer allocator.free(url); + try expect(url).toEqualString("otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA1&digits=6&period=30"); +} diff --git a/totp.zig b/totp.zig index df5e47fad30cbbec6956a3b30d1c776ece2982bd..34107a9ad041e273aedd367e041f857911e0dac8 100644 --- a/totp.zig +++ b/totp.zig @@ -29,3 +29,95 @@ fn powci(x: comptime_int, y: comptime_int) comptime_int { if (y == 0) return 1; return x * powci(x, y - 1); } + +pub const Algorithm = enum { + SHA1, + SHA256, + SHA512, + + pub fn ty(algo: Algorithm) type { + return switch (algo) { + .SHA1 => std.crypto.hash.Sha1, + .SHA256 => std.crypto.hash.sha2.Sha256, + .SHA512 => std.crypto.hash.sha2.Sha512, + }; + } + + pub fn digest_length(algo: Algorithm) u8 { + return switch (algo) { + inline else => |tag| tag.ty().digest_length, + }; + } +}; + +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 { + std.debug.assert(issuer.len > 0); + std.debug.assert(account.len > 0); + std.debug.assert(secret_raw.len == algo.digest_length()); + std.debug.assert(digits == 6 or digits == 7 or digits == 8); + std.debug.assert(period == 15 or period == 30 or period == 60); + var list: std.ArrayList(u8) = .init(allocator); + errdefer list.deinit(); + try list.appendSlice("otpauth://"); + try list.appendSlice("totp/"); + try url.percentEncodeAL(&list, issuer, url.is_path_percent_char); + try list.append(':'); + try url.percentEncodeAL(&list, account, url.is_path_percent_char); + try list.appendSlice("?secret="); + try encodeBase32(&list, secret_raw); + try list.appendSlice("&algorithm="); + try list.appendSlice(@tagName(algo)); + try list.appendSlice("&digits="); + try list.writer().print("{d}", .{digits}); + try list.appendSlice("&period="); + try list.writer().print("{d}", .{period}); + return list.toOwnedSlice(); +} + +// RFC3548 base32 +// input.len is gonna be 64 | 32 | 64 +fn encodeBase32(list: *std.ArrayList(u8), input: []const u8) !void { + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + var iter = BufBiterator.init(input); + while (iter.nextInt(u5)) |idx| try list.append(alphabet[idx]); +} + +const BufBiterator = struct { + buf: []const u8, + bits: std.bit_set.IntegerBitSet(8), + idx: u8, // buf index + jdx: u8, // bits index + + pub fn init(buf: []const u8) BufBiterator { + return .{ + .buf = buf, + .bits = .{ .mask = buf[0] }, + .idx = 0, + .jdx = 0, + }; + } + + pub fn nextInt(self: *BufBiterator, T: type) ?T { + const info = @typeInfo(T).int; + var result: T = 0; + for (0..info.bits) |_| { + result <<= 1; + const val = self.next() orelse return null; + result += val; + } + return result; + } + + pub fn next(self: *BufBiterator) ?u1 { + if (self.jdx == 8) { + self.jdx = 0; + self.idx += 1; + if (self.idx == self.buf.len) return null; + self.bits.mask = self.buf[self.idx]; + return self.next(); + } + const result = self.bits.isSet(7 - self.jdx); + self.jdx += 1; + return @intFromBool(result); + } +}; diff --git a/zigmod.yml b/zigmod.yml index ba63667d59b41127ccfba2305f13d47548c11ef3..df50742a403c0363841b77dc83cb4565218d95f6 100644 --- a/zigmod.yml +++ b/zigmod.yml @@ -5,6 +5,8 @@ license: MPL-2.0 description: "RFC 6238: TOTP: Time-Based One-Time Password Algorithm" dependencies: - src: git https://github.com/nektro/zig-extras + - src: git https://github.com/nektro/zig-whatwg-url root_dependencies: - src: git https://github.com/nektro/zig-extras - src: git https://github.com/nektro/zig-expect + - src: git https://github.com/nektro/zig-whatwg-url