[Crypto] Time-based one-time password (TOTP) for 2FA, part II: YubiKey

Previously.

Now YubiKey. It can store TOTP secrets inside and time-based password can be viewed via corresponding software. You see here also my other accounts. BTW, the exact timestamp when I took screenshot is 2026-09-06 17-57-35 EEST. Will you be able to find HMAC key for all of them? It's considered not possible, despite the fact that SHA1 is (partially?) broken.

Also, the YubiKey app, running on android with YubiKey connected via USB-C. The app doesn't allow taking screenshots, so here is a photo. Exact timestamp is 2026-09-06 18-27-15 EEST.

YubiKey has a nice bonus: HMAC keys are stored internally and can't be extracted (at least, in absence of known bugs).

However, there is a weak spot. As of linux, there is a command-line YubiKey program: ykman. The oath.py file in $HOME/.local/lib/python3.12/site-packages/yubikit can be patched a bit:

% diff -u oath.py-bak oath.py
--- oath.py-bak 2026-08-06 23:07:46.758828187 +0300
+++ oath.py     2026-09-13 15:36:37.616423075 +0300
@@ -497,7 +497,8 @@

         :param timestamp: A timestamp used for the TOTP challenge.
         """
-        timestamp = int(timestamp or time())
+        timestamp = 1900000000 # Sunday, March 17, 2030 at 5:46:40 PM (UTC)
         challenge = _get_challenge(timestamp, DEFAULT_PERIOD)
         logger.debug(f"Calculating all codes for time={timestamp}")

Now run ykman:

% ykman oath accounts code
Enter the password:
lichess.org:temp_user12              452373

% date -d @1900000000
Sun 17 Mar 19:46:40 EET 2030

The code is indeed for that date, we can verify it after running this:

#!/usr/bin/env python3
import hmac, hashlib, time, base64, sys, re

def hotp(key: bytes, ctr: int, length: int) -> str:
    m=hmac.new(key, digestmod=hashlib.sha1)
    m.update(ctr.to_bytes(8, 'big'))
    mac = m.digest()

    offset = mac[-1] & 0xf
    truncated = bytearray(mac[offset:offset+4])
    truncated[0] &= 0x7f
    value = int.from_bytes(truncated, 'big') % (10**length)

    return str(value).rjust(length, '0')

URL=sys.argv[1]
result=re.search('secret=(.*)&', URL)
assert result!=None
secret=result.group(1)
s=base64.b32decode(secret.upper())
t=1900000000 # Sunday, March 17, 2030 at 5:46:40 PM (UTC)
print ("The code:", hotp(s,int(t/30),6))
time_remaining=int(30-(t % 30)) # remainder from division
print (f"The code is valid for {time_remaining} seconds")

An attacker can do so, getting your physical YubiKey for some time, like several minutes, and he/she can generate key(s) for future use. And then login to your account (assuming he/she also knows your password) during that exact 30-seconds interval. This is not impossible practically. This should be kept in mind. YubiKey has a feature of setting password for TOTP accounts protection.

(the post first published at 20260914.)


List of my other blog posts. Subscribe to my news feed,
If you noticed a typo/bug/error or have any suggestions, do not hesitate to drop me a note: my emails. Or use my zulip for feedback. Or Discord. Thanks in advance!
Also, among my services is writing examples-rich manuals, references and help files. If you like my work and want something similar for your (commercial) product: contact me.
If you enjoy my work, you can support it on patreon.
Some time ago (before 24-Mar-2025) there was Disqus JS script for comments. I dropped it --- it was so motley, distracting, animated, with too much ads. I never liked it. Also, comments din't appeared correctly (Disqus was buggy). Also, my blog is too chamberlike --- not many people write comments here. So I decided to switch to the model I once had at least in 2020 --- send me your comments by email (don't forget to include URL to this blog post) and I will copy&paste it here manually.
Let's party like it's ~1993-1996, in this ultimate, radical and uncompromisingly primitive pre-web1.0-style blog and website. This website is best viewed under lynx/links/elinks/w3m.