Skip to content

Commit e34a0ec

Browse files
LinuxJedidanielinux
authored andcommitted
Completely refactor AES GCM
Some bad assumptions were made during the creation of our Python AES GCM code. This is now modified to be more in-line with other libraries. This is an API breaking change on unreleased code. This now allows for aad data to be used, varying length of authentication tags and fixes a bug for multipart. 1. Now unified to a single class AesGcmStream() 2. Used `encrypt()` and `decrypt()` instead of `update()` to avoid confusion over encryption and aad semantics 3. final tag_bytes is configurable in the constructor 4. `set_aad()` added to add the aad data 5. aad data is cleared after first `encrypt()` or `decrypt()` call due to quirk in the C API. 6. More tests added
1 parent 969681a commit e34a0ec

File tree

3 files changed

+148
-70
lines changed

3 files changed

+148
-70
lines changed

docs/streaming.rst

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ Steaming Encryption Classes
99
Interface
1010
~~~~~~~~~
1111

12-
AesGcmStreamEncrypt
13-
~~~~~~~~~~~~~~~~~~~
12+
AesGcmStream
13+
~~~~~~~~~~~~
1414

15-
.. autoclass:: AesGcmStreamEncrypt
15+
.. autoclass:: AesGcmStream
1616
:members:
1717
:inherited-members:
1818

@@ -22,29 +22,15 @@ AesGcmStreamEncrypt
2222

2323
>>> from wolfcrypt.ciphers import AesGcmStreamEncrypt
2424
>>> from binascii import hexlify as b2h
25-
>>> gcm = AesGcmStreamEncrypt(b'fedcba9876543210', b'0123456789abcdef')
26-
>>> buf = gcm.update("hello world")
25+
>>> gcm = AesGcmStream(b'fedcba9876543210', b'0123456789abcdef')
26+
>>> buf = gcm.encrypt("hello world")
2727
>>> authTag = gcm.final()
2828
>>> b2h(buf)
2929
b'5ba7d42e1bf01d7998e932'
3030
>>> b2h(authTag)
31-
b'cef91ba0c8c6431c7e19f64c9d9e371b'
32-
33-
AesGcmStreamDecrypt
34-
~~~~~~~~~~~~~~~~~~~
35-
36-
.. autoclass:: AesGcmStreamDecrypt
37-
:members:
38-
:inherited-members:
39-
40-
**Example:**
41-
42-
.. doctest::
43-
44-
>>> from wolfcrypt.ciphers import AesGcmStreamDecrypt, t2b
45-
>>> from binascii import unhexlify as h2b
46-
>>> gcm = AesGcmStreamDecrypt(b'fedcba9876543210', b'0123456789abcdef')
47-
>>> buf = gcm.update(h2b(b'5ba7d42e1bf01d7998e932'))
48-
>>> gcm.final(h2b(b'cef91ba0c8c6431c7e19f64c9d9e371b'))
31+
b'8f85338aa0b13f48f8b17482dbb8acca'
32+
>>> gcm = AesGcmStream(b'fedcba9876543210', b'0123456789abcdef')
33+
>>> buf = gcm.decrypt(h2b(b'5ba7d42e1bf01d7998e932'))
34+
>>> gcm.final(h2b(b'8f85338aa0b13f48f8b17482dbb8acca'))
4935
>>> t2b(buf)
5036
b'hello world'

tests/test_aesgcmstream.py

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,101 @@
2525
from wolfcrypt._ffi import ffi as _ffi
2626
from wolfcrypt._ffi import lib as _lib
2727
from wolfcrypt.utils import t2b
28+
from wolfcrypt.exceptions import WolfCryptError
2829
from binascii import hexlify as b2h, unhexlify as h2b
2930

30-
from wolfcrypt.ciphers import AesGcmStreamEncrypt, AesGcmStreamDecrypt
31+
from wolfcrypt.ciphers import AesGcmStream
3132

