Commits

Peter Sagerson committed 68df67e

Handle public IDs.

Also include a modhex converter and some sanity checks for yubikey.

  • Participants
  • Parent commits cfc5d84

Comments (0)

Files changed (4)

File bin/quicktest.sh

+#!/bin/sh
+
+config="~/.yubikey-test"
+
+function yubikey ()
+{
+    ./yubikey -f $config "$@"
+}
+
+
+unlink $config 2>/dev/null
+
+yubikey init -k 00112233445566778899aabbccddeeff -u abcdef -s 3 -P username
+yubikey -n 1 init
+yubikey list
+
+token=`yubikey gen`
+echo $token
+yubikey parse $token
+yubikey -n 1 parse $token
+yubikey delete
+yubikey -n 1 delete
+
+modhex=`yubikey modhex modhex-test`
+echo $modhex
+yubikey modhex -d $modhex
+
+unlink $config 2>/dev/null
 #!/usr/bin/env python
 
+"""
+This is a command-line interface to the yubiotp package. Its primary function
+is to simulate a YubiKey device for testing purposes. It also includes some
+utilities for things like converting to and from modhex.
+"""
 import sys
 from os.path import expanduser
-from optparse import OptionParser, OptionGroup, make_option
+from optparse import OptionParser, OptionGroup, Option, OptionValueError
 from ConfigParser import SafeConfigParser
 from ConfigParser import NoSectionError, NoOptionError, DuplicateSectionError
 from random import choice
 from binascii import hexlify, unhexlify
 
-from yubiotp.otp import YubiKey, decode, encode
+from yubiotp.modhex import modhex, unmodhex, modhex_to_hex, hex_to_modhex
+from yubiotp.otp import YubiKey, decode_otp, encode_otp
 
 
 def main():
         'delete': DeleteHandler(),
         'gen': GenHandler(),
         'parse': ParseHandler(),
+
+        'modhex': ModhexHandler(),
     }
 
     handler = handlers.get(args[0])
         handler.run()
 
 
+def check_hex_value(option, opt, value):
+    try:
+        return unhexlify(value)
+    except TypeError as e:
+        raise OptionValueError(str(e))
+
+
+class YubiKeyOption(Option):
+    """
+    Custom optparse Option class that adds the 'hex' value type. Values of this
+    type are expected to be strings of hex digits, which will be decoded into
+    binary strings before being stored.
+    """
+    TYPES = Option.TYPES + ('hex',)
+    TYPE_CHECKER = dict(Option.TYPE_CHECKER, hex=check_hex_value)
+
+
+make_option = YubiKeyOption
+
+
 class Handler(object):
     name = ''
     options = [
-        make_option('-c', '--config', dest='config', default='~/.yubikey', metavar='PATH', help=u'A config file to store device state. [%default]'),
-        make_option('-d', '--device', dest='device', default='0', metavar='NAME', help=u'The device number or name to operate on. [%default]'),
+        make_option('-f', '--config', dest='config', default='~/.yubikey', metavar='PATH', help=u'A config file to store device state. [%default]'),
+        make_option('-n', '--name', dest='device_name', default='0', metavar='NAME', help=u'The device number or name to operate on. [%default]'),
     ]
     args = ''
     description = u'Simulates one or more YubiKey devices from which you can generate tokens. Also parses tokens for verification. Choose an action for more information.'
     @classmethod
     def global_option_parser(cls):
         parser = OptionParser(
-            usage=u'%prog [global opts] <list|init|delete|gen|parse> [any opts] [args]',
+            usage=u'%prog [global opts] <list|init|delete|gen|parse|modhex> [any opts] [args]',
             description=cls.description,
             )
 
 class InitHandler(Handler):
     name = 'init'
     options = [
-        make_option('-k', '--key', dest='key', help='A hex-encoded 16-byte AES key. If omitted, one will be generated.'),
-        make_option('-u', '--uid', dest='uid', default='', help='A hex-encoded private ID.'),
+        make_option('-p', '--public', dest='public_id', type='hex', default='', help=u'A hex-encoded public ID (up to 16 bytes)'),
+        make_option('-P', '--public-ascii', dest='public_id', help=u'An ASCII public ID (up to 16 characters).'),
+        make_option('-k', '--key', dest='key', type='hex', help='A hex-encoded 16-byte AES key. If omitted, one will be generated.'),
+        make_option('-u', '--uid', dest='uid', type='hex', default='', help='A hex-encoded private ID.'),
         make_option('-s', '--session', dest='session', type='int', default=0, help='The initial session counter. [%default]'),
     ]
     description = u'Initialize a new virtual YubiKey.'
 
     def handle(self, opts, args):
         if opts.key is None:
-            opts.key = ''.join(choice('0123456789abcdef') for x in xrange(32))
+            opts.key = ''.join(chr(choice(xrange(0xff))) for x in xrange(16))
 
-        device = Device(opts.config, opts.device)
-        device.create(opts.key, opts.uid, opts.session)
+        device = Device(opts.config, opts.device_name)
+        device.create(opts.public_id, opts.key, opts.uid, opts.session)
         device.save()
 
 
     description = u'Remove a virtual YubiKey device.'
 
     def handle(self, opts, args):
-        device = Device(opts.config, opts.device)
+        device = Device(opts.config, opts.device_name)
         device.delete()
         device.save()
 
 class GenHandler(Handler):
     name = 'gen'
     options = [
-        make_option('-n', '--count', dest='count', type='int', default=1, help=u'Generate multiple tokens. [%default]'),
+        make_option('-c', '--count', dest='count', type='int', default=1, help=u'Generate multiple tokens. [%default]'),
         make_option('-i', '--interactive', action='store_true', dest='interactive', help=u'Generate a token for every line read from stdin until interrupted.'),
     ]
     args = ''
     description = u'Generate one or more tokens from the virtual device. This simulates pressing the YubiKey\'s button.'
 
     def handle(self, opts, args):
-        device = Device(opts.config, opts.device)
+        device = Device(opts.config, opts.device_name)
 
         for i in xrange(opts.count):
             print device.gen_token()
     description = u'Parse tokens generated by the selected virtual device and display its fields.'
 
     def handle(self, opts, args):
-        device = Device(opts.config, opts.device)
+        device = Device(opts.config, opts.device_name)
         key = device.get_config('key', binary=True)
 
         for token in args[1:]:
             try:
-                public_id, otp = decode(token, key)
+                public_id, otp = decode_otp(token, key)
             except ValueError as e:
                 print e
             else:
+                if all(ord(c) in xrange(0x20, 0x7f) for c in public_id):
+                    print u'public_id: {0} ({1})'.format(hexlify(public_id), public_id)
+                else:
+                    print u'public_id: {0}'.format(hexlify(public_id))
+
                 print u'uid: {0}'.format(hexlify(otp.uid))
                 print u'session: {0}'.format(otp.session)
                 print u'timestamp: {0}'.format(hex(otp.timestamp))
                 print
 
 
+class ModhexHandler(Handler):
+    name = 'modhex'
+    options = [
+        make_option('-d', '--decode', action='store_true', dest='decode', help=u'Decode from modhex. Default is to encode to modhex.'),
+        make_option('-H', '--hex', action='store_true', dest='hex', help=u'Encode to or decode from a string of hex digits. Default is a raw string.'),
+    ]
+    args = 'input ...'
+    description = u'Encode (default) or decode a modhex string.'
+
+    def handle(self, opts, args):
+        for arg in args[1:]:
+            try:
+                if opts.decode:
+                    if opts.hex:
+                        print modhex_to_hex(arg)
+                    else:
+                        print unmodhex(arg)
+                else:
+                    if opts.hex:
+                        print hex_to_modhex(arg)
+                    else:
+                        print modhex(arg)
+            except ValueError as e:
+                print >>sys.stderr, e
+
+
 def usage(message, parser=None):
     print message
     print
 
         return config
 
-    def create(self, key, uid, session):
+    def create(self, public_id, key, uid, session):
+        if len(key) != 16:
+            raise ValueError('AES keys must be exactly 16 bytes')
+
         try:
             self.config.add_section(self.section_name)
         except DuplicateSectionError:
             usage(u'A device named "{0}" already exists.'.format(self.name))
         else:
-            self.set_config('key', key)
-            self.set_config('uid', uid)
+            self.set_config('public_id', public_id, binary=True)
+            self.set_config('key', key, binary=True)
+            self.set_config('uid', uid, binary=True)
             self.set_config('session', session)
 
     def delete(self):
 
         otp = self.yubikey.generate()
         key = self.get_config('key', binary=True)
+        public_id = self.get_config('public_id', binary=True)
 
-        token = encode(otp, key)
+        token = encode_otp(otp, key, public_id=public_id)
 
         return token
 
     def ensure_yubikey(self):
         if self.yubikey is None:
             try:
-                self.get_config('key', binary=True)
                 uid = self.get_config('uid', binary=True)
                 session = int(self.get_config('session'))
+
+                self.get_config('key', binary=True)
             except (NoSectionError, NoOptionError):
                 usage(u'The device named "{0}" does not exist or is corrupt.'.format(self.name))
             else:

File docs/source/index.rst

 applications, presumably as part of a multi-factor authentication scheme. Note
 that this is *not* a YubiCloud client, it's the low-level implementation. Those
 wishing to verify YubiKey tokens in their application will be most interested in
-:meth:`yubiotp.otp.decode`.
+:meth:`yubiotp.otp.decode_otp`.
 
 For testing and experimenting, the included ``yubikey`` script simulates one or
-more YubiKey devices using a config file. See the tool's help text for
-documentation.
+more YubiKey devices using a config file. It also utility commands such as a
+modhex converter. See ``yubikey -h`` for details.
 
 
 yubiotp.otp

File yubiotp/otp.py

 
 >>> key = '0123456789abcdef'
 >>> otp = OTP('user00', 5, 0x0153f8, 0, 0x1234)
->>> token = encode(otp, key, 'mypublicid')
+>>> token = encode_otp(otp, key, 'mypublicid')
 >>> token
 'htikicighdhrhkhehkhfdlukjhukinceifnghknuccvbdciirtdu'
->>> public_id, otp2 = decode(token, key)
+>>> public_id, otp2 = decode_otp(token, key)
 >>> public_id
 'mypublicid'
 >>> otp2 == otp
 from Crypto.Cipher import AES
 
 
-__all__ = ['decode', 'encode', 'OTP', 'YubiKey', 'CRCError']
+__all__ = ['decode_otp', 'encode_otp', 'OTP', 'YubiKey', 'CRCError']
 
 
 class CRCError(ValueError):
     pass
 
 
-def decode(token, key):
+def decode_otp(token, key):
     """
     Decodes a modhex-encoded Yubico OTP value and returns the public ID and the
     unpacked :class:`OTP` object.
     return (public_id, otp)
 
 
-def encode(otp, key, public_id=''):
+def encode_otp(otp, key, public_id=''):
     """
     Encodes an :class:`OTP` structure, encrypts it with the given key and
     returns the modhex-encoded token.
 
         The volatile session counter. This defaults to 0 at init time, but the
         caller can override this.
-
-    .. attribute:: public_id
-
-        An optional public id to identify generated passwords. This will be
-        truncated to 16 bytes.
     """
-    def __init__(self, uid, session, counter=0, public_id=''):
+    def __init__(self, uid, session, counter=0):
         self.uid = uid[:6]
         self.session = min(session, 0x7fff)
         self.counter = min(counter, 0xff)
-        self.public_id = public_id[:16]
 
         self._init_timestamp()