diff options
author | Donald Stufft <donald@stufft.io> | 2013-10-29 15:33:06 -0700 |
---|---|---|
committer | Donald Stufft <donald@stufft.io> | 2013-10-29 15:33:06 -0700 |
commit | e0f7082d119296d809ac95f8bc1ade53dc9fdf55 (patch) | |
tree | 84f61706abf5e32f502bc6db352ddfaed23ea393 | |
parent | a9d9922f82d4e7b940679c4b548a4b14d0958ed9 (diff) | |
parent | f108871b04c27c557f3e1a7fa3982c6d9d77d7fd (diff) | |
download | cryptography-e0f7082d119296d809ac95f8bc1ade53dc9fdf55.tar.gz cryptography-e0f7082d119296d809ac95f8bc1ade53dc9fdf55.tar.bz2 cryptography-e0f7082d119296d809ac95f8bc1ade53dc9fdf55.zip |
Merge pull request #192 from alex/pkcs7-padding
PKCS7 padding
-rw-r--r-- | cryptography/hazmat/primitives/interfaces.py | 14 | ||||
-rw-r--r-- | cryptography/hazmat/primitives/padding.py | 121 | ||||
-rw-r--r-- | docs/hazmat/primitives/index.rst | 1 | ||||
-rw-r--r-- | docs/hazmat/primitives/padding.rst | 69 | ||||
-rw-r--r-- | docs/hazmat/primitives/symmetric-encryption.rst | 15 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_padding.py | 107 |
6 files changed, 319 insertions, 8 deletions
diff --git a/cryptography/hazmat/primitives/interfaces.py b/cryptography/hazmat/primitives/interfaces.py index 49c19d0e..217490fd 100644 --- a/cryptography/hazmat/primitives/interfaces.py +++ b/cryptography/hazmat/primitives/interfaces.py @@ -45,3 +45,17 @@ class CipherContext(six.with_metaclass(abc.ABCMeta)): """ finalize return bytes """ + + +class PaddingContext(six.with_metaclass(abc.ABCMeta)): + @abc.abstractmethod + def update(self, data): + """ + update takes bytes and return bytes + """ + + @abc.abstractmethod + def finalize(self): + """ + finalize return bytes + """ diff --git a/cryptography/hazmat/primitives/padding.py b/cryptography/hazmat/primitives/padding.py new file mode 100644 index 00000000..ddcadd89 --- /dev/null +++ b/cryptography/hazmat/primitives/padding.py @@ -0,0 +1,121 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import six + +from cryptography.hazmat.primitives import interfaces + + +class PKCS7(object): + def __init__(self, block_size): + super(PKCS7, self).__init__() + if not (0 <= block_size < 256): + raise ValueError("block_size must be in range(0, 256)") + + if block_size % 8 != 0: + raise ValueError("block_size must be a multiple of 8") + + self.block_size = block_size + + def padder(self): + return _PKCS7PaddingContext(self.block_size) + + def unpadder(self): + return _PKCS7UnpaddingContext(self.block_size) + + +@interfaces.register(interfaces.PaddingContext) +class _PKCS7PaddingContext(object): + def __init__(self, block_size): + super(_PKCS7PaddingContext, self).__init__() + self.block_size = block_size + # TODO: O(n ** 2) complexity for repeated concatentation, we should use + # zero-buffer (#193) + self._buffer = b"" + + def update(self, data): + if self._buffer is None: + raise ValueError("Context was already finalized") + + if isinstance(data, six.text_type): + raise TypeError("Unicode-objects must be encoded before padding") + + self._buffer += data + + finished_blocks = len(self._buffer) // (self.block_size // 8) + + result = self._buffer[:finished_blocks * (self.block_size // 8)] + self._buffer = self._buffer[finished_blocks * (self.block_size // 8):] + + return result + + def finalize(self): + if self._buffer is None: + raise ValueError("Context was already finalized") + + pad_size = self.block_size // 8 - len(self._buffer) + result = self._buffer + six.int2byte(pad_size) * pad_size + self._buffer = None + return result + + +@interfaces.register(interfaces.PaddingContext) +class _PKCS7UnpaddingContext(object): + def __init__(self, block_size): + super(_PKCS7UnpaddingContext, self).__init__() + self.block_size = block_size + # TODO: O(n ** 2) complexity for repeated concatentation, we should use + # zero-buffer (#193) + self._buffer = b"" + + def update(self, data): + if self._buffer is None: + raise ValueError("Context was already finalized") + + if isinstance(data, six.text_type): + raise TypeError("Unicode-objects must be encoded before unpadding") + + self._buffer += data + + finished_blocks = max( + len(self._buffer) // (self.block_size // 8) - 1, + 0 + ) + + result = self._buffer[:finished_blocks * (self.block_size // 8)] + self._buffer = self._buffer[finished_blocks * (self.block_size // 8):] + + return result + + def finalize(self): + if self._buffer is None: + raise ValueError("Context was already finalized") + + if not self._buffer: + raise ValueError("Invalid padding bytes") + + pad_size = six.indexbytes(self._buffer, -1) + + if pad_size > self.block_size // 8: + raise ValueError("Invalid padding bytes") + + mismatch = 0 + for b in six.iterbytes(self._buffer[-pad_size:]): + mismatch |= b ^ pad_size + + if mismatch != 0: + raise ValueError("Invalid padding bytes") + + res = self._buffer[:-pad_size] + self._buffer = None + return res diff --git a/docs/hazmat/primitives/index.rst b/docs/hazmat/primitives/index.rst index 3927f3f0..ee1e251c 100644 --- a/docs/hazmat/primitives/index.rst +++ b/docs/hazmat/primitives/index.rst @@ -14,3 +14,4 @@ Primitives cryptographic-hashes hmac symmetric-encryption + padding diff --git a/docs/hazmat/primitives/padding.rst b/docs/hazmat/primitives/padding.rst new file mode 100644 index 00000000..ba3ddcc0 --- /dev/null +++ b/docs/hazmat/primitives/padding.rst @@ -0,0 +1,69 @@ +.. danger:: + + This is a "Hazardous Materials" module. You should **ONLY** use it if + you're 100% absolutely sure that you know what you're doing because this + module is full of land mines, dragons, and dinosaurs with laser guns. + + +Padding +======= + +.. currentmodule:: cryptography.hazmat.primitives.padding + +Padding is a way to take data that may or may not be be a multiple of the block +size for a cipher and extend it out so that it is. This is required for many +block cipher modes as they require the data to be encrypted to be an exact +multiple of the block size. + + +.. class:: PKCS7(block_size) + + PKCS7 padding is a generalization of PKCS5 padding (also known as standard + padding). PKCS7 padding works by appending ``N`` bytes with the value of + ``chr(N)``, where ``N`` is the number of bytes required to make the final + block of data the same size as the block size. A simple example of padding + is: + + .. doctest:: + + >>> from cryptography.hazmat.primitives import padding + >>> padder = padding.PKCS7(128).padder() + >>> padder.update(b"1111111111") + '' + >>> padder.finalize() + '1111111111\x06\x06\x06\x06\x06\x06' + + :param block_size: The size of the block in bits that the data is being + padded to. + + .. method:: padder() + + :returns: A padding + :class:`~cryptography.hazmat.primitives.interfaces.PaddingContext` + provider. + + .. method:: unpadder() + + :returns: An unpadding + :class:`~cryptography.hazmat.primitives.interfaces.PaddingContext` + provider. + + +.. currentmodule:: cryptography.hazmat.primitives.interfaces + +.. class:: PaddingContext + + When calling ``padder()`` or ``unpadder()`` you will receive an a return + object conforming to the ``PaddingContext`` interface. You can then call + ``update(data)`` with data until you have fed everything into the context. + Once that is done call ``finalize()`` to finish the operation and obtain + the remainder of the data. + + .. method:: update(data) + + :param bytes data: The data you wish to pass into the context. + :return bytes: Returns the data that was padded or unpadded. + + .. method:: finalize() + + :return bytes: Returns the remainder of the data. diff --git a/docs/hazmat/primitives/symmetric-encryption.rst b/docs/hazmat/primitives/symmetric-encryption.rst index 758a4648..9a5bce07 100644 --- a/docs/hazmat/primitives/symmetric-encryption.rst +++ b/docs/hazmat/primitives/symmetric-encryption.rst @@ -54,18 +54,17 @@ where the encrypter and decrypter both use the same key. .. currentmodule:: cryptography.hazmat.primitives.interfaces -.. class:: CipherContext() - - When calling ``encryptor()`` or ``decryptor()`` on a BlockCipher object you - will receive a return object conforming to the CipherContext interface. You - can then call ``update(data)`` with data until you have fed everything into - the context. Once that is done call ``finalize()`` to finish the operation and - obtain the remainder of the data. +.. class:: CipherContext + When calling ``encryptor()`` or ``decryptor()`` on a ``BlockCipher`` object + you will receive a return object conforming to the ``CipherContext`` + interface. You can then call ``update(data)`` with data until you have fed + everything into the context. Once that is done call ``finalize()`` to + finish the operation and obtain the remainder of the data. .. method:: update(data) - :param bytes data: The text you wish to pass into the context. + :param bytes data: The data you wish to pass into the context. :return bytes: Returns the data that was encrypted or decrypted. .. method:: finalize() diff --git a/tests/hazmat/primitives/test_padding.py b/tests/hazmat/primitives/test_padding.py new file mode 100644 index 00000000..3cefafaf --- /dev/null +++ b/tests/hazmat/primitives/test_padding.py @@ -0,0 +1,107 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import six + +from cryptography.hazmat.primitives import padding + + +class TestPKCS7(object): + @pytest.mark.parametrize("size", [127, 4096, -2]) + def test_invalid_block_size(self, size): + with pytest.raises(ValueError): + padding.PKCS7(size) + + @pytest.mark.parametrize(("size", "padded"), [ + (128, b"1111"), + (128, b"1111111111111111"), + (128, b"111111111111111\x06"), + (128, b""), + ]) + def test_invalid_padding(self, size, padded): + unpadder = padding.PKCS7(size).unpadder() + with pytest.raises(ValueError): + unpadder.update(padded) + unpadder.finalize() + + def test_non_bytes(self): + padder = padding.PKCS7(128).padder() + with pytest.raises(TypeError): + padder.update(six.u("abc")) + unpadder = padding.PKCS7(128).unpadder() + with pytest.raises(TypeError): + unpadder.update(six.u("abc")) + + @pytest.mark.parametrize(("size", "unpadded", "padded"), [ + ( + 128, + b"1111111111", + b"1111111111\x06\x06\x06\x06\x06\x06", + ), + ( + 128, + b"111111111111111122222222222222", + b"111111111111111122222222222222\x02\x02", + ), + ( + 128, + b"1" * 16, + b"1" * 16 + b"\x10" * 16, + ), + ( + 128, + b"1" * 17, + b"1" * 17 + b"\x0F" * 15, + ) + ]) + def test_pad(self, size, unpadded, padded): + padder = padding.PKCS7(size).padder() + result = padder.update(unpadded) + result += padder.finalize() + assert result == padded + + @pytest.mark.parametrize(("size", "unpadded", "padded"), [ + ( + 128, + b"1111111111", + b"1111111111\x06\x06\x06\x06\x06\x06", + ), + ( + 128, + b"111111111111111122222222222222", + b"111111111111111122222222222222\x02\x02", + ), + ]) + def test_unpad(self, size, unpadded, padded): + unpadder = padding.PKCS7(size).unpadder() + result = unpadder.update(padded) + result += unpadder.finalize() + assert result == unpadded + + def test_use_after_finalize(self): + padder = padding.PKCS7(128).padder() + b = padder.finalize() + with pytest.raises(ValueError): + padder.update(b"") + with pytest.raises(ValueError): + padder.finalize() + + unpadder = padding.PKCS7(128).unpadder() + unpadder.update(b) + unpadder.finalize() + with pytest.raises(ValueError): + unpadder.update(b"") + with pytest.raises(ValueError): + unpadder.finalize() |