diff --git a/setup.py b/setup.py index b596896d..f09c1b1f 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,8 @@ install_requires = [ 'six', 'pyscard', 'pyusb', 'click', - 'cryptography', 'pyopenssl', 'fido2 >= 0.7' + 'cryptography', 'pyopenssl', 'fido2 >= 0.7', + 'python-pskc' ] if sys.version_info < (3, 4): install_requires.append('enum34') diff --git a/ykman/cli/oath.py b/ykman/cli/oath.py index e8dfdfeb..38216834 100644 --- a/ykman/cli/oath.py +++ b/ykman/cli/oath.py @@ -28,6 +28,7 @@ from __future__ import absolute_import import click import logging +from pskc import PSKC from threading import Timer from binascii import b2a_hex, a2b_hex from .util import ( @@ -249,6 +250,30 @@ def uri(ctx, uri, touch, force): _add_cred(ctx, data, force=force) +@oath.command('import') +@click_force_option +@click.pass_context +@click.option( + '-p', '--password', help='A password may be needed to decrypt the file.') +@click.argument('import_file', type=click.File('rb'), metavar='FILE') +def import_from_file(ctx, import_file, password, force): + """ + Import credentials from a file. + + Import credentials from a PSKC file. + """ + ensure_validated(ctx) + pskc = PSKC(import_file) + if pskc.encryption.is_encrypted: + if not password: + ctx.fail('File is encrypted, password required.') + else: + pskc.encryption.derive_key(password) + for key in pskc.keys: + cred_data = CredentialData.from_pskc(key) + _add_cred(ctx, cred_data, force=force) + + def _add_cred(ctx, data, force): controller = ctx.obj['controller'] diff --git a/ykman/oath.py b/ykman/oath.py index a444bcca..e8e53f4c 100644 --- a/ykman/oath.py +++ b/ykman/oath.py @@ -140,6 +140,39 @@ def from_uri(cls, uri): counter=int(params.get('counter', 0)) ) + @classmethod + def from_pskc(cls, pskc_key): + + def _get_oath_type(pskc_algo): + if 'hotp' in pskc_algo.lower(): + return OATH_TYPE.HOTP + elif 'totp' in pskc_algo.lower(): + return OATH_TYPE.TOTP + else: + raise ValueError('Unsupported PSKC algorithm!') + + def _get_algo(pskc_algo_suite): + if 'sha1' in pskc_algo_suite.lower(): + return ALGO.SHA1 + elif 'sha256' in pskc_algo_suite.lower(): + return ALGO.SHA256 + elif 'sha512' in pskc_algo_suite.lower(): + return ALGO.SHA512 + + return cls( + secret=pskc_key.secret, + issuer=pskc_key.issuer, + # TODO: Look over what we want as the name here. + name=pskc_key.friendly_name or pskc_key.userid or pskc_key.issuer, + oath_type=_get_oath_type(pskc_key.algorithm), + algorithm=_get_algo( + pskc_key.algorithm_suite) \ + if pskc_key.algorithm_suite else ALGO.SHA1, + digits=pskc_key.response_length or 6, + period=pskc_key.time_interval or 30, + counter=pskc_key.counter or 0 + ) + def make_key(self): key = self.name if self.issuer: