Make it possible to validate a code.

This commit is contained in:
2025-02-22 20:45:37 +01:00
parent e9168203f5
commit dd32db056e

View File

@@ -59,6 +59,13 @@ fn mainInternal() !void {
const authenticator = try getAuthenticator(allocator, arg.show.?, arg.config_location); const authenticator = try getAuthenticator(allocator, arg.show.?, arg.config_location);
const code = try authenticator.code(allocator, std.time.timestamp); const code = try authenticator.code(allocator, std.time.timestamp);
try std.io.getStdOut().writer().print("{s}\n", .{code}); 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 { } else {
printHelp(); printHelp();
} }
@@ -71,6 +78,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 \\ 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: \\Options:
\\ --bash Print script to set up Bash shell integration. Usage: add \\ --bash Print script to set up Bash shell integration. Usage: add
@@ -82,11 +90,7 @@ fn printHelp() void {
std.debug.print("{s}", .{msg}); std.debug.print("{s}", .{msg});
} }
const Args = struct { const Args = struct { list: bool, show: ?[]const u8, config_location: []const u8, validateAuthenticator: ?[]const u8, validateCode: ?[]const u8 };
list: bool,
show: ?[]const u8,
config_location: []const u8,
};
const OtpAuthUrl = struct { name: []const u8, secretEncoded: []const u8, url: []const u8, period: u32, digits: u4, algorithm: []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 }); //debug("code as 0-padded string: {s} ({d})\n", .{ result, token });
return result; 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 { 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 }; const ArgumentError = error{ InvalidOtpAuthUrl, UnknownParameter, MissingConfigLocation, AuthenticatorNotFound, MissingAuthenticatorParam, FailedToOpenConfigFile, TooManyParsinErrors };
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, .show = null, .config_location = try configLocation(allocator), .validateAuthenticator = null, .validateCode = null };
.list = false,
.show = null,
.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];
@@ -189,14 +195,17 @@ fn parseArgs(allocator: Allocator, args: []const []const u8) !Args {
const msg = const msg =
\\_zig_totp_completions() \\_zig_totp_completions()
\\{ \\{
\\ if [ "${COMP_WORDS[$COMP_CWORD-1]}" = 'show' ] \\ if [ "${COMP_WORDS[$COMP_CWORD-1]}" = 'show' ] || [ "${COMP_WORDS[$COMP_CWORD-1]}" = 'validate' ]
\\ then \\ then
\\ COMPREPLY=($(compgen -W "$(zig-totp list 2> /dev/null )" -- "${COMP_WORDS[$COMP_CWORD]}" )) \\ 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' ] \\ elif [ "${COMP_WORDS[$COMP_CWORD-1]}" = '--config' ]
\\ then \\ then
\\ COMPREPLY=($(compgen -A file -- "${COMP_WORDS[$COMP_CWORD]}" )) \\ COMPREPLY=($(compgen -A file -- "${COMP_WORDS[$COMP_CWORD]}" ))
\\ else \\ 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 \\ fi
\\} \\}
\\complete -F _zig_totp_completions zig-totp \\complete -F _zig_totp_completions zig-totp
@@ -220,6 +229,14 @@ fn parseArgs(allocator: Allocator, args: []const []const u8) !Args {
return error.MissingAuthenticatorParam; return error.MissingAuthenticatorParam;
} }
result.show = args[i]; 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 { } else {
std.debug.print("unknown argument: {s}\n", .{arg}); std.debug.print("unknown argument: {s}\n", .{arg});
return ArgumentError.UnknownParameter; return ArgumentError.UnknownParameter;
@@ -577,3 +594,19 @@ test "secret is not Base32 - invalid character" {
const actualError = authenticator.code(std.testing.allocator, _closure_return_1725695340); const actualError = authenticator.code(std.testing.allocator, _closure_return_1725695340);
try std.testing.expectError(Base32Error.InvalidCharacter, actualError); 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);
}