Last active
April 15, 2025 09:37
-
-
Save notpushkin/7ac32ddf35a0c73bc6f181a1b5dffa4f to your computer and use it in GitHub Desktop.
Minimal oathtool(1) reimplementation in Python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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