diff --git a/python/daead/__init__.py b/python/daead/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..eed4692505719532df3e960195e03f691487eefa --- /dev/null +++ b/python/daead/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2019 Google LLC. +# +# 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. + +"""Deterministic aead package.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import google_type_annotations +from __future__ import print_function + +from tink.python.daead import deterministic_aead +from tink.python.daead import deterministic_aead_key_manager +from tink.python.daead import deterministic_aead_key_templates +from tink.python.daead import deterministic_aead_wrapper + + +DeterministicAead = deterministic_aead.DeterministicAead +DeterministicAeadWrapper = deterministic_aead_wrapper.DeterministicAeadWrapper diff --git a/python/daead/deterministic_aead.py b/python/daead/deterministic_aead.py new file mode 100644 index 0000000000000000000000000000000000000000..3ad06678e1e5a2366ee2e172731b6acdf4b4b784 --- /dev/null +++ b/python/daead/deterministic_aead.py @@ -0,0 +1,59 @@ +# Copyright 2019 Google LLC. +# +# 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. + +"""This module defines the interface for Deterministic AEAD.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import google_type_annotations +from __future__ import print_function + +import abc + + +class DeterministicAead(object): + """Interface for Deterministic Authenticated Encryption with Associated Data. + + For why this interface is desirable and some of its use cases, see for + example https://tools.ietf.org/html/rfc5297#section-1.3. + + Warning! + + Unlike Aead, implementations of this interface are not semantically + secure, because encrypting the same plaintex always yields the same + ciphertext. + + Security guarantees + + Implementations of this interface provide 128-bit security level against + multi-user attacks with up to 2^32 keys. That means if an adversary + obtains 2^32 ciphertexts of the same message encrypted under 2^32 keys, + they need to do 2^128 computations to obtain a single key. + + Encryption with associated data ensures authenticity (who the sender is) + and integrity (the data has not been tampered with) of that data, but not + its secrecy. (see https://tools.ietf.org/html/rfc5116) + """ + + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def encrypt_deterministically(self, plaintext: bytes, + associated_data: bytes) -> bytes: + raise NotImplementedError() + + @abc.abstractmethod + def decrypt_deterministically(self, ciphertext: bytes, + associated_data: bytes) -> bytes: + raise NotImplementedError() diff --git a/python/daead/deterministic_aead_key_manager.py b/python/daead/deterministic_aead_key_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..bb27376b3a9a8272225d336b0f6f2bab39640b31 --- /dev/null +++ b/python/daead/deterministic_aead_key_manager.py @@ -0,0 +1,54 @@ +# Copyright 2019 Google LLC. +# +# 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. + +"""Python wrapper of the CLIF-wrapped C++ Deterministic AEAD key manager.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import google_type_annotations +from __future__ import print_function + +from typing import Text + +from tink.python.cc.clif import cc_key_manager +from tink.python.core import key_manager +from tink.python.core import tink_error +from tink.python.daead import deterministic_aead + + +class _DeterministicAeadCcToPyWrapper(deterministic_aead.DeterministicAead): + """Transforms cliffed C++ DeterministicAead into a Python primitive.""" + + def __init__(self, cc_deterministic_aead): + self._deterministic_aead = cc_deterministic_aead + + @tink_error.use_tink_errors + def encrypt_deterministically(self, plaintext: bytes, + associated_data: bytes) -> bytes: + return self._deterministic_aead.encrypt_deterministically( + plaintext, associated_data) + + @tink_error.use_tink_errors + def decrypt_deterministically(self, ciphertext: bytes, + associated_data: bytes) -> bytes: + return self._deterministic_aead.decrypt_deterministically( + ciphertext, associated_data) + + +def from_cc_registry( + type_url: Text +) -> key_manager.KeyManager[deterministic_aead.DeterministicAead]: + return key_manager.KeyManagerCcToPyWrapper( + cc_key_manager.DeterministicAeadKeyManager.from_cc_registry(type_url), + deterministic_aead.DeterministicAead, _DeterministicAeadCcToPyWrapper) diff --git a/python/daead/deterministic_aead_key_manager_test.py b/python/daead/deterministic_aead_key_manager_test.py new file mode 100644 index 0000000000000000000000000000000000000000..c8ad6e128d2ce8357f9e06d83b662985de9934ed --- /dev/null +++ b/python/daead/deterministic_aead_key_manager_test.py @@ -0,0 +1,81 @@ +# Copyright 2019 Google LLC. +# +# 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. + +"""Tests for tink.python.aead_key_manager.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import unittest +from tink.proto import aes_siv_pb2 +from tink.proto import tink_pb2 +from tink.python.core import tink_config +from tink.python.core import tink_error +from tink.python.daead import deterministic_aead +from tink.python.daead import deterministic_aead_key_manager +from tink.python.daead import deterministic_aead_key_templates + + +def setUpModule(): + tink_config.register() + + +class DeterministicAeadKeyManagerTest(googletest.TestCase): + + def setUp(self): + super(DeterministicAeadKeyManagerTest, self).setUp() + self.key_manager = deterministic_aead_key_manager.from_cc_registry( + 'type.googleapis.com/google.crypto.tink.AesSivKey') + + def test_primitive_class(self): + self.assertEqual(self.key_manager.primitive_class(), + deterministic_aead.DeterministicAead) + + def test_key_type(self): + self.assertEqual(self.key_manager.key_type(), + 'type.googleapis.com/google.crypto.tink.AesSivKey') + + def test_new_key_data(self): + key_template = deterministic_aead_key_templates.AES256_SIV + key_data = self.key_manager.new_key_data(key_template) + self.assertEqual(key_data.type_url, self.key_manager.key_type()) + self.assertEqual(key_data.key_material_type, tink_pb2.KeyData.SYMMETRIC) + key = aes_siv_pb2.AesSivKey() + key.ParseFromString(key_data.value) + self.assertEqual(key.version, 0) + self.assertLen(key.key_value, 64) + + def test_invalid_params_throw_exception(self): + key_template = deterministic_aead_key_templates.create_aes_siv_key_template( + 63) + with self.assertRaisesRegex(tink_error.TinkError, + 'Invalid AesSivKeyFormat'): + self.key_manager.new_key_data(key_template) + + def test_encrypt_decrypt(self): + daead_primitive = self.key_manager.primitive( + self.key_manager.new_key_data( + deterministic_aead_key_templates.AES256_SIV)) + plaintext = b'plaintext' + associated_data = b'associated_data' + ciphertext = daead_primitive.encrypt_deterministically( + plaintext, associated_data) + self.assertEqual( + daead_primitive.decrypt_deterministically(ciphertext, associated_data), + plaintext) + + +if __name__ == '__main__': + googletest.main() diff --git a/python/daead/deterministic_aead_key_templates.py b/python/daead/deterministic_aead_key_templates.py new file mode 100644 index 0000000000000000000000000000000000000000..c80a4bfb834855925fdcaafbc787ac5c718d4d30 --- /dev/null +++ b/python/daead/deterministic_aead_key_templates.py @@ -0,0 +1,46 @@ +# Copyright 2019 Google LLC. +# +# 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. + +"""Pre-generated KeyTemplate for DeterministicAead. + +One can use these templates to generate new tink_pb2.Keyset with +tink_pb2.KeysetHandle. To generate a new keyset that contains a single +tink_pb2.HmacKey, one can do: +handle = keyset_handle.KeysetHandle(aead_key_templates.AES128_EAX). +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import google_type_annotations +from __future__ import print_function + +from tink.proto import aes_siv_pb2 +from tink.proto import tink_pb2 + + +_AES_SIV_KEY_TYPE_URL = 'type.googleapis.com/google.crypto.tink.AesSivKey' + + +def create_aes_siv_key_template(key_size: int) -> tink_pb2.KeyTemplate: + """Creates an AES EAX KeyTemplate, and fills in its values.""" + key_format = aes_siv_pb2.AesSivKeyFormat() + key_format.key_size = key_size + key_template = tink_pb2.KeyTemplate() + key_template.type_url = _AES_SIV_KEY_TYPE_URL + key_template.output_prefix_type = tink_pb2.TINK + key_template.value = key_format.SerializeToString() + return key_template + + +AES256_SIV = create_aes_siv_key_template(key_size=64) diff --git a/python/daead/deterministic_aead_key_templates_test.py b/python/daead/deterministic_aead_key_templates_test.py new file mode 100644 index 0000000000000000000000000000000000000000..dd45ee9b9997f173a9600d3b20bedf0bac40234d --- /dev/null +++ b/python/daead/deterministic_aead_key_templates_test.py @@ -0,0 +1,51 @@ +# Copyright 2019 Google LLC. +# +# 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. + +"""Tests for tink.python.deterministic_aead_key_templates.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import unittest +from tink.proto import aes_siv_pb2 +from tink.proto import tink_pb2 +from tink.python.daead import deterministic_aead_key_templates + + +class DeterministicAeadKeyTemplatesTest(googletest.TestCase): + + def test_aes256_siv(self): + template = deterministic_aead_key_templates.AES256_SIV + self.assertEqual('type.googleapis.com/google.crypto.tink.AesSivKey', + template.type_url) + self.assertEqual(tink_pb2.TINK, template.output_prefix_type) + key_format = aes_siv_pb2.AesSivKeyFormat() + key_format.ParseFromString(template.value) + self.assertEqual(64, key_format.key_size) + + def test_create_aes_siv_key_template(self): + # Intentionally using 'weird' or invalid values for parameters, + # to test that the function correctly puts them in the resulting template. + template = deterministic_aead_key_templates.create_aes_siv_key_template( + key_size=42) + self.assertEqual('type.googleapis.com/google.crypto.tink.AesSivKey', + template.type_url) + self.assertEqual(tink_pb2.TINK, template.output_prefix_type) + key_format = aes_siv_pb2.AesSivKeyFormat() + key_format.ParseFromString(template.value) + self.assertEqual(42, key_format.key_size) + +if __name__ == '__main__': + googletest.main() diff --git a/python/daead/deterministic_aead_wrapper.py b/python/daead/deterministic_aead_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..f1b52e1e002cd03fd0f82a0f284f70b19677a491 --- /dev/null +++ b/python/daead/deterministic_aead_wrapper.py @@ -0,0 +1,84 @@ +# Copyright 2019 Google LLC. +# +# 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. + +"""Deterministic AEAD wrapper.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import google_type_annotations +from __future__ import print_function + +from typing import Type + +from pyglib import logging +from tink.python.core import crypto_format +from tink.python.core import primitive_set +from tink.python.core import primitive_wrapper +from tink.python.core import tink_error +from tink.python.daead import deterministic_aead + + +class _WrappedDeterministicAead(deterministic_aead.DeterministicAead): + """Implements DeterministicAead for a set of DeterministicAead primitives.""" + + def __init__(self, pset: primitive_set.PrimitiveSet): + self._primitive_set = pset + + def encrypt_deterministically(self, plaintext: bytes, + associated_data: bytes) -> bytes: + primary = self._primitive_set.primary() + return primary.identifier + primary.primitive.encrypt_deterministically( + plaintext, associated_data) + + def decrypt_deterministically(self, ciphertext: bytes, + associated_data: bytes) -> bytes: + if len(ciphertext) > crypto_format.NON_RAW_PREFIX_SIZE: + prefix = ciphertext[:crypto_format.NON_RAW_PREFIX_SIZE] + ciphertext_no_prefix = ciphertext[crypto_format.NON_RAW_PREFIX_SIZE:] + for entry in self._primitive_set.primitive_from_identifier(prefix): + try: + return entry.primitive.decrypt_deterministically(ciphertext_no_prefix, + associated_data) + except tink_error.TinkError as e: + logging.info( + 'ciphertext prefix matches a key, but cannot decrypt: %s', e) + # Let's try all RAW keys. + for entry in self._primitive_set.raw_primitives(): + try: + return entry.primitive.decrypt_deterministically(ciphertext, + associated_data) + except tink_error.TinkError as e: + pass + # nothing works. + raise tink_error.TinkError('Decryption failed.') + + +class DeterministicAeadWrapper( + primitive_wrapper.PrimitiveWrapper[deterministic_aead.DeterministicAead]): + """DeterministicAeadWrapper is a PrimitiveWrapper for DeterministicAead. + + The created primitive works with a keyset (rather than a single key). To + encrypt a plaintext, it uses the primary key in the keyset, and prepends to + the ciphertext a certain prefix associated with the primary key. To decrypt, + the primitive uses the prefix of the ciphertext to efficiently select the + right key in the set. If the keys associated with the prefix do not work, the + primitive tries all keys with OutputPrefixType RAW. + """ + + def wrap(self, pset: primitive_set.PrimitiveSet + ) -> deterministic_aead.DeterministicAead: + return _WrappedDeterministicAead(pset) + + def primitive_class(self) -> Type[deterministic_aead.DeterministicAead]: + return deterministic_aead.DeterministicAead diff --git a/python/daead/deterministic_aead_wrapper_test.py b/python/daead/deterministic_aead_wrapper_test.py new file mode 100644 index 0000000000000000000000000000000000000000..d112d07c664094756dbdae7b31033835c37ffaf4 --- /dev/null +++ b/python/daead/deterministic_aead_wrapper_test.py @@ -0,0 +1,167 @@ +# Copyright 2019 Google LLC. +# +# 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. + +"""Tests for tink.python.aead_wrapper.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import unittest +from tink.proto import tink_pb2 +from tink.python.core import primitive_set +from tink.python.core import tink_error +from tink.python.daead import deterministic_aead +from tink.python.daead import deterministic_aead_wrapper +from tink.python.testing import helper + + +class AeadWrapperTest(googletest.TestCase): + + def new_primitive_key_pair(self, key_id, output_prefix_type): + fake_key = helper.fake_key( + key_id=key_id, output_prefix_type=output_prefix_type) + fake_aead = helper.FakeDeterministicAead( + 'fakeDeterministicAead {}'.format(key_id)) + return fake_aead, fake_key + + def test_encrypt_decrypt(self): + primitive, key = self.new_primitive_key_pair(1234, tink_pb2.TINK) + pset = primitive_set.new_primitive_set(deterministic_aead.DeterministicAead) + entry = pset.add_primitive(primitive, key) + pset.set_primary(entry) + + wrapped_daead = deterministic_aead_wrapper.DeterministicAeadWrapper().wrap( + pset) + + plaintext = b'plaintext' + associated_data = b'associated_data' + ciphertext = wrapped_daead.encrypt_deterministically( + plaintext, associated_data) + self.assertEqual( + wrapped_daead.decrypt_deterministically(ciphertext, associated_data), + plaintext) + + def test_encrypt_decrypt_with_key_rotation(self): + primitive, key = self.new_primitive_key_pair(1234, tink_pb2.TINK) + pset = primitive_set.new_primitive_set(deterministic_aead.DeterministicAead) + entry = pset.add_primitive(primitive, key) + pset.set_primary(entry) + wrapped_daead = deterministic_aead_wrapper.DeterministicAeadWrapper().wrap( + pset) + ciphertext = wrapped_daead.encrypt_deterministically( + b'plaintext', b'associated_data') + + new_primitive, new_key = self.new_primitive_key_pair(5678, tink_pb2.TINK) + new_entry = pset.add_primitive(new_primitive, new_key) + pset.set_primary(new_entry) + new_ciphertext = wrapped_daead.encrypt_deterministically( + b'new_plaintext', b'new_associated_data') + + self.assertEqual( + wrapped_daead.decrypt_deterministically(ciphertext, b'associated_data'), + b'plaintext') + self.assertEqual( + wrapped_daead.decrypt_deterministically(new_ciphertext, + b'new_associated_data'), + b'new_plaintext') + + def test_encrypt_decrypt_with_key_rotation_from_raw(self): + primitive, raw_key = self.new_primitive_key_pair(1234, tink_pb2.RAW) + old_raw_ciphertext = primitive.encrypt_deterministically( + b'plaintext', b'associated_data') + + pset = primitive_set.new_primitive_set(deterministic_aead.DeterministicAead) + pset.add_primitive(primitive, raw_key) + new_primitive, new_key = self.new_primitive_key_pair(5678, tink_pb2.TINK) + new_entry = pset.add_primitive(new_primitive, new_key) + pset.set_primary(new_entry) + wrapped_daead = deterministic_aead_wrapper.DeterministicAeadWrapper().wrap( + pset) + new_ciphertext = wrapped_daead.encrypt_deterministically( + b'new_plaintext', b'new_associated_data') + + self.assertEqual( + wrapped_daead.decrypt_deterministically(old_raw_ciphertext, + b'associated_data'), + b'plaintext') + self.assertEqual( + wrapped_daead.decrypt_deterministically(new_ciphertext, + b'new_associated_data'), + b'new_plaintext') + + def test_encrypt_decrypt_two_raw_keys(self): + primitive1, raw_key1 = self.new_primitive_key_pair(1234, tink_pb2.RAW) + primitive2, raw_key2 = self.new_primitive_key_pair(5678, tink_pb2.RAW) + raw_ciphertext1 = primitive1.encrypt_deterministically( + b'plaintext1', b'associated_data1') + raw_ciphertext2 = primitive2.encrypt_deterministically( + b'plaintext2', b'associated_data2') + + pset = primitive_set.new_primitive_set(deterministic_aead.DeterministicAead) + pset.add_primitive(primitive1, raw_key1) + pset.set_primary(pset.add_primitive(primitive2, raw_key2)) + wrapped_daead = deterministic_aead_wrapper.DeterministicAeadWrapper().wrap( + pset) + + self.assertEqual( + wrapped_daead.decrypt_deterministically(raw_ciphertext1, + b'associated_data1'), + b'plaintext1') + self.assertEqual( + wrapped_daead.decrypt_deterministically(raw_ciphertext2, + b'associated_data2'), + b'plaintext2') + self.assertEqual( + wrapped_daead.decrypt_deterministically( + wrapped_daead.encrypt_deterministically(b'plaintext', + b'associated_data'), + b'associated_data'), b'plaintext') + + def test_decrypt_unknown_ciphertext_fails(self): + unknown_primitive = helper.FakeDeterministicAead( + 'unknownFakeDeterministicAead') + unknown_ciphertext = unknown_primitive.encrypt_deterministically( + b'plaintext', b'associated_data') + + pset = primitive_set.new_primitive_set(deterministic_aead.DeterministicAead) + primitive, raw_key = self.new_primitive_key_pair(1234, tink_pb2.RAW) + new_primitive, new_key = self.new_primitive_key_pair(5678, tink_pb2.TINK) + pset.add_primitive(primitive, raw_key) + new_entry = pset.add_primitive(new_primitive, new_key) + pset.set_primary(new_entry) + wrapped_daead = deterministic_aead_wrapper.DeterministicAeadWrapper().wrap( + pset) + + with self.assertRaisesRegex(tink_error.TinkError, 'Decryption failed'): + wrapped_daead.decrypt_deterministically(unknown_ciphertext, + b'associated_data') + + def test_decrypt_wrong_associated_data_fails(self): + primitive, key = self.new_primitive_key_pair(1234, tink_pb2.TINK) + pset = primitive_set.new_primitive_set(deterministic_aead.DeterministicAead) + entry = pset.add_primitive(primitive, key) + pset.set_primary(entry) + wrapped_daead = deterministic_aead_wrapper.DeterministicAeadWrapper().wrap( + pset) + + ciphertext = wrapped_daead.encrypt_deterministically( + b'plaintext', b'associated_data') + with self.assertRaisesRegex(tink_error.TinkError, 'Decryption failed'): + wrapped_daead.decrypt_deterministically(ciphertext, + b'wrong_associated_data') + + +if __name__ == '__main__': + googletest.main()