Skip to content

Instantly share code, notes, and snippets.

@habnabit
Last active April 26, 2016 04:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# 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
@click.group(context_settings=dict(
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']),
show_default=True,
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']),
show_default=True,
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']),
show_default=True,
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
ACCOUNT_NAME is 'alice@gmail.com'.
"""
@main.command()
@click.pass_context
@click.option('-p', '--period', default='30', type=click.Choice(['30', '60']),
show_default=True,
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)
@main.command()
@click.pass_context
@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)},
**ctx.parent.params)
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
params.update({
'secret': encoded_secret,
'issuer': issuer,
'algorithm': algorithm,
'digits': digits,
})
qrdata = 'otpauth://%s/%s?%s' % (
typ, urllib.quote(issuer + ':' + account_name),
urllib.urlencode(params))
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()
webbrowser.open('file://' + outfile.name)
click.echo(userfile_line)
click.echo('QR: ' + qrdata)
click.pause()
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment