Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for update_into on CipherContext #3190

Merged
merged 23 commits into from
Feb 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ Changelog
* Changed ASN.1 dependency from ``pyasn1`` to ``asn1crypto`` resulting in a
general performance increase when encoding/decoding ASN.1 structures. Also,
the ``pyasn1_modules`` test dependency is no longer required.

* Added support for
:meth:`~cryptography.hazmat.primitives.ciphers.CipherContext.update_into` on
:class:`~cryptography.hazmat.primitives.ciphers.CipherContext`.
* Added
:meth:`~cryptography.hazmat.primitives.asymmetric.dh.DHPrivateKeyWithSerialization.private_bytes`
to
Expand Down
43 changes: 43 additions & 0 deletions docs/hazmat/primitives/symmetric-encryption.rst
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,49 @@ Interfaces
return bytes immediately, however in other modes it will return chunks
whose size is determined by the cipher's block size.

.. method:: update_into(data, buf)

.. versionadded:: 1.8

.. warning::

This method allows you to avoid a memory copy by passing a writable
buffer and reading the resulting data. You are responsible for
correctly sizing the buffer and properly handling the data. This
method should only be used when extremely high performance is a
requirement and you will be making many small calls to
``update_into``.

:param bytes data: The data you wish to pass into the context.
:param buf: A writable Python buffer that the data will be written
into. This buffer should be ``len(data) + n - 1`` bytes where ``n``
is the block size (in bytes) of the cipher and mode combination.
:return int: Number of bytes written.
:raises NotImplementedError: This is raised if the version of ``cffi``
used is too old (this can happen on older PyPy releases).
:raises ValueError: This is raised if the supplied buffer is too small.

.. doctest::

>>> import os
>>> from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
>>> from cryptography.hazmat.backends import default_backend
>>> backend = default_backend()
>>> key = os.urandom(32)
>>> iv = os.urandom(16)
>>> cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
>>> encryptor = cipher.encryptor()
>>> # the buffer needs to be at least len(data) + n - 1 where n is cipher/mode block size in bytes
>>> buf = bytearray(31)
>>> len_encrypted = encryptor.update_into(b"a secret message", buf)
>>> # get the ciphertext from the buffer reading only the bytes written to it (len_encrypted)
>>> ct = bytes(buf[:len_encrypted]) + encryptor.finalize()
>>> decryptor = cipher.decryptor()
>>> len_decrypted = decryptor.update_into(ct, buf)
>>> # get the plaintext from the buffer reading only the bytes written (len_decrypted)
>>> bytes(buf[:len_decrypted]) + decryptor.finalize()
'a secret message'

.. method:: finalize()

:return bytes: Returns the remainder of the data.
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
requirements = [
"idna>=2.0",
"asn1crypto>=0.21.0",
"packaging",
"six>=1.4.1",
"setuptools>=11.3",
]
Expand Down
36 changes: 36 additions & 0 deletions src/cryptography/hazmat/backends/commoncrypto/ciphers.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,24 @@ def update(self, data):
self._backend._check_cipher_response(res)
return self._backend._ffi.buffer(buf)[:outlen[0]]

def update_into(self, data, buf):
if len(buf) < (len(data) + self._byte_block_size - 1):
raise ValueError(
"buffer must be at least {0} bytes for this "
"payload".format(len(data) + self._byte_block_size - 1)
)
# Count bytes processed to handle block alignment.
self._bytes_processed += len(data)
outlen = self._backend._ffi.new("size_t *")
buf = self._backend._ffi.cast(
"unsigned char *", self._backend._ffi.from_buffer(buf)
)
res = self._backend._lib.CCCryptorUpdate(
self._ctx[0], data, len(data), buf,
len(data) + self._byte_block_size - 1, outlen)
self._backend._check_cipher_response(res)
return outlen[0]

def finalize(self):
# Raise error if block alignment is wrong.
if self._bytes_processed % self._byte_block_size:
Expand Down Expand Up @@ -161,6 +179,24 @@ def update(self, data):
self._backend._check_cipher_response(res)
return self._backend._ffi.buffer(buf)[:]

def update_into(self, data, buf):
if len(buf) < len(data):
raise ValueError(
"buffer must be at least {0} bytes".format(len(data))
)

buf = self._backend._ffi.cast(
"unsigned char *", self._backend._ffi.from_buffer(buf)
)
args = (self._ctx[0], data, len(data), buf)
if self._operation == self._backend._lib.kCCEncrypt:
res = self._backend._lib.CCCryptorGCMEncrypt(*args)
else:
res = self._backend._lib.CCCryptorGCMDecrypt(*args)

self._backend._check_cipher_response(res)
return len(data)

def finalize(self):
# CommonCrypto has a yet another bug where you must make at least one
# call to update. If you pass just AAD and call finalize without a call
Expand Down
16 changes: 16 additions & 0 deletions src/cryptography/hazmat/backends/openssl/ciphers.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ def update(self, data):
self._backend.openssl_assert(res != 0)
return self._backend._ffi.buffer(buf)[:outlen[0]]

def update_into(self, data, buf):
if len(buf) < (len(data) + self._block_size_bytes - 1):
raise ValueError(
"buffer must be at least {0} bytes for this "
"payload".format(len(data) + self._block_size_bytes - 1)
)

buf = self._backend._ffi.cast(
"unsigned char *", self._backend._ffi.from_buffer(buf)
)
outlen = self._backend._ffi.new("int *")
res = self._backend._lib.EVP_CipherUpdate(self._ctx, buf, outlen,
data, len(data))
self._backend.openssl_assert(res != 0)
return outlen[0]

def finalize(self):
# OpenSSL 1.0.1 on Ubuntu 12.04 (and possibly other distributions)
# appears to have a bug where you must make at least one call to update
Expand Down
42 changes: 40 additions & 2 deletions src/cryptography/hazmat/primitives/ciphers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import abc

import cffi

import six

from cryptography import utils
Expand Down Expand Up @@ -50,6 +52,13 @@ def update(self, data):
as bytes.
"""

@abc.abstractmethod
def update_into(self, data, buf):
"""
Processes the provided bytes and writes the resulting data into the
provided buffer. Returns the number of bytes written.
"""

@abc.abstractmethod
def finalize(self):
"""
Expand Down Expand Up @@ -136,6 +145,20 @@ def update(self, data):
raise AlreadyFinalized("Context was already finalized.")
return self._ctx.update(data)

# cffi 1.7 supports from_buffer on bytearray, which is required. We can
# remove this check in the future when we raise our minimum PyPy version.
if utils._version_check(cffi.__version__, "1.7"):
def update_into(self, data, buf):
if self._ctx is None:
raise AlreadyFinalized("Context was already finalized.")
return self._ctx.update_into(data, buf)
else:
def update_into(self, data, buf):
raise NotImplementedError(
"update_into requires cffi 1.7+. To use this method please "
"update cffi."
)

def finalize(self):
if self._ctx is None:
raise AlreadyFinalized("Context was already finalized.")
Expand All @@ -154,20 +177,35 @@ def __init__(self, ctx):
self._tag = None
self._updated = False

def update(self, data):
def _check_limit(self, data_size):
if self._ctx is None:
raise AlreadyFinalized("Context was already finalized.")
self._updated = True
self._bytes_processed += len(data)
self._bytes_processed += data_size
if self._bytes_processed > self._ctx._mode._MAX_ENCRYPTED_BYTES:
raise ValueError(
"{0} has a maximum encrypted byte limit of {1}".format(
self._ctx._mode.name, self._ctx._mode._MAX_ENCRYPTED_BYTES
)
)

def update(self, data):
self._check_limit(len(data))
return self._ctx.update(data)

# cffi 1.7 supports from_buffer on bytearray, which is required. We can
# remove this check in the future when we raise our minimum PyPy version.
if utils._version_check(cffi.__version__, "1.7"):
def update_into(self, data, buf):
self._check_limit(len(data))
return self._ctx.update_into(data, buf)
else:
def update_into(self, data, buf):
raise NotImplementedError(
"update_into requires cffi 1.7+. To use this method please "
"update cffi."
)

def finalize(self):
if self._ctx is None:
raise AlreadyFinalized("Context was already finalized.")
Expand Down
7 changes: 7 additions & 0 deletions src/cryptography/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import sys
import warnings

from packaging.version import parse


# the functions deprecated in 1.0 and 1.4 are on an arbitrarily extended
# deprecation cycle and should not be removed until we agree on when that cycle
Expand Down Expand Up @@ -98,6 +100,11 @@ def bit_length(x):
return len(bin(x)) - (2 + (x <= 0))


def _version_check(version, required_version):
# This is used to check if we support update_into on CipherContext.
return parse(version) >= parse(required_version)


class _DeprecatedValue(object):
def __init__(self, value, message, warning_class):
self.value = value
Expand Down
20 changes: 20 additions & 0 deletions tests/hazmat/primitives/test_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import binascii

import cffi

import pytest

from cryptography.exceptions import (
Expand All @@ -15,6 +17,7 @@
from cryptography.hazmat.primitives.ciphers import (
Cipher, algorithms, base, modes
)
from cryptography.utils import _version_check

from .utils import (
generate_aead_exception_test, generate_aead_tag_exception_test
Expand Down Expand Up @@ -70,6 +73,23 @@ def test_use_after_finalize(self, backend):
with pytest.raises(AlreadyFinalized):
decryptor.finalize()

@pytest.mark.skipif(
not _version_check(cffi.__version__, '1.7'),
reason="cffi version too old"
)
def test_use_update_into_after_finalize(self, backend):
cipher = Cipher(
algorithms.AES(binascii.unhexlify(b"0" * 32)),
modes.CBC(binascii.unhexlify(b"0" * 32)),
backend
)
encryptor = cipher.encryptor()
encryptor.update(b"a" * 16)
encryptor.finalize()
with pytest.raises(AlreadyFinalized):
buf = bytearray(31)
encryptor.update_into(b"b" * 16, buf)

def test_unaligned_block_encryption(self, backend):
cipher = Cipher(
algorithms.AES(binascii.unhexlify(b"0" * 32)),
Expand Down
Loading