# Time-Based One-Time Password Generator

**URL:** <https://ziggit.dev/t/time-based-one-time-password-generator/16270>\
**Category:** Showcase\
**Created:** [June 20, 2026, 10:20pm UTC](https://ziggit.dev/t/time-based-one-time-password-generator/16270 "2026-06-20T22:20:01Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![unkempt6057](https://ziggit.dev/letter_avatar_proxy/v4/letter/u/3be4f8/32.png) [@unkempt6057](https://ziggit.dev/u/unkempt6057)\
**Post date:** [June 20, 2026, 10:20pm UTC](https://ziggit.dev/t/time-based-one-time-password-generator/16270/1 "2026-06-20T22:20:01Z")

</div>

# [TOTP]:

* * *

A tool to generate time-based one time passwords straight from the command line. Easy to wrap into a script that copies it to the clipboard for easy 2FA, so I don’t need to have my phone in the office.

> **[GitHub - matt-savvy/totp](https://github.com/matt-savvy/totp)**
>
> Contribute to matt-savvy/totp development by creating an account on GitHub.

For some reason, I’ve always wanted to implement this myself and it turned out to be a fun project. Implemented straight from the RFCs, which I’d never personally done before.

[RFC-6238](https://www.rfc-editor.org/info/rfc6238/) presents the Time-Based OTP, which builds on [RFC 4226](https://www.rfc-editor.org/info/rfc4226/) for the HMAC-Based OTP. Zig’s std lib has the HMAC functions and algorithms, so fortunately/sadly I did not have to implement those from scratch.

The time-based OTP alg is pretty cool. Essentially, it takes a shared secret and a shared start time (usually the Unix epoch), interval size (usually 30s), and number of digits (usually 6). When you need a password, you calculate how many intervals have passed since the shared start time, and use that as the input to the HOTP (HMAC based one-time password).

The HOTP is a similar idea, except using a shared counter instead of time offset. You then use an HMAC alg on the secret and input value to get a hash. You look at a certain byte, its value tells you where to grab other bytes from, you do a little math, and you end up with a 6 digit code.

This is how your authenticator apps are always ticking away and can generate a secret even if you have no signal.

I also needed a base 32 decoder, which I wrote (most of) an implementation of from scratch. It only handles cases where the encoded data fits evenly into the base 32 chars because that’s really all I needed. If someone gives me a key with the padding `=` chars, I suppose I’ll handle it then.

To decode, you break the input stream into groups of 8 base32 chars, each char represents a 5-bit value, and then you treat that 8 x 5-bit group as a 5 x 8-bit group. I did this by putting 8 `u5` values into a `packed struct` and then `@bitCast`ing into a `[5]u8`. I doubt this is the most effective way to do it, but I thought it was a fun opportunity to use Zig’s arbitrarily-sized integers.

Along the way, I learned a little bit about re-interpreting bytes ([@bitCast size mismatch for macos and windows but not linux](https://ziggit.dev/t/bitcast-size-mismatch-for-macos-and-windows-but-not-linux/15906) and [Reinterpreting bytes](https://ziggit.dev/t/reinterpreting-bytes/15374)).

It’s still rough around the edges and haven’t implemented all the options, but it’s working for me and gives me a little satisfaction every time I need to generate a 2FA now. Still fairly new to Zig, so if anyone has any suggestions to how to better structure (I tried to keep all my side-effects in `main` and have everything else be essentially pure functions) or test things (not sure how to test `main`, for example), I’m all ears.

Note: this was only tested on a little-endian machine, so if you have a big-endian, please give it a shot and let me know if the tests pass and/or it works for you! Or if you have a suggestion on how to easily test that.

---

<div class="post-metadata">

**Author:** ![Sze](https://ziggit.dev/user_avatar/ziggit.dev/sze/32/496_2.png) [@Sze](https://ziggit.dev/u/Sze)\
**Post date:** [June 20, 2026, 10:54pm UTC](https://ziggit.dev/t/time-based-one-time-password-generator/16270/2 "2026-06-20T22:54:34Z")

</div>

> [@unkempt6057](#):
>
> Note: this was only tested on a little-endian machine, so if you have a big-endian, please give it a shot and let me know if the tests pass and/or it works for you! Or if you have a suggestion on how to easily test that.

> [@Tagged pointer implementation](https://ziggit.dev/t/tagged-pointer-implementation/15829/7):
>
> [https://ziglang.org/documentation/master/#packed-struct](https://ziglang.org/documentation/master/#packed-struct): Each field of a packed struct is interpreted as a logical sequence of bits, arranged from least to most significant. So if you intend to use the low bit (alignment bits) as the tag then I think the tag needs to be the first field. Here is my suggestion for how to test something like this: mkdir testexample cd testexample zig init src/main.zig const std = @import("std"); // NOTE minimal main.zig so that you don't get confused by …

---

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [June 21, 2026, 7:23am UTC](https://ziggit.dev/t/time-based-one-time-password-generator/16270/3 "2026-06-21T07:23:02Z")

</div>

Really cool. Will def try it out tomorrow at work.

Right now Im using `oathtool` for creating a one time 2fa password and copy it to the clipboard (saved in a .desktop file, so I can run it with `fuzzel`). Will try to update this workflow with your tool.

---

<div class="post-metadata">

**Author:** ![unkempt6057](https://ziggit.dev/letter_avatar_proxy/v4/letter/u/3be4f8/32.png) [@unkempt6057](https://ziggit.dev/u/unkempt6057)\
**Post date:** [June 21, 2026, 2:11pm UTC](https://ziggit.dev/t/time-based-one-time-password-generator/16270/4 "2026-06-21T14:11:17Z")

</div>

Sweet, LMK how that works.

I was previously using `oathtool`. My only gripe was that I had to trim the trailing newline char before piping it into my clipboard, which maybe your wrapper script is also doing. So for this reason, my tool doesn’t include the trailing newline.

---

<div class="post-metadata">

**Author:** ![Gaweringo](https://ziggit.dev/user_avatar/ziggit.dev/gaweringo/32/7550_2.png) [@Gaweringo](https://ziggit.dev/u/Gaweringo)\
**Post date:** [June 21, 2026, 4:30pm UTC](https://ziggit.dev/t/time-based-one-time-password-generator/16270/5 "2026-06-21T16:30:27Z")

</div>

You could try [std.Io.File.isTty](https://ziglang.org/documentation/master/std/#std.Io.File.isTty) to remove or add the new line, based on if the output is being piped or not.

---

<div class="post-metadata">

**Author:** ![unkempt6057](https://ziggit.dev/letter_avatar_proxy/v4/letter/u/3be4f8/32.png) [@unkempt6057](https://ziggit.dev/u/unkempt6057)\
**Post date:** [June 21, 2026, 6:22pm UTC](https://ziggit.dev/t/time-based-one-time-password-generator/16270/6 "2026-06-21T18:22:38Z")

</div>

Oh very cool, that seems to do the trick
