diff --git a/yubioath/cli/__main__.py b/yubioath/cli/__main__.py index 1a77add6..c9cc7772 100644 --- a/yubioath/cli/__main__.py +++ b/yubioath/cli/__main__.py @@ -30,7 +30,7 @@ from __future__ import print_function from .. import __version__ from ..core.ccid import open_scard -from ..core.standard import TYPE_HOTP, TYPE_TOTP +from ..core.standard import ALG_SHA1, ALG_SHA256, TYPE_HOTP, TYPE_TOTP from ..core.utils import parse_uri from ..core.exc import NoSpaceError from .keystore import get_keystore @@ -136,23 +136,44 @@ def show(ctx, query, slot1, slot2, timestamp): @click.option('-S', '--destination', type=click.IntRange(0, 2), default=0) @click.option('-N', '--name', required=False, help='Credential name.') @click.option('-A', '--oath-type', type=click.Choice(['totp', 'hotp']), - default='totp', help='OATH algorithm.') + default='totp', help='Specify whether this is a time or counter-based OATH credential.') @click.option('-D', '--digits', type=click.Choice(['6', '8']), default='6', callback=lambda c, p, v: int(v), help='Number of digits.') +@click.option('-H', '--hmac-algorithm', type=click.Choice(['SHA1', 'SHA256']), default='SHA1', + help='HMAC algorithm for OTP generation.') @click.option('-I', '--imf', type=int, default=0, help='Initial moving factor.') @click.option('-T', '--touch', is_flag=True, help='Require touch.') @click.pass_context -def put(ctx, key, destination, name, oath_type, digits, imf, touch): +def put(ctx, key, destination, name, oath_type, hmac_algorithm, digits, imf, touch): """ Stores a new OATH credential in the YubiKey. """ if key.startswith('otpauth://'): parsed = parse_uri(key) key = parsed['secret'] - name = name or parsed.get('name') + name = parsed.get('name') oath_type = parsed.get('type') - digits = digits or int(parsed.get('digits', '6')) - imf = imf or int(parsed.get('counter', '0')) + hmac_algorithm = parsed.get('algorithm', 'SHA1').upper() + digits = int(parsed.get('digits', '6')) + imf = int(parsed.get('counter', '0')) + + if oath_type not in ['totp', 'hotp']: + ctx.fail('Invalid OATH credential type') + + if hmac_algorithm == 'SHA1': + algo = ALG_SHA1 + elif hmac_algorithm == 'SHA256': + algo = ALG_SHA256 + else: + ctx.fail('Invalid HMAC algorithm') + + if digits == 5 and name.startswith('Steam:'): + # Steam is a special case where we allow the otpauth URI to contain a 'digits' + # value of '5'. + digits = 6 + + if digits not in [6, 8]: + ctx.fail('Invalid number of digits for OTP') digits = digits or 6 unpadded = key.upper() @@ -165,7 +186,7 @@ def put(ctx, key, destination, name, oath_type, digits, imf, touch): oath_type = TYPE_TOTP if oath_type == 'totp' else TYPE_HOTP try: controller.add_cred(dev, name, key, oath_type, digits=digits, - imf=imf, require_touch=touch) + imf=imf, algo=algo, require_touch=touch) except NoSpaceError: ctx.fail('There is not enough space to add another credential on your device.' 'To create free space to add a new credential, delete those you no longer need.') diff --git a/yubioath/gui/__main__.py b/yubioath/gui/__main__.py index c0a37847..50ea111f 100644 --- a/yubioath/gui/__main__.py +++ b/yubioath/gui/__main__.py @@ -221,6 +221,7 @@ class YubiOathApplication(qt.Application): self._controller.add_cred( dialog.name, dialog.key, oath_type=dialog.oath_type, digits=dialog.n_digits, + algo=dialog.algorithm, require_touch=dialog.require_touch) except NoSpaceError: QtGui.QMessageBox.critical(self.window, m.no_space, m.no_space_desc) diff --git a/yubioath/gui/messages.py b/yubioath/gui/messages.py index c7d9a229..ce90def0 100644 --- a/yubioath/gui/messages.py +++ b/yubioath/gui/messages.py @@ -73,6 +73,7 @@ cred_key = "Secret key (base32)" cred_type = "Credential type" cred_totp = "Time based (TOTP)" cred_hotp = "Counter based (HOTP)" +algorithm = "Algorithm" invalid_name = "Invalid name" invalid_name_desc = "Name must be at least 3 characters" invalid_key = "Invalid key" @@ -111,6 +112,13 @@ qr_not_found_desc = "No usable QR code detected. Make sure the QR code is " \ qr_not_supported = "Credential not supported" qr_not_supported_desc = "This credential type is not supported for slot " \ "based usage." +qr_invalid_type = "Invalid OTP type" +qr_invalid_type_desc = "Only TOTP and HOTP types are supported." +qr_invalid_digits = "Invalid number of digits" +qr_invalid_digits_desc = "An OTP may only contain 6 or 8 digits." +qr_invalid_algo = "Unsupported algorithm" +qr_invalid_algo_desc = "SHA1 and SHA256 are the only supported OTP algorithms " \ + "at this time." tt_slot_enabled_1 = "Check to calculate TOTP codes using the YubiKey " \ "standard slot %d credential." tt_num_digits = "The number of digits to show for the credential." diff --git a/yubioath/gui/view/add_cred.py b/yubioath/gui/view/add_cred.py index 45e13ee8..d14bf609 100644 --- a/yubioath/gui/view/add_cred.py +++ b/yubioath/gui/view/add_cred.py @@ -25,7 +25,7 @@ # for the parts of OpenSSL used as well as that of the covered work. from yubioath.yubicommon import qt -from ...core.standard import TYPE_TOTP, TYPE_HOTP +from ...core.standard import ALG_SHA1, ALG_SHA256, TYPE_TOTP, TYPE_HOTP from ...core.utils import parse_uri from .. import messages as m from ..qrparse import parse_qr_codes @@ -99,6 +99,10 @@ class AddCredDialog(qt.Dialog): self._n_digits.addItems(['6', '8']) layout.addRow(m.n_digits, self._n_digits) + self._algorithm = QtGui.QComboBox() + self._algorithm.addItems(['SHA-1', 'SHA-256']) + layout.addRow(m.algorithm, self._algorithm) + self._require_touch = QtGui.QCheckBox(m.require_touch) # Touch-required support not available before 4.2.6 if self._version >= (4, 2, 6): @@ -128,11 +132,26 @@ class AddCredDialog(qt.Dialog): def _handle_qr(self, parsed): if parsed: + otp_type = parsed['type'].lower() + n_digits = parsed.get('digits', '6') + algo = parsed.get('algorithm', 'SHA1').upper() + + if otp_type not in ['totp', 'hotp']: + QtGui.QMessageBox.warning(self, m.qr_invalid_type, m.qr_invalid_type_desc) + return + if n_digits not in ['6', '8']: + QtGui.QMessageBox.warning(self, m.qr_invalid_digits, m.qr_invalid_digits_desc) + return + if algo not in ['SHA1', 'SHA256']: + # RFC6238 says SHA512 is also supported, but it's not implemented here yet. + QtGui.QMessageBox.warning(self, m.qr_invalid_algo, m.qr_invalid_algo_desc) + return + self._cred_name.setText(parsed['name']) self._cred_key.setText(parsed['secret']) - n_digits = parsed.get('digits', '6') self._n_digits.setCurrentIndex(0 if n_digits == '6' else 1) - if parsed['type'] == 'totp': + self._algorithm.setCurrentIndex(0 if algo == 'SHA1' else 1) + if otp_type == 'totp': self._cred_totp.setChecked(True) else: self._cred_hotp.setChecked(True) @@ -177,6 +196,10 @@ class AddCredDialog(qt.Dialog): def n_digits(self): return int(self._n_digits.currentText()) + @property + def algorithm(self): + return ALG_SHA1 if self._algorithm.currentIndex() == 0 else ALG_SHA256 + @property def require_touch(self): return self._require_touch.isChecked()