From dd32db056e340bc3c2a59e6d4828da7b157ec728 Mon Sep 17 00:00:00 2001 From: Andreas Huber Date: Sat, 22 Feb 2025 20:45:37 +0100 Subject: [PATCH] Make it possible to validate a code. --- src/main.zig | 57 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/src/main.zig b/src/main.zig index 12d5604..0130e20 100644 --- a/src/main.zig +++ b/src/main.zig @@ -59,6 +59,13 @@ fn mainInternal() !void { const authenticator = try getAuthenticator(allocator, arg.show.?, arg.config_location); const code = try authenticator.code(allocator, std.time.timestamp); try std.io.getStdOut().writer().print("{s}\n", .{code}); + } else if (arg.validateAuthenticator != null and arg.validateCode != null) { + const authenticator = try getAuthenticator(allocator, arg.validateAuthenticator.?, arg.config_location); + const matches = try authenticator.validate(allocator, arg.validateCode.?, std.time.timestamp); + if (!matches) { + std.process.exit(1); + } + std.process.exit(0); } else { printHelp(); } @@ -71,6 +78,7 @@ fn printHelp() void { \\Commands: \\ list List the configured authentiators \\ show NAME Show the code for the authenticator with name NAME + \\ validate NAME CODE Validate the code CODE for the authenticator with name NAME \\ \\Options: \\ --bash Print script to set up Bash shell integration. Usage: add @@ -82,11 +90,7 @@ fn printHelp() void { std.debug.print("{s}", .{msg}); } -const Args = struct { - list: bool, - show: ?[]const u8, - config_location: []const u8, -}; +const Args = struct { list: bool, show: ?[]const u8, config_location: []const u8, validateAuthenticator: ?[]const u8, validateCode: ?[]const u8 }; const OtpAuthUrl = struct { name: []const u8, secretEncoded: []const u8, url: []const u8, period: u32, digits: u4, algorithm: []const u8 }; @@ -148,6 +152,12 @@ const Authenticator = struct { //debug("code as 0-padded string: {s} ({d})\n", .{ result, token }); return result; } + + fn validate(self: Authenticator, allocator: Allocator, expectedCode: []const u8, timeFunc: *const fn () i64) !bool { + const actualCode = try self.code(allocator, timeFunc); + defer allocator.free(actualCode); + return eql(u8, actualCode, expectedCode); + } }; fn zeroPad(allocator: Allocator, digits: u4, x: anytype) ![]u8 { @@ -176,11 +186,7 @@ fn zeroPad(allocator: Allocator, digits: u4, x: anytype) ![]u8 { const ArgumentError = error{ InvalidOtpAuthUrl, UnknownParameter, MissingConfigLocation, AuthenticatorNotFound, MissingAuthenticatorParam, FailedToOpenConfigFile, TooManyParsinErrors }; fn parseArgs(allocator: Allocator, args: []const []const u8) !Args { - var result = Args{ - .list = false, - .show = null, - .config_location = try configLocation(allocator), - }; + var result = Args{ .list = false, .show = null, .config_location = try configLocation(allocator), .validateAuthenticator = null, .validateCode = null }; var i: u17 = 1; while (i < args.len) : (i += 1) { const arg = args[i]; @@ -189,14 +195,17 @@ fn parseArgs(allocator: Allocator, args: []const []const u8) !Args { const msg = \\_zig_totp_completions() \\{ - \\ if [ "${COMP_WORDS[$COMP_CWORD-1]}" = 'show' ] + \\ if [ "${COMP_WORDS[$COMP_CWORD-1]}" = 'show' ] || [ "${COMP_WORDS[$COMP_CWORD-1]}" = 'validate' ] \\ then \\ COMPREPLY=($(compgen -W "$(zig-totp list 2> /dev/null )" -- "${COMP_WORDS[$COMP_CWORD]}" )) + \\ elif [ "${COMP_WORDS[$COMP_CWORD-2]}" = 'validate' ] + \\ then + \\ COMPREPLY=($(compgen -W "CODE" -- "${COMP_WORDS[$COMP_CWORD]}")) \\ elif [ "${COMP_WORDS[$COMP_CWORD-1]}" = '--config' ] \\ then \\ COMPREPLY=($(compgen -A file -- "${COMP_WORDS[$COMP_CWORD]}" )) \\ else - \\ COMPREPLY=($(compgen -W "list show --bash --config" -- "${COMP_WORDS[$COMP_CWORD]}" )) + \\ COMPREPLY=($(compgen -W "list show validate --bash --config" -- "${COMP_WORDS[$COMP_CWORD]}" )) \\ fi \\} \\complete -F _zig_totp_completions zig-totp @@ -220,6 +229,14 @@ fn parseArgs(allocator: Allocator, args: []const []const u8) !Args { return error.MissingAuthenticatorParam; } result.show = args[i]; + } else if (eql(u8, "validate", arg)) { + i += 2; + if (i >= args.len) { + std.debug.print("expected two parameters for 'validate'. The name for an authenticator and a code to validate.\n", .{}); + return error.MissingValidateParam; + } + result.validateAuthenticator = args[i - 1]; + result.validateCode = args[i]; } else { std.debug.print("unknown argument: {s}\n", .{arg}); return ArgumentError.UnknownParameter; @@ -577,3 +594,19 @@ test "secret is not Base32 - invalid character" { const actualError = authenticator.code(std.testing.allocator, _closure_return_1725695340); try std.testing.expectError(Base32Error.InvalidCharacter, actualError); } + +test "testcases for 'validate'" { + const secretForSha1 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; // plain: "12345678901234567890" hex: "3132333435363738393031323334353637383930" + + try testValidate(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, _closure_return_59, "94287082", true); + + try testValidate(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, _closure_return_1111111109, "07081804", true); + + try testValidate(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, _closure_return_1111111109, "12345678", false); +} +fn testValidate(otpAuthUrl: OtpAuthUrl, timeFunc: *const fn () i64, expected: []const u8, expectedMatch: bool) !void { + const authenticator = Authenticator{ .url = otpAuthUrl }; + + const match = try authenticator.validate(std.testing.allocator, expected, timeFunc); + try std.testing.expectEqual(expectedMatch, match); +}