lists.arthurdejong.org
RSS feed

python-pskc branch master updated. 0.5-26-g01507af

[Date Prev][Date Next] [Thread Prev][Thread Next]

python-pskc branch master updated. 0.5-26-g01507af



This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "python-pskc".

The branch, master has been updated
       via  01507af106c431bbce9e44f96b85fddeb4cefd21 (commit)
      from  dcf19199c4e79fab0e279d813a8abf0bb7d9d623 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
https://arthurdejong.org/git/python-pskc/commit/?id=01507af106c431bbce9e44f96b85fddeb4cefd21

commit 01507af106c431bbce9e44f96b85fddeb4cefd21
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Fri Dec 15 16:30:21 2017 +0100

    Refactor internal storate of encrypted values
    
    This changes the way encrypted values are stored internally before being
    decrypted. For example, the internal _secret property can now be a
    decrypted plain value or an EncryptedValue instance instead of always
    being a DataType, simplifying some things (e.g. all XML
    encoding/decoding is now done in the corresponding module).
    
    This should not change the public API but does have consequences for
    those who use custom serialisers or parsers.

diff --git a/pskc/key.py b/pskc/key.py
index 3ab8681..b1ede98 100644
--- a/pskc/key.py
+++ b/pskc/key.py
@@ -22,118 +22,57 @@
 
 
 import array
-import base64
 import binascii
 
 from pskc.policy import Policy
 
 
-class DataType(object):
-    """Provide access to possibly encrypted, MAC'ed information.
+class EncryptedValue(object):
+    """A container for an encrypted value."""
 
-    This class is meant to be subclassed to provide typed access to stored
-    values. Instances of this class provide the following attributes:
+    def __init__(self, cipher_value, mac_value, algorithm):
+        self.cipher_value = cipher_value
+        self.mac_value = mac_value
+        self.algorithm = algorithm
 
-      value: unencrypted value
-      cipher_value: encrypted value
-      algorithm: encryption algorithm of encrypted value
-      value_mac: MAC of the encrypted value
-    """
-
-    def __init__(self, pskc):
-        self.pskc = pskc
-        self.value = None
-        self.cipher_value = None
-        self.algorithm = None
-        self.value_mac = None
-
-    @staticmethod
-    def _from_text(value):
-        """Convert the plain value to native representation."""
-        raise NotImplementedError  # pragma: no cover
-
-    @staticmethod
-    def _from_bin(value):
-        """Convert the unencrypted binary to native representation."""
-        raise NotImplementedError  # pragma: no cover
-
-    @staticmethod
-    def _to_text(value):
-        """Convert the value to an unencrypted string representation."""
-        raise NotImplementedError  # pragma: no cover
-
-    def get_value(self):
-        """Provide the attribute value, decrypting as needed."""
-        from pskc.exceptions import DecryptionError
-        if self.value is not None:
-            return self.value
-        if self.cipher_value:
-            plaintext = self.pskc.encryption.decrypt_value(
-                self.cipher_value, self.algorithm)
-            # allow MAC over plaintext or cipertext
-            # (RFC6030 implies MAC over ciphertext but older draft used
-            # MAC over plaintext)
-            if self.value_mac and self.value_mac not in (
-                    self.pskc.mac.generate_mac(self.cipher_value),
-                    self.pskc.mac.generate_mac(plaintext)):
-                raise DecryptionError('MAC value does not match')
-            return self._from_bin(plaintext)
-
-    def set_value(self, value):
-        """Set the unencrypted value."""
-        self.value = value
-        self.cipher_value = None
-        self.algorithm = None
-        self.value_mac = None
-
-
-class BinaryDataType(DataType):
-    """Subclass of DataType for binary data (e.g. keys)."""
-
-    @staticmethod
-    def _from_text(value):
-        """Convert the plain value to native representation."""
-        return base64.b64decode(value)
-
-    @staticmethod
-    def _from_bin(value):
-        """Convert the unencrypted binary to native representation."""
-        return value
-
-    @staticmethod
-    def _to_text(value):
-        """Convert the value to an unencrypted string representation."""
+    @classmethod
+    def create(cls, pskc, value):
         # force conversion to bytestring on Python 3
         if not isinstance(value, type(b'')):
             value = value.encode()  # pragma: no cover (Python 3 specific)
-        return base64.b64encode(value).decode()
-
-    @staticmethod
-    def _to_bin(value):
-        """Convert the value to binary representation for encryption."""
-        # force conversion to bytestring on Python 3
-        if not isinstance(value, type(b'')):
-            value = value.encode()  # pragma: no cover (Python 3 specific)
-        return value
-
-
-class IntegerDataType(DataType):
-    """Subclass of DataType for integer types (e.g. counters)."""
+        cipher_value = pskc.encryption.encrypt_value(value)
+        mac_value = None
+        if pskc.mac.algorithm:
+            mac_value = pskc.mac.generate_mac(cipher_value)
+        return cls(cipher_value, mac_value, pskc.encryption.algorithm)
+
+    def get_value(self, pskc):
+        """Provide the decrypted value."""
+        from pskc.exceptions import DecryptionError
+        plaintext = pskc.encryption.decrypt_value(
+            self.cipher_value, self.algorithm)
+        # allow MAC over plaintext or cipertext
+        # (RFC6030 implies MAC over ciphertext but older draft used
+        # MAC over plaintext)
+        if self.mac_value and self.mac_value not in (
+                pskc.mac.generate_mac(self.cipher_value),
+                pskc.mac.generate_mac(plaintext)):
+            raise DecryptionError('MAC value does not match')
+        return plaintext
+
+
+class EncryptedIntegerValue(EncryptedValue):
+
+    @classmethod
+    def create(cls, pskc, value):
+        value = '%x' % value
+        n = len(value)
+        value = binascii.unhexlify(value.zfill(n + (n & 1)))
+        return super(EncryptedIntegerValue, cls).create(pskc, value)
 
-    @staticmethod
-    def _from_text(value):
-        """Convert the plain value to native representation."""
-        # try normal integer string parsing
-        try:
-            return int(value)
-        except ValueError:
-            pass
-        # fall back to base64 decoding
-        return IntegerDataType._from_bin(base64.b64decode(value))
-
-    @staticmethod
-    def _from_bin(value):
-        """Convert the unencrypted binary to native representation."""
+    def get_value(self, pskc):
+        """Provide the decrypted integer value."""
+        value = super(EncryptedIntegerValue, self).get_value(pskc)
         # try to handle value as ASCII representation
         if value.isdigit():
             return int(value)
@@ -143,18 +82,6 @@ class IntegerDataType(DataType):
             result = (result << 8) + x
         return result
 
-    @staticmethod
-    def _to_text(value):
-        """Convert the value to an unencrypted string representation."""
-        return str(value)
-
-    @staticmethod
-    def _to_bin(value):
-        """Convert the value to binary representation for encryption."""
-        value = '%x' % value
-        n = len(value)
-        return binascii.unhexlify(value.zfill(n + (n & 1)))
-
 
 class DataTypeProperty(object):
     """A data descriptor that delegates actions to DataType instances."""
@@ -164,10 +91,14 @@ class DataTypeProperty(object):
         self.__doc__ = doc
 
     def __get__(self, obj, objtype):
-        return getattr(obj, '_' + self.name).get_value()
+        value = getattr(obj, '_' + self.name, None)
+        if hasattr(value, 'get_value'):
+            return value.get_value(obj.device.pskc)
+        else:
+            return value
 
     def __set__(self, obj, val):
-        getattr(obj, '_' + self.name).set_value(val)
+        setattr(obj, '_' + self.name, val)
 
 
 class DeviceProperty(object):
@@ -222,12 +153,6 @@ class Key(object):
         self.id = None
         self.algorithm = None
 
-        self._secret = BinaryDataType(self.device.pskc)
-        self._counter = IntegerDataType(self.device.pskc)
-        self._time_offset = IntegerDataType(self.device.pskc)
-        self._time_interval = IntegerDataType(self.device.pskc)
-        self._time_drift = IntegerDataType(self.device.pskc)
-
         self.issuer = None
         self.key_profile = None
         self.key_reference = None
diff --git a/pskc/parser.py b/pskc/parser.py
index dd7d01a..1dff363 100644
--- a/pskc/parser.py
+++ b/pskc/parser.py
@@ -21,12 +21,35 @@
 """Module for parsing PSKC files."""
 
 
+import array
+import base64
+
 from pskc.exceptions import ParseError
+from pskc.key import EncryptedIntegerValue, EncryptedValue
 from pskc.xml import (
     find, findall, findbin, findint, findtext, findtime, getbool, getint,
     parse, remove_namespaces)
 
 
+def plain2int(value):
+    """Convert a plain text value to an int."""
+    # try normal integer string parsing
+    try:
+        return int(value)
+    except ValueError:
+        pass
+    # fall back to base64 decoding
+    value = base64.b64decode(value)
+    # try to handle value as ASCII representation
+    if value.isdigit():
+        return int(value)
+    # fall back to do big-endian decoding
+    result = 0
+    for x in array.array('B', value):
+        result = (result << 8) + x
+    return result
+
+
 class PSKCParser(object):
 
     @classmethod
@@ -143,20 +166,20 @@ class PSKCParser(object):
 
         data = find(key_elm, 'Data')
         if data is not None:
-            cls.parse_datatype(key._secret, find(data, 'Secret'))
-            cls.parse_datatype(key._counter, find(data, 'Counter'))
-            cls.parse_datatype(key._time_offset, find(data, 'Time'))
-            cls.parse_datatype(key._time_interval, find(data, 'TimeInterval'))
-            cls.parse_datatype(key._time_drift, find(data, 'TimeDrift'))
+            cls.parse_data(key, 'secret', find(data, 'Secret'))
+            cls.parse_data(key, 'counter', find(data, 'Counter'))
+            cls.parse_data(key, 'time_offset', find(data, 'Time'))
+            cls.parse_data(key, 'time_interval', find(data, 'TimeInterval'))
+            cls.parse_data(key, 'time_drift', find(data, 'TimeDrift'))
 
         for data in findall(key_elm, 'Data'):
             name = data.get('Name')
             if name:
-                cls.parse_datatype(dict(
-                    secret=key._secret,
-                    counter=key._counter,
-                    time=key._time_offset,
-                    time_interval=key._time_interval,
+                cls.parse_data(key, dict(
+                    secret='secret',
+                    counter='counter',
+                    time='time_offset',
+                    time_interval='time_interval',
                 ).get(name.lower()), data)
 
         key.issuer = findtext(key_elm, 'Issuer')
@@ -221,7 +244,7 @@ class PSKCParser(object):
         return (algorithm, cipher_value)
 
     @classmethod
-    def parse_datatype(cls, dt, element):
+    def parse_data(cls, key, field, element):
         """Read information from the provided element.
 
         The element is expected to contain <PlainValue>, <EncryptedValue>
@@ -229,29 +252,44 @@ class PSKCParser(object):
         value."""
         if element is None:
             return
+        pskc = key.device.pskc
+        plain_value = None
+        cipher_value = None
+        algorithm = None
+        # get the plain2value function and encryption storage
+        if field == 'secret':
+            plain2value = base64.b64decode
+            encrypted_value_cls = EncryptedValue
+        else:
+            plain2value = plain2int
+            encrypted_value_cls = EncryptedIntegerValue
         # read plaintext value from <PlainValue>
         plain_value = findtext(element, 'PlainValue')
         if plain_value is not None:
-            dt.value = dt._from_text(plain_value)
+            plain_value = plain2value(plain_value)
         # read encrypted data from <EncryptedValue>
         encrypted_value = find(element, 'EncryptedValue')
         if encrypted_value is not None:
-            dt.algorithm, dt.cipher_value = cls.parse_encrypted_value(
+            algorithm, cipher_value = cls.parse_encrypted_value(
                 encrypted_value)
             # store the found algorithm in the pskc.encryption property
-            if not dt.pskc.encryption.algorithm and dt.algorithm:
-                dt.pskc.encryption.algorithm = dt.algorithm
+            if not pskc.encryption.algorithm and algorithm:
+                pskc.encryption.algorithm = algorithm
         # read MAC information from <ValueMAC>
-        value_mac = findbin(element, 'ValueMAC', 'ValueDigest')
-        if value_mac is not None:
-            dt.value_mac = value_mac
+        mac_value = findbin(element, 'ValueMAC', 'ValueDigest')
         # read legacy <Value> elements (can be plain or encrypted)
-        value = find(element, 'Value')
+        value = findtext(element, 'Value')
         if value is not None:
-            if dt.pskc.encryption.algorithm and dt.value_mac:
-                dt.cipher_value = findbin(element, 'Value')
+            if pskc.encryption.algorithm and mac_value:
+                cipher_value = findbin(element, 'Value')
             else:
-                dt.value = dt._from_text(findtext(element, 'Value'))
+                plain_value = plain2value(value)
+        # store the found information
+        if plain_value is not None:
+            setattr(key, field, plain_value)
+        elif cipher_value:
+            setattr(key, field,
+                    encrypted_value_cls(cipher_value, mac_value, algorithm))
 
     @classmethod
     def parse_policy(cls, policy, policy_elm):
diff --git a/pskc/serialiser.py b/pskc/serialiser.py
index 6c9a5ab..eb0b2d8 100644
--- a/pskc/serialiser.py
+++ b/pskc/serialiser.py
@@ -23,9 +23,17 @@
 
 import base64
 
+from pskc.key import EncryptedIntegerValue, EncryptedValue
 from pskc.xml import find, mk_elem, tostring
 
 
+def my_b64encode(value):
+    """Wrap around b64encode to handle types correctly."""
+    if not isinstance(value, type(b'')):
+        value = value.encode()  # pragma: no cover (Python 3 specific)
+    return base64.b64encode(value).decode()
+
+
 class PSKCSerialiser(object):
 
     @classmethod
@@ -146,55 +154,59 @@ class PSKCSerialiser(object):
         mk_elem(key_elm, 'pskc:KeyProfileId', key.key_profile)
         mk_elem(key_elm, 'pskc:KeyReference', key.key_reference)
         mk_elem(key_elm, 'pskc:FriendlyName', key.friendly_name)
-        cls.serialise_datatype(
-            key._secret, key_elm, 'pskc:Secret', 'secret')
-        cls.serialise_datatype(
-            key._counter, key_elm, 'pskc:Counter', 'counter')
-        cls.serialise_datatype(
-            key._time_offset, key_elm, 'pskc:Time', 'time_offset')
-        cls.serialise_datatype(
-            key._time_interval, key_elm, 'pskc:TimeInterval', 'time_interval')
-        cls.serialise_datatype(
-            key._time_drift, key_elm, 'pskc:TimeDrift', 'time_drif')
+        cls.serialise_data(
+            key, 'secret', key_elm, 'pskc:Secret')
+        cls.serialise_data(
+            key, 'counter', key_elm, 'pskc:Counter')
+        cls.serialise_data(
+            key, 'time_offset', key_elm, 'pskc:Time')
+        cls.serialise_data(
+            key, 'time_interval', key_elm, 'pskc:TimeInterval')
+        cls.serialise_data(
+            key, 'time_drift', key_elm, 'pskc:TimeDrift')
         mk_elem(key_elm, 'pskc:UserId', key.key_userid)
         cls.serialise_policy(key.policy, key_elm)
 
     @classmethod
-    def serialise_datatype(cls, dt, key_elm, tag, field):
+    def serialise_data(cls, key, field, key_elm, tag):
+        value = getattr(key, '_%s' % field, None)
+        pskc = key.device.pskc
         # skip empty values
-        if dt.value in (None, '') and not dt.cipher_value:
+        if value in (None, ''):
             return
+        # get the value2text and encryption storage
+        if field == 'secret':
+            value2text = my_b64encode
+            encrypted_value_cls = EncryptedValue
+        else:
+            value2text = str
+            encrypted_value_cls = EncryptedIntegerValue
         # find the data tag and create our tag under it
         data = find(key_elm, 'pskc:Data')
         if data is None:
             data = mk_elem(key_elm, 'pskc:Data', empty=True)
         element = mk_elem(data, tag, empty=True)
-        # see if we should encrypt
-        if field in dt.pskc.encryption.fields and not dt.cipher_value:
-            dt.cipher_value = dt.pskc.encryption.encrypt_value(
-                dt._to_bin(dt.value))
-            dt.algorithm = dt.pskc.encryption.algorithm
-            dt.value = None
+        # see if we should encrypt the value
+        if field in pskc.encryption.fields and not hasattr(
+                value, 'get_value'):
+            value = encrypted_value_cls.create(pskc, value)
         # write out value
-        if dt.cipher_value:
+        if not hasattr(value, 'get_value'):
+            # unencrypted value
+            mk_elem(element, 'pskc:PlainValue', value2text(value))
+        else:
+            # encrypted value
             encrypted_value = mk_elem(
                 element, 'pskc:EncryptedValue', empty=True)
-            mk_elem(
-                encrypted_value, 'xenc:EncryptionMethod',
-                Algorithm=dt.algorithm)
+            mk_elem(encrypted_value, 'xenc:EncryptionMethod',
+                    Algorithm=value.algorithm)
             cipher_data = mk_elem(
                 encrypted_value, 'xenc:CipherData', empty=True)
-            mk_elem(
-                cipher_data, 'xenc:CipherValue',
-                base64.b64encode(dt.cipher_value).decode())
-            if dt.value_mac:
-                mk_elem(element, 'pskc:ValueMAC', base64.b64encode(
-                    dt.value_mac).decode())
-            elif dt.pskc.mac.algorithm:
-                mk_elem(element, 'pskc:ValueMAC', base64.b64encode(
-                    dt.pskc.mac.generate_mac(dt.cipher_value)).decode())
-        else:
-            mk_elem(element, 'pskc:PlainValue', dt._to_text(dt.value))
+            mk_elem(cipher_data, 'xenc:CipherValue',
+                    base64.b64encode(value.cipher_value).decode())
+            if value.mac_value:
+                mk_elem(element, 'pskc:ValueMAC',
+                        base64.b64encode(value.mac_value).decode())
 
     @classmethod
     def serialise_policy(cls, policy, key_elm):
diff --git a/tests/misc/partialxml.pskcxml b/tests/misc/partialxml.pskcxml
new file mode 100644
index 0000000..c86d89b
--- /dev/null
+++ b/tests/misc/partialxml.pskcxml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  This test file contains various incomplete but otherwise valid key entries.
+-->
+
+<KeyContainer Version="1.0"
+  xmlns="urn:ietf:params:xml:ns:keyprov:pskc"
+  xmlns:ds="http://www.w3.org/2000/09/xmldsig#";
+  xmlns:xenc="http://www.w3.org/2001/04/xmlenc#";>
+ <!-- empty key package -->
+ <KeyPackage></KeyPackage>
+ <!-- key package with empty key -->
+ <KeyPackage><Key></Key></KeyPackage>
+ <!-- key with empty data -->
+ <KeyPackage><Key><Data></Data></Key></KeyPackage>
+ <!-- key with empty counter -->
+ <KeyPackage><Key><Data><Counter></Counter></Data></Key></KeyPackage>
+ <!-- key with empty counter -->
+ <KeyPackage><Key><Data><Counter></Counter></Data></Key></KeyPackage>
+ <!-- key with empty policy -->
+ <KeyPackage><Key><Policy></Policy></Key></KeyPackage>
+</KeyContainer>
diff --git a/tests/test_misc.doctest b/tests/test_misc.doctest
index 26b3af7..f01e89f 100644
--- a/tests/test_misc.doctest
+++ b/tests/test_misc.doctest
@@ -1,6 +1,6 @@
 test_misc.doctest - miscellaneous tests
 
-Copyright (C) 2014-2016 Arthur de Jong
+Copyright (C) 2014-2017 Arthur de Jong
 
 This library is free software; you can redistribute it and/or
 modify it under the terms of the GNU Lesser General Public
@@ -256,3 +256,11 @@ challenge 'DECIMAL' 16 87 False
 response  'DECIMAL' 3 True
 challenge 'HEXADECIMAL' 4 6 None
 response  'ALPHANUMERIC' 6 None
+
+
+This checks an PSKC file with a number of different empty sections that
+normally contain data.
+
+>>> pskc = PSKC('tests/misc/partialxml.pskcxml')
+>>> all(key.counter is None for key in pskc.keys)
+True
diff --git a/tests/test_write.doctest b/tests/test_write.doctest
index 8e847f3..3c39f85 100644
--- a/tests/test_write.doctest
+++ b/tests/test_write.doctest
@@ -264,7 +264,7 @@ Set up an encrypted PSKC file and generate a pre-shared key 
for it.
 
 >>> pskc = PSKC()
 >>> key = pskc.add_key(
-...     id='1', serial='123456', secret='1234', counter=42)
+...     id='1', serial='123456', secret=b'1234', counter=42)
 >>> pskc.encryption.setup_preshared_key(
 ...     algorithm='aes128-cbc',
 ...     key=a2b_hex('12345678901234567890123456789012'),
@@ -336,7 +336,7 @@ Use PBKDF2 to derive a key instead of using a pre-shared 
key.
 
 >>> pskc = PSKC()
 >>> key = pskc.add_key(
-...     id='1', serial='123456', secret='1234', counter=42)
+...     id='1', serial='123456', secret=b'1234', counter=42)
 >>> pskc.encryption.setup_pbkdf2(
 ...     'passphrase', key_name='Passphrase')
 >>> pskc.write(f.name)

-----------------------------------------------------------------------

Summary of changes:
 pskc/key.py                   | 167 ++++++++++++------------------------------
 pskc/parser.py                |  82 +++++++++++++++------
 pskc/serialiser.py            |  78 +++++++++++---------
 tests/misc/partialxml.pskcxml |  23 ++++++
 tests/test_misc.doctest       |  10 ++-
 tests/test_write.doctest      |   4 +-
 6 files changed, 185 insertions(+), 179 deletions(-)
 create mode 100644 tests/misc/partialxml.pskcxml


hooks/post-receive
-- 
python-pskc
-- 
To unsubscribe send an email to
python-pskc-commits-unsubscribe@lists.arthurdejong.org or see
https://lists.arthurdejong.org/python-pskc-commits/