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