3233
def test_encrypt():
3334
key = "fedcba9876543210"
3435
iv = "0123456789abcdef"
35-
gcm = AesGcmStreamEncrypt(key, iv)
36-
buf = gcm.update("hello world")
36+
gcm = AesGcmStream(key, iv)
37+
buf = gcm.encrypt("hello world")
3738
authTag = gcm.final()
38-
assert b2h(authTag) == bytes('cef91ba0c8c6431c7e19f64c9d9e371b', 'utf-8')
39+
assert b2h(authTag) == bytes('ac8fcee96dc6ef8e5236da19b6197d2e', 'utf-8')
3940
assert b2h(buf) == bytes('5ba7d42e1bf01d7998e932', "utf-8")
40-
gcmdec = AesGcmStreamDecrypt(key, iv)
41-
bufdec = gcmdec.update(buf)
41+
gcmdec = AesGcmStream(key, iv)
42+
bufdec = gcmdec.decrypt(buf)
43+
gcmdec.final(authTag)
44+
assert bufdec == t2b("hello world")
45+
46+
def test_encrypt_short_tag():
47+
key = "fedcba9876543210"
48+
iv = "0123456789abcdef"
49+
gcm = AesGcmStream(key, iv, 12)
50+
buf = gcm.encrypt("hello world")
51+
authTag = gcm.final()
52+
assert b2h(authTag) == bytes('ac8fcee96dc6ef8e5236da19', 'utf-8')
53+
assert b2h(buf) == bytes('5ba7d42e1bf01d7998e932', "utf-8")
54+
gcmdec = AesGcmStream(key, iv)
55+
bufdec = gcmdec.decrypt(buf)
4256
gcmdec.final(authTag)
4357
assert bufdec == t2b("hello world")
4458

4559
def test_multipart():
4660
key = "fedcba9876543210"
4761
iv = "0123456789abcdef"
48-
gcm = AesGcmStreamEncrypt(key, iv)
49-
buf = gcm.update("hello")
50-
buf += gcm.update(" world")
62+
gcm = AesGcmStream(key, iv)
63+
buf = gcm.encrypt("hello")
64+
buf += gcm.encrypt(" world")
65+
authTag = gcm.final()
66+
assert b2h(authTag) == bytes('ac8fcee96dc6ef8e5236da19b6197d2e', 'utf-8')
67+
assert b2h(buf) == bytes('5ba7d42e1bf01d7998e932', "utf-8")
68+
gcmdec = AesGcmStream(key, iv)
69+
bufdec = gcmdec.decrypt(buf[:5])
70+
bufdec += gcmdec.decrypt(buf[5:])
71+
gcmdec.final(authTag)
72+
assert bufdec == t2b("hello world")
73+
74+
def test_encrypt_aad():
75+
key = "fedcba9876543210"
76+
iv = "0123456789abcdef"
77+
aad = "aad data"
78+
gcm = AesGcmStream(key, iv)
79+
gcm.set_aad(aad)
80+
buf = gcm.encrypt("hello world")
5181
authTag = gcm.final()
52-
assert b2h(authTag) == bytes('6862647a27c7b6aa0a6882b3e117e944', 'utf-8')
82+
print(b2h(authTag))
83+
assert b2h(authTag) == bytes('8f85338aa0b13f48f8b17482dbb8acca', 'utf-8')
5384
assert b2h(buf) == bytes('5ba7d42e1bf01d7998e932', "utf-8")
54-
gcmdec = AesGcmStreamDecrypt(key, iv)
55-
bufdec = gcmdec.update(buf[:5])
56-
bufdec += gcmdec.update(buf[5:])
85+
gcmdec = AesGcmStream(key, iv)
86+
gcmdec.set_aad(aad)
87+
bufdec = gcmdec.decrypt(buf)
5788
gcmdec.final(authTag)
5889
assert bufdec == t2b("hello world")
90+
91+
def test_multipart_aad():
92+
key = "fedcba9876543210"
93+
iv = "0123456789abcdef"
94+
aad = "aad data"
95+
gcm = AesGcmStream(key, iv)
96+
gcm.set_aad(aad)
97+
buf = gcm.encrypt("hello")
98+
buf += gcm.encrypt(" world")
99+
authTag = gcm.final()
100+
assert b2h(authTag) == bytes('8f85338aa0b13f48f8b17482dbb8acca', 'utf-8')
101+
assert b2h(buf) == bytes('5ba7d42e1bf01d7998e932', "utf-8")
102+
gcmdec = AesGcmStream(key, iv)
103+
gcmdec.set_aad(aad)
104+
bufdec = gcmdec.decrypt(buf[:5])
105+
bufdec += gcmdec.decrypt(buf[5:])
106+
gcmdec.final(authTag)
107+
assert bufdec == t2b("hello world")
108+
109+
def test_encrypt_aad_bad():
110+
key = "fedcba9876543210"
111+
iv = "0123456789abcdef"
112+
aad = "aad data"
113+
aad_bad = "bad data"
114+
gcm = AesGcmStream(key, iv)
115+
gcm.set_aad(aad)
116+
buf = gcm.encrypt("hello world")
117+
authTag = gcm.final()
118+
print(b2h(authTag))
119+
assert b2h(authTag) == bytes('8f85338aa0b13f48f8b17482dbb8acca', 'utf-8')
120+
assert b2h(buf) == bytes('5ba7d42e1bf01d7998e932', "utf-8")
121+
gcmdec = AesGcmStream(key, iv)
122+
gcmdec.set_aad(aad_bad)
123+
gcmdec.decrypt(buf)
124+
with pytest.raises(WolfCryptError):
125+
gcmdec.final(authTag)

wolfcrypt/ciphers.py

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -273,72 +273,97 @@ def _decrypt(self, destination, source):
273273
raise ValueError("Invalid mode associated to cipher")
274274

275275
if _lib.AESGCM_STREAM:
276-
class _AesGcmStream(object):
276+
class AesGcmStream(object):
277277
"""
278278
AES GCM Stream
279279
"""
280280
block_size = 16
281281
_key_sizes = [16, 24, 32]
282282
_native_type = "Aes *"
283+
_aad = bytes()
284+
_tag_bytes = 16
285+
_mode = None
283286

284-
def __init__(self, key, IV):
287+
def __init__(self, key, IV, tag_bytes=16):
288+
"""
289+
tag_bytes is the number of bytes to use for the authentication tag during encryption
290+
"""
285291
key = t2b(key)
286292
IV = t2b(IV)
293+
self._tag_bytes = tag_bytes
287294
if len(key) not in self._key_sizes:
288295
raise ValueError("key must be %s in length, not %d" %
289296
(self._key_sizes, len(key)))
290297
self._native_object = _ffi.new(self._native_type)
291298
_lib.wc_AesInit(self._native_object, _ffi.NULL, -2)
292-
self._authIn = _ffi.new("byte[%d]" % self.block_size)
293299
ret = _lib.wc_AesGcmInit(self._native_object, key, len(key), IV, len(IV))
294300
if ret < 0:
295301
raise WolfCryptError("Init error (%d)" % ret)
296302

297-
def update(self, data):
303+
def set_aad(self, data):
304+
"""
305+
Set the additional authentication data for the stream
306+
"""
307+
if self._mode is not None:
308+
raise WolfCryptError("AAD can only be set before encrypt() or decrypt() is called")
309+
self._aad = t2b(data)
310+
311+
def encrypt(self, data):
298312
"""
299-
Updates the stream with another segment of data.
313+
Add more data to the encryption stream
300314
"""
301-
ret = 0
302315
data = t2b(data)
316+
if self._mode is None:
317+
self._mode = _ENCRYPTION
318+
elif self._mode == _DECRYPTION:
319+
raise WolfCryptError("Class instance already in use for decryption")
303320
self._buf = _ffi.new("byte[%d]" % (len(data)))
304-
ret = self._update(data)
321+
ret = _lib.wc_AesGcmEncryptUpdate(self._native_object, self._buf, data, len(data), self._aad, len(self._aad))
305322
if ret < 0:
306323
raise WolfCryptError("Decryption error (%d)" % ret)
324+
# Reset aad after first packet
325+
self._aad = bytes()
307326
return bytes(self._buf)
308327

309-
class AesGcmStreamEncrypt(_AesGcmStream):
310-
"""
311-
AES GCM Streaming Encryption
312-
"""
313-
def _update(self, data):
314-
return _lib.wc_AesGcmEncryptUpdate(self._native_object, self._buf, data, len(data), self._authIn, self.block_size)
315-
316-
def final(self):
328+
def decrypt(self, data):
317329
"""
318-
Finalize the stream and return an authentication tag for the stream.
330+
Add more data to the decryption stream
319331
"""
320-
authTag = _ffi.new("byte[%d]" % self.block_size)
321-
ret = _lib.wc_AesGcmEncryptFinal(self._native_object, authTag, self.block_size)
332+
data = t2b(data)
333+
if self._mode is None:
334+
self._mode = _DECRYPTION
335+
elif self._mode == _ENCRYPTION:
336+
raise WolfCryptError("Class instance already in use for decryption")
337+
self._buf = _ffi.new("byte[%d]" % (len(data)))
338+
ret = _lib.wc_AesGcmDecryptUpdate(self._native_object, self._buf, data, len(data), self._aad, len(self._aad))
339+
# Reset after first packet
340+
self._aad = bytes()
322341
if ret < 0:
323-
raise WolfCryptError("Encryption error (%d)" % ret)
324-
return _ffi.buffer(authTag)[:]
325-
326-
327-
class AesGcmStreamDecrypt(_AesGcmStream):
328-
"""
329-
AES GCM Streaming Decryption
330-
"""
331-
def _update(self, data):
332-
return _lib.wc_AesGcmDecryptUpdate(self._native_object, self._buf, data, len(data), self._authIn, self.block_size)
342+
raise WolfCryptError("Decryption error (%d)" % ret)
343+
return bytes(self._buf)
333344

334-
def final(self, authTag):
345+
def final(self, authTag=None):
335346
"""
336-
Finalize the stream and verify using the provided authentication tag.
347+
When encrypting, finalize the stream and return an authentication tag for the stream.
348+
When decrypting, verify the authentication tag for the stream.
349+
The authTag parameter is only used for decrypting.
337350
"""
338-
authTag = t2b(authTag)
339-
ret = _lib.wc_AesGcmDecryptFinal(self._native_object, authTag, self.block_size)
340-
if ret < 0:
341-
raise WolfCryptError("Decryption error (%d)" % ret)
351+
if self._mode is None:
352+
raise WolfCryptError("Final called with no encryption or decryption")
353+
elif self._mode == _ENCRYPTION:
354+
authTag = _ffi.new("byte[%d]" % self._tag_bytes)
355+
ret = _lib.wc_AesGcmEncryptFinal(self._native_object, authTag, self._tag_bytes)
356+
if ret < 0:
357+
raise WolfCryptError("Encryption error (%d)" % ret)
358+
return _ffi.buffer(authTag)[:]
359+
else:
360+
if authTag is None:
361+
raise WolfCryptError("authTag parameter required")
362+
authTag = t2b(authTag)
363+
ret = _lib.wc_AesGcmDecryptFinal(self._native_object, authTag, len(authTag))
364+
if ret < 0:
365+
raise WolfCryptError("Decryption error (%d)" % ret)
366+
342367

343368
if _lib.CHACHA_ENABLED:
344369
class ChaCha(_Cipher):

0 commit comments

Comments
 (0)