Skip to content

Instantly share code, notes, and snippets.

Last active April 26, 2016 04:09
Show Gist options
  • Save habnabit/e3f0691a932a70b8646f1e5f724490c7 to your computer and use it in GitHub Desktop.
Save habnabit/e3f0691a932a70b8646f1e5f724490c7 to your computer and use it in GitHub Desktop.
OATH (HOTP/TOTP) QR codes with UsersFile format output
# Copyright 2016 Google Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import binascii
import os
import random
import tempfile
import urllib
import webbrowser
import click
import pyqrcode
def check_no_colon(ctx, param, value):
if ':' in value:
raise click.BadParameter('":" is not allowed anywhere.')
return value
help_option_names=('-h', '--help'),
@click.option('--secret-len', '-l', default=30, show_default=True,
help=('The length of the secret, in bytes. RFC 4226 says this'
' must be >=8 and recommends >=20. The default is longer'
" and won't cause any negative effects."))
@click.option('--format', '-f',
default='base32', type=click.Choice(['base32', 'hex']),
help=('The encoding to use for the secret. Most things will want'
' base32.'))
@click.option('--algorithm', '-a',
default='SHA1', type=click.Choice(['SHA1', 'SHA256', 'SHA512']),
help=('The hash to use for the HMAC. Most things only know how'
' to handle SHA1.'))
@click.option('--digits', '-d', default='6', type=click.Choice(['6', '8']),
help=('The number of digits in the code. 6 is common, but 8 is'
' widely supported.'))
@click.argument('issuer', callback=check_no_colon)
@click.argument('account-name', callback=check_no_colon)
def main(**ign):
Generate a HOTP/TOTP secret, show a QR code to allow authenticators to add
the secret, and emit a UsersFile-style credential.
The ISSUER is a string value indicating the provider or service this
account is associated with. The ACCOUNT_NAME is used to identify which
account a key is associated with. Neither are allowed to contain a colon
An example of an ISSUER is 'Big Corporation', and an example of an
@click.option('-p', '--period', default='30', type=click.Choice(['30', '60']),
help=('The TOTP time step. Most things will want 30.'))
def totp(ctx, period):
Generate a TOTP otpauth:// URI.
generate_qr(typ='totp', params={'period': period}, **ctx.parent.params)
@click.option('-c', '--counter', type=int,
help=('The HOTP counter initial value. If unspecified, it will'
' be randomly selected from [0, 2^32).'))
def hotp(ctx, counter):
Generate a HOTP otpauth:// URI.
if counter is None:
counter = random.SystemRandom().randrange(2 ** 32)
generate_qr(typ='hotp', params={'counter': str(counter)},
def generate_qr(secret_len, format, algorithm, digits, issuer, account_name,
typ, params):
secret = os.urandom(secret_len)
hex_secret = binascii.b2a_hex(secret)
if format == 'base32':
encoded_secret = base64.b32encode(secret)
elif format == 'hex':
encoded_secret = hex_secret
'secret': encoded_secret,
'issuer': issuer,
'algorithm': algorithm,
'digits': digits,
qrdata = 'otpauth://%s/%s?%s' % (
typ, urllib.quote(issuer + ':' + account_name),
userfile_line = 'HOTP/%s/%s\t_USERNAME_\t-\t%s' % (
'T' + params['period'] if 'period' in params else 'E',
digits, hex_secret)
if 'counter' in params:
userfile_line += '\t%s' % params['counter']
code = pyqrcode.create(qrdata)
with tempfile.NamedTemporaryFile(suffix='.svg') as outfile:
code.svg(outfile, scale=2)
outfile.flush()'file://' +
click.echo('QR: ' + qrdata)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment