From 02ecb1bf0996bd7f360dca7ed8febbd6cccf8bbe Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Fri, 28 Jan 2022 15:09:03 +0000 Subject: [PATCH] Add support for AES GCM streaming --- .gitignore | 1 + docs/index.rst | 1 + docs/streaming.rst | 50 +++++++++++++++++++++++++++ tests/test_aesgcmstream.py | 58 +++++++++++++++++++++++++++++++ wolfcrypt/_build_ffi.py | 25 ++++++++++++++ wolfcrypt/_build_wolfssl.py | 14 +++++--- wolfcrypt/ciphers.py | 68 +++++++++++++++++++++++++++++++++++++ 7 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 docs/streaming.rst create mode 100644 tests/test_aesgcmstream.py diff --git a/.gitignore b/.gitignore index d7781d1..151193b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ # C extensions *.so *.a +wolfcrypt/_ffi.* # Distribution / packaging .Python diff --git a/docs/index.rst b/docs/index.rst index efd3431..4af5ca9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,5 +11,6 @@ Summary digest mac random + streaming .. include:: ../LICENSING.rst diff --git a/docs/streaming.rst b/docs/streaming.rst new file mode 100644 index 0000000..485b79e --- /dev/null +++ b/docs/streaming.rst @@ -0,0 +1,50 @@ +Streaming Encryption Algorithms +=============================== + +.. module:: wolfcrypt.ciphers + +Steaming Encryption Classes +--------------------------- + +Interface +~~~~~~~~~ + +AesGcmStreamEncrypt +~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: AesGcmStreamEncrypt + :members: + :inherited-members: + +**Example:** + +.. doctest:: + + >>> from wolfcrypt.ciphers import AesGcmStreamEncrypt + >>> from binascii import hexlify as b2h + >>> gcm = AesGcmStreamEncrypt(b'fedcba9876543210', b'0123456789abcdef') + >>> buf = gcm.update("hello world") + >>> authTag = gcm.final() + >>> b2h(buf) + b'5ba7d42e1bf01d7998e932' + >>> b2h(authTag) + b'cef91ba0c8c6431c7e19f64c9d9e371b' + +AesGcmStreamDecrypt +~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: AesGcmStreamDecrypt + :members: + :inherited-members: + +**Example:** + +.. doctest:: + + >>> from wolfcrypt.ciphers import AesGcmStreamDecrypt, t2b + >>> from binascii import unhexlify as h2b + >>> gcm = AesGcmStreamDecrypt(b'fedcba9876543210', b'0123456789abcdef') + >>> buf = gcm.update(h2b(b'5ba7d42e1bf01d7998e932')) + >>> gcm.final(h2b(b'cef91ba0c8c6431c7e19f64c9d9e371b')) + >>> t2b(buf) + b'hello world' diff --git a/tests/test_aesgcmstream.py b/tests/test_aesgcmstream.py new file mode 100644 index 0000000..492bf54 --- /dev/null +++ b/tests/test_aesgcmstream.py @@ -0,0 +1,58 @@ +# test_hashes.py +# +# Copyright (C) 2006-2022 wolfSSL Inc. +# +# This file is part of wolfSSL. (formerly known as CyaSSL) +# +# wolfSSL is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# wolfSSL is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +# pylint: disable=redefined-outer-name + +from collections import namedtuple +import pytest +from wolfcrypt._ffi import ffi as _ffi +from wolfcrypt._ffi import lib as _lib +from wolfcrypt.utils import t2b +from binascii import hexlify as b2h, unhexlify as h2b + +from wolfcrypt.ciphers import AesGcmStreamEncrypt, AesGcmStreamDecrypt + +def test_encrypt(): + key = "fedcba9876543210" + iv = "0123456789abcdef" + gcm = AesGcmStreamEncrypt(key, iv) + buf = gcm.update("hello world") + authTag = gcm.final() + assert b2h(authTag) == bytes('cef91ba0c8c6431c7e19f64c9d9e371b', 'utf-8') + assert b2h(buf) == bytes('5ba7d42e1bf01d7998e932', "utf-8") + gcmdec = AesGcmStreamDecrypt(key, iv) + bufdec = gcmdec.update(buf) + gcmdec.final(authTag) + assert bufdec == t2b("hello world") + +def test_multipart(): + key = "fedcba9876543210" + iv = "0123456789abcdef" + gcm = AesGcmStreamEncrypt(key, iv) + buf = gcm.update("hello") + buf += gcm.update(" world") + authTag = gcm.final() + assert b2h(authTag) == bytes('6862647a27c7b6aa0a6882b3e117e944', 'utf-8') + assert b2h(buf) == bytes('5ba7d42e1bf01d7998e932', "utf-8") + gcmdec = AesGcmStreamDecrypt(key, iv) + bufdec = gcmdec.update(buf[:5]) + bufdec += gcmdec.update(buf[5:]) + gcmdec.final(authTag) + assert bufdec == t2b("hello world") diff --git a/wolfcrypt/_build_ffi.py b/wolfcrypt/_build_ffi.py index 18118fd..92dcb94 100644 --- a/wolfcrypt/_build_ffi.py +++ b/wolfcrypt/_build_ffi.py @@ -102,6 +102,7 @@ FIPS_VERSION = 0 ERROR_STRINGS_ENABLED = 1 ASN_ENABLED = 1 WC_RNG_SEED_CB_ENABLED = 0 +AESGCM_STREAM = 1 # detect native features based on options.h defines if featureDetection: @@ -126,6 +127,7 @@ if featureDetection: ERROR_STRINGS_ENABLED = 0 if '#define NO_ERROR_STRINGS' in optionsHeaderStr else 1 ASN_ENABLED = 0 if '#define NO_ASN' in optionsHeaderStr else 1 WC_RNG_SEED_CB_ENABLED = 1 if '#define WC_RNG_SEED_CB' in optionsHeaderStr else 0 + AESGCM_STREAM = 1 if '#define WOLFSSL_AESGCM_STREAM' in optionsHeaderStr else 0 if '#define HAVE_FIPS' in optionsHeaderStr: FIPS_ENABLED = 1 @@ -202,6 +204,7 @@ extern "C" { int FIPS_VERSION = """ + str(FIPS_VERSION) + """; int ASN_ENABLED = """ + str(ASN_ENABLED) + """; int WC_RNG_SEED_CB_ENABLED = """ + str(WC_RNG_SEED_CB_ENABLED) + """; + int AESGCM_STREAM = """ + str(AESGCM_STREAM) + """; """, include_dirs=[wolfssl_inc_path()], library_dirs=[wolfssl_lib_path()], @@ -231,6 +234,7 @@ _cdef = """ extern int FIPS_VERSION; extern int ASN_ENABLED; extern int WC_RNG_SEED_CB_ENABLED; + extern int AESGCM_STREAM; typedef unsigned char byte; typedef unsigned int word32; @@ -322,6 +326,27 @@ if AES_ENABLED: int wc_AesCbcDecrypt(Aes*, byte*, const byte*, word32); """ +if AES_ENABLED and AESGCM_STREAM: + _cdef += """ + int wc_AesInit(Aes* aes, void* heap, int devId); + int wc_AesGcmInit(Aes* aes, const byte* key, word32 len, + const byte* iv, word32 ivSz); + int wc_AesGcmEncryptInit(Aes* aes, const byte* key, word32 len, + const byte* iv, word32 ivSz); + int wc_AesGcmEncryptInit_ex(Aes* aes, const byte* key, word32 len, + byte* ivOut, word32 ivOutSz); + int wc_AesGcmEncryptUpdate(Aes* aes, byte* out, const byte* in, + word32 sz, const byte* authIn, word32 authInSz); + int wc_AesGcmEncryptFinal(Aes* aes, byte* authTag, + word32 authTagSz); + int wc_AesGcmDecryptInit(Aes* aes, const byte* key, word32 len, + const byte* iv, word32 ivSz); + int wc_AesGcmDecryptUpdate(Aes* aes, byte* out, const byte* in, + word32 sz, const byte* authIn, word32 authInSz); + int wc_AesGcmDecryptFinal(Aes* aes, const byte* authTag, + word32 authTagSz); + """ + if CHACHA_ENABLED: _cdef += """ typedef struct { ...; } ChaCha; diff --git a/wolfcrypt/_build_wolfssl.py b/wolfcrypt/_build_wolfssl.py index a9ca49c..7977ff3 100644 --- a/wolfcrypt/_build_wolfssl.py +++ b/wolfcrypt/_build_wolfssl.py @@ -142,7 +142,7 @@ def make_flags(prefix): flags.append("-DWOLFSSL_AES=yes") flags.append("-DWOLFSSL_DES3=yes") flags.append("-DWOLFSSL_CHACHA=yes") - flags.append("-DWOLFSSL_AESGCM=no") + flags.append("-DWOLFSSL_AESGCM=yes") flags.append("-DWOLFSSL_SHA=yes") flags.append("-DWOLFSSL_SHA384=yes") flags.append("-DWOLFSSL_SHA512=yes") @@ -164,7 +164,7 @@ def make_flags(prefix): flags.append("-DWOLFSSL_EXTENDED_MASTER=no") flags.append("-DWOLFSSL_ERROR_STRINGS=no") # Part of hack for missing CMake option - flags.append("-DCMAKE_C_FLAGS=\"/DWOLFSSL_KEY_GEN=1 /DWOLFCRYPT_ONLY=1\"") + flags.append("-DCMAKE_C_FLAGS=\"/DWOLFSSL_KEY_GEN=1 /DWOLFCRYPT_ONLY=1 /DWOLFSSL_AESGCM_STREAM=1\"") return " ".join(flags) else: @@ -186,7 +186,9 @@ def make_flags(prefix): flags.append("--enable-des3") flags.append("--enable-chacha") - flags.append("--disable-aesgcm") + flags.append("--enable-aesgcm-stream") + + flags.append("--enable-aesgcm") # hashes and MACs flags.append("--enable-sha") @@ -232,6 +234,8 @@ def cmake_hack(): contents.insert(27, "#define WOLFSSL_KEY_GEN\n") contents.insert(28, "#undef WOLFCRYPT_ONLY\n") contents.insert(29, "#define WOLFCRYPT_ONLY\n") + contents.insert(30, "#undef WOLFSSL_AESGCM_STREAM\n") + contents.insert(31, "#define WOLFSSL_AESGCM_STREAM\n") with open(options_file, "w") as f: contents = "".join(contents) @@ -272,9 +276,9 @@ def build_wolfssl(version="master"): else: libfile = os.path.join(prefix, 'lib/libwolfssl.la') - rebuild = ensure_wolfssl_src(version) + ensure_wolfssl_src(version) - if rebuild or not os.path.isfile(libfile): + if not os.path.isfile(libfile): make(make_flags(prefix)) if __name__ == "__main__": diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index d97ced1..9075bc7 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -230,6 +230,74 @@ if _lib.AES_ENABLED: return _lib.wc_AesCbcDecrypt(self._dec, destination, source, len(source)) +if _lib.AESGCM_STREAM: + class _AesGcmStream(object): + """ + AES GCM Stream + """ + block_size = 16 + _key_sizes = [16, 24, 32] + _native_type = "Aes *" + + def __init__(self, key, IV): + key = t2b(key) + IV = t2b(IV) + if len(key) not in self._key_sizes: + raise ValueError("key must be %s in length, not %d" % + (self._key_sizes, len(key))) + self._native_object = _ffi.new(self._native_type) + _lib.wc_AesInit(self._native_object, _ffi.NULL, -2) + self._authIn = _ffi.new("byte[%d]" % self.block_size) + ret = _lib.wc_AesGcmInit(self._native_object, key, len(key), IV, len(IV)) + if ret < 0: + raise WolfCryptError("Init error (%d)" % ret) + + def update(self, data): + """ + Updates the stream with another segment of data. + """ + ret = 0 + data = t2b(data) + self._buf = _ffi.new("byte[%d]" % (len(data))) + ret = self._update(data) + if ret < 0: + raise WolfCryptError("Decryption error (%d)" % ret) + return bytes(self._buf) + + class AesGcmStreamEncrypt(_AesGcmStream): + """ + AES GCM Streaming Encryption + """ + def _update(self, data): + return _lib.wc_AesGcmEncryptUpdate(self._native_object, self._buf, data, len(data), self._authIn, self.block_size) + + def final(self): + """ + Finalize the stream and return an authentication tag for the stream. + """ + authTag = _ffi.new("byte[%d]" % self.block_size) + ret = _lib.wc_AesGcmEncryptFinal(self._native_object, authTag, self.block_size) + if ret < 0: + raise WolfCryptError("Encryption error (%d)" % ret) + return _ffi.buffer(authTag)[:] + + + class AesGcmStreamDecrypt(_AesGcmStream): + """ + AES GCM Streaming Decryption + """ + def _update(self, data): + return _lib.wc_AesGcmDecryptUpdate(self._native_object, self._buf, data, len(data), self._authIn, self.block_size) + + def final(self, authTag): + """ + Finalize the stream and verify using the provided authentication tag. + """ + authTag = t2b(authTag) + ret = _lib.wc_AesGcmDecryptFinal(self._native_object, authTag, self.block_size) + if ret < 0: + raise WolfCryptError("Decryption error (%d)" % ret) + if _lib.CHACHA_ENABLED: class ChaCha(_Cipher): """