Skip to content

Instantly share code, notes, and snippets.

@notpushkin
Last active April 15, 2025 09:37
Show Gist options
  • Save notpushkin/7ac32ddf35a0c73bc6f181a1b5dffa4f to your computer and use it in GitHub Desktop.
Save notpushkin/7ac32ddf35a0c73bc6f181a1b5dffa4f to your computer and use it in GitHub Desktop.
Minimal oathtool(1) reimplementation in Python
#!/usr/bin/env python3
# (c) 2025 Alexander Pushkov <[email protected]>
# Based on https://github.com/susam/mintotp, copyright (c) 2019 Susam Pal
# SPDX-License-Identifier: MIT
import argparse
import base64
import hmac
import struct
import time
from typing import Literal
DigestType = Literal["sha1", "sha256", "sha512"]
def hotp(key: bytes, counter: int, digits: int = 6, digest: DigestType = "sha1"):
mac = hmac.digest(key, struct.pack(">Q", counter), digest)
offset = mac[-1] & 0x0F
binary: int = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF
return str(binary)[-digits:].zfill(digits)
def totp(key: bytes, time_step: int = 30, digits: int = 6, digest: DigestType = "sha1"):
return hotp(key, int(time.time() / time_step), digits, digest)
def main():
parser = argparse.ArgumentParser(
description="Generate and validate OATH one-time passwords."
)
parser.add_argument(
"key",
nargs="?",
help="The key for generating OTPs",
)
parser.add_argument(
"-V",
"--version",
action="version",
help="Print version and exit",
version="pyoathtool 0.1.0",
)
parser.add_argument(
"--totp",
"--totp=SHA1",
action="store_true",
help="Use time-variant TOTP mode",
)
parser.add_argument(
"--totp=SHA256",
action="store_true",
help="Use time-variant TOTP mode with SHA-256 algorithm",
)
parser.add_argument(
"--totp=SHA512",
action="store_true",
help="Use time-variant TOTP mode with SHA-512 algorithm",
)
parser.add_argument(
"-b",
"--base32",
action="store_true",
help="Use base32 encoding of KEY instead of hex (default=off)",
)
parser.add_argument(
"-s",
"--time-step-size",
type=int,
help="TOTP time-step duration in seconds (default=30)",
default=30,
)
parser.add_argument(
"-d",
"--digits",
type=int,
help="Number of digits in one-time password",
default=6,
)
args = parser.parse_args()
if not args.key:
print("Key is required")
exit(1)
if not args.base32:
print("Only --base32 is supported")
exit(1)
digest: DigestType
if args.totp:
digest = "sha1"
elif getattr(args, "totp=SHA256"):
digest = "sha256"
elif getattr(args, "totp=SHA512"):
digest = "sha512"
else:
print("Only --totp is supported")
exit(1)
key = base64.b32decode(args.key.upper() + "=" * ((8 - len(args.key)) % 8))
print(
totp(
key,
time_step=args.time_step_size,
digits=args.digits,
digest=digest,
)
)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment