From 36b19427e32e8086a5e858573b9ccc9c9d08aca4 Mon Sep 17 00:00:00 2001 From: Andreas Huber Date: Mon, 2 Sep 2024 18:57:20 +0200 Subject: [PATCH] better parsing of the otpauth url Multiple parameters can be parsed. And we can set the period and digits via the url. --- src/base32.zig | 48 +------------------ src/main.zig | 124 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 104 insertions(+), 68 deletions(-) diff --git a/src/base32.zig b/src/base32.zig index 3a6fd12..d83a258 100644 --- a/src/base32.zig +++ b/src/base32.zig @@ -5,54 +5,8 @@ const Allocator = std.mem.Allocator; const Base32Error = error{ InvalidLength, InvalidCharacter, InvalidPadding }; -const Base32 = struct { +pub const Base32 = struct { /// - pub fn decodeU8_old(allocator: Allocator, data: []const u8) ![]const u8 { - const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - - if (data.len % 8 != 0) { - return error.InvalidLength; - } - - const result = try allocator.alloc(u8, (data.len / 8) * 5); - - var i: u64 = 0; - var r: usize = 0; - while (i < data.len) { - var bytes: u40 = 0; - const v1: u8 = @truncate(std.mem.indexOfScalar(u8, alphabet, data[i]).?); - const v2: u8 = @truncate(std.mem.indexOfScalar(u8, alphabet, data[i + 1]).?); - const v3: u8 = @truncate(std.mem.indexOfScalar(u8, alphabet, data[i + 2]).?); - const v4: u8 = @truncate(std.mem.indexOfScalar(u8, alphabet, data[i + 3]).?); - const v5: u8 = @truncate(std.mem.indexOfScalar(u8, alphabet, data[i + 4]).?); - const v6: u8 = @truncate(std.mem.indexOfScalar(u8, alphabet, data[i + 5]).?); - const v7: u8 = @truncate(std.mem.indexOfScalar(u8, alphabet, data[i + 6]).?); - const v8: u8 = @truncate(std.mem.indexOfScalar(u8, alphabet, data[i + 7]).?); - bytes = v1; - bytes = bytes << 5 | v2; - bytes = bytes << 5 | v3; - bytes = bytes << 5 | v4; - bytes = bytes << 5 | v5; - bytes = bytes << 5 | v6; - bytes = bytes << 5 | v7; - bytes = bytes << 5 | v8; - i += 8; - - result[r] = @as(u8, @truncate((bytes >> 32) & 0b11111111)); - r += 1; - result[r] = @as(u8, @truncate((bytes >> 24) & 0b11111111)); - r += 1; - result[r] = @as(u8, @truncate((bytes >> 16) & 0b11111111)); - r += 1; - result[r] = @as(u8, @truncate((bytes >> 8) & 0b11111111)); - r += 1; - result[r] = @as(u8, @truncate((bytes) & 0b11111111)); - r += 1; - } - - return result; - } - pub fn decodeU8(allocator: Allocator, data: []const u8) ![]const u8 { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; const expectedLength = data.len / 8 * 5 + (data.len % 8) * 5 / 8; diff --git a/src/main.zig b/src/main.zig index 3171aa4..1df9783 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,12 +2,15 @@ const std = @import("std"); const info = std.log.info; const debug = std.log.debug; const print = std.debug.print; +const eql = std.mem.eql; const Allocator = std.mem.Allocator; const testing = std.testing; const expect = testing.expect; const ArrayList = std.ArrayList; +const Base32 = @import("base32.zig").Base32; // otpauth://totp/AWS+Dev?secret=47STA47VFCMMLLWOLHWO3KY7MYNC36MLCDTHOLIYKJCTTSSAMKVM7YA3VWT2AJEP&digits=6&icon=Amazon +// otpauth://totp/Gitea%20%28git.lucares.de%29:andi?algorithm=SHA1&digits=6&issuer=Gitea&period=30&secret=MSP53Q672UJMSCLQVRCJKMMZKK7MWSMYFL77OQ24JBM65RZWY7F2Y45FMTNLYM36 pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; @@ -24,6 +27,9 @@ pub fn main() !void { for (0..names.items.len) |i| { std.debug.print("{s}\n", .{names.items[i]}); } + } else if (arg.show != null) { + const authenticator = try getAuthenticator(allocator, arg.show.?, arg.config_location); + debug("number for {s}", .{try authenticator.code()}); } else { printHelp(); } @@ -37,6 +43,7 @@ fn printHelp() void { \\ \\Commands: \\ list List the configured authentiators + \\ show NAME Show the code for the authenticator with name NAME \\ \\Options: \\ --config path The path to a config file (default is $XDG_CONFIG_HOME/zig-totp or $HOME/.zig-totp). @@ -47,35 +54,55 @@ fn printHelp() void { const Args = struct { list: bool, + show: ?[]const u8, config_location: []const u8, }; -const OtpAuthUrl = struct { - name: []const u8, - secretEncoded: []const u8, - url: []const u8, +const OtpAuthUrl = struct { name: []const u8, secretEncoded: []const u8, url: []const u8, period: u32, digits: u4 }; + +const Authenticator = struct { + url: OtpAuthUrl, + + pub fn code(self: Authenticator) ![]const u8 { + if (self.url.name.len > 0) {} + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + const secret = try Base32.decodeU8(allocator, self.url.secretEncoded); + if (false) { + debug("secret: {s}\n", .{secret}); + } + + return "code"; + } }; -const ArgumentError = error{ InvalidOtpAuthUrl, UnknownParameter, MissingConfigLocation }; +const ArgumentError = error{ InvalidOtpAuthUrl, UnknownParameter, MissingConfigLocation, AuthenticatorNotFound }; fn parseArgs(allocator: Allocator, args: []const []const u8) !Args { var result = Args{ .list = false, + .show = null, .config_location = try configLocation(allocator), }; var i: u17 = 1; while (i < args.len) : (i += 1) { const arg = args[i]; - if (std.mem.eql(u8, "--config", arg)) { + if (eql(u8, "--config", arg)) { i += 1; if (i >= args.len) { return error.MissingConfigLocation; } - const config_location = args[i]; - result.config_location = config_location; - } else if (std.mem.eql(u8, "list", arg)) { + result.config_location = args[i]; + } else if (eql(u8, "list", arg)) { result.list = true; + } else if (eql(u8, "show", arg)) { + i += 1; + if (i >= args.len) { + return error.MissingConfigLocation; + } + result.show = args[i]; } else { std.debug.print("unknown parameter: {s}\n", .{arg}); return ArgumentError.UnknownParameter; @@ -84,23 +111,57 @@ fn parseArgs(allocator: Allocator, args: []const []const u8) !Args { return result; } +// todo use a real url parser fn parseOtpAuthUrl(url: []const u8) !OtpAuthUrl { - const index = std.mem.indexOf(u8, url, "otpauth://totp/"); - if (index != 0) { + const uri = try std.Uri.parse(url); + + if (!eql(u8, uri.scheme, "otpauth")) { return error.InvalidOtpAuthUrl; } - var it = std.mem.splitSequence(u8, url[15..], "?secret="); - - const name = it.next(); - const secret = it.next(); - const empty = it.next(); - - if (name != null and secret != null and empty == null) { - return OtpAuthUrl{ .name = name.?, .secretEncoded = secret.?, .url = url }; - } else { - return ArgumentError.InvalidOtpAuthUrl; + if (uri.query == null) { + return error.InvalidOtpAuthUrl; } + + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + var name = try uri.path.toRawMaybeAlloc(allocator); + name = std.mem.trimLeft(u8, name, "/"); + + const params = try parseQueryString(allocator, try uri.query.?.toRawMaybeAlloc(allocator)); + + return OtpAuthUrl{ + .name = name, + .secretEncoded = params.get("secret").?, + .url = url, + .period = if (params.get("period") != null) try std.fmt.parseInt(u32, params.get("period").?, 10) else 30, + .digits = if (params.get("digits") != null) try std.fmt.parseInt(u4, params.get("digits").?, 10) else 6, + }; +} + +fn parseQueryString(allocator: Allocator, query: []const u8) !std.StringHashMap([]const u8) { + var map = std.StringHashMap([]const u8).init(allocator); + + var it = std.mem.splitSequence(u8, query, "&"); + + while (it.peek() != null) { + const next = it.next().?; + + var paramIt = std.mem.splitSequence(u8, next, "="); + const name = paramIt.next(); + const value = paramIt.next(); + + if (name) |n| { + if (value) |v| { + try map.put(n, v); + } else { + try map.put(n, ""); + } + } + } + + return map; } fn configLocation(allocator: Allocator) ![]const u8 { @@ -153,6 +214,18 @@ fn executeGetList(allocator: Allocator, config_location: []const u8) !std.ArrayL return names; } +fn getAuthenticator(allocator: Allocator, name: []const u8, config_location: []const u8) !Authenticator { + const otpAuthUrls = try read_config(allocator, config_location); + + for (0..otpAuthUrls.items.len) |i| { + const url: OtpAuthUrl = otpAuthUrls.items[i]; + if (eql(u8, url.name, name)) { + return Authenticator{ .url = url }; + } + } + return error.AuthenticatorNotFound; +} + test "parse command line parameter: 'list'" { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); @@ -180,3 +253,12 @@ test "read list of entries" { try std.testing.expectEqualStrings("token2", list.items[1]); try std.testing.expectEqual(2, list.items.len); } + +test "parse oth pauth url" { + const actual: OtpAuthUrl = try parseOtpAuthUrl("otpauth://totp/foo?secret=MFQQ&period=31&digits=7"); + + try std.testing.expectEqualStrings("MFQQ", actual.secretEncoded); + try std.testing.expectEqualStrings("foo", actual.name); + try std.testing.expectEqual(31, actual.period); + try std.testing.expectEqual(7, actual.digits); +}