|
#!/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() |