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:
@@ -5,54 +5,8 @@ const Allocator = std.mem.Allocator;
|
|||||||
|
|
||||||
const Base32Error = error{ InvalidLength, InvalidCharacter, InvalidPadding };
|
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 {
|
pub fn decodeU8(allocator: Allocator, data: []const u8) ![]const u8 {
|
||||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||||
const expectedLength = data.len / 8 * 5 + (data.len % 8) * 5 / 8;
|
const expectedLength = data.len / 8 * 5 + (data.len % 8) * 5 / 8;
|
||||||
|
|||||||
124
src/main.zig
124
src/main.zig
@@ -2,12 +2,15 @@ const std = @import("std");
|
|||||||
const info = std.log.info;
|
const info = std.log.info;
|
||||||
const debug = std.log.debug;
|
const debug = std.log.debug;
|
||||||
const print = std.debug.print;
|
const print = std.debug.print;
|
||||||
|
const eql = std.mem.eql;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const expect = testing.expect;
|
const expect = testing.expect;
|
||||||
const ArrayList = std.ArrayList;
|
const ArrayList = std.ArrayList;
|
||||||
|
const Base32 = @import("base32.zig").Base32;
|
||||||
|
|
||||||
// otpauth://totp/AWS+Dev?secret=47STA47VFCMMLLWOLHWO3KY7MYNC36MLCDTHOLIYKJCTTSSAMKVM7YA3VWT2AJEP&digits=6&icon=Amazon
|
// 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 {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
@@ -24,6 +27,9 @@ pub fn main() !void {
|
|||||||
for (0..names.items.len) |i| {
|
for (0..names.items.len) |i| {
|
||||||
std.debug.print("{s}\n", .{names.items[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 {
|
} else {
|
||||||
printHelp();
|
printHelp();
|
||||||
}
|
}
|
||||||
@@ -37,6 +43,7 @@ fn printHelp() void {
|
|||||||
\\
|
\\
|
||||||
\\Commands:
|
\\Commands:
|
||||||
\\ list List the configured authentiators
|
\\ list List the configured authentiators
|
||||||
|
\\ show NAME Show the code for the authenticator with name NAME
|
||||||
\\
|
\\
|
||||||
\\Options:
|
\\Options:
|
||||||
\\ --config path The path to a config file (default is $XDG_CONFIG_HOME/zig-totp or $HOME/.zig-totp).
|
\\ --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 {
|
const Args = struct {
|
||||||
list: bool,
|
list: bool,
|
||||||
|
show: ?[]const u8,
|
||||||
config_location: []const u8,
|
config_location: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
const OtpAuthUrl = struct {
|
const OtpAuthUrl = struct { name: []const u8, secretEncoded: []const u8, url: []const u8, period: u32, digits: u4 };
|
||||||
name: []const u8,
|
|
||||||
secretEncoded: []const u8,
|
const Authenticator = struct {
|
||||||
url: []const u8,
|
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 {
|
fn parseArgs(allocator: Allocator, args: []const []const u8) !Args {
|
||||||
var result = Args{
|
var result = Args{
|
||||||
.list = false,
|
.list = false,
|
||||||
|
.show = null,
|
||||||
.config_location = try configLocation(allocator),
|
.config_location = try configLocation(allocator),
|
||||||
};
|
};
|
||||||
var i: u17 = 1;
|
var i: u17 = 1;
|
||||||
while (i < args.len) : (i += 1) {
|
while (i < args.len) : (i += 1) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--config", arg)) {
|
if (eql(u8, "--config", arg)) {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i >= args.len) {
|
if (i >= args.len) {
|
||||||
return error.MissingConfigLocation;
|
return error.MissingConfigLocation;
|
||||||
}
|
}
|
||||||
const config_location = args[i];
|
result.config_location = args[i];
|
||||||
result.config_location = config_location;
|
} else if (eql(u8, "list", arg)) {
|
||||||
} else if (std.mem.eql(u8, "list", arg)) {
|
|
||||||
result.list = true;
|
result.list = true;
|
||||||
|
} else if (eql(u8, "show", arg)) {
|
||||||
|
i += 1;
|
||||||
|
if (i >= args.len) {
|
||||||
|
return error.MissingConfigLocation;
|
||||||
|
}
|
||||||
|
result.show = args[i];
|
||||||
} else {
|
} else {
|
||||||
std.debug.print("unknown parameter: {s}\n", .{arg});
|
std.debug.print("unknown parameter: {s}\n", .{arg});
|
||||||
return ArgumentError.UnknownParameter;
|
return ArgumentError.UnknownParameter;
|
||||||
@@ -84,23 +111,57 @@ fn parseArgs(allocator: Allocator, args: []const []const u8) !Args {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo use a real url parser
|
||||||
fn parseOtpAuthUrl(url: []const u8) !OtpAuthUrl {
|
fn parseOtpAuthUrl(url: []const u8) !OtpAuthUrl {
|
||||||
const index = std.mem.indexOf(u8, url, "otpauth://totp/");
|
const uri = try std.Uri.parse(url);
|
||||||
if (index != 0) {
|
|
||||||
|
if (!eql(u8, uri.scheme, "otpauth")) {
|
||||||
return error.InvalidOtpAuthUrl;
|
return error.InvalidOtpAuthUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
var it = std.mem.splitSequence(u8, url[15..], "?secret=");
|
if (uri.query == null) {
|
||||||
|
return error.InvalidOtpAuthUrl;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
fn configLocation(allocator: Allocator) ![]const u8 {
|
||||||
@@ -153,6 +214,18 @@ fn executeGetList(allocator: Allocator, config_location: []const u8) !std.ArrayL
|
|||||||
return names;
|
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'" {
|
test "parse command line parameter: 'list'" {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
@@ -180,3 +253,12 @@ test "read list of entries" {
|
|||||||
try std.testing.expectEqualStrings("token2", list.items[1]);
|
try std.testing.expectEqualStrings("token2", list.items[1]);
|
||||||
try std.testing.expectEqual(2, list.items.len);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user