aes: add missing length check for multi-chunk encrypt/decrypt, with tests
What changed, and why it matters
This commit fixes a missing safety check in libwally-core's AES encryption/decryption function. Previously, callers could pass an output buffer smaller than the input data, which could lead to writing past the end of the buffer (a buffer overflow). The patch now rejects such calls and adds tests for many invalid input combinations. The issue was reported by an outside security researcher.
Treat as a security fix and include in release notes. Users should upgrade to a version containing this commit. Developers using libwally's AES API should ensure output buffers are at least as large as input data and block-aligned.
Security signals we found
Missing length check enabling potential buffer overflow
Out-of-bounds write risk in symmetric crypto routine
Reported by independent external researcher
Patch adds explicit output-size validation
New negative test cases for invalid arguments
Evidence from the diff
The change modifies wally_aes() in src/aes.c. It replaces a local argument validation helper with wally_aes_len() and adds an explicit len < bytes_len check. Before this patch, the function did not verify that the caller-supplied output length was at least as large as the input length. Because AES-ECB encryption/decrypts in fixed-size blocks and writes one output block per input block, a smaller output buffer could result in an out-of-bounds write. The commit also expands Python tests to cover null pointers, invalid key lengths, non-block-aligned input, conflicting/unknown flags, and short output buffers.
Changed components
src/aes.c:wally_aes()AES-ECB encryption/decryption pathPython test suite for AESInspect captured patch +35 / −12
### src/aes.c
@@ -92,9 +92,8 @@ int wally_aes(const unsigned char *key, size_t key_len,
AES256_ctx ctx;
size_t written;
- if (!are_valid_args(key, key_len, bytes, bytes_len, flags, &written) ||
- len % AES_BLOCK_LEN || !bytes_len || bytes_len % AES_BLOCK_LEN ||
- !bytes_out || !len)
+ if (wally_aes_len(key, key_len, bytes, bytes_len, flags, &written) != WALLY_OK ||
+ !bytes_out || !len || len < bytes_len)
return WALLY_EINVAL;
if (flags & AES_FLAG_ENCRYPT)
### src/test/test_aes.py
@@ -51,9 +51,10 @@
"23304b7a39f9f3ff067d8d8f9e24ecc7" ],
]
-class AESTests(unittest.TestCase):
+ENCRYPT, DECRYPT = 1, 2
+
- ENCRYPT, DECRYPT = 1, 2
+class AESTests(unittest.TestCase):
def test_aes(self):
@@ -62,14 +63,37 @@ def test_aes(self):
key_bytes = { 128: 16, 192: 24, 256: 32}[c[0]]
self.assertEqual(len(key), key_bytes)
- for p, f, o in [(plain, self.ENCRYPT, cypher),
- (cypher, self.DECRYPT, plain)]:
+ for p, f, o in [(plain, ENCRYPT, cypher),
+ (cypher, DECRYPT, plain)]:
out_buf, out_len = make_cbuffer('00' * len(o))
ret = wally_aes(key, len(key), p, len(p), f, out_buf, out_len)
- self.assertEqual(ret, 0)
+ self.assertEqual(ret, WALLY_OK)
self.assertEqual(h(out_buf), h(o))
+ # Invalid args
+ key = make_cbuffer('2b7e151628aed2a6abf7158809cf4f3c')[0]
+ plain = make_cbuffer('ae2d8a571e03ac9c9eb76fac45af8e51ae2d8a571e03ac9c9eb76fac45af8e51')[0]
+ out, out_len = make_cbuffer('00' * len(plain))
+ invalid_cases = [
+ (None, len(key), plain, len(plain), ENCRYPT, out, out_len), # NULL key
+ (key, 0, plain, len(plain), ENCRYPT, out, out_len), # Empty key
+ (key, 15, plain, len(plain), ENCRYPT, out, out_len), # Invalid key len
+ (key, len(key), None, len(plain), ENCRYPT, out, out_len), # NULL plaintext
+ (key, len(key), plain, 0, ENCRYPT, out, out_len), # Empty plaintext
+ (key, len(key), plain, 15, ENCRYPT, out, out_len), # Non-blocksize plaintext
+ (key, len(key), plain, len(plain), 0, out, out_len), # No flags
+ (key, len(key), plain, len(plain), ENCRYPT | \
+ DECRYPT, out, out_len), # Conflicting flags
+ (key, len(key), plain, len(plain), 4, out, out_len), # Unknown flags
+ (key, len(key), plain, len(plain), ENCRYPT, None, out_len), # NULL output
+ (key, len(key), plain, len(plain), ENCRYPT, out, 0), # Empty output
+ (key, len(key), plain, len(plain), ENCRYPT, out, 16), # Too short output
+ ]
+ for c in invalid_cases:
+ ret = wally_aes(*c)
+ self.assertEqual(ret, WALLY_EINVAL)
+
def get_cbc_cases(self):
lines = []
@@ -81,12 +105,11 @@ def get_cbc_cases(self):
def test_aes_cbc(self):
out_buf, out_len = make_cbuffer('00' * 80)
- E, D = self.ENCRYPT, self.DECRYPT
# Encryption/decryption cases
for c in self.get_cbc_cases():
plain, key, iv, cypher = [make_cbuffer(s)[0] for s in c]
- for p, f, o in [(plain, E, cypher), (cypher, D, plain)]:
+ for p, f, o in [(plain, ENCRYPT, cypher), (cypher, DECRYPT, plain)]:
ret, written = wally_aes_cbc(key, len(key), iv, len(iv),
p or None, len(p), f, out_buf, out_len)
self.assertEqual((ret, written), (0, len(o)))
@@ -100,10 +123,11 @@ def test_aes_cbc(self):
# number of bytes required.
ret, max_len = wally_aes_cbc_get_maximum_length(key, len(key), iv, len(iv),
p or None, len(p), f)
- self.assertEqual(ret, 0)
+ self.assertEqual(ret, WALLY_OK)
self.assertTrue(max_len >= written and max_len % 16 == 0)
# Invalid args
+ D = DECRYPT
invalid_cases = [
# NULL key
(None, len(key), iv, len(iv), cypher, len(cypher), D, out_buf, out_len),
@@ -130,7 +154,6 @@ def test_aes_cbc(self):
self.assertEqual((ret, written), (WALLY_EINVAL, 0))
def test_aes_cbc_with_ecdh_key(self):
- ENCRYPT, DECRYPT, _ = 1, 2, True
a_priv = make_cbuffer('1c6a837d1ac663fdc7f1002327ca38452766eaf4fe3b80ce620bf7cd3f584cf6')[0]
a_pub = make_cbuffer('03e581be89d1ef8ce11d60746d08e4f8aedf934d1d861dd436042ee2e3b16db918')[0]
b_priv = make_cbuffer('0b6b3dc90d203d854100110788ac87d43aa00620c9cdb361b281b09022ef4b53')[0]
@@ -148,6 +171,7 @@ def test_aes_cbc_with_ecdh_key(self):
self.assertEqual(ret, WALLY_OK) # Make sure good args work
encrypted = make_cbuffer(buf[:written].hex())[0]
+ _ = True
invalid_cases = [
(None, _, _, _, _, _, _, _, _, _, _, _, _), # NULL privkey
(_, 0, _, _, _, _, _, _, _, _, _, _, _), # Empty privkeyWhy this scored 47/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.