Commits

Peter Sagerson committed a0ba10d

Change the type of private_id back to a raw string and change public_id to modhex-encoded.

Comments (0)

Files changed (5)

 ---------------------
 
 Added the Yubico web service client.
-
-.. warning::
-
-    The ``uid`` field of :class:`~yubiotp.otp.OTP` changed from a 6-byte binary
-    string to an integer.
 
 def check_hex_value(option, opt, value):
     try:
-        return unhexlify(value)
+        unhexlify(value)
     except TypeError as e:
         raise OptionValueError(str(e))
 
+    return value
+
+
+def check_modhex_value(option, opt, value):
+    try:
+        unmodhex(value)
+    except ValueError as e:
+        raise OptionValueError(str(e))
+
+    return value
+
 
 class YubiKeyOption(Option):
     """
     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)
+    TYPES = Option.TYPES + ('hex', 'modhex')
+    TYPE_CHECKER = dict(Option.TYPE_CHECKER,
+                        hex=check_hex_value,
+                        modhex=check_modhex_value)
 
 
 make_option = YubiKeyOption
 
         return parser
 
+    @staticmethod
+    def random_hex(count):
+        return ''.join(choice('0123456789abcdef') for i in xrange(count*2))
+
 
 class ListHandler(Handler):
     name = 'list'
 class InitHandler(Handler):
     name = 'init'
     options = [
-        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('-p', '--public', dest='public_id', type='modhex', default='', help=u'A modhex-encoded public ID (up to 16 bytes)'),
         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='int', default=0, help='A private ID.'),
+        make_option('-u', '--uid', dest='uid', type='hex', help='A hex-encoded 6-byte private ID. If omitted, one will be generated.'),
         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(chr(choice(xrange(0xff))) for x in xrange(16))
+            opts.key = self.random_hex(16)
+
+        if opts.uid is None:
+            opts.uid = self.random_hex(6)
 
         device = Device(opts.config, opts.device_name)
         device.create(opts.public_id, opts.key, opts.uid, opts.session)
 
     def handle(self, opts, args):
         device = Device(opts.config, opts.device_name)
-        key = device.get_config('key', binary=True)
+        key = device.get_config('key', unhex=True)
 
         for token in args[1:]:
             try:
             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: 0x{0:x}'.format(otp.uid)
+                print u'public_id: {0}'.format(public_id)
+                print u'uid: {0}'.format(hexlify(otp.uid))
                 print u'session: {0}'.format(otp.session)
                 print u'timestamp: 0x{0:x}'.format(otp.timestamp)
                 print u'counter: {0}'.format(otp.counter)
         return config
 
     def create(self, public_id, key, uid, session):
-        if len(key) != 16:
+        if len(key) != 32:
             raise ValueError('AES keys must be exactly 16 bytes')
 
         try:
         except DuplicateSectionError:
             usage(u'A device named "{0}" already exists.'.format(self.name))
         else:
-            self.set_config('public_id', public_id, binary=True)
-            self.set_config('key', key, binary=True)
+            self.set_config('public_id', public_id)
+            self.set_config('key', key)
             self.set_config('uid', uid)
             self.set_config('session', session)
 
         self.ensure_yubikey()
 
         otp = self.yubikey.generate()
-        key = self.get_config('key', binary=True)
-        public_id = self.get_config('public_id', binary=True)
+        key = self.get_config('key', unhex=True)
+        public_id = self.get_config('public_id')
 
         token = encode_otp(otp, key, public_id=public_id)
 
     def ensure_yubikey(self):
         if self.yubikey is None:
             try:
-                uid = int(self.get_config('uid'))
+                uid = self.get_config('uid', unhex=True)
                 session = int(self.get_config('session'))
 
-                self.get_config('key', binary=True)
-            except StandardError:
-                usage(u'The device named "{0}" does not exist or is corrupt.'.format(self.name))
+                self.get_config('key')
+            except StandardError as e:
+                usage(u'The device named "{0}" does not exist or is corrupt. ({1})'.format(self.name, e))
             else:
                 self.yubikey = YubiKey(uid=uid, session=session)
 
         with open(self.config_path, 'w') as f:
             self.config.write(f)
 
-    def get_config(self, key, binary=False):
+    def get_config(self, key, unhex=False):
         value = self.config.get(self.section_name, key)
-        if binary:
+        if unhex:
             value = unhexlify(value)
 
         return value
 
-    def set_config(self, key, value, binary=False):
-        value = str(value)
-        if binary:
-            value = hexlify(value)
-
-        self.config.set(self.section_name, key, value)
+    def set_config(self, key, value):
+        self.config.set(self.section_name, key, str(value))
 
 
 if __name__ == '__main__':

yubiotp/client.py

 from urllib import urlencode
 from urllib2 import urlopen
 
-from .modhex import unmodhex
-
 
 class YubiClient10(object):
     """
     """
     _NONCE_CHARS = string.ascii_letters + string.digits
 
-    def __init__(self, api_id=1, api_key=None, ssl=True):
+    def __init__(self, api_id=1, api_key=None, ssl=False):
         self.api_id = api_id
         self.api_key = api_key
         self.ssl = ssl
         custom validation service. Defaults to
         ``'http[s]://api.yubico.com/wsapi/verify'``.
     """
-    def __init__(self, api_id=1, api_key=None, ssl=True, timestamp=False):
+    def __init__(self, api_id=1, api_key=None, ssl=False, timestamp=False):
         super(YubiClient11, self).__init__(api_id, api_key, ssl)
 
         self.timestamp = timestamp
         custom validation service. Defaults to
         ``'http[s]://api.yubico.com/wsapi/2.0/verify'``.
     """
-    def __init__(self, api_id=1, api_key=None, ssl=True, timestamp=False, sl=None, timeout=None):
+    def __init__(self, api_id=1, api_key=None, ssl=False, timestamp=False, sl=None, timeout=None):
         super(YubiClient20, self).__init__(api_id, api_key, ssl, timestamp)
 
         self.sl = sl
     @property
     def public_id(self):
         """
-        Returns the public id of the response token as a 48-bit unsigned int.
+        Returns the public id of the response token as a modhex string.
 
-        :returns: The fully decoded public ID portion of ``token``, if any.
         :rtype: str or ``None``.
         """
         try:
-            public_id = unmodhex(self.fields['otp'])[:-16]
+            public_id = self.fields['otp'][:-32]
         except KeyError:
             public_id = None
 

yubiotp/modhex.py

 from binascii import hexlify, unhexlify
 from functools import partial
 
-__all__ = ['modhex', 'unmodhex', 'hex_to_modhex', 'modhex_to_hex']
+__all__ = ['modhex', 'unmodhex', 'is_modhex', 'hex_to_modhex', 'modhex_to_hex']
 
 
 def modhex(data):
     """
     return unhexlify(modhex_to_hex(encoded))
 
+def is_modhex(encoded):
+    """
+    Returns ``True`` iff the given string is valid modhex.
+
+    >>> is_modhex('cbdefghijklnrtuv')
+    True
+    >>> is_modhex('cbdefghijklnrtuvv')
+    False
+    >>> is_modhex('cbdefghijklnrtuvyy')
+    False
+    """
+    if any(c not in modhex_chars for c in encoded):
+        return False
+    elif len(encoded) % 2 != 0:
+        return False
+    else:
+        return True
+
 def hex_to_modhex(hex_str):
     """
     Convert a string of hex digits to a string of modhex digits.
 
 >>> key = '0123456789abcdef'
 >>> otp = OTP(0xba9876543210, 5, 0x0153f8, 0, 0x1234)
->>> token = encode_otp(otp, key, 'mypublicid')
+>>> token = encode_otp(otp, key, 'cclngiuv')
 >>> token
-'htikicighdhrhkhehkhfefnijnthcvncrgbrrklcfrhndchilifi'
+'cclngiuvefnijnthcvncrgbrrklcfrhndchilifi'
 >>> public_id, otp2 = decode_otp(token, key)
 >>> public_id
-'mypublicid'
+'cclngiuv'
 >>> otp2 == otp
 True
 """
 from struct import pack, unpack
 
 from .crc import crc16, verify_crc16
