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