lists.arthurdejong.org
RSS feed

python-pskc branch master updated. 0.4-15-ge23a467

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

python-pskc branch master updated. 0.4-15-ge23a467



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  e23a4674c960f09af8cac28f3cbe876f2bc496fc (commit)
       via  beafc6b24c7210641392df12ba32770248d0339c (commit)
       via  84bfb8a6ebd71be8119b03a9e88dde9554c5900d (commit)
       via  426e821b1366095990304b04bbc41b3af384fe21 (commit)
      from  bf34209656fa477744d284bb2d8e9c4e3e201fed (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 -----------------------------------------------------------------
http://arthurdejong.org/git/python-pskc/commit/?id=e23a4674c960f09af8cac28f3cbe876f2bc496fc

commit e23a4674c960f09af8cac28f3cbe876f2bc496fc
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sat Sep 17 15:23:24 2016 +0200

    Use custom data descriptors for key properties
    
    This uses a custom data descriptor (property) for secret, counter,
    time_offset, time_interval and time_drift.

diff --git a/pskc/key.py b/pskc/key.py
index b5cec51..69f710f 100644
--- a/pskc/key.py
+++ b/pskc/key.py
@@ -156,6 +156,20 @@ class IntegerDataType(DataType):
         return binascii.unhexlify(value.zfill(n + (n & 1)))
 
 
+class DataTypeProperty(object):
+    """A data descriptor that delegates actions to DataType instances."""
+
+    def __init__(self, name, doc):
+        self.name = name
+        self.__doc__ = doc
+
+    def __get__(self, obj, objtype):
+        return getattr(obj, '_' + self.name).get_value()
+
+    def __set__(self, obj, val):
+        getattr(obj, '_' + self.name).set_value(val)
+
+
 class DeviceProperty(object):
     """A data descriptor that delegates actions to the Device instance."""
 
@@ -233,30 +247,17 @@ class Key(object):
 
         self.policy = Policy(self)
 
-    secret = property(
-        fget=lambda self: self._secret.get_value(),
-        fset=lambda self, x: self._secret.set_value(x),
-        doc="The secret key itself.")
-
-    counter = property(
-        fget=lambda self: self._counter.get_value(),
-        fset=lambda self, x: self._counter.set_value(x),
-        doc="An event counter for event-based OTP.")
-
-    time_offset = property(
-        fget=lambda self: self._time_offset.get_value(),
-        fset=lambda self, x: self._time_offset.set_value(x),
-        doc="A time offset for time-based OTP (number of intervals).")
-
-    time_interval = property(
-        fget=lambda self: self._time_interval.get_value(),
-        fset=lambda self, x: self._time_interval.set_value(x),
-        doc="A time interval in seconds.")
-
-    time_drift = property(
-        fget=lambda self: self._time_drift.get_value(),
-        fset=lambda self, x: self._time_drift.set_value(x),
-        doc="Device clock drift value (number of time intervals).")
+    secret = DataTypeProperty(
+        'secret', 'The secret key itself.')
+    counter = DataTypeProperty(
+        'counter', 'An event counter for event-based OTP.')
+    time_offset = DataTypeProperty(
+        'time_offset',
+        'A time offset for time-based OTP (number of intervals).')
+    time_interval = DataTypeProperty(
+        'time_interval', 'A time interval in seconds.')
+    time_drift = DataTypeProperty(
+        'time_drift', 'Device clock drift value (number of time intervals).')
 
     manufacturer = DeviceProperty('manufacturer')
     serial = DeviceProperty('serial')

http://arthurdejong.org/git/python-pskc/commit/?id=beafc6b24c7210641392df12ba32770248d0339c

commit beafc6b24c7210641392df12ba32770248d0339c
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sat Sep 17 15:05:21 2016 +0200

    Support separate device from key
    
    This allows having multiple keys per device while also maintaining the
    previous API.
    
    Note that having multiple keys per device is not allowed by the RFC 6030
    schema but is allowed by some older internet drafts.

diff --git a/docs/usage.rst b/docs/usage.rst
index 5e0edfc..48f71de 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -3,8 +3,9 @@ Basic usage
 
 The :mod:`pskc` module implements a simple and efficient API for parsing and
 creating PSKC files. The :class:`~pskc.PSKC` class is used to access the file
-as a whole which provides access to a list of :class:`~pskc.key.Key`
-instances which contain most of the useful information of the PSKC file.
+as a whole which provides access to a list of :class:`~pskc.device.Device`
+and :class:`~pskc.key.Key` instances which contain most of the useful
+information of the PSKC file.
 
 
 Reading a PSKC file
@@ -89,6 +90,11 @@ The PSKC class
 
       A unique identifier for the container.
 
+   .. attribute:: devices
+
+      A list of :class:`~pskc.device.Device` instances that represent the key
+      containers within the PSKC file.
+
    .. attribute:: keys
 
       A list of :class:`~pskc.key.Key` instances that represent the keys
@@ -105,11 +111,18 @@ The PSKC class
       See :doc:`mac` for more information.
 
 
+   .. function:: add_device([**kwargs])
+
+      Add a new key package to the PSKC instance. The keyword arguments may
+      refer to any attributes of the :class:`~pskc.device.Device` class with
+      which the new device is initialised.
+
    .. function:: add_key([**kwargs])
 
       Add a new key to the PSKC instance. The keyword arguments may refer to
-      any attributes of the :class:`~pskc.key.Key` class with which the new
-      key is initialised.
+      any attributes of the :class:`~pskc.key.Key` or
+      :class:`~pskc.device.Device` class with which the new key is
+      initialised.
 
    .. function:: write(filename)
 
@@ -186,7 +199,7 @@ The Key class
    .. attribute:: issuer
 
       The name of the party that issued the key. This may be different from
-      the :attr:`manufacturer` of the device.
+      the :attr:`~pskc.device.Device.manufacturer` of the device.
 
    .. attribute:: key_profile
 
@@ -208,62 +221,7 @@ The Key class
    .. attribute:: key_userid
 
       The distinguished name of the user associated with the key.
-      Also see :attr:`device_userid`.
-
-   .. attribute:: manufacturer
-
-      The name of the manufacturer of the device to which the key is
-      provisioned.
-      `RFC 6030 <https://tools.ietf.org/html/rfc6030#section-4.3.1>`__
-      prescribes that the value is of the form ``oath.prefix`` for `OATH
-      Manufacturer Prefixes 
<http://www.openauthentication.org/oath-id/prefixes/>`_
-      or ``iana.organisation`` for `IANA Private Enterprise Numbers
-      
<https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers>`_
-      however, it is generally just a string. The value may be different from
-      the :attr:`issuer` of the key on the device.
-
-   .. attribute:: serial
-
-      The serial number of the device to which the key is provisioned.
-      Together with :attr:`manufacturer` (and possibly :attr:`issue_no`) this
-      should uniquely identify the device.
-
-   .. attribute:: model
-
-      A manufacturer-specific description of the model of the device.
-
-   .. attribute:: issue_no
-
-      The issue number in case there are devices with the same :attr:`serial`
-      number so that they can be distinguished by different issue numbers.
-
-   .. attribute:: device_binding
-
-      Reference to a device identifier (e.g. IMEI) that allows a provisioning
-      server to ensure that the key is going to be loaded into a specific
-      device.
-
-   .. attribute:: start_date
-
-      :class:`datetime.datetime` value that indicates that the device should
-      only be used after this date.
-
-   .. attribute:: expiry_date
-
-      :class:`datetime.datetime` value that indicates that the device should
-      only be used before this date. Systems should not rely upon the device
-      to enforce key usage date restrictions, as some devices do not have an
-      internal clock.
-
-   .. attribute:: device_userid
-
-      The distinguished name of the user associated with the device.
-      Also see :attr:`key_userid`.
-
-   .. attribute:: crypto_module
-
-      Implementation specific unique identifier of the cryptographic module
-      on the device to which the keys have been (or will be) provisioned.
+      Also see :attr:`~pskc.device.Device.device_userid`.
 
    .. attribute:: algorithm_suite
 
@@ -322,3 +280,94 @@ The Key class
       This will return None if there is no MAC to be checked. It will return
       True if all the MACs match. If any MAC fails a
       :exc:`~pskc.exceptions.DecryptionError` exception is raised.
+
+   Apart from the above, all properties of the :class:`~pskc.device.Device`
+   class are also transparently available in :class:`~pskc.key.Key`
+   instances.
+
+
+The Device class
+----------------
+
+.. module:: pskc.device
+
+.. class:: Device()
+
+   Instances of this class provide the following attributes and functions:
+
+   .. attribute:: keys
+
+      A list of :class:`~pskc.key.Key` instances that represent the keys that
+      are linked to this device. Most PSKC files only allow one key per
+      device which is why all :class:`~pskc.device.Device` attributes are
+      available in :class:`~pskc.key.Key`.
+
+   .. function:: add_key([**kwargs])
+
+      Add a new key to the device. The keyword arguments may refer to
+      any attributes of the :class:`~pskc.key.Key` or
+      :class:`~pskc.device.Device` class with which the new key is
+      initialised.
+
+
+        """Create a new key instance for the device.
+
+        The new key is initialised with properties from the provided keyword
+        arguments if any."""
+
+
+   .. attribute:: manufacturer
+
+      The name of the manufacturer of the device to which the key is
+      provisioned.
+      `RFC 6030 <https://tools.ietf.org/html/rfc6030#section-4.3.1>`__
+      prescribes that the value is of the form ``oath.prefix`` for `OATH
+      Manufacturer Prefixes 
<http://www.openauthentication.org/oath-id/prefixes/>`_
+      or ``iana.organisation`` for `IANA Private Enterprise Numbers
+      
<https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers>`_
+      however, it is generally just a string.
+      The value may be different from the :attr:`~pskc.key.Key.issuer` of
+      the key on the device.
+
+   .. attribute:: serial
+
+      The serial number of the device to which the key is provisioned.
+      Together with :attr:`manufacturer` (and possibly :attr:`issue_no`) this
+      should uniquely identify the device.
+
+   .. attribute:: model
+
+      A manufacturer-specific description of the model of the device.
+
+   .. attribute:: issue_no
+
+      The issue number in case there are devices with the same :attr:`serial`
+      number so that they can be distinguished by different issue numbers.
+
+   .. attribute:: device_binding
+
+      Reference to a device identifier (e.g. IMEI) that allows a provisioning
+      server to ensure that the key is going to be loaded into a specific
+      device.
+
+   .. attribute:: start_date
+
+      :class:`datetime.datetime` value that indicates that the device should
+      only be used after this date.
+
+   .. attribute:: expiry_date
+
+      :class:`datetime.datetime` value that indicates that the device should
+      only be used before this date. Systems should not rely upon the device
+      to enforce key usage date restrictions, as some devices do not have an
+      internal clock.
+
+   .. attribute:: device_userid
+
+      The distinguished name of the user associated with the device.
+      Also see :attr:`~pskc.key.Key.key_userid`.
+
+   .. attribute:: crypto_module
+
+      Implementation specific unique identifier of the cryptographic module
+      on the device to which the keys have been (or will be) provisioned.
diff --git a/pskc/__init__.py b/pskc/__init__.py
index b9126a3..a99a4a5 100644
--- a/pskc/__init__.py
+++ b/pskc/__init__.py
@@ -57,6 +57,7 @@ class PSKC(object):
       id: identifier
       encryption: information on used encryption (Encryption instance)
       mac: information on used MAC method (MAC instance)
+      devices: list of devices (Device instances)
       keys: list of keys (Key instances)
     """
 
@@ -67,7 +68,7 @@ class PSKC(object):
         self.id = None
         self.encryption = Encryption(self)
         self.mac = MAC(self)
-        self.keys = []
+        self.devices = []
         if filename is not None:
             from pskc.exceptions import ParseError
             from pskc.parser import PSKCParser
@@ -81,14 +82,32 @@ class PSKC(object):
         else:
             self.version = '1.0'
 
+    @property
+    def keys(self):
+        return tuple(key for device in self.devices for key in device.keys)
+
+    def add_device(self, **kwargs):
+        """Create a new device instance for the PSKC file.
+
+        The device is initialised with properties from the provided keyword
+        arguments if any."""
+        from pskc.device import Device
+        device = Device(self)
+        self.devices.append(device)
+        # assign the kwargs as key properties
+        for k, v in kwargs.items():
+            if not hasattr(device, k):
+                raise AttributeError()
+            setattr(device, k, v)
+        return device
+
     def add_key(self, **kwargs):
         """Create a new key instance for the PSKC file.
 
         The new key is initialised with properties from the provided keyword
         arguments if any."""
-        from pskc.key import Key
-        key = Key(self)
-        self.keys.append(key)
+        device = self.add_device()
+        key = device.add_key()
         # assign the kwargs as key properties
         for k, v in kwargs.items():
             if not hasattr(key, k):
diff --git a/pskc/device.py b/pskc/device.py
new file mode 100644
index 0000000..6eac716
--- /dev/null
+++ b/pskc/device.py
@@ -0,0 +1,69 @@
+# device.py - module for handling device info from pskc files
+# coding: utf-8
+#
+# Copyright (C) 2016 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+"""Module that handles device information stored in PSKC files."""
+
+
+class Device(object):
+    """Representation of a single key from a PSKC file.
+
+    Instances of this class provide the following properties:
+
+      manufacturer: name of the organisation that made the device
+      serial: serial number of the device
+      model: device model description
+      issue_no: issue number per serial number
+      device_binding: device (class) identifier for the key to be loaded upon
+      start_date: key should not be used before this date
+      expiry_date: key or device may expire after this date
+      device_userid: user distinguished name associated with the device
+      crypto_module: id of module to which keys are provisioned within device
+    """
+
+    def __init__(self, pskc):
+
+        self.pskc = pskc
+
+        self.manufacturer = None
+        self.serial = None
+        self.model = None
+        self.issue_no = None
+        self.device_binding = None
+        self.start_date = None
+        self.expiry_date = None
+        self.device_userid = None
+        self.crypto_module = None
+
+        self.keys = []
+
+    def add_key(self, **kwargs):
+        """Create a new key instance for the device.
+
+        The new key is initialised with properties from the provided keyword
+        arguments if any."""
+        from pskc.key import Key
+        key = Key(self)
+        self.keys.append(key)
+        # assign the kwargs as key properties
+        for k, v in kwargs.items():
+            if not hasattr(key, k):
+                raise AttributeError()
+            setattr(key, k, v)
+        return key
diff --git a/pskc/key.py b/pskc/key.py
index fe84db2..b5cec51 100644
--- a/pskc/key.py
+++ b/pskc/key.py
@@ -40,8 +40,8 @@ class DataType(object):
       value_mac: MAC of the encrypted value
     """
 
-    def __init__(self, key):
-        self.pskc = key.pskc
+    def __init__(self, pskc):
+        self.pskc = pskc
         self.value = None
         self.cipher_value = None
         self.algorithm = None
@@ -156,6 +156,19 @@ class IntegerDataType(DataType):
         return binascii.unhexlify(value.zfill(n + (n & 1)))
 
 
+class DeviceProperty(object):
+    """A data descriptor that delegates actions to the Device instance."""
+
+    def __init__(self, name):
+        self.name = name
+
+    def __get__(self, obj, objtype):
+        return getattr(obj.device, self.name)
+
+    def __set__(self, obj, val):
+        setattr(obj.device, self.name, val)
+
+
 class Key(object):
     """Representation of a single key from a PSKC file.
 
@@ -173,15 +186,6 @@ class Key(object):
       key_reference: reference to an external key
       friendly_name: human-readable name for the secret key
       key_userid: user distinguished name associated with the key
-      manufacturer: name of the organisation that made the device
-      serial: serial number of the device
-      model: device model description
-      issue_no: issue number per serial number
-      device_binding: device (class) identifier for the key to be loaded upon
-      start_date: key should not be used before this date
-      expiry_date: key or device may expire after this date
-      device_userid: user distinguished name associated with the device
-      crypto_module: id of module to which keys are provisioned within device
       algorithm_suite: additional algorithm characteristics (e.g. used hash)
       challenge_encoding: format of the challenge for CR devices
       challenge_min_length: minimum accepted challenge length by device
@@ -191,20 +195,24 @@ class Key(object):
       response_length: the length of the response of the device
       response_check: whether the device appends a Luhn check digit
       policy: reference to policy information (see Policy class)
+
+    This class also provides access to the manufacturer, serial, model,
+    issue_no, device_binding, start_date, expiry_date, device_userid and
+    crypto_module properties of the Device class.
     """
 
-    def __init__(self, pskc):
+    def __init__(self, device):
 
-        self.pskc = pskc
+        self.device = device
 
         self.id = None
         self.algorithm = None
 
-        self._secret = BinaryDataType(self)
-        self._counter = IntegerDataType(self)
-        self._time_offset = IntegerDataType(self)
-        self._time_interval = IntegerDataType(self)
-        self._time_drift = IntegerDataType(self)
+        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
@@ -212,17 +220,6 @@ class Key(object):
         self.friendly_name = None
         self.key_userid = None
 
-        self.manufacturer = None
-        self.serial = None
-        self.model = None
-        self.issue_no = None
-        self.device_binding = None
-        self.start_date = None
-        self.expiry_date = None
-        self.device_userid = None
-
-        self.crypto_module = None
-
         self.algorithm_suite = None
 
         self.challenge_encoding = None
@@ -261,6 +258,16 @@ class Key(object):
         fset=lambda self, x: self._time_drift.set_value(x),
         doc="Device clock drift value (number of time intervals).")
 
+    manufacturer = DeviceProperty('manufacturer')
+    serial = DeviceProperty('serial')
+    model = DeviceProperty('model')
+    issue_no = DeviceProperty('issue_no')
+    device_binding = DeviceProperty('device_binding')
+    start_date = DeviceProperty('start_date')
+    expiry_date = DeviceProperty('expiry_date')
+    device_userid = DeviceProperty('device_userid')
+    crypto_module = DeviceProperty('crypto_module')
+
     def check(self):
         """Check if all MACs in the message are valid."""
         if all(x is not False for x in (
diff --git a/pskc/parser.py b/pskc/parser.py
index 50e3cc7..98bfeef 100644
--- a/pskc/parser.py
+++ b/pskc/parser.py
@@ -46,7 +46,7 @@ class PSKCParser(object):
         cls.parse_mac_method(pskc.mac, find(container, 'MACMethod'))
         # handle KeyPackage entries
         for key_package in findall(container, 'KeyPackage'):
-            cls.parse_key(pskc.add_key(), key_package)
+            cls.parse_key_package(pskc.add_device(), key_package)
 
     @classmethod
     def parse_encryption(cls, encryption, key_info):
@@ -97,15 +97,32 @@ class PSKCParser(object):
         mac_key_reference = findtext(mac_method, 'MACKeyReference')
 
     @classmethod
-    def parse_key(cls, key, key_package):
+    def parse_key_package(cls, device, key_package):
         """Read key information from the provided <KeyPackage> tree."""
 
-        key_elm = find(key_package, 'Key')
-        if key_elm is not None:
-            key.id = key_elm.get('Id')
-            key.algorithm = key_elm.get('Algorithm')
+        device.manufacturer = findtext(key_package, 'DeviceInfo/Manufacturer')
+        device.serial = findtext(key_package, 'DeviceInfo/SerialNo')
+        device.model = findtext(key_package, 'DeviceInfo/Model')
+        device.issue_no = findtext(key_package, 'DeviceInfo/IssueNo')
+        device.device_binding = findtext(
+            key_package, 'DeviceInfo/DeviceBinding')
+        device.start_date = findtime(key_package, 'DeviceInfo/StartDate')
+        device.expiry_date = findtime(key_package, 'DeviceInfo/ExpiryDate')
+        device.device_userid = findtext(key_package, 'DeviceInfo/UserId')
+
+        device.crypto_module = findtext(key_package, 'CryptoModuleInfo/Id')
+
+        for key_elm in findall(key_package, 'Key'):
+            cls.parse_key(device.add_key(), key_elm)
+
+    @classmethod
+    def parse_key(cls, key, key_elm):
+        """Read key information from the provided <KeyPackage> tree."""
+
+        key.id = key_elm.get('Id')
+        key.algorithm = key_elm.get('Algorithm')
 
-        data = find(key_package, 'Key/Data')
+        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'))
@@ -113,30 +130,18 @@ class PSKCParser(object):
             cls.parse_datatype(key._time_interval, find(data, 'TimeInterval'))
             cls.parse_datatype(key._time_drift, find(data, 'TimeDrift'))
 
-        key.issuer = findtext(key_package, 'Key/Issuer')
-        key.key_profile = findtext(key_package, 'Key/KeyProfileId')
-        key.key_reference = findtext(key_package, 'Key/KeyReference')
-        key.friendly_name = findtext(key_package, 'Key/FriendlyName')
+        key.issuer = findtext(key_elm, 'Issuer')
+        key.key_profile = findtext(key_elm, 'KeyProfileId')
+        key.key_reference = findtext(key_elm, 'KeyReference')
+        key.friendly_name = findtext(key_elm, 'FriendlyName')
         # TODO: support multi-language values of <FriendlyName>
-        key.key_userid = findtext(key_package, 'Key/UserId')
-
-        key.manufacturer = findtext(key_package, 'DeviceInfo/Manufacturer')
-        key.serial = findtext(key_package, 'DeviceInfo/SerialNo')
-        key.model = findtext(key_package, 'DeviceInfo/Model')
-        key.issue_no = findtext(key_package, 'DeviceInfo/IssueNo')
-        key.device_binding = findtext(
-            key_package, 'DeviceInfo/DeviceBinding')
-        key.start_date = findtime(key_package, 'DeviceInfo/StartDate')
-        key.expiry_date = findtime(key_package, 'DeviceInfo/ExpiryDate')
-        key.device_userid = findtext(key_package, 'DeviceInfo/UserId')
-
-        key.crypto_module = findtext(key_package, 'CryptoModuleInfo/Id')
+        key.key_userid = findtext(key_elm, 'UserId')
 
         key.algorithm_suite = findtext(
-            key_package, 'Key/AlgorithmParameters/Suite')
+            key_elm, 'AlgorithmParameters/Suite')
 
         challenge_format = find(
-            key_package, 'Key/AlgorithmParameters/ChallengeFormat')
+            key_elm, 'AlgorithmParameters/ChallengeFormat')
         if challenge_format is not None:
             key.challenge_encoding = challenge_format.get('Encoding')
             key.challenge_min_length = getint(challenge_format, 'Min')
@@ -146,7 +151,7 @@ class PSKCParser(object):
                     challenge_format, 'CheckDigit'))
 
         response_format = find(
-            key_package, 'Key/AlgorithmParameters/ResponseFormat')
+            key_elm, 'AlgorithmParameters/ResponseFormat')
         if response_format is not None:
             key.response_encoding = response_format.get('Encoding')
             key.response_length = getint(response_format, 'Length')
@@ -154,7 +159,7 @@ class PSKCParser(object):
                 response_format, 'CheckDigits', getbool(
                     response_format, 'CheckDigit'))
 
-        cls.parse_policy(key.policy, find(key_package, 'Key/Policy'))
+        cls.parse_policy(key.policy, find(key_elm, 'Policy'))
 
     @classmethod
     def parse_datatype(cls, dt, element):
diff --git a/pskc/policy.py b/pskc/policy.py
index 3cfdad1..09ea13b 100644
--- a/pskc/policy.py
+++ b/pskc/policy.py
@@ -137,8 +137,8 @@ class Policy(object):
     @property
     def pin_key(self):
         """Reference to the PSKC Key that holds the PIN (if any)."""
-        if self.pin_key_id and self.key and self.key.pskc:
-            for key in self.key.pskc.keys:
+        if self.pin_key_id and self.key and self.key.device.pskc:
+            for key in self.key.device.pskc.keys:
                 if key.id == self.pin_key_id:
                     return key
 
diff --git a/pskc/serialiser.py b/pskc/serialiser.py
index f1bcdf9..5542be0 100644
--- a/pskc/serialiser.py
+++ b/pskc/serialiser.py
@@ -34,8 +34,8 @@ class PSKCSerialiser(object):
                             Id=pskc.id)
         cls.serialise_encryption(pskc.encryption, container)
         cls.serialise_mac(pskc.mac, container)
-        for key in pskc.keys:
-            cls.serialise_key(key, container)
+        for device in pskc.devices:
+            cls.serialise_key_package(device, container)
         return container
 
     @classmethod
@@ -92,33 +92,34 @@ class PSKCSerialiser(object):
                         mac.key_plain_value)).decode())
 
     @classmethod
-    def serialise_key(cls, key, container):
-
+    def serialise_key_package(cls, device, container):
         key_package = mk_elem(container, 'pskc:KeyPackage', empty=True)
-
         if any(x is not None
-               for x in (key.manufacturer, key.serial, key.model,
-                         key.issue_no, key.device_binding, key.start_date,
-                         key.expiry_date, key.device_userid)):
+               for x in (device.manufacturer, device.serial, device.model,
+                         device.issue_no, device.device_binding,
+                         device.start_date, device.expiry_date,
+                         device.device_userid)):
             device_info = mk_elem(key_package, 'pskc:DeviceInfo', empty=True)
-            mk_elem(device_info, 'pskc:Manufacturer', key.manufacturer)
-            mk_elem(device_info, 'pskc:SerialNo', key.serial)
-            mk_elem(device_info, 'pskc:Model', key.model)
-            mk_elem(device_info, 'pskc:IssueNo', key.issue_no)
-            mk_elem(device_info, 'pskc:DeviceBinding', key.device_binding)
-            mk_elem(device_info, 'pskc:StartDate', key.start_date)
-            mk_elem(device_info, 'pskc:ExpiryDate', key.expiry_date)
-            mk_elem(device_info, 'pskc:UserId', key.device_userid)
-
-        if key.crypto_module is not None:
+            mk_elem(device_info, 'pskc:Manufacturer', device.manufacturer)
+            mk_elem(device_info, 'pskc:SerialNo', device.serial)
+            mk_elem(device_info, 'pskc:Model', device.model)
+            mk_elem(device_info, 'pskc:IssueNo', device.issue_no)
+            mk_elem(device_info, 'pskc:DeviceBinding', device.device_binding)
+            mk_elem(device_info, 'pskc:StartDate', device.start_date)
+            mk_elem(device_info, 'pskc:ExpiryDate', device.expiry_date)
+            mk_elem(device_info, 'pskc:UserId', device.device_userid)
+        if device.crypto_module is not None:
             crypto_module = mk_elem(key_package, 'pskc:CryptoModuleInfo',
                                     empty=True)
-            mk_elem(crypto_module, 'pskc:Id', key.crypto_module)
+            mk_elem(crypto_module, 'pskc:Id', device.crypto_module)
+        for key in device.keys:
+            cls.serialise_key(key, key_package)
 
+    @classmethod
+    def serialise_key(cls, key, key_package):
         key_elm = mk_elem(key_package, 'pskc:Key', empty=True, Id=key.id,
                           Algorithm=key.algorithm, )
         mk_elem(key_elm, 'pskc:Issuer', key.issuer)
-
         if any((key.algorithm_suite, key.challenge_encoding,
                 key.response_encoding, key.response_length)):
             parameters = mk_elem(key_elm, 'pskc:AlgorithmParameters',
@@ -133,7 +134,6 @@ class PSKCSerialiser(object):
                     Encoding=key.response_encoding,
                     Length=key.response_length,
                     CheckDigits=key.response_check)
-
         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)
@@ -148,7 +148,6 @@ class PSKCSerialiser(object):
         cls.serialise_datatype(
             key._time_drift, key_elm, 'pskc:TimeDrift', 'time_drif')
         mk_elem(key_elm, 'pskc:UserId', key.key_userid)
-
         cls.serialise_policy(key.policy, key_elm)
 
     @classmethod
diff --git a/tests/test_misc.doctest b/tests/test_misc.doctest
index 53bc059..26b3af7 100644
--- a/tests/test_misc.doctest
+++ b/tests/test_misc.doctest
@@ -44,8 +44,10 @@ This tests the most minimal valid PSKC file with one empty 
key.
 ... </KeyContainer>
 ... '''.strip())
 >>> pskc = PSKC(minimal_pskc)
->>> [key.id for key in pskc.keys]
-[None]
+>>> len(pskc.keys)
+0
+>>> len(pskc.devices)
+1
 
 
 Check creation of empty PSKC structure and adding an empty key to the list.
@@ -58,12 +60,37 @@ Check creation of empty PSKC structure and adding an empty 
key to the list.
 True
 
 
-Adding a key with unknown attributes raises an error.
+We can also put device-specific properties in a device:
+
+>>> pskc = PSKC()
+>>> device = pskc.add_device(manufacturer='Tokens INC.')
+>>> len(pskc.keys)
+0
+>>> key = device.add_key(id='123', serial='456')
+>>> len(pskc.keys)
+1
+>>> key.id
+'123'
+>>> key.manufacturer
+'Tokens INC.'
+>>> device.serial
+'456'
+
+
+Adding a key or device with unknown attributes raises an error.
 
 >>> key = pskc.add_key(foo='bar')  # doctest: +IGNORE_EXCEPTION_DETAIL
 Traceback (most recent call last):
     ...
 AttributeError
+>>> device.add_key(foo='bar')  # doctest: +IGNORE_EXCEPTION_DETAIL
+Traceback (most recent call last):
+    ...
+AttributeError
+>>> device = pskc.add_device(foo='bar')  # doctest: +IGNORE_EXCEPTION_DETAIL
+Traceback (most recent call last):
+    ...
+AttributeError
 
 
 Setting secret, counter, etc. also works
diff --git a/tests/test_write.doctest b/tests/test_write.doctest
index e5d1417..096194f 100644
--- a/tests/test_write.doctest
+++ b/tests/test_write.doctest
@@ -509,3 +509,40 @@ We can make the PKKDF2 salt have to be transmitted 
out-of-bounds:
   ...
  </pskc:KeyPackage>
 </pskc:KeyContainer>
+
+
+Write a PSKC file with two keys in onde KeyPackage section. Note that this
+is not allowed in the RFC 6030 schema. Note that device properties that are
+set on one key end up being applied to both keys.
+
+>>> pskc = PSKC()
+>>> device = pskc.add_device(manufacturer='TokenVendorAcme')
+>>> key = device.add_key(id='1', serial='123456', secret='1234', counter=42)
+>>> key = device.add_key(id='pin0', secret='5678')
+>>> pskc.write(sys.stdout)  #doctest: +ELLIPSIS +REPORT_UDIFF
+<?xml version="1.0" encoding="UTF-8"?>
+<pskc:KeyContainer Version="1.0" 
xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc">
+ <pskc:KeyPackage>
+  <pskc:DeviceInfo>
+   <pskc:Manufacturer>TokenVendorAcme</pskc:Manufacturer>
+   <pskc:SerialNo>123456</pskc:SerialNo>
+  </pskc:DeviceInfo>
+  <pskc:Key Id="1">
+   <pskc:Data>
+    <pskc:Secret>
+     <pskc:PlainValue>MTIzNA==</pskc:PlainValue>
+    </pskc:Secret>
+    <pskc:Counter>
+     <pskc:PlainValue>42</pskc:PlainValue>
+    </pskc:Counter>
+   </pskc:Data>
+  </pskc:Key>
+  <pskc:Key Id="pin0">
+   <pskc:Data>
+    <pskc:Secret>
+     <pskc:PlainValue>NTY3OA==</pskc:PlainValue>
+    </pskc:Secret>
+   </pskc:Data>
+  </pskc:Key>
+ </pskc:KeyPackage>
+</pskc:KeyContainer>

http://arthurdejong.org/git/python-pskc/commit/?id=84bfb8a6ebd71be8119b03a9e88dde9554c5900d

commit 84bfb8a6ebd71be8119b03a9e88dde9554c5900d
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Fri Sep 16 20:04:45 2016 +0200

    Move XML generation to own module
    
    Similar to the change for parsing, move the XML serialisation of PSKC
    data to a single class in a separate module.

diff --git a/pskc/__init__.py b/pskc/__init__.py
index 71401cd..b9126a3 100644
--- a/pskc/__init__.py
+++ b/pskc/__init__.py
@@ -81,16 +81,6 @@ class PSKC(object):
         else:
             self.version = '1.0'
 
-    def make_xml(self):
-        from pskc.xml import mk_elem
-        container = mk_elem('pskc:KeyContainer', Version=self.version,
-                            Id=self.id)
-        self.encryption.make_xml(container)
-        self.mac.make_xml(container)
-        for key in self.keys:
-            key.make_xml(container)
-        return container
-
     def add_key(self, **kwargs):
         """Create a new key instance for the PSKC file.
 
@@ -109,8 +99,9 @@ class PSKC(object):
     def write(self, filename):
         """Write the PSKC file to the provided file."""
         from pskc.xml import tostring
+        from pskc.serialiser import PSKCSerialiser
         if hasattr(filename, 'write'):
-            xml = tostring(self.make_xml())
+            xml = tostring(PSKCSerialiser.serialise_document(self))
             try:
                 filename.write(xml)
             except TypeError:  # pragma: no cover (Python 3 specific)
diff --git a/pskc/encryption.py b/pskc/encryption.py
index 80eb1b4..a493e79 100644
--- a/pskc/encryption.py
+++ b/pskc/encryption.py
@@ -26,8 +26,6 @@ algorithms and decryption.
 The encryption key can be derived using the KeyDerivation class.
 """
 
-import base64
-
 
 def algorithm_key_lengths(algorithm):
     """Return the possible key lengths for the configured algorithm."""
@@ -144,24 +142,6 @@ class KeyDerivation(object):
         self.pbkdf2_key_length = None
         self.pbkdf2_prf = None
 
-    def make_xml(self, encryption_key, key_names):
-        from pskc.xml import mk_elem
-        derived_key = mk_elem(encryption_key, 'xenc11:DerivedKey', empty=True)
-        key_derivation = mk_elem(derived_key, 'xenc11:KeyDerivationMethod',
-                                 Algorithm=self.algorithm)
-        if self.algorithm.endswith('#pbkdf2'):
-            pbkdf2 = mk_elem(key_derivation, 'xenc11:PBKDF2-params',
-                             empty=True)
-            if self.pbkdf2_salt:
-                salt = mk_elem(pbkdf2, 'Salt', empty=True)
-                mk_elem(salt, 'Specified', base64.b64encode(self.pbkdf2_salt))
-            mk_elem(pbkdf2, 'IterationCount', self.pbkdf2_iterations)
-            mk_elem(pbkdf2, 'KeyLength', self.pbkdf2_key_length)
-            mk_elem(pbkdf2, 'PRF', self.pbkdf2_prf)
-        # TODO: serialise ReferenceList/DataReference
-        for name in key_names:
-            mk_elem(derived_key, 'xenc11:MasterKeyName', name)
-
     def derive_pbkdf2(self, password):
         from Crypto.Protocol.KDF import PBKDF2
         from pskc.mac import get_hmac
@@ -239,20 +219,6 @@ class Encryption(object):
         self.derivation = KeyDerivation()
         self.fields = []
 
-    def make_xml(self, container):
-        from pskc.xml import mk_elem
-        if all(x is None
-               for x in (self.id, self.key_name, self.key,
-                         self.derivation.algorithm)):
-            return
-        encryption_key = mk_elem(container, 'pskc:EncryptionKey',
-                                 Id=self.id, empty=True)
-        if self.derivation.algorithm:
-            self.derivation.make_xml(encryption_key, self.key_names)
-        else:
-            for name in self.key_names:
-                mk_elem(encryption_key, 'ds:KeyName', name)
-
     @property
     def key_name(self):
         """Provide the name of the (first) key."""
diff --git a/pskc/key.py b/pskc/key.py
index 4678f32..fe84db2 100644
--- a/pskc/key.py
+++ b/pskc/key.py
@@ -62,44 +62,6 @@ class DataType(object):
         """Convert the value to an unencrypted string representation."""
         raise NotImplementedError  # pragma: no cover
 
-    def make_xml(self, key, tag, field):
-        from pskc.xml import find, mk_elem
-        # skip empty values
-        if self.value in (None, '') and not self.cipher_value:
-            return
-        # find the data tag and create our tag under it
-        data = find(key, 'pskc:Data')
-        if data is None:
-            data = mk_elem(key, 'pskc:Data', empty=True)
-        element = mk_elem(data, tag, empty=True)
-        # see if we should encrypt
-        if field in self.pskc.encryption.fields and not self.cipher_value:
-            self.cipher_value = self.pskc.encryption.encrypt_value(
-                self._to_bin(self.value))
-            self.algorithm = self.pskc.encryption.algorithm
-            self.value = None
-        # write out value
-        if self.cipher_value:
-            encrypted_value = mk_elem(
-                element, 'pskc:EncryptedValue', empty=True)
-            mk_elem(
-                encrypted_value, 'xenc:EncryptionMethod',
-                Algorithm=self.algorithm)
-            cipher_data = mk_elem(
-                encrypted_value, 'xenc:CipherData', empty=True)
-            mk_elem(
-                cipher_data, 'xenc:CipherValue',
-                base64.b64encode(self.cipher_value).decode())
-            if self.value_mac:
-                mk_elem(element, 'pskc:ValueMAC', base64.b64encode(
-                    self.value_mac).decode())
-            elif self.pskc.mac.algorithm:
-                mk_elem(element, 'pskc:ValueMAC', base64.b64encode(
-                    self.pskc.mac.generate_mac(self.cipher_value)
-                ).decode())
-        else:
-            mk_elem(element, 'pskc:PlainValue', self._to_text(self.value))
-
     def get_value(self):
         """Provide the attribute value, decrypting as needed."""
         if self.value is not None:
@@ -274,60 +236,6 @@ class Key(object):
 
         self.policy = Policy(self)
 
-    def make_xml(self, container):
-        from pskc.xml import mk_elem
-
-        key_package = mk_elem(container, 'pskc:KeyPackage', empty=True)
-
-        if any(x is not None
-               for x in (self.manufacturer, self.serial, self.model,
-                         self.issue_no, self.device_binding, self.start_date,
-                         self.expiry_date, self.device_userid)):
-            device_info = mk_elem(key_package, 'pskc:DeviceInfo', empty=True)
-            mk_elem(device_info, 'pskc:Manufacturer', self.manufacturer)
-            mk_elem(device_info, 'pskc:SerialNo', self.serial)
-            mk_elem(device_info, 'pskc:Model', self.model)
-            mk_elem(device_info, 'pskc:IssueNo', self.issue_no)
-            mk_elem(device_info, 'pskc:DeviceBinding', self.device_binding)
-            mk_elem(device_info, 'pskc:StartDate', self.start_date)
-            mk_elem(device_info, 'pskc:ExpiryDate', self.expiry_date)
-            mk_elem(device_info, 'pskc:UserId', self.device_userid)
-
-        if self.crypto_module is not None:
-            crypto_module = mk_elem(key_package, 'pskc:CryptoModuleInfo',
-                                    empty=True)
-            mk_elem(crypto_module, 'pskc:Id', self.crypto_module)
-
-        key = mk_elem(key_package, 'pskc:Key', empty=True, Id=self.id,
-                      Algorithm=self.algorithm, )
-        mk_elem(key, 'pskc:Issuer', self.issuer)
-
-        if any((self.algorithm_suite, self.challenge_encoding,
-                self.response_encoding, self.response_length)):
-            parameters = mk_elem(key, 'pskc:AlgorithmParameters', empty=True)
-            mk_elem(parameters, 'pskc:Suite', self.algorithm_suite)
-            mk_elem(parameters, 'pskc:ChallengeFormat',
-                    Encoding=self.challenge_encoding,
-                    Min=self.challenge_min_length,
-                    Max=self.challenge_max_length,
-                    CheckDigits=self.challenge_check)
-            mk_elem(parameters, 'pskc:ResponseFormat',
-                    Encoding=self.response_encoding,
-                    Length=self.response_length,
-                    CheckDigits=self.response_check)
-
-        mk_elem(key, 'pskc:KeyProfileId', self.key_profile)
-        mk_elem(key, 'pskc:KeyReference', self.key_reference)
-        mk_elem(key, 'pskc:FriendlyName', self.friendly_name)
-        self._secret.make_xml(key, 'pskc:Secret', 'secret')
-        self._counter.make_xml(key, 'pskc:Counter', 'counter')
-        self._time_offset.make_xml(key, 'pskc:Time', 'time_offset')
-        self._time_interval.make_xml(key, 'pskc:TimeInterval', 'time_interval')
-        self._time_drift.make_xml(key, 'pskc:TimeDrift', 'time_drif')
-        mk_elem(key, 'pskc:UserId', self.key_userid)
-
-        self.policy.make_xml(key)
-
     secret = property(
         fget=lambda self: self._secret.get_value(),
         fset=lambda self, x: self._secret.set_value(x),
diff --git a/pskc/mac.py b/pskc/mac.py
index 4ad8aa6..c5ac1ec 100644
--- a/pskc/mac.py
+++ b/pskc/mac.py
@@ -29,7 +29,6 @@ with the PSKC encryption key.
 """
 
 
-import base64
 import re
 
 
@@ -83,27 +82,6 @@ class MAC(object):
         self.key_cipher_value = None
         self.key_algorithm = None
 
-    def make_xml(self, container):
-        from pskc.xml import mk_elem
-        if not self.algorithm and not self.key:
-            return
-        mac_method = mk_elem(
-            container, 'pskc:MACMethod', Algorithm=self.algorithm, empty=True)
-        mac_key = mk_elem(mac_method, 'pskc:MACKey', empty=True)
-        mk_elem(
-            mac_key, 'xenc:EncryptionMethod',
-            Algorithm=self.pskc.encryption.algorithm)
-        cipher_data = mk_elem(mac_key, 'xenc:CipherData', empty=True)
-        if self.key_cipher_value:
-            mk_elem(
-                cipher_data, 'xenc:CipherValue',
-                base64.b64encode(self.key_cipher_value).decode())
-        elif self.key_plain_value:
-            mk_elem(
-                cipher_data, 'xenc:CipherValue', base64.b64encode(
-                    self.pskc.encryption.encrypt_value(self.key_plain_value)
-                ).decode())
-
     @property
     def key(self):
         """Provides access to the MAC key binary value if available."""
diff --git a/pskc/policy.py b/pskc/policy.py
index a955a16..3cfdad1 100644
--- a/pskc/policy.py
+++ b/pskc/policy.py
@@ -114,30 +114,6 @@ class Policy(object):
         self.pin_encoding = None
         self.unknown_policy_elements = False
 
-    def make_xml(self, key):
-        from pskc.xml import mk_elem
-        # check if any policy attribute is set
-        if not self.key_usage and all(x is None for x in (
-                self.start_date, self.expiry_date,
-                self.number_of_transactions, self.pin_key_id, self.pin_usage,
-                self.pin_max_failed_attemtps, self.pin_min_length,
-                self.pin_max_length, self.pin_encoding)):
-            return
-        policy = mk_elem(key, 'pskc:Policy', empty=True)
-        mk_elem(policy, 'pskc:StartDate', self.start_date)
-        mk_elem(policy, 'pskc:ExpiryDate', self.expiry_date)
-        mk_elem(policy, 'pskc:PINPolicy',
-                PINKeyId=self.pin_key_id,
-                PINUsageMode=self.pin_usage,
-                MaxFailedAttempts=self.pin_max_failed_attemtps,
-                MinLength=self.pin_min_length,
-                MaxLength=self.pin_max_length,
-                PINEncoding=self.pin_encoding)
-        for usage in self.key_usage:
-            mk_elem(policy, 'pskc:KeyUsage', usage)
-        mk_elem(policy, 'pskc:NumberOfTransactions',
-                self.number_of_transactions)
-
     def may_use(self, usage=None, now=None):
         """Check whether the key may be used for the provided purpose."""
         import datetime
diff --git a/pskc/serialiser.py b/pskc/serialiser.py
new file mode 100644
index 0000000..f1bcdf9
--- /dev/null
+++ b/pskc/serialiser.py
@@ -0,0 +1,214 @@
+# serialiser.py - PSKC file parsing functions
+# coding: utf-8
+#
+# Copyright (C) 2016 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+"""Module for serialising PSKC files to XML."""
+
+
+import base64
+
+from pskc.xml import find, mk_elem
+
+
+class PSKCSerialiser(object):
+
+    @classmethod
+    def serialise_document(cls, pskc):
+        container = mk_elem('pskc:KeyContainer', Version=pskc.version,
+                            Id=pskc.id)
+        cls.serialise_encryption(pskc.encryption, container)
+        cls.serialise_mac(pskc.mac, container)
+        for key in pskc.keys:
+            cls.serialise_key(key, container)
+        return container
+
+    @classmethod
+    def serialise_encryption(cls, encryption, container):
+        if all(x is None
+               for x in (encryption.id, encryption.key_name, encryption.key,
+                         encryption.derivation.algorithm)):
+            return
+        encryption_key = mk_elem(container, 'pskc:EncryptionKey',
+                                 Id=encryption.id, empty=True)
+        if encryption.derivation.algorithm:
+            cls.serialise_key_derivation(
+                encryption.derivation, encryption_key, encryption.key_names)
+        else:
+            for name in encryption.key_names:
+                mk_elem(encryption_key, 'ds:KeyName', name)
+
+    @classmethod
+    def serialise_key_derivation(cls, derivation, encryption_key, key_names):
+        derived_key = mk_elem(encryption_key, 'xenc11:DerivedKey', empty=True)
+        key_derivation = mk_elem(derived_key, 'xenc11:KeyDerivationMethod',
+                                 Algorithm=derivation.algorithm)
+        if derivation.algorithm.endswith('#pbkdf2'):
+            pbkdf2 = mk_elem(key_derivation, 'xenc11:PBKDF2-params',
+                             empty=True)
+            if derivation.pbkdf2_salt:
+                salt = mk_elem(pbkdf2, 'Salt', empty=True)
+                mk_elem(salt, 'Specified',
+                        base64.b64encode(derivation.pbkdf2_salt))
+            mk_elem(pbkdf2, 'IterationCount', derivation.pbkdf2_iterations)
+            mk_elem(pbkdf2, 'KeyLength', derivation.pbkdf2_key_length)
+            mk_elem(pbkdf2, 'PRF', derivation.pbkdf2_prf)
+        # TODO: serialise ReferenceList/DataReference
+        for name in key_names:
+            mk_elem(derived_key, 'xenc11:MasterKeyName', name)
+
+    @classmethod
+    def serialise_mac(cls, mac, container):
+        if not mac.algorithm and not mac.key:
+            return
+        mac_method = mk_elem(
+            container, 'pskc:MACMethod', Algorithm=mac.algorithm, empty=True)
+        mac_key = mk_elem(mac_method, 'pskc:MACKey', empty=True)
+        mk_elem(
+            mac_key, 'xenc:EncryptionMethod',
+            Algorithm=mac.pskc.encryption.algorithm)
+        cipher_data = mk_elem(mac_key, 'xenc:CipherData', empty=True)
+        if mac.key_cipher_value:
+            mk_elem(cipher_data, 'xenc:CipherValue',
+                    base64.b64encode(mac.key_cipher_value).decode())
+        elif mac.key_plain_value:
+            mk_elem(cipher_data, 'xenc:CipherValue',
+                    base64.b64encode(mac.pskc.encryption.encrypt_value(
+                        mac.key_plain_value)).decode())
+
+    @classmethod
+    def serialise_key(cls, key, container):
+
+        key_package = mk_elem(container, 'pskc:KeyPackage', empty=True)
+
+        if any(x is not None
+               for x in (key.manufacturer, key.serial, key.model,
+                         key.issue_no, key.device_binding, key.start_date,
+                         key.expiry_date, key.device_userid)):
+            device_info = mk_elem(key_package, 'pskc:DeviceInfo', empty=True)
+            mk_elem(device_info, 'pskc:Manufacturer', key.manufacturer)
+            mk_elem(device_info, 'pskc:SerialNo', key.serial)
+            mk_elem(device_info, 'pskc:Model', key.model)
+            mk_elem(device_info, 'pskc:IssueNo', key.issue_no)
+            mk_elem(device_info, 'pskc:DeviceBinding', key.device_binding)
+            mk_elem(device_info, 'pskc:StartDate', key.start_date)
+            mk_elem(device_info, 'pskc:ExpiryDate', key.expiry_date)
+            mk_elem(device_info, 'pskc:UserId', key.device_userid)
+
+        if key.crypto_module is not None:
+            crypto_module = mk_elem(key_package, 'pskc:CryptoModuleInfo',
+                                    empty=True)
+            mk_elem(crypto_module, 'pskc:Id', key.crypto_module)
+
+        key_elm = mk_elem(key_package, 'pskc:Key', empty=True, Id=key.id,
+                          Algorithm=key.algorithm, )
+        mk_elem(key_elm, 'pskc:Issuer', key.issuer)
+
+        if any((key.algorithm_suite, key.challenge_encoding,
+                key.response_encoding, key.response_length)):
+            parameters = mk_elem(key_elm, 'pskc:AlgorithmParameters',
+                                 empty=True)
+            mk_elem(parameters, 'pskc:Suite', key.algorithm_suite)
+            mk_elem(parameters, 'pskc:ChallengeFormat',
+                    Encoding=key.challenge_encoding,
+                    Min=key.challenge_min_length,
+                    Max=key.challenge_max_length,
+                    CheckDigits=key.challenge_check)
+            mk_elem(parameters, 'pskc:ResponseFormat',
+                    Encoding=key.response_encoding,
+                    Length=key.response_length,
+                    CheckDigits=key.response_check)
+
+        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')
+        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):
+        # skip empty values
+        if dt.value in (None, '') and not dt.cipher_value:
+            return
+        # 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
+        # write out value
+        if dt.cipher_value:
+            encrypted_value = mk_elem(
+                element, 'pskc:EncryptedValue', empty=True)
+            mk_elem(
+                encrypted_value, 'xenc:EncryptionMethod',
+                Algorithm=dt.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))
+
+    @classmethod
+    def serialise_policy(cls, policy, key_elm):
+        # check if any policy attribute is set
+        if not policy.key_usage and all(x is None for x in (
+                policy.start_date, policy.expiry_date,
+                policy.number_of_transactions, policy.pin_key_id, 
policy.pin_usage,
+                policy.pin_max_failed_attemtps, policy.pin_min_length,
+                policy.pin_max_length, policy.pin_encoding)):
+            return
+        policy_elm = mk_elem(key_elm, 'pskc:Policy', empty=True)
+        mk_elem(policy_elm, 'pskc:StartDate', policy.start_date)
+        mk_elem(policy_elm, 'pskc:ExpiryDate', policy.expiry_date)
+        mk_elem(policy_elm, 'pskc:PINPolicy',
+                PINKeyId=policy.pin_key_id,
+                PINUsageMode=policy.pin_usage,
+                MaxFailedAttempts=policy.pin_max_failed_attemtps,
+                MinLength=policy.pin_min_length,
+                MaxLength=policy.pin_max_length,
+                PINEncoding=policy.pin_encoding)
+        for usage in policy.key_usage:
+            mk_elem(policy_elm, 'pskc:KeyUsage', usage)
+        mk_elem(policy_elm, 'pskc:NumberOfTransactions',
+                policy.number_of_transactions)

http://arthurdejong.org/git/python-pskc/commit/?id=426e821b1366095990304b04bbc41b3af384fe21

commit 426e821b1366095990304b04bbc41b3af384fe21
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Wed Sep 14 23:39:56 2016 +0200

    Move document parsing to own module
    
    This moves all the parse() functions to a single class in a dedicated
    module that can be used for parsing PSKC files. This should make it
    easier to subclass the parser.

diff --git a/pskc/__init__.py b/pskc/__init__.py
index 17021c7..71401cd 100644
--- a/pskc/__init__.py
+++ b/pskc/__init__.py
@@ -62,7 +62,6 @@ class PSKC(object):
 
     def __init__(self, filename=None):
         from pskc.encryption import Encryption
-        from pskc.exceptions import ParseError
         from pskc.mac import MAC
         self.version = None
         self.id = None
@@ -70,39 +69,18 @@ class PSKC(object):
         self.mac = MAC(self)
         self.keys = []
         if filename is not None:
+            from pskc.exceptions import ParseError
+            from pskc.parser import PSKCParser
             from pskc.xml import parse, remove_namespaces
             try:
                 tree = parse(filename)
             except Exception:
                 raise ParseError('Error parsing XML')
             remove_namespaces(tree)
-            self.parse(tree.getroot())
+            PSKCParser.parse_document(self, tree.getroot())
         else:
             self.version = '1.0'
 
-    def parse(self, container):
-        """Read information from the provided <KeyContainer> tree."""
-        from pskc.exceptions import ParseError
-        from pskc.key import Key
-        from pskc.xml import find, findall
-        if container.tag != 'KeyContainer':
-            raise ParseError('Missing KeyContainer')
-        # the version of the PSKC schema
-        self.version = container.get('Version')
-        if self.version != '1.0':
-            raise ParseError('Unsupported version %r' % self.version)
-        # unique identifier for the container
-        self.id = container.get('Id')
-        # handle EncryptionKey entries
-        self.encryption.parse(find(container, 'EncryptionKey'))
-        # handle MACMethod entries
-        self.mac.parse(find(container, 'MACMethod'))
-        # handle KeyPackage entries
-        for key_package in findall(container, 'KeyPackage'):
-            key = Key(self)
-            key.parse(key_package)
-            self.keys.append(key)
-
     def make_xml(self):
         from pskc.xml import mk_elem
         container = mk_elem('pskc:KeyContainer', Version=self.version,
diff --git a/pskc/encryption.py b/pskc/encryption.py
index fd83f92..80eb1b4 100644
--- a/pskc/encryption.py
+++ b/pskc/encryption.py
@@ -144,27 +144,6 @@ class KeyDerivation(object):
         self.pbkdf2_key_length = None
         self.pbkdf2_prf = None
 
-    def parse(self, key_derivation):
-        """Read derivation parameters from a <KeyDerivationMethod> element."""
-        from pskc.xml import find, findint, findbin
-        if key_derivation is None:
-            return
-        self.algorithm = key_derivation.get('Algorithm')
-        # PBKDF2 properties
-        pbkdf2 = find(key_derivation, 'PBKDF2-params')
-        if pbkdf2 is not None:
-            # get used salt
-            self.pbkdf2_salt = findbin(pbkdf2, 'Salt/Specified')
-            # required number of iterations
-            self.pbkdf2_iterations = findint(pbkdf2, 'IterationCount')
-            # key length
-            self.pbkdf2_key_length = findint(pbkdf2, 'KeyLength')
-            # pseudorandom function used
-            prf = find(pbkdf2, 'PRF')
-            if prf is not None:
-                from pskc.algorithms import normalise_algorithm
-                self.pbkdf2_prf = normalise_algorithm(prf.get('Algorithm'))
-
     def make_xml(self, encryption_key, key_names):
         from pskc.xml import mk_elem
         derived_key = mk_elem(encryption_key, 'xenc11:DerivedKey', empty=True)
@@ -260,19 +239,6 @@ class Encryption(object):
         self.derivation = KeyDerivation()
         self.fields = []
 
-    def parse(self, key_info):
-        """Read encryption information from the <EncryptionKey> XML tree."""
-        from pskc.xml import find, findall, findtext
-        if key_info is None:
-            return
-        self.id = key_info.get('Id')
-        for name in findall(key_info, 'KeyName'):
-            self.key_names.append(findtext(name, '.'))
-        for name in findall(key_info, 'DerivedKey/MasterKeyName'):
-            self.key_names.append(findtext(name, '.'))
-        self.derivation.parse(find(
-            key_info, 'DerivedKey/KeyDerivationMethod'))
-
     def make_xml(self, container):
         from pskc.xml import mk_elem
         if all(x is None
diff --git a/pskc/key.py b/pskc/key.py
index 3b48756..4678f32 100644
--- a/pskc/key.py
+++ b/pskc/key.py
@@ -47,35 +47,6 @@ class DataType(object):
         self.algorithm = None
         self.value_mac = None
 
-    def parse(self, element):
-        """Read information from the provided element.
-
-        The element is expected to contain <PlainValue>, <EncryptedValue>
-        and/or <ValueMAC> elements that contain information on the actual
-        value."""
-        from pskc.xml import find, findtext, findbin
-        if element is None:
-            return
-        # read plaintext value from <PlainValue>
-        plain_value = findtext(element, 'PlainValue')
-        if plain_value is not None:
-            self.value = self._from_text(plain_value)
-        # read encrypted data from <EncryptedValue>
-        encrypted_value = find(element, 'EncryptedValue')
-        if encrypted_value is not None:
-            self.cipher_value = findbin(
-                encrypted_value, 'CipherData/CipherValue')
-            encryption_method = find(encrypted_value, 'EncryptionMethod')
-            if encryption_method is not None:
-                self.algorithm = encryption_method.attrib.get('Algorithm')
-                # store the found algorithm in the pskc.encryption property
-                if not self.pskc.encryption.algorithm and self.algorithm:
-                    self.pskc.encryption.algorithm = self.algorithm
-        # read MAC information from <ValueMAC>
-        value_mac = findbin(element, 'ValueMAC')
-        if value_mac is not None:
-            self.value_mac = value_mac
-
     @staticmethod
     def _from_text(value):
         """Convert the plain value to native representation."""
@@ -303,67 +274,6 @@ class Key(object):
 
         self.policy = Policy(self)
 
-    def parse(self, key_package):
-        """Read key information from the provided <KeyPackage> tree."""
-        from pskc.xml import find, findtext, findtime, getint, getbool
-
-        key = find(key_package, 'Key')
-        if key is not None:
-            self.id = key.get('Id')
-            self.algorithm = key.get('Algorithm')
-
-        data = find(key_package, 'Key/Data')
-        if data is not None:
-            self._secret.parse(find(data, 'Secret'))
-            self._counter.parse(find(data, 'Counter'))
-            self._time_offset.parse(find(data, 'Time'))
-            self._time_interval.parse(find(data, 'TimeInterval'))
-            self._time_drift.parse(find(data, 'TimeDrift'))
-
-        self.issuer = findtext(key_package, 'Key/Issuer')
-        self.key_profile = findtext(key_package, 'Key/KeyProfileId')
-        self.key_reference = findtext(key_package, 'Key/KeyReference')
-        self.friendly_name = findtext(key_package, 'Key/FriendlyName')
-        # TODO: support multi-language values of <FriendlyName>
-        self.key_userid = findtext(key_package, 'Key/UserId')
-
-        self.manufacturer = findtext(key_package, 'DeviceInfo/Manufacturer')
-        self.serial = findtext(key_package, 'DeviceInfo/SerialNo')
-        self.model = findtext(key_package, 'DeviceInfo/Model')
-        self.issue_no = findtext(key_package, 'DeviceInfo/IssueNo')
-        self.device_binding = findtext(
-            key_package, 'DeviceInfo/DeviceBinding')
-        self.start_date = findtime(key_package, 'DeviceInfo/StartDate')
-        self.expiry_date = findtime(key_package, 'DeviceInfo/ExpiryDate')
-        self.device_userid = findtext(key_package, 'DeviceInfo/UserId')
-
-        self.crypto_module = findtext(key_package, 'CryptoModuleInfo/Id')
-
-        self.algorithm_suite = findtext(
-            key_package, 'Key/AlgorithmParameters/Suite')
-
-        challenge_format = find(
-            key_package, 'Key/AlgorithmParameters/ChallengeFormat')
-        if challenge_format is not None:
-            self.challenge_encoding = challenge_format.get('Encoding')
-            self.challenge_min_length = getint(challenge_format, 'Min')
-            self.challenge_max_length = getint(challenge_format, 'Max')
-            self.challenge_check = getbool(
-                challenge_format, 'CheckDigits', getbool(
-                    challenge_format, 'CheckDigit'))
-
-        response_format = find(
-            key_package,
-            'Key/AlgorithmParameters/ResponseFormat')
-        if response_format is not None:
-            self.response_encoding = response_format.get('Encoding')
-            self.response_length = getint(response_format, 'Length')
-            self.response_check = getbool(
-                response_format, 'CheckDigits', getbool(
-                    response_format, 'CheckDigit'))
-
-        self.policy.parse(find(key_package, 'Key/Policy'))
-
     def make_xml(self, container):
         from pskc.xml import mk_elem
 
diff --git a/pskc/mac.py b/pskc/mac.py
index 4517d43..4ad8aa6 100644
--- a/pskc/mac.py
+++ b/pskc/mac.py
@@ -83,20 +83,6 @@ class MAC(object):
         self.key_cipher_value = None
         self.key_algorithm = None
 
-    def parse(self, mac_method):
-        """Read MAC information from the <MACMethod> XML tree."""
-        from pskc.xml import find, findtext, findbin
-        if mac_method is None:
-            return
-        self.algorithm = mac_method.get('Algorithm')
-        mac_key = find(mac_method, 'MACKey')
-        if mac_key is not None:
-            self.key_cipher_value = findbin(mac_key, 'CipherData/CipherValue')
-            encryption_method = find(mac_key, 'EncryptionMethod')
-            if encryption_method is not None:
-                self.key_algorithm = encryption_method.attrib.get('Algorithm')
-        mac_key_reference = findtext(mac_method, 'MACKeyReference')
-
     def make_xml(self, container):
         from pskc.xml import mk_elem
         if not self.algorithm and not self.key:
diff --git a/pskc/parser.py b/pskc/parser.py
new file mode 100644
index 0000000..50e3cc7
--- /dev/null
+++ b/pskc/parser.py
@@ -0,0 +1,226 @@
+# parser.py - PSKC file parsing functions
+# coding: utf-8
+#
+# Copyright (C) 2016 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+"""Module for parsing PSKC files."""
+
+
+from pskc.algorithms import normalise_algorithm
+from pskc.exceptions import ParseError
+from pskc.xml import (
+    find, findall, findtext, findint, findbin, findtime, getint, getbool)
+
+
+class PSKCParser(object):
+
+    @classmethod
+    def parse_document(cls, pskc, container):
+        """Read information from the provided <KeyContainer> tree."""
+        if container.tag != 'KeyContainer':
+            raise ParseError('Missing KeyContainer')
+        # the version of the PSKC schema
+        pskc.version = container.get('Version')
+        if pskc.version != '1.0':
+            raise ParseError('Unsupported version %r' % pskc.version)
+        # unique identifier for the container
+        pskc.id = container.get('Id')
+        # handle EncryptionKey entries
+        cls.parse_encryption(pskc.encryption, find(container, 'EncryptionKey'))
+        # handle MACMethod entries
+        cls.parse_mac_method(pskc.mac, find(container, 'MACMethod'))
+        # handle KeyPackage entries
+        for key_package in findall(container, 'KeyPackage'):
+            cls.parse_key(pskc.add_key(), key_package)
+
+    @classmethod
+    def parse_encryption(cls, encryption, key_info):
+        """Read encryption information from the <EncryptionKey> XML tree."""
+        if key_info is None:
+            return
+        encryption.id = key_info.get('Id')
+        for name in findall(key_info, 'KeyName'):
+            encryption.key_names.append(findtext(name, '.'))
+        for name in findall(key_info, 'DerivedKey/MasterKeyName'):
+            encryption.key_names.append(findtext(name, '.'))
+        cls.parse_key_derivation(encryption.derivation, find(
+            key_info, 'DerivedKey/KeyDerivationMethod'))
+
+    @classmethod
+    def parse_key_derivation(cls, derivation, key_derivation):
+        """Read derivation parameters from a <KeyDerivationMethod> element."""
+        if key_derivation is None:
+            return
+        derivation.algorithm = key_derivation.get('Algorithm')
+        # PBKDF2 properties
+        pbkdf2 = find(key_derivation, 'PBKDF2-params')
+        if pbkdf2 is not None:
+            # get used salt
+            derivation.pbkdf2_salt = findbin(pbkdf2, 'Salt/Specified')
+            # required number of iterations
+            derivation.pbkdf2_iterations = findint(pbkdf2, 'IterationCount')
+            # key length
+            derivation.pbkdf2_key_length = findint(pbkdf2, 'KeyLength')
+            # pseudorandom function used
+            prf = find(pbkdf2, 'PRF')
+            if prf is not None:
+                derivation.pbkdf2_prf = normalise_algorithm(
+                    prf.get('Algorithm'))
+
+    @classmethod
+    def parse_mac_method(cls, mac, mac_method):
+        """Read MAC information from the <MACMethod> XML tree."""
+        if mac_method is None:
+            return
+        mac.algorithm = mac_method.get('Algorithm')
+        mac_key = find(mac_method, 'MACKey')
+        if mac_key is not None:
+            mac.key_cipher_value = findbin(mac_key, 'CipherData/CipherValue')
+            encryption_method = find(mac_key, 'EncryptionMethod')
+            if encryption_method is not None:
+                mac.key_algorithm = encryption_method.attrib.get('Algorithm')
+        mac_key_reference = findtext(mac_method, 'MACKeyReference')
+
+    @classmethod
+    def parse_key(cls, key, key_package):
+        """Read key information from the provided <KeyPackage> tree."""
+
+        key_elm = find(key_package, 'Key')
+        if key_elm is not None:
+            key.id = key_elm.get('Id')
+            key.algorithm = key_elm.get('Algorithm')
+
+        data = find(key_package, 'Key/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'))
+
+        key.issuer = findtext(key_package, 'Key/Issuer')
+        key.key_profile = findtext(key_package, 'Key/KeyProfileId')
+        key.key_reference = findtext(key_package, 'Key/KeyReference')
+        key.friendly_name = findtext(key_package, 'Key/FriendlyName')
+        # TODO: support multi-language values of <FriendlyName>
+        key.key_userid = findtext(key_package, 'Key/UserId')
+
+        key.manufacturer = findtext(key_package, 'DeviceInfo/Manufacturer')
+        key.serial = findtext(key_package, 'DeviceInfo/SerialNo')
+        key.model = findtext(key_package, 'DeviceInfo/Model')
+        key.issue_no = findtext(key_package, 'DeviceInfo/IssueNo')
+        key.device_binding = findtext(
+            key_package, 'DeviceInfo/DeviceBinding')
+        key.start_date = findtime(key_package, 'DeviceInfo/StartDate')
+        key.expiry_date = findtime(key_package, 'DeviceInfo/ExpiryDate')
+        key.device_userid = findtext(key_package, 'DeviceInfo/UserId')
+
+        key.crypto_module = findtext(key_package, 'CryptoModuleInfo/Id')
+
+        key.algorithm_suite = findtext(
+            key_package, 'Key/AlgorithmParameters/Suite')
+
+        challenge_format = find(
+            key_package, 'Key/AlgorithmParameters/ChallengeFormat')
+        if challenge_format is not None:
+            key.challenge_encoding = challenge_format.get('Encoding')
+            key.challenge_min_length = getint(challenge_format, 'Min')
+            key.challenge_max_length = getint(challenge_format, 'Max')
+            key.challenge_check = getbool(
+                challenge_format, 'CheckDigits', getbool(
+                    challenge_format, 'CheckDigit'))
+
+        response_format = find(
+            key_package, 'Key/AlgorithmParameters/ResponseFormat')
+        if response_format is not None:
+            key.response_encoding = response_format.get('Encoding')
+            key.response_length = getint(response_format, 'Length')
+            key.response_check = getbool(
+                response_format, 'CheckDigits', getbool(
+                    response_format, 'CheckDigit'))
+
+        cls.parse_policy(key.policy, find(key_package, 'Key/Policy'))
+
+    @classmethod
+    def parse_datatype(cls, dt, element):
+        """Read information from the provided element.
+
+        The element is expected to contain <PlainValue>, <EncryptedValue>
+        and/or <ValueMAC> elements that contain information on the actual
+        value."""
+        if element is None:
+            return
+        # read plaintext value from <PlainValue>
+        plain_value = findtext(element, 'PlainValue')
+        if plain_value is not None:
+            dt.value = dt._from_text(plain_value)
+        # read encrypted data from <EncryptedValue>
+        encrypted_value = find(element, 'EncryptedValue')
+        if encrypted_value is not None:
+            dt.cipher_value = findbin(
+                encrypted_value, 'CipherData/CipherValue')
+            encryption_method = find(encrypted_value, 'EncryptionMethod')
+            if encryption_method is not None:
+                dt.algorithm = encryption_method.attrib.get('Algorithm')
+                # store the found algorithm in the pskc.encryption property
+                if not dt.pskc.encryption.algorithm and dt.algorithm:
+                    dt.pskc.encryption.algorithm = dt.algorithm
+        # read MAC information from <ValueMAC>
+        value_mac = findbin(element, 'ValueMAC')
+        if value_mac is not None:
+            dt.value_mac = value_mac
+
+    @classmethod
+    def parse_policy(cls, policy, policy_elm):
+        """Read key policy information from the provided <Policy> tree."""
+        if policy_elm is None:
+            return
+
+        policy.start_date = findtime(policy_elm, 'StartDate')
+        policy.expiry_date = findtime(policy_elm, 'ExpiryDate')
+        policy.number_of_transactions = findint(
+            policy_elm, 'NumberOfTransactions')
+        for key_usage in findall(policy_elm, 'KeyUsage'):
+            policy.key_usage.append(findtext(key_usage, '.'))
+
+        pin_policy_elm = find(policy_elm, 'PINPolicy')
+        if pin_policy_elm is not None:
+            policy.pin_key_id = pin_policy_elm.get('PINKeyId')
+            policy.pin_usage = pin_policy_elm.get('PINUsageMode')
+            policy.pin_max_failed_attemtps = getint(
+                pin_policy_elm, 'MaxFailedAttempts')
+            policy.pin_min_length = getint(pin_policy_elm, 'MinLength')
+            policy.pin_max_length = getint(pin_policy_elm, 'MaxLength')
+            policy.pin_encoding = pin_policy_elm.get('PINEncoding')
+            # check for child elements
+            if list(pin_policy_elm):
+                policy.unknown_policy_elements = True
+            # check for unknown attributes
+            known_attributes = set([
+                'PINKeyId', 'PINUsageMode', 'MaxFailedAttempts', 'MinLength',
+                'MaxLength', 'PINEncoding'])
+            if set(pin_policy_elm.keys()) - known_attributes:
+                policy.unknown_policy_elements = True
+
+        # check for other child elements
+        known_children = set([
+            'StartDate', 'ExpiryDate', 'NumberOfTransactions', 'KeyUsage',
+            'PINPolicy'])
+        for child in policy_elm:
+            if child.tag not in known_children:
+                policy.unknown_policy_elements = True
diff --git a/pskc/policy.py b/pskc/policy.py
index 20f5795..a955a16 100644
--- a/pskc/policy.py
+++ b/pskc/policy.py
@@ -114,47 +114,6 @@ class Policy(object):
         self.pin_encoding = None
         self.unknown_policy_elements = False
 
-    def parse(self, policy):
-        """Read key policy information from the provided <Policy> tree."""
-        from pskc.xml import (
-            find, findall, findtext, findint, findtime, getint)
-        if policy is None:
-            return
-
-        self.start_date = findtime(policy, 'StartDate')
-        self.expiry_date = findtime(policy, 'ExpiryDate')
-        self.number_of_transactions = findint(
-            policy, 'NumberOfTransactions')
-        for key_usage in findall(policy, 'KeyUsage'):
-            self.key_usage.append(findtext(key_usage, '.'))
-
-        pin_policy = find(policy, 'PINPolicy')
-        if pin_policy is not None:
-            self.pin_key_id = pin_policy.get('PINKeyId')
-            self.pin_usage = pin_policy.get('PINUsageMode')
-            self.pin_max_failed_attemtps = getint(
-                pin_policy, 'MaxFailedAttempts')
-            self.pin_min_length = getint(pin_policy, 'MinLength')
-            self.pin_max_length = getint(pin_policy, 'MaxLength')
-            self.pin_encoding = pin_policy.get('PINEncoding')
-            # check for child elements
-            if list(pin_policy):
-                self.unknown_policy_elements = True
-            # check for unknown attributes
-            known_attributes = set([
-                'PINKeyId', 'PINUsageMode', 'MaxFailedAttempts', 'MinLength',
-                'MaxLength', 'PINEncoding'])
-            if set(pin_policy.keys()) - known_attributes:
-                self.unknown_policy_elements = True
-
-        # check for other child elements
-        known_children = set([
-            'StartDate', 'ExpiryDate', 'NumberOfTransactions', 'KeyUsage',
-            'PINPolicy'])
-        for child in policy:
-            if child.tag not in known_children:
-                self.unknown_policy_elements = True
-
     def make_xml(self, key):
         from pskc.xml import mk_elem
         # check if any policy attribute is set

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

Summary of changes:
 docs/usage.rst           | 171 +++++++++++++++++----------
 pskc/__init__.py         |  66 +++++------
 pskc/device.py           |  69 +++++++++++
 pskc/encryption.py       |  68 -----------
 pskc/key.py              | 296 ++++++++++-------------------------------------
 pskc/mac.py              |  36 ------
 pskc/parser.py           | 231 ++++++++++++++++++++++++++++++++++++
 pskc/policy.py           |  69 +----------
 pskc/serialiser.py       | 213 ++++++++++++++++++++++++++++++++++
 tests/test_misc.doctest  |  33 +++++-
 tests/test_write.doctest |  37 ++++++
 11 files changed, 780 insertions(+), 509 deletions(-)
 create mode 100644 pskc/device.py
 create mode 100644 pskc/parser.py
 create mode 100644 pskc/serialiser.py


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/