Skip to content

Instantly share code, notes, and snippets.

@isometry
Last active March 20, 2021 13:20
Show Gist options
  • Save isometry/cfa704c94811363f005a4f0f69fb6690 to your computer and use it in GitHub Desktop.
Save isometry/cfa704c94811363f005a4f0f69fb6690 to your computer and use it in GitHub Desktop.
HOTP/TOTP Secret Generator to enable use of third-party authenticator apps as Soft Tokens with Duo (or similar)
#!/usr/bin/env python3
"""
HOTP/TOTP Secret Generator to enable use of third-party authenticator apps as Soft Tokens with Duo (or similar)
"""
from base64 import b32encode
from secrets import token_bytes
from urllib.parse import quote
class OTP:
def __init__(self, username, issuer, type="hotp", bits=160, algorithm="SHA1", digits=6, counter=0, period=30):
self.username = username
self.issuer = issuer
self.type = type
self.algorithm = algorithm
self.digits = digits
self.counter = counter
self.period = period
self.secret = token_bytes(bits//8)
@property
def otpauth(self):
params = {
"secret": b32encode(self.secret).decode("utf-8"),
"issuer": quote(self.issuer),
"algorithm": self.algorithm,
"digits": self.digits,
}
if self.type == "hotp":
params["counter"] = self.counter
elif self.type == "totp":
params["period"] = self.period
config = "&".join([f"{key}={value}" for key, value in params.items()])
return f"otpauth://{self.type}/{quote(self.issuer)}:{quote(self.username)}?{config}"
@property
def duo(self):
if self.type == "hotp":
return f"{self.username}/softtoken,{self.secret.hex().upper()},{self.counter}"
else:
return f"{self.username}/softtoken,{self.secret.hex().upper()},{self.period}"
@property
def qrcode(self):
from qrcode import make as make_qrcode
return make_qrcode(self.otpauth)
def parse_args():
from argparse import ArgumentParser
parser = ArgumentParser(description="OTP Secret Generator")
parser.add_argument("-t", "--type", choices=("hotp", "totp"), default="hotp", help="OTP type (default: %(default)s)")
parser.add_argument("users", metavar="USER", type=str, nargs="+", help="Username")
parser.add_argument("-o", "--output", choices=("uri", "png", "tmp"), default="tmp",
help="Output format (default: %(default)s)")
parser_common = parser.add_argument_group("Common options")
parser_common.add_argument("-i", "--issuer", action="store", default="Duo", help="Issuer (default: %(default)s)")
parser_common.add_argument("-b", "--bits", type=int, choices=(160, 200, 240, 280, 320), default=160,
help="Bits in OTP shared secret (default: %(default)d)")
parser_common.add_argument("-a", "--algorithm", choices=("SHA1", "SHA256"), default="SHA1",
help="OTP algorithm (default: %(default)s)")
parser_common.add_argument("-d", "--digits", type=int, choices=(6, 7, 8), default=6,
help="OTP digits (default: %(default)d)")
parser_hotp = parser.add_argument_group("HOTP options")
parser_hotp.add_argument("-c", "--counter", type=int, default=0, help="HOTP counter start value (default: %(default)d)")
parser_totp = parser.add_argument_group("TOTP options")
parser_totp.add_argument("-p", "--period", type=int, choices=(30, 60), default=30,
help="TOTP refresh period (default: %(default)d)")
return parser.parse_args()
def main():
args = parse_args()
for user in args.users:
secret = OTP(username=user, issuer=args.issuer, type=args.type, bits=args.bits, algorithm=args.algorithm,
digits=args.digits, counter=args.counter, period=args.period)
print(secret.duo)
if args.output == "uri":
print(secret.otpauth)
elif args.output == "png":
with open(f"{user}.png", "wb") as img:
secret.qrcode.save(img, format="PNG")
else:
secret.qrcode.show()
if __name__ == "__main__":
main()
usage: otp_secret_generator.py [-h] [-t {hotp,totp}] [-o {uri,png,tmp}]
                               [-i ISSUER] [-b {160,200,240,280,320}]
                               [-a {SHA1,SHA256}] [-d {6,7,8}] [-c COUNTER]
                               [-p {30,60}]
                               USER [USER ...]

OTP Secret Generator

positional arguments:
  USER                  Username

optional arguments:
  -h, --help            show this help message and exit
  -t {hotp,totp}, --type {hotp,totp}
                        OTP type (default: hotp)
  -o {uri,png,tmp}, --output {uri,png,tmp}
                        Output format (default: tmp)

Common options:
  -i ISSUER, --issuer ISSUER
                        Issuer (default: Duo)
  -b {160,200,240,280,320}, --bits {160,200,240,280,320}
                        Bits in OTP shared secret (default: 160)
  -a {SHA1,SHA256}, --algorithm {SHA1,SHA256}
                        OTP algorithm (default: SHA1)
  -d {6,7,8}, --digits {6,7,8}
                        OTP digits (default: 6)

HOTP options:
  -c COUNTER, --counter COUNTER
                        HOTP counter start value (default: 0)

TOTP options:
  -p {30,60}, --period {30,60}
                        TOTP refresh period (default: 30)
  • uri output displays the otpauth://... URI.
  • tmp output shows the generated QR code via a temporary file.
  • png output saves the generated QR code to <username>.png.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment