| ... | ... | @@ -29,3 +29,95 @@ fn powci(x: comptime_int, y: comptime_int) comptime_int { |
| 29 | 29 | if (y == 0) return 1; |
| 30 | 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 | }; |