-from .modhex import modhex, unmodhex
+from .modhex import modhex, unmodhex, is_modhex
 
 from Crypto.Cipher import AES
 
         followed by 16 bytes of encrypted OTP data.
     :param str key: A 16-byte AES key as a binary string.
 
-    :returns: The public ID as a decoded string and the OTP structure.
+    :returns: The public ID in its modhex-encoded form and the OTP structure.
     :rtype: (str, :class:`OTP`)
 
     :raises: ``ValueError`` if the string can not be decoded.
     if len(key) != 16:
         raise ValueError('Key must be exactly 16 bytes')
 
+    public_id, token = token[:-32], token[-32:]
+
     buf = unmodhex(token)
-    public_id, buf = buf[:-16], buf[-16:]
     buf = AES.new(key, AES.MODE_ECB).decrypt(buf)
     otp = OTP.unpack(buf)
 
     :type otp: :class:`OTP`
 
     :param str key: A 16-byte AES key as a binary string.
-    :param str public_id: An optional public id. This will be truncated to 16
-        bytes.
+    :param str public_id: An optional public id, modhex-encoded. This can be at
+        most 32 characters.
+
+    :raises: ValueError if any parameters are out of range.
     """
     if len(key) != 16:
         raise ValueError('Key must be exactly 16 bytes')
 
+    if not is_modhex(public_id):
+        raise ValueError('public_id must be a valid modhex string')
+
+    if len(public_id) > 32:
+        raise ValueError('public_id may be no longer than 32 modhex characters')
+
     buf = otp.pack()
     buf = AES.new(key, AES.MODE_ECB).encrypt(buf)
-    buf = public_id[:16] + buf
     token = modhex(buf)
 
-    return token
-
-
-def public_id(token):
-    """
-    Returns the fully decoded public ID from an otp token.
-
-    :param str token: A monhex-encoded YubiKey OTP token.
-
-    :returns: The public ID as a fully decoded string.
-    :rtype: str
-    """
-    return unmodhex(token[:-32])
+    return public_id + token
 
 
 class OTP(object):
     A single YubiKey OTP. This is typically instantiated by parsing an encoded
     OTP.
 
-    :param int uid: The private ID in [0..2^48].
+    :param str uid: The private ID as a 6-byte binary string.
     :param int session: The non-volatile usage counter.
     :param int timestamp: An integer in [0..2^24].
     :param int counter: The volatile usage counter.
         encoded.
         """
         fields = (
-            self.uid & 0xffffffff, (self.uid >> 32) & 0xffff,
+            self.uid,
             self.session,
             self.timestamp & 0xff, (self.timestamp >> 8) & 0xffff,
             self.counter,
             self.rand,
         )
 
-        buf = pack('<IHHBHBH', *fields)
+        buf = pack('<6s H BH B H', *fields)
 
         crc = ~crc16(buf) & 0xffff
         buf += pack('<H', crc)
         if not verify_crc16(buf):
             raise CRCError('OTP checksum is invalid')
 
-        u1, u2, session, t1, t2, counter, rand, crc = unpack('<IHHBHBHH', buf)
+        uid, session, t1, t2, counter, rand, crc = unpack('<6s H BH B H H', buf)
 
-        uid = (u2 << 32) | u1
         timestamp = (t2 << 8) | t1
 
         return cls(uid, session, timestamp, counter, rand)
     A simulated YubiKey device. This can be used to generate a sequence of
     Yubico OTP tokens.
 
-    :param int uid: The private ID in [0..2^48].
+    :param int uid: The private ID as a 6-byte binary string.
     :param int session: The non-volatile usage counter. It is the caller's
         responsibility to persist this. Note that this may increment if the
         volatile counter wraps, so you should only increment and persist this