better parsing of the otpauth url

Multiple parameters can be parsed. And we can set the period and digits
via the url.
This commit is contained in:
2024-09-02 18:57:20 +02:00
parent 0afbb7a757
commit 36b19427e3
2 changed files with 104 additions and 68 deletions

View File

@@ -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;

View File

@@ -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);
}