# Authors: # Nathaniel McCallum # # Copyright (C) 2014 Red Hat # see file 'COPYING' for use and warranty information # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import six from ipalib import _, api, IntEnum from ipalib.errors import NotFound, SkipPluginModule from ipalib.frontend import Command, Method, Object from ipalib.plugable import Registry from ipalib.util import classproperty try: import usb.core import yubico except ImportError: # python-yubico depends on pyusb raise SkipPluginModule(reason=_("python-yubico is not installed.")) if six.PY3: unicode = str __doc__ = _(""" YubiKey Tokens """) + _(""" Manage YubiKey tokens. """) + _(""" This code is an extension to the otptoken plugin and provides support for reading/writing YubiKey tokens directly. """) + _(""" EXAMPLES: """) + _(""" Add a new token: ipa otptoken-add-yubikey --owner=jdoe --desc="My YubiKey" """) register = Registry() topic = 'otp' @register(no_fail=True) class _fake_otptoken(Object): name = 'otptoken' @register(no_fail=True) class _fake_otptoken_add(Method): name = 'otptoken_add' NO_CLI = True @register() class otptoken_add_yubikey(Command): __doc__ = _('Add a new YubiKey OTP token.') takes_options = ( IntEnum('slot?', cli_name='slot', label=_('YubiKey slot'), values=(1, 2), ), ) has_output_params = takes_options @classmethod def __NO_CLI_getter(cls): return api.Command.get_plugin('otptoken_add') is _fake_otptoken_add NO_CLI = classproperty(__NO_CLI_getter) @property def api_version(self): return self.api.Command.otptoken_add.api_version def get_args(self): for arg in self.api.Command.otptoken_add.args(): yield arg for arg in super(otptoken_add_yubikey, self).get_args(): yield arg def get_options(self): for option in self.api.Command.otptoken_add.options(): if option.name not in ('type', 'ipatokenvendor', 'ipatokenmodel', 'ipatokenserial', 'ipatokenotpalgorithm', 'ipatokenhotpcounter', 'ipatokenotpkey', 'ipatokentotpclockoffset', 'ipatokentotptimestep', 'no_qrcode', 'qrcode', 'version'): yield option for option in super(otptoken_add_yubikey, self).get_options(): yield option def get_output_params(self): for param in self.api.Command.otptoken_add.output_params(): yield param for param in super(otptoken_add_yubikey, self).get_output_params(): yield param def _iter_output(self): return self.api.Command.otptoken_add.output() def forward(self, *args, **kwargs): # Open the YubiKey try: yk = yubico.find_yubikey() except usb.core.USBError as e: raise NotFound(reason="No YubiKey found: %s" % e.strerror) except yubico.yubikey.YubiKeyError as e: raise NotFound(reason=e.reason) except ValueError as e: raise NotFound(reason=str(e) + ". Please install 'libyubikey' " "and 'libusb' packages first.") assert yk.version_num() >= (2, 1) # If no slot is specified, find the first free slot. if kwargs.get('slot', None) is None: try: used = yk.status().valid_configs() kwargs['slot'] = sorted({1, 2}.difference(used))[0] except IndexError: raise NotFound(reason=_('No free YubiKey slot!')) # Create the key (NOTE: the length is fixed). key = os.urandom(20) # Write the config. cfg = yk.init_config() cfg.mode_oath_hotp(key, kwargs.get( 'ipatokenotpdigits', self.get_default_of('ipatokenotpdigits') )) cfg.extended_flag('SERIAL_API_VISIBLE', True) yk.write_config(cfg, slot=kwargs['slot']) # Filter the options we want to pass. options = {k: v for k, v in kwargs.items() if k in ( 'version', 'description', 'ipatokenowner', 'ipatokendisabled', 'ipatokennotbefore', 'ipatokennotafter', 'ipatokenotpdigits', )} # Run the command. answer = self.Backend.rpcclient.forward('otptoken_add', *args, type=u'hotp', ipatokenvendor=u'YubiCo', ipatokenmodel=unicode(yk.model), ipatokenserial=unicode(yk.serial()), ipatokenotpalgorithm=u'sha1', ipatokenhotpcounter=0, ipatokenotpkey=key, no_qrcode=True, **options) # Suppress values we don't want to return. for k in (u'uri', u'ipatokenotpkey'): if k in answer.get('result', {}): del answer['result'][k] # Return which slot was used for writing. answer.get('result', {})['slot'] = kwargs['slot'] return answer