Compare commits
21 Commits
b4f82ebfda
...
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b329bc73e1 | |||
| cd8a9b0c76 | |||
| 4405bf4ab0 | |||
| f5acf8af4e | |||
| 6510314dea | |||
| ea5dc4d144 | |||
| 4ff826d9f4 | |||
| 769573a23b | |||
| d5632dea6e | |||
| 83efe0547e | |||
| 761d9086cd | |||
| 2214ae92d1 | |||
| a0147b8216 | |||
| e1d195f236 | |||
| 0fbea3d981 | |||
| 25e2c10bb2 | |||
| a296b509ae | |||
| 8b946e3423 | |||
| f5f1e1593b | |||
| ff7d1ede59 | |||
| 3f3c3198f5 |
34
.gitea/workflows/build.yaml
Normal file
34
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Build
|
||||
run-name: ${{ gitea.actor }} is building
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
|
||||
release:
|
||||
name: ${{ matrix.target }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-linux-musl
|
||||
- target: x86_64-linux-gnu
|
||||
- target: x86_64-windows
|
||||
steps:
|
||||
- run: printenv
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: ''
|
||||
- run: wget http://172.17.0.1:8081/repository/ziglang.org/download/0.14.1/zig-x86_64-linux-0.14.1.tar.xz
|
||||
- run: tar xfv zig-x86_64-linux-0.14.1.tar.xz
|
||||
- run: ./zig-x86_64-linux-0.14.1/zig build -Doptimize=ReleaseSmall -Dtarget=${{ matrix.target }} -Dexe_name=zig-totp-$GITHUB_REF_NAME-${{ matrix.target }}
|
||||
- name: upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: zig-totp-${{matrix.target }}
|
||||
path: zig-out
|
||||
|
||||
|
||||
|
||||
21
build.zig
21
build.zig
@@ -22,8 +22,10 @@ pub fn build(b: *std.Build) void {
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const exe_name = b.option([]const u8, "exe_name", "Name of the executable") orelse "zig-totp";
|
||||
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "zig-totp",
|
||||
.name = exe_name,
|
||||
// In this case the main source file is merely a path, however, in more
|
||||
// complicated build scripts, this could be a generated file.
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
@@ -37,7 +39,7 @@ pub fn build(b: *std.Build) void {
|
||||
b.installArtifact(lib);
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "zig-totp",
|
||||
.name = exe_name,
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
@@ -122,6 +124,21 @@ fn getVersion(b: *std.Build) std.SemanticVersion {
|
||||
}
|
||||
return totp_version;
|
||||
},
|
||||
1 => {
|
||||
// prerelease version: 1.0.0-dev
|
||||
var iter = std.mem.splitScalar(u8, output_trimmed, '-');
|
||||
const tag = iter.first();
|
||||
const pre_release = iter.next().?;
|
||||
|
||||
const v: std.SemanticVersion = std.SemanticVersion.parse(tag) catch unreachable;
|
||||
|
||||
return .{
|
||||
.major = v.major,
|
||||
.minor = v.minor,
|
||||
.patch = v.patch,
|
||||
.pre = b.fmt("{s}", .{pre_release}),
|
||||
};
|
||||
},
|
||||
2 => {
|
||||
// development version, e.g. 1.0.0-7-64es356
|
||||
var iter = std.mem.splitScalar(u8, output_trimmed, '-');
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
.{
|
||||
.name = "zig-totp",
|
||||
.name = .f,
|
||||
// This is a [Semantic Version](https://semver.org/).
|
||||
// In a future version of Zig it will be used for package deduplication.
|
||||
.version = "0.1.0-dev",
|
||||
|
||||
// Together with name, this represents a globally unique package
|
||||
// identifier. This field is generated by the Zig toolchain when the
|
||||
// package is first created, and then *never changes*. This allows
|
||||
// unambiguous detection of one package being an updated version of
|
||||
// another.
|
||||
//
|
||||
// When forking a Zig project, this id should be regenerated (delete the
|
||||
// field and run `zig build`) if the upstream project is still maintained.
|
||||
// Otherwise, the fork is *hostile*, attempting to take control over the
|
||||
// original project's identity. Thus it is recommended to leave the comment
|
||||
// on the following line intact, so that it shows up in code reviews that
|
||||
// modify the field.
|
||||
.fingerprint = 0x76d32be0e41821b4, // Changing this has security and trust implications.
|
||||
|
||||
// This field is optional.
|
||||
// This is currently advisory only; Zig does not yet do anything
|
||||
// with this value.
|
||||
//.minimum_zig_version = "0.11.0",
|
||||
//.minimum_zig_version = "0.14.0",
|
||||
|
||||
// This field is optional.
|
||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||
|
||||
159
src/main.zig
159
src/main.zig
@@ -11,6 +11,8 @@ const ArrayList = std.ArrayList;
|
||||
const Base32 = @import("base32.zig").Base32;
|
||||
const Base32Error = @import("base32.zig").Base32Error;
|
||||
const options = @import("config");
|
||||
const Time = @import("time.zig").Time;
|
||||
const TimeError = @import("time.zig").TimeError;
|
||||
|
||||
// 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
|
||||
@@ -33,6 +35,9 @@ pub fn main() !void {
|
||||
ArgumentError.AuthenticatorNotFound => {
|
||||
try std.io.getStdErr().writer().print("authenticator not found\n", .{});
|
||||
},
|
||||
TimeError.ParsingError => {
|
||||
try std.io.getStdErr().writer().print("Invalid time. The argument for the --time parameter must be a valid ISO 8601 date with second precision for timezone UTC. E.g. 2000-01-01T00:00:00\n", .{});
|
||||
},
|
||||
else => |leftover_err| {
|
||||
try std.io.getStdErr().writer().print("{?}\n", .{leftover_err});
|
||||
},
|
||||
@@ -60,11 +65,13 @@ fn mainInternal() !void {
|
||||
}
|
||||
} else if (arg.show != null) {
|
||||
const authenticator = try getAuthenticator(allocator, arg.show.?, arg.config_location);
|
||||
const code = try authenticator.code(allocator, std.time.timestamp);
|
||||
const time = if (arg.time != null) try Time.parseIso8601(arg.time.?) else std.time.timestamp();
|
||||
const code = try authenticator.code(allocator, time);
|
||||
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);
|
||||
const time = if (arg.time != null) try Time.parseIso8601(arg.time.?) else std.time.timestamp();
|
||||
const matches = try authenticator.validate(allocator, arg.validateCode.?, time);
|
||||
if (!matches) {
|
||||
std.process.exit(1);
|
||||
}
|
||||
@@ -84,18 +91,21 @@ fn printHelp() void {
|
||||
\\ 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
|
||||
\\ --bash Generate shell integration for bash. Usage: add
|
||||
\\ eval "$(zig-totp --bash)"
|
||||
\\ to your ~/.bashrc
|
||||
\\ --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).
|
||||
\\ --fish Generate shell integration for fish. Usage:
|
||||
\\ zig-totp --fish > $__fish_config_dir/completions/zig-totp.fish
|
||||
\\ --help Show this help
|
||||
\\ --time Set the time at which a code is computed. Time format is ISO 8601 with second precision for timezone UTC, e.g., 2000-01-01T00:00:00
|
||||
\\ --version Print the version number
|
||||
\\
|
||||
;
|
||||
std.debug.print("{s}", .{msg});
|
||||
}
|
||||
|
||||
const Args = struct { list: bool, show: ?[]const u8, config_location: []const u8, validateAuthenticator: ?[]const u8, validateCode: ?[]const u8, print_version: bool, show_help: bool };
|
||||
const Args = struct { list: bool, show: ?[]const u8, config_location: []const u8, validateAuthenticator: ?[]const u8, validateCode: ?[]const u8, print_version: bool, show_help: bool, time: ?[]const u8 };
|
||||
|
||||
const OtpAuthUrl = struct { name: []const u8, secretEncoded: []const u8, url: []const u8, period: u32, digits: u4, algorithm: []const u8 };
|
||||
|
||||
@@ -103,7 +113,7 @@ const Authenticator = struct {
|
||||
url: OtpAuthUrl,
|
||||
|
||||
/// algorithm based on https://www.rfc-editor.org/rfc/rfc6238
|
||||
pub fn code(self: Authenticator, allocator: Allocator, timeFunc: *const fn () i64) ![]const u8 {
|
||||
pub fn code(self: Authenticator, allocator: Allocator, time: i64) ![]const u8 {
|
||||
if (self.url.name.len > 0) {}
|
||||
|
||||
const secret = try Base32.decodeU8(allocator, self.url.secretEncoded);
|
||||
@@ -112,7 +122,7 @@ const Authenticator = struct {
|
||||
debug("secret: {s}\n", .{secret});
|
||||
}
|
||||
|
||||
const intervalNumber = @divTrunc(timeFunc(), self.url.period);
|
||||
const intervalNumber = @divTrunc(time, self.url.period);
|
||||
|
||||
var intervalAsU8Array: [8]u8 = undefined;
|
||||
std.mem.writePackedInt(i64, intervalAsU8Array[0..], 0, intervalNumber, .big);
|
||||
@@ -158,8 +168,8 @@ const Authenticator = struct {
|
||||
return result;
|
||||
}
|
||||
|
||||
fn validate(self: Authenticator, allocator: Allocator, expectedCode: []const u8, timeFunc: *const fn () i64) !bool {
|
||||
const actualCode = try self.code(allocator, timeFunc);
|
||||
fn validate(self: Authenticator, allocator: Allocator, expectedCode: []const u8, time: i64) !bool {
|
||||
const actualCode = try self.code(allocator, time);
|
||||
defer allocator.free(actualCode);
|
||||
return eql(u8, actualCode, expectedCode);
|
||||
}
|
||||
@@ -191,7 +201,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), .validateAuthenticator = null, .validateCode = null, .print_version = false, .show_help = false };
|
||||
var result = Args{ .list = false, .show = null, .config_location = try configLocation(allocator), .validateAuthenticator = null, .validateCode = null, .print_version = false, .show_help = false, .time = null };
|
||||
var i: u17 = 1;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
@@ -210,18 +220,46 @@ fn parseArgs(allocator: Allocator, args: []const []const u8) !Args {
|
||||
\\ then
|
||||
\\ COMPREPLY=($(compgen -A file -- "${COMP_WORDS[$COMP_CWORD]}" ))
|
||||
\\ else
|
||||
\\ COMPREPLY=($(compgen -W "list show validate --bash --config" -- "${COMP_WORDS[$COMP_CWORD]}" ))
|
||||
\\ COMPREPLY=($(compgen -W "list show validate --bash --fish --help --time --config" -- "${COMP_WORDS[$COMP_CWORD]}" ))
|
||||
\\ fi
|
||||
\\}
|
||||
\\complete -F _zig_totp_completions zig-totp
|
||||
\\
|
||||
;
|
||||
try std.io.getStdOut().writer().print("{s}\n", .{msg});
|
||||
std.process.exit(0);
|
||||
} else if (eql(u8, "--fish", arg)) {
|
||||
const msg =
|
||||
\\# all subcommands zig-totp knows, is useful later
|
||||
\\set -l commands list show validate
|
||||
\\
|
||||
\\# disable file completion so that we have more control
|
||||
\\complete -c zig-totp --no-files
|
||||
\\
|
||||
\\# parameters:
|
||||
\\complete -c zig-totp -l bash -d "generate bash integration code"
|
||||
\\complete -c zig-totp -l fish -d "generate fish integration code"
|
||||
\\complete -c zig-totp -l config --force-files --require-parameter -d 'override config location'
|
||||
\\complete -c zig-totp -l time --require-parameter -d 'set time'
|
||||
\\complete -c zig-totp -l help -d "show help"
|
||||
\\
|
||||
\\# sub commands:
|
||||
\\complete -c zig-totp -n "not __fish_seen_subcommand_from $commands" -a 'list' -d 'show available authenticators'
|
||||
\\
|
||||
\\complete -c zig-totp -n "not __fish_seen_subcommand_from $commands" -a 'show' -d 'show the code for an authenticator'
|
||||
\\complete -c zig-totp -n "__fish_prev_arg_in show" -xa '(zig-totp list)'
|
||||
\\
|
||||
\\complete -c zig-totp -n "not __fish_seen_subcommand_from $commands" -a 'validate' -d 'validate code for an authenticator'
|
||||
\\complete -c zig-totp -n "__fish_prev_arg_in validate" -xa '(zig-totp list)'
|
||||
\\
|
||||
;
|
||||
|
||||
try std.io.getStdOut().writer().print("{s}\n", .{msg});
|
||||
std.process.exit(0);
|
||||
} else if (eql(u8, "--config", arg)) {
|
||||
i += 1;
|
||||
if (i >= args.len) {
|
||||
std.debug.print("expected a path after parameter '--config'\n see 'zig-totp' for help\n", .{});
|
||||
std.debug.print("expected a path after parameter '--config'\n see 'zig-totp --help' for help\n", .{});
|
||||
return error.MissingConfigLocation;
|
||||
}
|
||||
result.config_location = args[i];
|
||||
@@ -229,6 +267,13 @@ fn parseArgs(allocator: Allocator, args: []const []const u8) !Args {
|
||||
result.show_help = true;
|
||||
} else if (eql(u8, "--version", arg)) {
|
||||
result.print_version = true;
|
||||
} else if (eql(u8, "--time", arg)) {
|
||||
i += 1;
|
||||
if (i >= args.len) {
|
||||
std.debug.print("expected a path after parameter '--time'\n see 'zig-totp --help' for help\n", .{});
|
||||
return error.MissingConfigLocation;
|
||||
}
|
||||
result.time = args[i];
|
||||
} else if (eql(u8, "list", arg)) {
|
||||
result.list = true;
|
||||
} else if (eql(u8, "show", arg)) {
|
||||
@@ -486,14 +531,14 @@ test "zero padding" {
|
||||
test "authenticator generate code with 6 digits" {
|
||||
const authenticator = Authenticator{ .url = OtpAuthUrl{ .name = "TestAuthenticator", .secretEncoded = "HJ5PX4IFQKD37HFNXLJYBAVD5G6NMMUOKUCDXJ4XTLUUBWSXRXEN5SR4MLAZA5M2", .url = "not needed", .period = 30, .digits = 6, .algorithm = "SHA1" } };
|
||||
|
||||
const code = try authenticator.code(std.testing.allocator, _closure_return_1725695340);
|
||||
const code = try authenticator.code(std.testing.allocator, 1725695340);
|
||||
defer std.testing.allocator.free(code);
|
||||
try std.testing.expectEqualStrings("218139", code);
|
||||
}
|
||||
test "authenticator generate code with 10 digits and zero padding" {
|
||||
const authenticator = Authenticator{ .url = OtpAuthUrl{ .name = "TestAuthenticator", .secretEncoded = "HJ5PX4IFQKD37HFNXLJYBAVD5G6NMMUOKUCDXJ4XTLUUBWSXRXEN5SR4MLAZA5M2", .url = "not needed", .period = 60, .digits = 10, .algorithm = "SHA1" } };
|
||||
|
||||
const code = try authenticator.code(std.testing.allocator, _closure_return_1725695340);
|
||||
const code = try authenticator.code(std.testing.allocator, 1725695340);
|
||||
defer std.testing.allocator.free(code);
|
||||
try std.testing.expectEqualStrings("0844221464", code);
|
||||
}
|
||||
@@ -505,117 +550,89 @@ test "testcases from https://www.rfc-editor.org/rfc/rfc6238#appendix-A" {
|
||||
|
||||
const secretForSha512 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA"; // hex: "313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334353637383930"
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, _closure_return_59, "94287082");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, 59, "94287082");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, _closure_return_1111111109, "07081804");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, 1111111109, "07081804");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, _closure_return_1111111111, "14050471");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, 1111111111, "14050471");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, _closure_return_1234567890, "89005924");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, 1234567890, "89005924");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, _closure_return_2000000000, "69279037");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, 2000000000, "69279037");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, _closure_return_20000000000, "65353130");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, 20000000000, "65353130");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, _closure_return_59, "46119246");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, 59, "46119246");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, _closure_return_1111111109, "68084774");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, 1111111109, "68084774");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, _closure_return_1111111111, "67062674");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, 1111111111, "67062674");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, _closure_return_1234567890, "91819424");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, 1234567890, "91819424");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, _closure_return_2000000000, "90698825");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, 2000000000, "90698825");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, _closure_return_20000000000, "77737706");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha256, .url = "", .period = 30, .digits = 8, .algorithm = "SHA256" }, 20000000000, "77737706");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, _closure_return_59, "90693936");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, 59, "90693936");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, _closure_return_1111111109, "25091201");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, 1111111109, "25091201");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, _closure_return_1111111111, "99943326");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, 1111111111, "99943326");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, _closure_return_1234567890, "93441116");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, 1234567890, "93441116");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, _closure_return_2000000000, "38618901");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, 2000000000, "38618901");
|
||||
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, _closure_return_20000000000, "47863826");
|
||||
try testTOTP(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha512, .url = "", .period = 30, .digits = 8, .algorithm = "SHA512" }, 20000000000, "47863826");
|
||||
}
|
||||
|
||||
fn testTOTP(otpAuthUrl: OtpAuthUrl, timeFunc: *const fn () i64, expected: []const u8) !void {
|
||||
fn testTOTP(otpAuthUrl: OtpAuthUrl, time: i64, expected: []const u8) !void {
|
||||
const authenticator = Authenticator{ .url = otpAuthUrl };
|
||||
|
||||
const code = try authenticator.code(std.testing.allocator, timeFunc);
|
||||
const code = try authenticator.code(std.testing.allocator, time);
|
||||
defer std.testing.allocator.free(code);
|
||||
try std.testing.expectEqualStrings(expected, code);
|
||||
}
|
||||
|
||||
fn _closure_return_1725695340() i64 {
|
||||
return 1725695340;
|
||||
}
|
||||
|
||||
fn _closure_return_59() i64 {
|
||||
return 59;
|
||||
}
|
||||
|
||||
fn _closure_return_1111111109() i64 {
|
||||
return 1111111109;
|
||||
}
|
||||
|
||||
fn _closure_return_1111111111() i64 {
|
||||
return 1111111111;
|
||||
}
|
||||
|
||||
fn _closure_return_1234567890() i64 {
|
||||
return 1234567890;
|
||||
}
|
||||
|
||||
fn _closure_return_2000000000() i64 {
|
||||
return 2000000000;
|
||||
}
|
||||
|
||||
fn _closure_return_20000000000() i64 {
|
||||
return 20000000000;
|
||||
}
|
||||
|
||||
test "secret is not Base32 - invalid padding" {
|
||||
const authenticator = Authenticator{ .url = OtpAuthUrl{ .name = "", .secretEncoded = "H", .url = "", .period = 60, .digits = 6, .algorithm = "SHA1" } };
|
||||
|
||||
const actualError = authenticator.code(std.testing.allocator, _closure_return_1725695340);
|
||||
const actualError = authenticator.code(std.testing.allocator, 1725695340);
|
||||
try std.testing.expectError(Base32Error.InvalidPadding, actualError);
|
||||
}
|
||||
test "secret is not Base32 - invalid padding, input has too many bytes" {
|
||||
const authenticator = Authenticator{ .url = OtpAuthUrl{ .name = "", .secretEncoded = "HAX", .url = "", .period = 60, .digits = 6, .algorithm = "SHA1" } };
|
||||
|
||||
const actualError = authenticator.code(std.testing.allocator, _closure_return_1725695340);
|
||||
const actualError = authenticator.code(std.testing.allocator, 1725695340);
|
||||
try std.testing.expectError(Base32Error.InvalidPadding, actualError);
|
||||
}
|
||||
test "secret is not Base32 - invalid padding, trailing bits are not 0" {
|
||||
const authenticator = Authenticator{ .url = OtpAuthUrl{ .name = "", .secretEncoded = "HX", .url = "", .period = 60, .digits = 6, .algorithm = "SHA1" } };
|
||||
|
||||
const actualError = authenticator.code(std.testing.allocator, _closure_return_1725695340);
|
||||
const actualError = authenticator.code(std.testing.allocator, 1725695340);
|
||||
try std.testing.expectError(Base32Error.InvalidPadding, actualError);
|
||||
}
|
||||
|
||||
test "secret is not Base32 - invalid character" {
|
||||
const authenticator = Authenticator{ .url = OtpAuthUrl{ .name = "", .secretEncoded = "0", .url = "", .period = 60, .digits = 6, .algorithm = "SHA1" } };
|
||||
|
||||
const actualError = authenticator.code(std.testing.allocator, _closure_return_1725695340);
|
||||
const actualError = authenticator.code(std.testing.allocator, 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" }, 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" }, 1111111109, "07081804", true);
|
||||
|
||||
try testValidate(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, _closure_return_1111111109, "12345678", false);
|
||||
try testValidate(OtpAuthUrl{ .name = "", .secretEncoded = secretForSha1, .url = "", .period = 30, .digits = 8, .algorithm = "SHA1" }, 1111111109, "12345678", false);
|
||||
}
|
||||
fn testValidate(otpAuthUrl: OtpAuthUrl, timeFunc: *const fn () i64, expected: []const u8, expectedMatch: bool) !void {
|
||||
fn testValidate(otpAuthUrl: OtpAuthUrl, time: i64, expected: []const u8, expectedMatch: bool) !void {
|
||||
const authenticator = Authenticator{ .url = otpAuthUrl };
|
||||
|
||||
const match = try authenticator.validate(std.testing.allocator, expected, timeFunc);
|
||||
const match = try authenticator.validate(std.testing.allocator, expected, time);
|
||||
try std.testing.expectEqual(expectedMatch, match);
|
||||
}
|
||||
|
||||
102
src/time.zig
Normal file
102
src/time.zig
Normal file
@@ -0,0 +1,102 @@
|
||||
const std = @import("std");
|
||||
|
||||
const expect = std.testing.expect;
|
||||
const expectEqualStrings = std.testing.expectEqualStrings;
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const TimeError = error{ParsingError};
|
||||
|
||||
pub const Time = struct {
|
||||
/// expecting a date in the format yyyy-MM-dd'T'HH:mm:ss
|
||||
/// returning a unix timestamp
|
||||
/// does not handle leap seconds
|
||||
/// only handles UTC
|
||||
pub fn parseIso8601(data: []const u8) !i64 {
|
||||
const format = "nnnn-nn-nnTnn:nn:nn";
|
||||
// 0123456789012345678
|
||||
|
||||
// validate the format
|
||||
if (data.len != 19) {
|
||||
return TimeError.ParsingError;
|
||||
}
|
||||
|
||||
for (format, 0..) |formatChar, index| {
|
||||
if (index >= data.len) {
|
||||
return TimeError.ParsingError;
|
||||
}
|
||||
|
||||
const c = data[index];
|
||||
switch (formatChar) {
|
||||
'n' => {
|
||||
if (!isDigit(c)) return TimeError.ParsingError;
|
||||
},
|
||||
else => {
|
||||
if (formatChar != c) return TimeError.ParsingError;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const year = try std.fmt.parseInt(i64, data[0..4], 10);
|
||||
const month = try std.fmt.parseInt(i64, data[5..7], 10);
|
||||
const day = try std.fmt.parseInt(i64, data[8..10], 10);
|
||||
const hour = try std.fmt.parseInt(i64, data[11..13], 10);
|
||||
const minute = try std.fmt.parseInt(i64, data[14..16], 10);
|
||||
const second = try std.fmt.parseInt(i64, data[17..19], 10);
|
||||
|
||||
const unixTime: i64 = (year - 1970) * 365 * 24 * 3600 //
|
||||
+ leapDays(year, month) * 24 * 3600 //
|
||||
+ dayInYear(month, day) * 24 * 3600 //
|
||||
+ hour * 3600 //
|
||||
+ minute * 60 //
|
||||
+ second;
|
||||
|
||||
return unixTime;
|
||||
}
|
||||
|
||||
fn isDigit(byte: u8) bool {
|
||||
return byte >= '0' and byte <= '9';
|
||||
}
|
||||
|
||||
fn dayInYear(month: i64, day: i64) i64 {
|
||||
const daysPerMonth = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
||||
var result: i64 = 0;
|
||||
var i: u8 = 0;
|
||||
while (i < (month - 1)) : (i += 1) {
|
||||
result += daysPerMonth[i];
|
||||
}
|
||||
result += day - 1;
|
||||
return result;
|
||||
}
|
||||
fn leapDays(year: i64, month: i64) i64 {
|
||||
var i: u64 = 1972;
|
||||
var result: i64 = 0;
|
||||
while (i < year) : (i += 1) {
|
||||
if (@mod(i, 4) == 0 and (i % 100 != 0 or @mod(i, 400) == 0)) {
|
||||
result += 1;
|
||||
}
|
||||
}
|
||||
if (month > 2 and @mod(year, 4) == 0 and (@mod(year, 100) != 0 or @mod(year, 400) == 0)) {
|
||||
result += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
test "parsing error 'x025-01-01T00:00:00'" {
|
||||
try std.testing.expectError(TimeError.ParsingError, Time.parseIso8601("x025-01-01T00:00:00"));
|
||||
}
|
||||
|
||||
test "parsing error '2025-01-01'" {
|
||||
try std.testing.expectError(TimeError.ParsingError, Time.parseIso8601("2025-01-01T"));
|
||||
}
|
||||
|
||||
//test "parsing validation successfull" {
|
||||
// const y = try Time.parseIso8601("2025-12-13T01:02:03");
|
||||
// try expectEqual(12, y);
|
||||
//}
|
||||
|
||||
test "parse 2025-02-18T18:37:45, expecting 1739903865" {
|
||||
const unixTime = try Time.parseIso8601("2025-02-18T18:37:45");
|
||||
try expectEqual(1739903865, unixTime);
|
||||
}
|
||||
Reference in New Issue
Block a user