lists.arthurdejong.org
RSS feed

python-pskc branch master updated. 0.3-36-gb4a6c72

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

python-pskc branch master updated. 0.3-36-gb4a6c72



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  b4a6c720cb202f44b07ad2d0f9d8812ab7212ea5 (commit)
       via  59aa65be6d3349e78d2f17e94ae34f6767113a87 (commit)
       via  5f325280b9afd718790dada72e2d68ecb737e7eb (commit)
       via  7ede4a110efddfd401b48b20cdf2ebcf89a165ad (commit)
       via  1ff3237f704c0a107d5eccc1deb754888a71a1ac (commit)
       via  50414a3a2ac0ce3b8bde1c58ead1cb470e40fc8c (commit)
       via  5ac9d4384252dd22ea5fb9510c367fb7967409b7 (commit)
       via  16da531142af8f5052029864df2691ad3fd61858 (commit)
       via  ca0fa368f6f60d02d1dfcf41d4f9961d65560c98 (commit)
       via  8fd35ba1f50fb11ac1f853146b969d92e94fed3c (commit)
       via  eba541e9d976472582ad3dc31cd69087be102fa6 (commit)
       via  fe2123101634cbe3b64d18999c43d417720122c6 (commit)
       via  08936405fe0481ab40005ec5a6a134c67452e2d7 (commit)
      from  8b5f6c27e0dde5f8b995b89dd2e3c9fa3caed3d5 (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=b4a6c720cb202f44b07ad2d0f9d8812ab7212ea5

commit b4a6c720cb202f44b07ad2d0f9d8812ab7212ea5
Merge: 8b5f6c2 59aa65b
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sun Mar 27 17:53:26 2016 +0200

    Implement writing encrypted files
    
    This adds support for setting up encryption keys and password-based key
    derivation when writing PSKC files. Also MAC keys are set up when
    needed.


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

commit 59aa65be6d3349e78d2f17e94ae34f6767113a87
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sat Mar 26 23:39:50 2016 +0100

    Document writing encrypted files

diff --git a/README b/README
index 2231c13..d52ded3 100644
--- a/README
+++ b/README
@@ -3,12 +3,12 @@ Python PSKC module
 
 A Python module to handle Portable Symmetric Key Container (PSKC) files as
 defined in `RFC 6030 <https://tools.ietf.org/html/rfc6030>`_. PSKC files are
-used to transport and provision symmetric keys and key meta data to different
-types of crypto modules. The format is commonly used for one-time password
-tokens or other authentication devices.
+used to transport and provision symmetric keys and key meta data (seed files)
+to different types of crypto modules, commonly one-time password tokens or
+other authentication devices.
 
-The goal of this module is mainly to provide parsing of PSKC files in order
-to extract secret keys for use in an OTP authentication system.
+This module can be used to extract keys from PSKC files for use in an OTP
+authentication system. The module can also be used for authoring PSKC files.
 
 http://arthurdejong.org/python-pskc/
 
@@ -19,7 +19,7 @@ API
 The module provides a straightforward API that is mostly geared towards
 parsing existing PSKC files.
 
-Extracting key matarial from encrypted PSKC files is as simple as.
+Extracting key material from encrypted PSKC files is as simple as.
 
 >>> from pskc import PSKC
 >>> pskc = PSKC('tests/rfc6030/figure7.pskcxml')
@@ -44,7 +44,7 @@ private key material.
 Copyright
 ---------
 
-Copyright (C) 2014-2015 Arthur de Jong
+Copyright (C) 2014-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
diff --git a/docs/conf.py b/docs/conf.py
index 268d158..5a7ecbf 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -46,7 +46,7 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'python-pskc'
-copyright = u'2014-2015 Arthur de Jong'
+copyright = u'2014-2016 Arthur de Jong'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
diff --git a/docs/encryption.rst b/docs/encryption.rst
index 9bedf8c..cafdb74 100644
--- a/docs/encryption.rst
+++ b/docs/encryption.rst
@@ -23,11 +23,23 @@ or::
 Once the encryption key has been set up, any encrypted key values from the
 PSKC file are available transparently.
 
-If no key or an incorrect key has been set configured, upon accessing encrypted
+If no key or an incorrect key has been configured, upon accessing encrypted
 information (e.g. the :attr:`~pskc.key.Key.secret` attribute of a
 :class:`~pskc.key.Key` instance) a :exc:`~pskc.exceptions.DecryptionError`
 exception will be raised.
 
+When writing out a PSKC file, encryption can be configured with the
+:func:`~pskc.encryption.Encryption.setup_preshared_key()` or
+:func:`~pskc.encryption.Encryption.setup_pbkdf2()` function::
+
+   >>> from pskc import PSKC
+   >>> pskc = PSKC()
+   >>> pskc.encryption.setup_preshared_key(algorithm='AES256-CBC')
+
+or::
+
+   >>> pskc.encryption.setup_pbkdf2(password='verysecure')
+
 
 The Encryption class
 --------------------
@@ -40,7 +52,10 @@ The Encryption class
 
    .. attribute:: algorithm
 
-      A URI of the encryption algorithm used.
+      A URI of the encryption algorithm used. Setting a value for this
+      attribute will result in an attempt to use the canonical URI for this
+      algorithm. For instance setting a `3DES-CBC` value will automatically
+      be converted to `http://www.w3.org/2001/04/xmlenc#aes128-cbc`.
 
    .. attribute:: key_names
 
@@ -68,3 +83,58 @@ The Encryption class
 
       This function may raise a :exc:`~pskc.exceptions.KeyDerivationError`
       exception if key derivation fails for some reason.
+
+   .. attribute:: fields
+
+      A list of :class:`~pskc.key.Key` instance field names that will be
+      encrypted when the PSKC file is written. List values can contain
+      ``secret``, ``counter``, ``time_offset``, ``time_interval`` and
+      ``time_drift``.
+
+   .. function:: setup_preshared_key(...)
+
+      Configure pre-shared key encryption.
+
+      :param binary key: the encryption key to use
+      :param str id: encryption key identifier
+      :param str algorithm: encryption algorithm
+      :param int key_length: encryption key length in bytes
+      :param str key_name: a name for the key
+      :param list key_names: a number of names for the key
+      :param list fields: a list of fields to encrypt
+
+      This is a utility function to easily set up encryption. Encryption can
+      also be set up by manually by setting the
+      :class:`~pskc.encryption.Encryption` properties.
+
+      This method will generate a key if required and set the passed values.
+      By default AES128-CBC encryption will be configured and unless a key is
+      specified one of the correct length will be generated. If the algorithm
+      does not provide integrity checks (e.g. CBC-mode algorithms) integrity
+      checking in the PSKC file will be set up using
+      :func:`~pskc.mac.MAC.setup()`.
+
+      By default only the :attr:`~pskc.key.Key.secret` property will be
+      encrypted when writing the file.
+
+   .. function:: setup_pbkdf2(...)
+
+      Configure password-based PSKC encryption.
+
+      :param str password: the password to use (required)
+      :param str id: encryption key identifier
+      :param str algorithm: encryption algorithm
+      :param int key_length: encryption key length in bytes
+      :param str key_name: a name for the key
+      :param list key_names: a number of names for the key
+      :param list fields: a list of fields to encrypt
+      :param binary salt: PBKDF2 salt
+      :param int salt_length: used when generating random salt
+      :param int iterations: number of PBKDF2 iterations
+      :param function prf: PBKDF2 pseudorandom function
+
+      Defaults for the above parameters are similar to those for
+      :func:`setup_preshared_key()` but the password parameter is required.
+
+      By default 12000 iterations will be used and a random salt with the
+      length of the to-be-generated encryption key will be used.
diff --git a/docs/mac.rst b/docs/mac.rst
index 4847071..0e8e5d5 100644
--- a/docs/mac.rst
+++ b/docs/mac.rst
@@ -38,3 +38,14 @@ The MAC class
       MAC key is generated specifically for each PSKC file and encrypted with
       the PSKC encryption key, so the PSKC file should be decrypted first
       (see :doc:`encryption`).
+
+   .. function:: setup(...)
+
+      Configure an encrypted MAC key.
+
+      :param str algorithm: encryption algorithm
+      :param binary key: the encryption key to use
+
+      None of the arguments are required. By default HMAC-SHA1 will be used
+      as a MAC algorithm. If no key is configured a random key will be
+      generated with the length of the output of the configured hash.
diff --git a/docs/usage.rst b/docs/usage.rst
index 7a9cdcb..5e0edfc 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -1,13 +1,13 @@
 Basic usage
 ===========
 
-The :mod:`pskc` module implements a simple and efficient API for parsing 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 from the PSKC file.
+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.
 
 
-Opening a PSKC file
+Reading a PSKC file
 -------------------
 
 Importing data from a PSKC file can be done by instantiating the
@@ -50,16 +50,15 @@ adding keys with :func:`~pskc.PSKC.add_key()` and writing 
the result::
 
     >>> from pskc import PSKC
     >>> pskc = PSKC()
-    >>> key = pskc.add_key(id='456', manufacturer='Manufacturer')
-    >>> key.id
-    '456'
-    >>> key.secret = '987654321'
-    >>> key.algorithm = 'urn:ietf:params:xml:ns:keyprov:pskc:hotp'
+    >>> key = pskc.add_key(
+    ...     id='456', secret='987654321', manufacturer='Manufacturer',
+    ...     algorithm = 'urn:ietf:params:xml:ns:keyprov:pskc:hotp')
     >>> pskc.write('output.pskcxml')
 
-Writing the data in encrypted form in the PSKC file is not yet supported so
-currently opening an encrypted PSKC file, providing the encryption key and
-writing the file should result in the same file but with encryption removed.
+By default an unencrypted PSKC file will be created but an encryption can be
+configured using the
+:func:`~pskc.encryption.Encryption.setup_preshared_key()` or
+:func:`~pskc.encryption.Encryption.setup_pbkdf2()` function.
 
 
 The PSKC class
diff --git a/pskc/__init__.py b/pskc/__init__.py
index c6d4aad..0ff2001 100644
--- a/pskc/__init__.py
+++ b/pskc/__init__.py
@@ -25,8 +25,8 @@ defined in RFC 6030. PSKC files are used to transport and 
provision symmetric
 keys (seed files) to different types of crypto modules, commonly one-time
 password tokens or other authentication devices.
 
-The main goal of this module is to be able to extract keys from PSKC files
-for use in an OTP authentication system.
+This module can be used to extract keys from PSKC files for use in an OTP
+authentication system. The module can also be used for authoring PSKC files.
 
 The following prints all keys, decrypting using a password:
 

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

commit 5f325280b9afd718790dada72e2d68ecb737e7eb
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Mon Mar 21 23:22:53 2016 +0100

    Add encryption error tests

diff --git a/tests/test_write.doctest b/tests/test_write.doctest
index 352e93d..8c9ca72 100644
--- a/tests/test_write.doctest
+++ b/tests/test_write.doctest
@@ -23,6 +23,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 >>> import datetime
 >>> import sys
 >>> import tempfile
+>>> from Crypto import Random
 >>> from binascii import a2b_hex
 >>> from dateutil.tz import tzutc
 
@@ -340,3 +341,76 @@ Use PBKDF2 to derive a key instead of using a pre-shared 
key.
   </pskc:Key>
  </pskc:KeyPackage>
 </pskc:KeyContainer>
+
+
+Test encryption and decryption of the generated file to test encryption/
+decryption combinations.
+
+>>> def test_algorithm(algorithm):
+...     f = tempfile.NamedTemporaryFile()
+...     pskc1 = PSKC()
+...     pskc1.add_key(secret=Random.get_random_bytes(16))
+...     pskc1.encryption.setup_preshared_key(algorithm=algorithm)
+...     pskc1.write(f.name)
+...     pskc2 = PSKC(f.name)
+...     pskc2.encryption.key = pskc1.encryption.key
+...     assert pskc1.keys[0].secret == pskc2.keys[0].secret
+...     return (pskc1, pskc2)
+>>> pskc1, pskc2 = test_algorithm('aes192-cbc')
+>>> len(pskc1.encryption.key)
+24
+>>> pskc1, pskc2 = test_algorithm('aes256-cbc')
+>>> len(pskc1.encryption.key)
+32
+>>> pskc1, pskc2 = test_algorithm('tripledes-cbc')
+>>> len(pskc1.encryption.key)
+24
+>>> pskc1, pskc2 = test_algorithm('kw-aes128')
+>>> len(pskc1.encryption.key)
+16
+>>> pskc1, pskc2 = test_algorithm('kw-aes192')
+>>> len(pskc1.encryption.key)
+24
+>>> pskc1, pskc2 = test_algorithm('kw-aes256')
+>>> len(pskc1.encryption.key)
+32
+>>> pskc1, pskc2 = test_algorithm('kw-tripledes')
+>>> len(pskc1.encryption.key)
+24
+
+
+Not having a key and trying encryption will fail.
+
+>>> f = tempfile.NamedTemporaryFile()
+>>> pskc = PSKC()
+>>> key = pskc.add_key(secret='1234')
+>>> pskc.encryption.setup_preshared_key()
+>>> pskc.encryption.key = None
+>>> pskc.write(f.name)
+Traceback (most recent call last):
+    ...
+EncryptionError: No key available
+>>> pskc = PSKC()
+>>> key = pskc.add_key(secret='1234')
+>>> pskc.encryption.setup_preshared_key()
+>>> pskc.encryption.algorithm = None
+>>> pskc.write(f.name)
+Traceback (most recent call last):
+    ...
+EncryptionError: No algorithm specified
+>>> pskc = PSKC()
+>>> key = pskc.add_key(secret='1234')
+>>> pskc.encryption.setup_preshared_key()
+>>> pskc.encryption.algorithm = 'FOOBAR'
+>>> pskc.write(f.name)
+Traceback (most recent call last):
+    ...
+DecryptionError: Unsupported algorithm: 'FOOBAR'
+>>> pskc = PSKC()
+>>> key = pskc.add_key(secret='1234')
+>>> pskc.encryption.setup_preshared_key()
+>>> pskc.encryption.algorithm = 'aes256-cbc'
+>>> pskc.write(f.name)
+Traceback (most recent call last):
+    ...
+EncryptionError: Invalid key length

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

commit 7ede4a110efddfd401b48b20cdf2ebcf89a165ad
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Mon Mar 21 21:12:57 2016 +0100

    Add tests for writing encrypted PSKC files

diff --git a/tests/test_write.doctest b/tests/test_write.doctest
index b4cc634..352e93d 100644
--- a/tests/test_write.doctest
+++ b/tests/test_write.doctest
@@ -165,3 +165,178 @@ Read an encrypted PSKC file and write it out as an 
unencrypted file.
   </pskc:Key>
  </pskc:KeyPackage>
 </pskc:KeyContainer>
+
+
+Read an encrypted PSKC file and write it out as-is. This does not require
+providing the encryption key.
+
+>>> pskc = PSKC('tests/rfc6030/figure6.pskcxml')
+>>> pskc.write(sys.stdout)  #doctest: +REPORT_UDIFF
+<?xml version="1.0" encoding="UTF-8"?>
+<pskc:KeyContainer Version="1.0" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"; 
xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc" 
xmlns:xenc="http://www.w3.org/2001/04/xmlenc#";>
+ <pskc:EncryptionKey>
+  <ds:KeyName>Pre-shared-key</ds:KeyName>
+ </pskc:EncryptionKey>
+ <pskc:MACMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1";>
+  <pskc:MACKey>
+   <xenc:EncryptionMethod 
Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+   <xenc:CipherData>
+    
<xenc:CipherValue>ESIzRFVmd4iZABEiM0RVZgKn6WjLaTC1sbeBMSvIhRejN9vJa2BOlSaMrR7I5wSX</xenc:CipherValue>
+   </xenc:CipherData>
+  </pskc:MACKey>
+ </pskc:MACMethod>
+ <pskc:KeyPackage>
+  <pskc:DeviceInfo>
+   <pskc:Manufacturer>Manufacturer</pskc:Manufacturer>
+   <pskc:SerialNo>987654321</pskc:SerialNo>
+  </pskc:DeviceInfo>
+  <pskc:CryptoModuleInfo>
+   <pskc:Id>CM_ID_001</pskc:Id>
+  </pskc:CryptoModuleInfo>
+  <pskc:Key Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:hotp" Id="12345678">
+   <pskc:Issuer>Issuer</pskc:Issuer>
+   <pskc:AlgorithmParameters>
+    <pskc:ResponseFormat Encoding="DECIMAL" Length="8"/>
+   </pskc:AlgorithmParameters>
+   <pskc:Data>
+    <pskc:Secret>
+     <pskc:EncryptedValue>
+      <xenc:EncryptionMethod 
Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+      <xenc:CipherData>
+       
<xenc:CipherValue>AAECAwQFBgcICQoLDA0OD+cIHItlB3Wra1DUpxVvOx2lef1VmNPCMl8jwZqIUqGv</xenc:CipherValue>
+      </xenc:CipherData>
+     </pskc:EncryptedValue>
+     <pskc:ValueMAC>Su+NvtQfmvfJzF6bmQiJqoLRExc=</pskc:ValueMAC>
+    </pskc:Secret>
+    <pskc:Counter>
+     <pskc:PlainValue>0</pskc:PlainValue>
+    </pskc:Counter>
+   </pskc:Data>
+  </pskc:Key>
+ </pskc:KeyPackage>
+</pskc:KeyContainer>
+
+
+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)
+>>> pskc.encryption.setup_preshared_key(
+...     key_name='Pre-shared KEY', fields = ['secret', 'counter'])
+>>> f = tempfile.NamedTemporaryFile()
+>>> pskc.write(f.name)
+>>> x = sys.stdout.write(open(f.name, 'r').read())  #doctest: +ELLIPSIS 
+REPORT_UDIFF
+<?xml version="1.0" encoding="UTF-8"?>
+<pskc:KeyContainer Version="1.0" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"; 
xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc" 
xmlns:xenc="http://www.w3.org/2001/04/xmlenc#";>
+ <pskc:EncryptionKey>
+  <ds:KeyName>Pre-shared KEY</ds:KeyName>
+ </pskc:EncryptionKey>
+ <pskc:MACMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1";>
+  <pskc:MACKey>
+   <xenc:EncryptionMethod 
Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+   <xenc:CipherData>
+    <xenc:CipherValue>...</xenc:CipherValue>
+   </xenc:CipherData>
+  </pskc:MACKey>
+ </pskc:MACMethod>
+ <pskc:KeyPackage>
+  <pskc:DeviceInfo>
+   <pskc:SerialNo>123456</pskc:SerialNo>
+  </pskc:DeviceInfo>
+  <pskc:Key Id="1">
+   <pskc:Data>
+    <pskc:Secret>
+     <pskc:EncryptedValue>
+      <xenc:EncryptionMethod 
Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+      <xenc:CipherData>
+       <xenc:CipherValue>...</xenc:CipherValue>
+      </xenc:CipherData>
+     </pskc:EncryptedValue>
+     <pskc:ValueMAC>...</pskc:ValueMAC>
+    </pskc:Secret>
+    <pskc:Counter>
+     <pskc:EncryptedValue>
+      <xenc:EncryptionMethod 
Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+      <xenc:CipherData>
+       <xenc:CipherValue>...</xenc:CipherValue>
+      </xenc:CipherData>
+     </pskc:EncryptedValue>
+     <pskc:ValueMAC>...</pskc:ValueMAC>
+    </pskc:Counter>
+   </pskc:Data>
+  </pskc:Key>
+ </pskc:KeyPackage>
+</pskc:KeyContainer>
+
+
+Read the generated file back in and verify that it matches the original data.
+
+>>> newpskc = PSKC(f.name)
+>>> newpskc.encryption.algorithm == pskc.encryption.algorithm
+True
+>>> newpskc.encryption.key = pskc.encryption.key
+>>> all(newkey.check() for newkey in newpskc.keys)
+True
+>>> key = pskc.keys[0]
+>>> newkey = newpskc.keys[0]
+>>> newkey.secret == key.secret
+True
+>>> newkey.counter == key.counter
+True
+
+
+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)
+>>> pskc.encryption.setup_pbkdf2(
+...     'passphrase', key_name='Passphrase')
+>>> 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" 
xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"; 
xmlns:xenc11="http://www.w3.org/2009/xmlenc11#";>
+ <pskc:EncryptionKey>
+  <xenc11:DerivedKey>
+   <xenc11:KeyDerivationMethod 
Algorithm="http://www.rsasecurity.com/rsalabs/pkcs/schemas/pkcs-5v2-0#pbkdf2";>
+    <xenc11:PBKDF2-params>
+     <Salt>
+      <Specified>...</Specified>
+     </Salt>
+     <IterationCount>12000</IterationCount>
+     <KeyLength>16</KeyLength>
+    </xenc11:PBKDF2-params>
+   </xenc11:KeyDerivationMethod>
+   <xenc11:MasterKeyName>Passphrase</xenc11:MasterKeyName>
+  </xenc11:DerivedKey>
+ </pskc:EncryptionKey>
+ <pskc:MACMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1";>
+  <pskc:MACKey>
+   <xenc:EncryptionMethod 
Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+   <xenc:CipherData>
+    <xenc:CipherValue>...</xenc:CipherValue>
+   </xenc:CipherData>
+  </pskc:MACKey>
+ </pskc:MACMethod>
+ <pskc:KeyPackage>
+  <pskc:DeviceInfo>
+   <pskc:SerialNo>123456</pskc:SerialNo>
+  </pskc:DeviceInfo>
+  <pskc:Key Id="1">
+   <pskc:Data>
+    <pskc:Secret>
+     <pskc:EncryptedValue>
+      <xenc:EncryptionMethod 
Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+      <xenc:CipherData>
+       <xenc:CipherValue>...</xenc:CipherValue>
+      </xenc:CipherData>
+     </pskc:EncryptedValue>
+     <pskc:ValueMAC>...</pskc:ValueMAC>
+    </pskc:Secret>
+    <pskc:Counter>
+     <pskc:PlainValue>42</pskc:PlainValue>
+    </pskc:Counter>
+   </pskc:Data>
+  </pskc:Key>
+ </pskc:KeyPackage>
+</pskc:KeyContainer>

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

commit 1ff3237f704c0a107d5eccc1deb754888a71a1ac
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sun Mar 20 22:35:08 2016 +0100

    Allow configuring a pre-shared key
    
    This method allows configuring a pre-shared encryption key and will
    chose reasonable defaults for needed encryption values (e.g. it will
    choose an algorithm, generate a new key of the appropriate length if
    needed, etc.).

diff --git a/pskc/encryption.py b/pskc/encryption.py
index d92a79c..5a476c1 100644
--- a/pskc/encryption.py
+++ b/pskc/encryption.py
@@ -269,6 +269,43 @@ class Encryption(object):
         """Derive a key from the password."""
         self.key = self.derivation.derive(password)
 
+    def _setup_encryption(self, kwargs):
+        for k in ('id', 'algorithm', 'key_name', 'key_names', 'fields'):
+            v = kwargs.pop(k, None)
+            if v is not None:
+                setattr(self, k, v)
+        # default encryption to AES128-CBC
+        if not self.algorithm:
+            self.algorithm = 'aes128-cbc'
+        # default to encrypting the secret only
+        if not self.fields:
+            self.fields = ['secret', ]
+        # if we're using a CBC mode of encryption, add a MAC
+        if self.algorithm.endswith('-cbc'):
+            self.pskc.mac.setup()
+
+    def setup_preshared_key(self, **kwargs):
+        """Configure pre-shared key encryption.
+
+        The following arguments may be supplied:
+          key: the encryption key to use
+          id: encryption key identifier
+          algorithm: encryption algorithm
+          key_length: encryption key length in bytes
+          key_name: a name for the key
+          key_names: a number of names for the key
+          fields: a list of fields to encrypt
+
+        None of the arguments are required, reasonable defaults will be
+        chosen for missing arguments.
+        """
+        self._setup_encryption(kwargs)
+        key = kwargs.pop('key', self.key)
+        if not key:
+            from Crypto import Random
+            self.key = Random.get_random_bytes(kwargs.pop(
+                'key_length', self.algorithm_key_lengths[-1]))
+
     def setup_pbkdf2(self, password, **kwargs):
         """Configure password-based PSKC encryption.
 
@@ -288,19 +325,7 @@ class Encryption(object):
         Only password is required, for the other arguments reasonable
         defaults will be chosen.
         """
-        for k in ('id', 'algorithm', 'key_name', 'key_names', 'fields'):
-            v = kwargs.pop(k, None)
-            if v is not None:
-                setattr(self, k, v)
-        # default encryption to AES128-CBC
-        if not self.algorithm:
-            self.algorithm = 'aes128-cbc'
-        # default to encrypting the secret only
-        if not self.fields:
-            self.fields = ['secret', ]
-        # if we're using a CBC mode of encryption, add a MAC
-        if self.algorithm.endswith('-cbc'):
-            self.pskc.mac.setup()
+        self._setup_encryption(kwargs)
         # pass a key length to PBKDF2
         kwargs.setdefault('key_length', self.algorithm_key_lengths[-1])
         self.key = self.derivation.setup_pbkdf2(password, **kwargs)

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

commit 50414a3a2ac0ce3b8bde1c58ead1cb470e40fc8c
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sat Mar 19 15:07:58 2016 +0100

    Allow configuring PBKDF2 key derivation
    
    This factors out the PBKDF2 key derivation to a separate function and
    introduces a function to configure KeyDerivation instances with PBKDF2.

diff --git a/pskc/encryption.py b/pskc/encryption.py
index e69db02..d92a79c 100644
--- a/pskc/encryption.py
+++ b/pskc/encryption.py
@@ -51,6 +51,8 @@ _algorithms = {
     'hmac-sha384': 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha384',
     'hmac-sha512': 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha512',
     'hmac-ripemd160': 'http://www.w3.org/2001/04/xmldsig-more#hmac-ripemd160',
+    'pbkdf2': 'http://www.rsasecurity.com/rsalabs/pkcs/schemas/' +
+              'pkcs-5v2-0#pbkdf2',
 }
 
 # translation table to change old encryption names to new names
@@ -147,28 +149,47 @@ class KeyDerivation(object):
         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
+        from pskc.exceptions import KeyDerivationError
+        prf = None
+        if self.pbkdf2_prf:
+            prf = get_hmac(self.pbkdf2_prf)
+            if prf is None:
+                raise KeyDerivationError(
+                    'Pseudorandom function unsupported: %r' %
+                    self.pbkdf2_prf)
+        return PBKDF2(
+            password, self.pbkdf2_salt, dkLen=self.pbkdf2_key_length,
+            count=self.pbkdf2_iterations, prf=prf)
+
     def derive(self, password):
         """Derive a key from the password."""
         from pskc.exceptions import KeyDerivationError
         if self.algorithm is None:
             raise KeyDerivationError('No algorithm specified')
         if self.algorithm.endswith('#pbkdf2'):
-            from Crypto.Protocol.KDF import PBKDF2
-            from pskc.mac import get_hmac
-            prf = None
-            if self.pbkdf2_prf:
-                prf = get_hmac(self.pbkdf2_prf)
-                if prf is None:
-                    raise KeyDerivationError(
-                        'Pseudorandom function unsupported: %r' %
-                        self.pbkdf2_prf)
-            return PBKDF2(
-                password, self.pbkdf2_salt, dkLen=self.pbkdf2_key_length,
-                count=self.pbkdf2_iterations, prf=prf)
+            return self.derive_pbkdf2(password)
         else:
             raise KeyDerivationError(
                 'Unsupported algorithm: %r' % self.algorithm)
 
+    def setup_pbkdf2(self, password, salt=None, salt_length=16,
+                     key_length=None, iterations=None, prf=None):
+        from Crypto import Random
+        self.algorithm = normalise_algorithm('pbkdf2')
+        if salt is None:
+            salt = Random.get_random_bytes(salt_length)
+        self.pbkdf2_salt = salt
+        if iterations:
+            self.pbkdf2_iterations = iterations
+        elif self.pbkdf2_iterations is None:
+            self.pbkdf2_iterations = 12 * 1000
+        if key_length:
+            self.pbkdf2_key_length = key_length
+        return self.derive_pbkdf2(password)
+
 
 class Encryption(object):
     """Class for handling encryption keys that are used in the PSKC file.
@@ -248,6 +269,42 @@ class Encryption(object):
         """Derive a key from the password."""
         self.key = self.derivation.derive(password)
 
+    def setup_pbkdf2(self, password, **kwargs):
+        """Configure password-based PSKC encryption.
+
+        The following arguments may be supplied:
+          password: the password to use (required)
+          id: encryption key identifier
+          algorithm: encryption algorithm
+          key_length: encryption key length in bytes
+          key_name: a name for the key
+          key_names: a number of names for the key
+          fields: a list of fields to encrypt
+          salt: PBKDF2 salt
+          salt_length: used when generating random salt
+          iterations: number of PBKDF2 iterations
+          prf: PBKDF2 pseudorandom function
+
+        Only password is required, for the other arguments reasonable
+        defaults will be chosen.
+        """
+        for k in ('id', 'algorithm', 'key_name', 'key_names', 'fields'):
+            v = kwargs.pop(k, None)
+            if v is not None:
+                setattr(self, k, v)
+        # default encryption to AES128-CBC
+        if not self.algorithm:
+            self.algorithm = 'aes128-cbc'
+        # default to encrypting the secret only
+        if not self.fields:
+            self.fields = ['secret', ]
+        # if we're using a CBC mode of encryption, add a MAC
+        if self.algorithm.endswith('-cbc'):
+            self.pskc.mac.setup()
+        # pass a key length to PBKDF2
+        kwargs.setdefault('key_length', self.algorithm_key_lengths[-1])
+        self.key = self.derivation.setup_pbkdf2(password, **kwargs)
+
     @property
     def algorithm_key_lengths(self):
         """Provide the possible key lengths for the configured algorithm."""
diff --git a/tests/test_encryption.doctest b/tests/test_encryption.doctest
index 603da30..aba2014 100644
--- a/tests/test_encryption.doctest
+++ b/tests/test_encryption.doctest
@@ -24,6 +24,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 >>> def decode(f):
 ...     return lambda x: tostr(f(x))
 >>> b2a_hex = decode(b2a_hex)
+>>> import base64
 
 >>> from pskc import PSKC
 
@@ -148,3 +149,60 @@ specified.
 28
 >>> len(pskc.mac.key)
 28
+
+
+Test PBKDF2 key derivation set-up. Only specifying a passphrase picks
+reasonable defaults.
+
+>>> pskc = PSKC()
+>>> pskc.encryption.setup_pbkdf2('test')
+>>> pskc.encryption.derivation.algorithm
+'http://www.rsasecurity.com/rsalabs/pkcs/schemas/pkcs-5v2-0#pbkdf2'
+>>> pskc.encryption.derivation.pbkdf2_iterations
+12000
+>>> len(pskc.encryption.derivation.pbkdf2_salt)
+16
+>>> pskc.encryption.derivation.pbkdf2_key_length
+16
+>>> pskc.encryption.algorithm
+'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
+>>> len(pskc.encryption.key)
+16
+
+>>> pskc = PSKC()
+>>> pskc.encryption.algorithm = 'aes256-cbc'
+>>> pskc.encryption.setup_pbkdf2('test')
+>>> pskc.encryption.derivation.algorithm
+'http://www.rsasecurity.com/rsalabs/pkcs/schemas/pkcs-5v2-0#pbkdf2'
+>>> pskc.encryption.derivation.pbkdf2_iterations
+12000
+>>> len(pskc.encryption.derivation.pbkdf2_salt)
+16
+>>> pskc.encryption.derivation.pbkdf2_key_length
+32
+>>> pskc.encryption.algorithm
+'http://www.w3.org/2001/04/xmlenc#aes256-cbc'
+>>> len(pskc.encryption.key)
+32
+
+
+All properties can also be manually specified.
+
+>>> pskc.encryption.setup_pbkdf2(
+...    'qwerty', iterations=1000, algorithm='aes256-cbc', key_length=24,
+...    salt=base64.b64decode('Ej7/PEpyEpw='),
+...    key_name='PBKDF2 passphrase')
+>>> pskc.encryption.derivation.algorithm
+'http://www.rsasecurity.com/rsalabs/pkcs/schemas/pkcs-5v2-0#pbkdf2'
+>>> pskc.encryption.derivation.pbkdf2_iterations
+1000
+>>> b2a_hex(pskc.encryption.derivation.pbkdf2_salt)
+'123eff3c4a72129c'
+>>> pskc.encryption.derivation.pbkdf2_key_length
+24
+>>> pskc.encryption.algorithm
+'http://www.w3.org/2001/04/xmlenc#aes256-cbc'
+>>> pskc.encryption.key_name
+'PBKDF2 passphrase'
+>>> b2a_hex(pskc.encryption.key)
+'651e63cd57008476af1ff6422cd02e41a13be8f92db69ec9'

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

commit 5ac9d4384252dd22ea5fb9510c367fb7967409b7
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Mon Mar 21 18:26:00 2016 +0100

    Allow configuring a MAC key
    
    This method will set up a MAC key and algorithm as specified or use
    reasonable defauts.

diff --git a/pskc/mac.py b/pskc/mac.py
index 0b6c7e6..b4ddd53 100644
--- a/pskc/mac.py
+++ b/pskc/mac.py
@@ -129,6 +129,17 @@ class MAC(object):
         from pskc.encryption import normalise_algorithm
         self._algorithm = normalise_algorithm(value)
 
+    @property
+    def algorithm_key_length(self):
+        """Recommended minimal key length in bytes for the set algorithm."""
+        # https://tools.ietf.org/html/rfc2104#section-3
+        # an HMAC key should be at least as long as the hash output length
+        hashfn = get_hash(self.algorithm)
+        if hashfn is not None:
+            return int(hashfn().digest_size)
+        else:
+            return 16
+
     def generate_mac(self, value):
         """Generate the MAC over the specified value."""
         from pskc.exceptions import DecryptionError
@@ -151,3 +162,25 @@ class MAC(object):
         if self.generate_mac(value) != value_mac:
             raise DecryptionError('MAC value does not match')
         return True
+
+    def setup(self, key=None, algorithm=None):
+        """Configure an encrypted MAC key.
+
+        The following arguments may be supplied:
+          key: the MAC key to use
+          algorithm: MAC algorithm
+
+        None of the arguments are required, reasonable defaults will be
+        chosen for missing arguments.
+        """
+        if key:
+            self.key = key
+        if algorithm:
+            self.algorithm = algorithm
+        # default to HMAC-SHA1
+        if not self.algorithm:
+            self.algorithm = 'hmac-sha1'
+        # generate an HMAC key
+        if not self.key:
+            from Crypto import Random
+            self.key = Random.get_random_bytes(self.algorithm_key_length)
diff --git a/tests/test_encryption.doctest b/tests/test_encryption.doctest
index 398b12e..603da30 100644
--- a/tests/test_encryption.doctest
+++ b/tests/test_encryption.doctest
@@ -121,3 +121,30 @@ DecryptionError: Invalid key length
 >>> pskc.encryption.key = 
 >>> a2b_hex('255e0d1c07b646dfb3134cc843ba8aa71f025b7c0838251f')
 >>> b2a_hex(pskc.keys[0].secret)
 '2923bf85e06dd6ae529149f1f1bae9eab3a7da3d860d3e98'
+
+
+MAC key and algorithm will use useful defaults but can also be manually
+specified.
+
+>>> pskc = PSKC()
+>>> pskc.mac.setup()
+>>> pskc.mac.algorithm
+'http://www.w3.org/2000/09/xmldsig#hmac-sha1'
+>>> len(pskc.mac.key)
+20
+>>> pskc.mac.setup(key=a2b_hex('548512684595'), algorithm='unknown')
+>>> pskc.mac.algorithm
+'unknown'
+>>> len(pskc.mac.key)
+6
+>>> pskc.mac.algorithm_key_length  # this is the default
+16
+>>> pskc.mac.algorithm = None
+>>> pskc.mac.key = None
+>>> pskc.mac.setup(algorithm='hmac-sha224')
+>>> pskc.mac.algorithm
+'http://www.w3.org/2001/04/xmldsig-more#hmac-sha224'
+>>> pskc.mac.algorithm_key_length
+28
+>>> len(pskc.mac.key)
+28

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

commit 16da531142af8f5052029864df2691ad3fd61858
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sun Mar 20 22:11:43 2016 +0100

    Generate MAC values

diff --git a/pskc/key.py b/pskc/key.py
index 140a194..d7d1cb0 100644
--- a/pskc/key.py
+++ b/pskc/key.py
@@ -120,6 +120,13 @@ class DataType(object):
             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.key:
+                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))
 
diff --git a/pskc/mac.py b/pskc/mac.py
index dccabf7..0b6c7e6 100644
--- a/pskc/mac.py
+++ b/pskc/mac.py
@@ -36,16 +36,21 @@ import re
 _hmac_url_re = re.compile(r'^.*#hmac-(?P<hash>[a-z0-9]+)$')
 
 
+def get_hash(algorithm):
+    """Return the hash function for the specifies HMAC algorithm."""
+    import hashlib
+    match = _hmac_url_re.search(algorithm)
+    if match:
+        return getattr(hashlib, match.group('hash'), None)
+
+
 def get_hmac(algorithm):
     """Return an HMAC function that takes a secret and a value and returns a
     digest."""
-    import hashlib
     import hmac
-    match = _hmac_url_re.search(algorithm)
-    if match:
-        digestmod = getattr(hashlib, match.group('hash'), None)
-        if digestmod is not None:
-            return lambda key, value: hmac.new(key, value, digestmod).digest()
+    digestmod = get_hash(algorithm)
+    if digestmod is not None:
+        return lambda key, value: hmac.new(key, value, digestmod).digest()
 
 
 class MAC(object):
@@ -124,12 +129,8 @@ class MAC(object):
         from pskc.encryption import normalise_algorithm
         self._algorithm = normalise_algorithm(value)
 
-    def check_value(self, value, value_mac):
-        """Check if the provided value matches the MAC.
-
-        This will return None if there is no MAC to be checked. It will
-        return True if the MAC matches and raise an exception if it fails.
-        """
+    def generate_mac(self, value):
+        """Generate the MAC over the specified value."""
         from pskc.exceptions import DecryptionError
         key = self.key
         if key is None:
@@ -138,6 +139,15 @@ class MAC(object):
         if hmacfn is None:
             raise DecryptionError(
                 'Unsupported MAC algorithm: %r' % self.algorithm)
-        if hmacfn(key, value) != value_mac:
+        return hmacfn(key, value)
+
+    def check_value(self, value, value_mac):
+        """Check if the provided value matches the MAC.
+
+        This will return None if there is no MAC to be checked. It will
+        return True if the MAC matches and raise an exception if it fails.
+        """
+        from pskc.exceptions import DecryptionError
+        if self.generate_mac(value) != value_mac:
             raise DecryptionError('MAC value does not match')
         return True

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

commit ca0fa368f6f60d02d1dfcf41d4f9961d65560c98
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sun Mar 20 21:54:55 2016 +0100

    Write MACMethod
    
    This also makes the MAC.algorithm a property similarly as what is done
    for Encryption (normalise algorithm names) and adds a setter for the
    MAC.key property.

diff --git a/pskc/__init__.py b/pskc/__init__.py
index 7c9184b..c6d4aad 100644
--- a/pskc/__init__.py
+++ b/pskc/__init__.py
@@ -106,6 +106,7 @@ class PSKC(object):
         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
diff --git a/pskc/encryption.py b/pskc/encryption.py
index 7b2da65..e69db02 100644
--- a/pskc/encryption.py
+++ b/pskc/encryption.py
@@ -28,7 +28,7 @@ The encryption key can be derived using the KeyDerivation 
class.
 
 import base64
 
-# cannonical URIs of known encryption algorithms
+# cannonical URIs of known algorithms
 _algorithms = {
     'tripledes-cbc': 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc',
     'kw-tripledes': 'http://www.w3.org/2001/04/xmlenc#kw-tripledes',
@@ -44,6 +44,13 @@ _algorithms = {
     'kw-camellia128': 'http://www.w3.org/2001/04/xmldsig-more#kw-camellia128',
     'kw-camellia192': 'http://www.w3.org/2001/04/xmldsig-more#kw-camellia192',
     'kw-camellia256': 'http://www.w3.org/2001/04/xmldsig-more#kw-camellia256',
+    'hmac-md5': 'http://www.w3.org/2001/04/xmldsig-more#hmac-md5',
+    'hmac-sha1': 'http://www.w3.org/2000/09/xmldsig#hmac-sha1',
+    'hmac-sha224': 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha224',
+    'hmac-sha256': 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha256',
+    'hmac-sha384': 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha384',
+    'hmac-sha512': 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha512',
+    'hmac-ripemd160': 'http://www.w3.org/2001/04/xmldsig-more#hmac-ripemd160',
 }
 
 # translation table to change old encryption names to new names
diff --git a/pskc/mac.py b/pskc/mac.py
index 5291d3e..dccabf7 100644
--- a/pskc/mac.py
+++ b/pskc/mac.py
@@ -29,6 +29,7 @@ with the PSKC encryption key.
 """
 
 
+import base64
 import re
 
 
@@ -58,7 +59,8 @@ class MAC(object):
 
     def __init__(self, pskc):
         self.pskc = pskc
-        self.algorithm = None
+        self._algorithm = None
+        self.key_plain_value = None
         self.key_cipher_value = None
         self.key_algorithm = None
 
@@ -76,13 +78,52 @@ class MAC(object):
                 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:
+            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."""
-        if self.key_cipher_value:
+        if self.key_plain_value:
+            return self.key_plain_value
+        elif self.key_cipher_value:
             return self.pskc.encryption.decrypt_value(
                 self.key_cipher_value, self.key_algorithm)
 
+    @key.setter
+    def key(self, value):
+        self.key_plain_value = value
+        self.key_cipher_value = None
+
+    @property
+    def algorithm(self):
+        """Provide the MAC algorithm used."""
+        if self._algorithm:
+            return self._algorithm
+
+    @algorithm.setter
+    def algorithm(self, value):
+        from pskc.encryption import normalise_algorithm
+        self._algorithm = normalise_algorithm(value)
+
     def check_value(self, value, value_mac):
         """Check if the provided value matches the MAC.
 

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

commit 8fd35ba1f50fb11ac1f853146b969d92e94fed3c
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Mon Mar 21 21:12:47 2016 +0100

    Write out encrypted values
    
    The Encryption class now has a fields property that lists the fields
    that should be encrypted when writing the PSKC file.
    
    This adds an encrypt_value() function that performs the encryption and
    various functions to convert the plain value to binary before writing
    the encrypted XML elements.

diff --git a/pskc/encryption.py b/pskc/encryption.py
index 652c7c8..7b2da65 100644
--- a/pskc/encryption.py
+++ b/pskc/encryption.py
@@ -26,6 +26,7 @@ algorithms and decryption.
 The encryption key can be derived using the KeyDerivation class.
 """
 
+import base64
 
 # cannonical URIs of known encryption algorithms
 _algorithms = {
@@ -69,6 +70,12 @@ def normalise_algorithm(algorithm):
     return _algorithms.get(algorithm.rsplit('#', 1)[-1].lower(), algorithm)
 
 
+def pad(value, block_size):
+    """Pad the value to block_size length."""
+    padding = block_size - (len(value) % block_size)
+    return value + padding * chr(padding).encode('ascii')
+
+
 def unpad(value):
     """Remove padding from the plaintext."""
     return value[0:-ord(value[-1:])]
@@ -168,6 +175,7 @@ class Encryption(object):
       key_names: list of names for the key
       key_name: (first) name of the key (usually there is only one)
       key: the key value itself (binary form)
+      fields: a list of Key fields that will be encrypted on writing
 
     The key can either be assigned to the key property or derived using the
     derive_key() method.
@@ -180,6 +188,7 @@ class Encryption(object):
         self.key = None
         self._algorithm = None
         self.derivation = KeyDerivation()
+        self.fields = []
 
     def parse(self, key_info):
         """Read encryption information from the <EncryptionKey> XML tree."""
@@ -287,3 +296,37 @@ class Encryption(object):
         elif algorithm.endswith('#kw-tripledes'):
             from pskc.crypto.tripledeskw import unwrap
             return unwrap(cipher_value, key)
+
+    def encrypt_value(self, plaintext):
+        """Encrypt the provided value and return the cipher_value."""
+        from pskc.exceptions import EncryptionError
+        key = self.key
+        if key is None:
+            raise EncryptionError('No key available')
+        algorithm = self.algorithm
+        if algorithm is None:
+            raise EncryptionError('No algorithm specified')
+        if len(key) not in self.algorithm_key_lengths:
+            raise EncryptionError('Invalid key length')
+        if algorithm.endswith('#aes128-cbc') or \
+                algorithm.endswith('#aes192-cbc') or \
+                algorithm.endswith('#aes256-cbc'):
+            from Crypto import Random
+            from Crypto.Cipher import AES
+            iv = Random.get_random_bytes(AES.block_size)
+            cipher = AES.new(key, AES.MODE_CBC, iv)
+            return iv + cipher.encrypt(pad(plaintext, AES.block_size))
+        elif algorithm.endswith('#tripledes-cbc'):
+            from Crypto import Random
+            from Crypto.Cipher import DES3
+            iv = Random.get_random_bytes(DES3.block_size)
+            cipher = DES3.new(key, DES3.MODE_CBC, iv)
+            return iv + cipher.encrypt(pad(plaintext, DES3.block_size))
+        elif algorithm.endswith('#kw-aes128') or \
+                algorithm.endswith('#kw-aes192') or \
+                algorithm.endswith('#kw-aes256'):
+            from pskc.crypto.aeskw import wrap
+            return wrap(plaintext, key)
+        elif algorithm.endswith('#kw-tripledes'):
+            from pskc.crypto.tripledeskw import wrap
+            return wrap(plaintext, key)
diff --git a/pskc/key.py b/pskc/key.py
index c332efa..140a194 100644
--- a/pskc/key.py
+++ b/pskc/key.py
@@ -23,6 +23,7 @@
 
 import array
 import base64
+import binascii
 
 from pskc.policy import Policy
 
@@ -91,18 +92,36 @@ class DataType(object):
         """Convert the value to an unencrypted string representation."""
         raise NotImplementedError  # pragma: no cover
 
-    def make_xml(self, key, tag):
+    def make_xml(self, key, tag, field):
         from pskc.xml import find, mk_elem
         # skip empty values
-        value = self.get_value()
-        if value is None:
+        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)
-        mk_elem(element, 'pskc:PlainValue', self._to_text(value))
+        # 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())
+        else:
+            mk_elem(element, 'pskc:PlainValue', self._to_text(self.value))
 
     def get_value(self):
         """Provide the attribute value, decrypting as needed."""
@@ -150,6 +169,14 @@ class BinaryDataType(DataType):
             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)."""
@@ -182,6 +209,13 @@ class IntegerDataType(DataType):
         """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 Key(object):
     """Representation of a single key from a PSKC file.
@@ -314,7 +348,7 @@ class Key(object):
             self.challenge_max_length = getint(challenge_format, 'Max')
             self.challenge_check = getbool(
                 challenge_format, 'CheckDigits', getbool(
-                challenge_format, 'CheckDigit'))
+                    challenge_format, 'CheckDigit'))
 
         response_format = find(
             key_package,
@@ -324,7 +358,7 @@ class Key(object):
             self.response_length = getint(response_format, 'Length')
             self.response_check = getbool(
                 response_format, 'CheckDigits', getbool(
-                response_format, 'CheckDigit'))
+                    response_format, 'CheckDigit'))
 
         self.policy.parse(find(key_package, 'Key/Policy'))
 
@@ -373,11 +407,11 @@ class Key(object):
         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')
-        self._counter.make_xml(key, 'pskc:Counter')
-        self._time_offset.make_xml(key, 'pskc:Time')
-        self._time_interval.make_xml(key, 'pskc:TimeInterval')
-        self._time_drift.make_xml(key, 'pskc:TimeDrift')
+        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)

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

commit eba541e9d976472582ad3dc31cd69087be102fa6
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sun Mar 20 21:54:55 2016 +0100

    Make Encryption and MAC constructors consistent
    
    This removes calling parse() from the Encryption and MAC constructors
    and stores a reference to the PSKC object in both objects so it can be
    used later on.

diff --git a/pskc/__init__.py b/pskc/__init__.py
index 9ae5fdf..7c9184b 100644
--- a/pskc/__init__.py
+++ b/pskc/__init__.py
@@ -66,7 +66,7 @@ class PSKC(object):
         from pskc.mac import MAC
         self.version = None
         self.id = None
-        self.encryption = Encryption()
+        self.encryption = Encryption(self)
         self.mac = MAC(self)
         self.keys = []
         if filename is not None:
diff --git a/pskc/encryption.py b/pskc/encryption.py
index 891e63e..652c7c8 100644
--- a/pskc/encryption.py
+++ b/pskc/encryption.py
@@ -173,14 +173,13 @@ class Encryption(object):
     derive_key() method.
     """
 
-    def __init__(self, key_info=None):
+    def __init__(self, pskc):
+        self.pskc = pskc
         self.id = None
         self.key_names = []
         self.key = None
         self._algorithm = None
-        self._encrypted_values = []
         self.derivation = KeyDerivation()
-        self.parse(key_info)
 
     def parse(self, key_info):
         """Read encryption information from the <EncryptionKey> XML tree."""
diff --git a/pskc/mac.py b/pskc/mac.py
index 4cab63c..5291d3e 100644
--- a/pskc/mac.py
+++ b/pskc/mac.py
@@ -56,12 +56,11 @@ class MAC(object):
       key: the binary value of the MAC key if it can be decrypted
     """
 
-    def __init__(self, pskc, mac_method=None):
+    def __init__(self, pskc):
         self.pskc = pskc
         self.algorithm = None
         self.key_cipher_value = None
         self.key_algorithm = None
-        self.parse(mac_method)
 
     def parse(self, mac_method):
         """Read MAC information from the <MACMethod> XML tree."""

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

commit fe2123101634cbe3b64d18999c43d417720122c6
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sun Mar 20 21:27:18 2016 +0100

    Write encryption key information
    
    This writes information about a pre-shared key or PBKDF2 key derivation
    in the PSKC file. This also means that writing a decrypted version of a
    previously encrypted file requires actively removing the encryption.

diff --git a/pskc/__init__.py b/pskc/__init__.py
index 1b02e81..9ae5fdf 100644
--- a/pskc/__init__.py
+++ b/pskc/__init__.py
@@ -105,6 +105,7 @@ class PSKC(object):
         from pskc.xml import mk_elem
         container = mk_elem('pskc:KeyContainer', Version=self.version,
                             Id=self.id)
+        self.encryption.make_xml(container)
         for key in self.keys:
             key.make_xml(container)
         return container
diff --git a/pskc/encryption.py b/pskc/encryption.py
index ce57381..891e63e 100644
--- a/pskc/encryption.py
+++ b/pskc/encryption.py
@@ -82,7 +82,7 @@ class KeyDerivation(object):
 
       pbkdf2_salt: salt value
       pbkdf2_iterations: number of iterations to use
-      pbkdf2_key_length: required key lengt
+      pbkdf2_key_length: required key length in bytes
       pbkdf2_prf: name of pseudorandom function used
     """
 
@@ -115,6 +115,24 @@ class KeyDerivation(object):
             if prf is not None:
                 self.pbkdf2_prf = 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)
+        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(self, password):
         """Derive a key from the password."""
         from pskc.exceptions import KeyDerivationError
@@ -151,8 +169,8 @@ class Encryption(object):
       key_name: (first) name of the key (usually there is only one)
       key: the key value itself (binary form)
 
-    The key can either be included in the PSKC file (in that case it
-    automatically picked up) or derived using the derive_key() method.
+    The key can either be assigned to the key property or derived using the
+    derive_key() method.
     """
 
     def __init__(self, key_info=None):
@@ -177,6 +195,20 @@ class Encryption(object):
         self.derivation.parse(find(
             key_info, 'DerivedKey/KeyDerivationMethod'))
 
+    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/tests/test_write.doctest b/tests/test_write.doctest
index d0fa3d4..b4cc634 100644
--- a/tests/test_write.doctest
+++ b/tests/test_write.doctest
@@ -148,6 +148,10 @@ Read an encrypted PSKC file and write it out as an 
unencrypted file.
 
 >>> pskc = PSKC('tests/encryption/kw-aes128.pskcxml')
 >>> pskc.encryption.key = a2b_hex('000102030405060708090a0b0c0d0e0f')
+>>> for key in pskc.keys:
+...     key.secret = key.secret  # force decryption of values
+>>> pskc.encryption.key = None
+>>> pskc.encryption.key_name = None
 >>> pskc.write(sys.stdout)  #doctest: +REPORT_UDIFF
 <?xml version="1.0" encoding="UTF-8"?>
 <pskc:KeyContainer Version="1.0" 
xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc">

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

commit 08936405fe0481ab40005ec5a6a134c67452e2d7
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sat Mar 19 14:18:21 2016 +0100

    Add algorithm_key_lengths property
    
    This property on the Encryption object provides a list of key sizes (in
    bytes) that the configured encryption algorithm supports.

diff --git a/pskc/encryption.py b/pskc/encryption.py
index 5cdb781..ce57381 100644
--- a/pskc/encryption.py
+++ b/pskc/encryption.py
@@ -201,6 +201,28 @@ class Encryption(object):
         """Derive a key from the password."""
         self.key = self.derivation.derive(password)
 
+    @property
+    def algorithm_key_lengths(self):
+        """Provide the possible key lengths for the configured algorithm."""
+        from pskc.exceptions import DecryptionError
+        algorithm = self.algorithm
+        if algorithm is None:
+            raise DecryptionError('No algorithm specified')
+        elif algorithm.endswith('#aes128-cbc') or \
+                algorithm.endswith('#aes192-cbc') or \
+                algorithm.endswith('#aes256-cbc'):
+            return [int(algorithm[-7:-4]) // 8]
+        elif algorithm.endswith('#tripledes-cbc') or \
+                algorithm.endswith('#kw-tripledes'):
+            from Crypto.Cipher import DES3
+            return list(DES3.key_size)
+        elif algorithm.endswith('#kw-aes128') or \
+                algorithm.endswith('#kw-aes192') or \
+                algorithm.endswith('#kw-aes256'):
+            return [int(algorithm[-3:]) // 8]
+        else:
+            raise DecryptionError('Unsupported algorithm: %r' % algorithm)
+
     def decrypt_value(self, cipher_value, algorithm=None):
         """Decrypt the cipher_value and return the plaintext value."""
         from pskc.exceptions import DecryptionError
@@ -210,21 +232,18 @@ class Encryption(object):
         algorithm = algorithm or self.algorithm
         if algorithm is None:
             raise DecryptionError('No algorithm specified')
+        if len(key) not in self.algorithm_key_lengths:
+            raise DecryptionError('Invalid key length')
         if algorithm.endswith('#aes128-cbc') or \
-           algorithm.endswith('#aes192-cbc') or \
-           algorithm.endswith('#aes256-cbc'):
+                algorithm.endswith('#aes192-cbc') or \
+                algorithm.endswith('#aes256-cbc'):
             from Crypto.Cipher import AES
-            if len(key) * 8 != int(algorithm[-7:-4]) or \
-               len(key) not in AES.key_size:
-                raise DecryptionError('Invalid key length')
             iv = cipher_value[:AES.block_size]
             ciphertext = cipher_value[AES.block_size:]
             cipher = AES.new(key, AES.MODE_CBC, iv)
             return unpad(cipher.decrypt(ciphertext))
         elif algorithm.endswith('#tripledes-cbc'):
             from Crypto.Cipher import DES3
-            if len(key) not in DES3.key_size:
-                raise DecryptionError('Invalid key length')
             iv = cipher_value[:DES3.block_size]
             ciphertext = cipher_value[DES3.block_size:]
             cipher = DES3.new(key, DES3.MODE_CBC, iv)
@@ -233,16 +252,7 @@ class Encryption(object):
                 algorithm.endswith('#kw-aes192') or \
                 algorithm.endswith('#kw-aes256'):
             from pskc.crypto.aeskw import unwrap
-            from Crypto.Cipher import AES
-            if len(key) * 8 != int(algorithm[-3:]) or \
-               len(key) not in AES.key_size:
-                raise DecryptionError('Invalid key length')
             return unwrap(cipher_value, key)
         elif algorithm.endswith('#kw-tripledes'):
             from pskc.crypto.tripledeskw import unwrap
-            from Crypto.Cipher import DES3
-            if len(key) not in DES3.key_size:
-                raise DecryptionError('Invalid key length')
             return unwrap(cipher_value, key)
-        else:
-            raise DecryptionError('Unsupported algorithm: %r' % algorithm)
diff --git a/tests/test_misc.doctest b/tests/test_misc.doctest
index 94af520..317e278 100644
--- a/tests/test_misc.doctest
+++ b/tests/test_misc.doctest
@@ -86,9 +86,20 @@ True
 >>> pskc.encryption.algorithm = 'aes128-cbc'
 >>> pskc.encryption.algorithm
 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
+>>> pskc.encryption.algorithm_key_lengths
+[16]
+>>> pskc.encryption.algorithm = '3des-cbc'
+>>> pskc.encryption.algorithm
+'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'
+>>> pskc.encryption.algorithm_key_lengths
+[16, 24]
 >>> pskc.encryption.algorithm = 'none'
 >>> pskc.encryption.algorithm is None
 True
+>>> pskc.encryption.algorithm_key_lengths  # doctest: +IGNORE_EXCEPTION_DETAIL
+Traceback (most recent call last):
+    ...
+DecryptionError: No algorithm specified
 
 
 Load an PSKC file with an odd namespace.

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

Summary of changes:
 README                        |  14 +--
 docs/conf.py                  |   2 +-
 docs/encryption.rst           |  74 +++++++++++-
 docs/mac.rst                  |  11 ++
 docs/usage.rst                |  25 ++---
 pskc/__init__.py              |   8 +-
 pskc/encryption.py            | 243 ++++++++++++++++++++++++++++++++++------
 pskc/key.py                   |  63 +++++++++--
 pskc/mac.py                   | 115 ++++++++++++++++---
 tests/test_encryption.doctest |  85 ++++++++++++++
 tests/test_misc.doctest       |  11 ++
 tests/test_write.doctest      | 253 ++++++++++++++++++++++++++++++++++++++++++
 12 files changed, 816 insertions(+), 88 deletions(-)


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