1const std = @import("std");
2const extras = @import("extras");
3const url = @import("url");
4const nio = @import("nio");
5
6pub fn totp(digits: comptime_int, Hash: type, epoch: u64, X: u64, K: *const [Hash.digest_length]u8, time_now_s: u64) [digits]u8 {
7 const T = (time_now_s - epoch) / X;
8 var Thex: [16]u8 = undefined;
9 _ = std.fmt.bufPrint(&Thex, "{X:0>16}", .{T}) catch unreachable;
10 const Hmac = std.crypto.auth.hmac.Hmac(Hash);
11 var HS: [Hmac.mac_length]u8 = undefined;
12 Hmac.create(&HS, &extras.from_hex(&Thex), K);
13 return hotp(digits, &HS);
14}
15
16pub fn hotp(digits: comptime_int, hmac_sha: []const u8) [digits]u8 {
17 const last = hmac_sha[hmac_sha.len - 1];
18 const offset = last & 0xf;
19 const value: u31 = @truncate(std.mem.readInt(u32, hmac_sha[offset..][0..4], .big));
20 const dbc = value % powci(10, digits);
21
22 var out: [digits]u8 = undefined;
23 for (0..digits) |i| out[digits - 1 - i] = @intCast(dbc / (std.math.powi(u32, 10, @intCast(i)) catch unreachable) % 10);
24 for (0..digits) |i| out[i] += '0';
25 return out;
26}
27
28fn powci(x: comptime_int, y: comptime_int) comptime_int {
29 if (y < 0) @compileError("use sqrt etc");
30 if (y == 0) return 1;
31 return x * powci(x, y - 1);
32}
33
34pub const Algorithm = enum {
35 SHA1,
36 SHA256,
37 SHA512,
38
39 pub fn ty(algo: Algorithm) type {
40 return switch (algo) {
41 .SHA1 => std.crypto.hash.Sha1,
42 .SHA256 => std.crypto.hash.sha2.Sha256,
43 .SHA512 => std.crypto.hash.sha2.Sha512,
44 };
45 }
46
47 pub fn digest_length(algo: Algorithm) u8 {
48 return switch (algo) {
49 inline else => |tag| tag.ty().digest_length,
50 };
51 }
52};
53
54pub fn generateUrl(allocator: std.mem.Allocator, issuer: []const u8, account: []const u8, secret_raw: []const u8, algo: Algorithm, digits: u8, period: u8) ![]const u8 {
55 std.debug.assert(issuer.len > 0);
56 std.debug.assert(account.len > 0);
57 std.debug.assert(secret_raw.len <= algo.digest_length());
58 std.debug.assert(digits == 6 or digits == 7 or digits == 8);
59 std.debug.assert(period == 15 or period == 30 or period == 60);
60 var list: nio.AllocatingWriter = .init(allocator);
61 errdefer list.deinit();
62 try list.writeAll("otpauth://");
63 try list.writeAll("totp/");
64 try url.percentEncodeAL(&list, issuer, url.is_path_percent_char);
65 try list.writeAll(":");
66 try url.percentEncodeAL(&list, account, url.is_path_percent_char);
67 try list.writeAll("?secret=");
68 try encodeBase32(&list, secret_raw);
69 try list.writeAll("&algorithm=");
70 try list.writeAll(@tagName(algo));
71 try list.writeAll("&digits=");
72 try list.print("{d}", .{digits});
73 try list.writeAll("&period=");
74 try list.print("{d}", .{period});
75 try list.writeAll("&issuer=");
76 try url.percentEncodeAL(&list, issuer, url.is_query_percent_char);
77 return list.toOwnedSlice();
78}
79
80// RFC3548 base32
81// input.len is gonna be 64 | 32 | 64
82fn encodeBase32(list: *nio.AllocatingWriter, input: []const u8) !void {
83 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
84 var iter = BufBiterator.init(input);
85 while (iter.nextInt(u5)) |idx| try list.writeAll(alphabet[idx..][0..1]);
86}
87
88const BufBiterator = struct {
89 buf: []const u8,
90 bits: std.bit_set.IntegerBitSet(8),
91 idx: u8, // buf index
92 jdx: u8, // bits index
93
94 pub fn init(buf: []const u8) BufBiterator {
95 return .{
96 .buf = buf,
97 .bits = .{ .mask = buf[0] },
98 .idx = 0,
99 .jdx = 0,
100 };
101 }
102
103 pub fn nextInt(self: *BufBiterator, T: type) ?T {
104 const info = @typeInfo(T).int;
105 var result: T = 0;
106 for (0..info.bits) |_| {
107 result <<= 1;
108 const val = self.next() orelse return null;
109 result += val;
110 }
111 return result;
112 }
113
114 pub fn next(self: *BufBiterator) ?u1 {
115 if (self.jdx == 8) {
116 self.jdx = 0;
117 self.idx += 1;
118 if (self.idx == self.buf.len) return null;
119 self.bits.mask = self.buf[self.idx];
120 return self.next();
121 }
122 const result = self.bits.isSet(7 - self.jdx);
123 self.jdx += 1;
124 return @intFromBool(result);
125 }
126};
127
128pub fn decodeBase32(input: []const u8, Hash: type) ![Hash.digest_length]u8 {
129 var result: std.bit_set.ArrayBitSet(u8, Hash.digest_length * 8) = .initEmpty();
130 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
131 for (input, 0..) |c, i| {
132 const x = std.mem.indexOfScalar(u8, alphabet, c) orelse return error.InvalidCharacter;
133 const n: u5 = @intCast(x);
134 inline for (0..5) |j| blk: {
135 if (i * 5 > result.capacity()) return error.Overflow;
136 const d = i * 5 + j;
137 if (d >= result.capacity()) break :blk;
138 result.setValue(d, n & (1 << 5 - 1 - (j)) != 0);
139 }
140 }
141 return bitReverse(result.masks);
142}
143
144fn bitReverse(array: anytype) [array.len]std.meta.Child(@TypeOf(array)) {
145 var result: [array.len]std.meta.Child(@TypeOf(array)) = @splat(0);
146 for (&array, &result) |x, *y| y.* = @bitReverse(x);
147 return result;
148}