Merge pull request #10885 from SomberNight/202608_crypto_sanity_checks
What changed, and why it matters
This commit adds safety checks to Electrum's encryption helper functions and includes a self-test that runs when the program starts. The new checks make sure AES keys and initialization vectors (IVs) are the correct length before encrypting or decrypting, which helps catch programming mistakes early. The self-tests verify that hash and encryption functions produce known correct outputs, so Electrum will fail immediately if a cryptographic library is broken or misconfigured rather than silently producing bad results.
No immediate action required. Reviewers should confirm the new assertions execute at application startup as intended and that the known-answer test vectors are correct. Consider whether the asserts should be converted to explicit exceptions for clearer error handling, but this is a style/reliability choice, not a security emergency.
Security signals we found
Defensive input validation added to AES encryption/decryption helpers
Runtime self-tests for critical cryptographic primitives
Early-fail behavior on cryptographic backend misbehavior
No bug fix, CVE reference, or exploit path described in commit
Evidence from the diff
The patch modifies electrum/crypto.py to add length assertions in aes_encrypt_with_iv and aes_decrypt_with_iv: keys must be 16 or 32 bytes and IVs must be 16 bytes. It also adds a module-level runtime sanity-check block using assert statements to verify SHA-256, SHA-512, RIPEMD-160, AES-128-CBC, AES-256-CBC, ChaCha20, and ChaCha20-Poly1305 against known test vectors. These assertions execute at import time, causing the application to crash early if a backend is broken or a primitive is misimplemented. The change is defensive hardening, not a fix for a known exploitable vulnerability.
Changed components
electrum/crypto.pyAES-CBC encryption/decryption helpersModule-level cryptographic primitive self-testsInspect captured patch +65 / −0
### electrum/crypto.py
@@ -136,6 +136,8 @@ def strip_PKCS7_padding(data: bytes) -> bytes:
def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
assert_bytes(key, iv, data)
+ assert len(key) in (16, 32), f"unexpected key size: {len(key)} (expected: 16 or 32)"
+ assert len(iv) == 16, f"unexpected iv size: {len(iv)} (expected: 16)"
data = append_PKCS7_padding(data)
if HAS_CRYPTODOME:
e = CD_AES.new(key, CD_AES.MODE_CBC, iv).encrypt(data)
@@ -154,6 +156,8 @@ def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
assert_bytes(key, iv, data)
+ assert len(key) in (16, 32), f"unexpected key size: {len(key)} (expected: 16 or 32)"
+ assert len(iv) == 16, f"unexpected iv size: {len(iv)} (expected: 16)"
if HAS_CRYPTODOME:
cipher = CD_AES.new(key, CD_AES.MODE_CBC, iv)
data = cipher.decrypt(data)
@@ -500,3 +504,64 @@ def get_ecdh(priv: bytes, pub: bytes) -> bytes:
def privkey_to_pubkey(priv: bytes) -> bytes:
return ecc.ECPrivkey(priv[:32]).get_public_key_bytes()
+
+
+########################################
+# Run-time sanity checks.
+# - If one of the important cryptographic primitives is broken, we better panic.
+# - For several primitives we support multiple backends. By now at runtime,
+# we have already selected the backend: check if it really works and fail early if not.
+# - This whole section takes 5-10 msec.
+#
+# Hash functions. SHA2. single backend: hashlib
+assert sha256(b"satoshi_nakamoto").hex() == "5f94a8490efe9e06c590dd34e37b5ab8f482f1af7578c6a41542761938a42426"
+assert hashlib.sha512(b"satoshi_nakamoto").digest().hex() \
+ == "cae681a7f07bd26128f8536c59a38f10c9e648898426cd1dd94144c7d3ea0512186400edc5d38ac677d43e97ebb877bf76d69c44de1f6e074435adc79caf8f14"
+# Hash functions. ripemd. two supported backends: hashlib, ripemd.py
+assert ripemd(b"satoshi_nakamoto").hex() == "a9a1c16007c20fa031f97f712c5eabd6f2ec54c4"
+# AES-128: three supported backends: pycryptodomex, cryptography, pyaes
+assert aes_encrypt_with_iv(
+ key=b"satoshi_nakamoto",
+ iv=b"thetimes20090103",
+ data=b"The quick brown fox jumps over the lazy dog").hex() \
+ == "9128466a087892f5f945ca48fe8c4b1d34e126d19fb0c50ce7f127a19508146734152f38d65377cd0add2599042a55e2"
+assert aes_decrypt_with_iv(
+ key=b"satoshi_nakamoto",
+ iv=b"thetimes20090103",
+ data=bytes.fromhex("9128466a087892f5f945ca48fe8c4b1d34e126d19fb0c50ce7f127a19508146734152f38d65377cd0add2599042a55e2")) \
+ == b"The quick brown fox jumps over the lazy dog"
+# AES-256: three supported backends: pycryptodomex, cryptography, pyaes
+assert aes_encrypt_with_iv(
+ key=b"satoshi_nakamoto_wanted_32_bytes",
+ iv=b"thetimes20090103",
+ data=b"The quick brown fox jumps over the lazy dog").hex() \
+ == "65e532e8fb643e192d66cfebd3328ea6d52c9d43b18c8c8c7754b6a7c5927b7395201ff221315b51bfb1ad1c6184afce"
+assert aes_decrypt_with_iv(
+ key=b"satoshi_nakamoto_wanted_32_bytes",
+ iv=b"thetimes20090103",
+ data=bytes.fromhex("65e532e8fb643e192d66cfebd3328ea6d52c9d43b18c8c8c7754b6a7c5927b7395201ff221315b51bfb1ad1c6184afce")) \
+ == b"The quick brown fox jumps over the lazy dog"
+# chacha20: two supported backends: pycryptodomex, cryptography
+assert chacha20_encrypt(
+ key=b"satoshi_nakamoto_wanted_32_bytes",
+ nonce=b"thetimes2009",
+ data=b"The quick brown fox jumps over the lazy dog").hex() \
+ == "b34faa354952d09a5c052e490678866f0d1ad37e567b2ea9247438c0d91f5a00343dc8a681a60bb4b2973b"
+assert chacha20_decrypt(
+ key=b"satoshi_nakamoto_wanted_32_bytes",
+ nonce=b"thetimes2009",
+ data=bytes.fromhex("b34faa354952d09a5c052e490678866f0d1ad37e567b2ea9247438c0d91f5a00343dc8a681a60bb4b2973b")) \
+ == b"The quick brown fox jumps over the lazy dog"
+# chacha20-poly1305: two supported backends: pycryptodomex, cryptography
+assert chacha20_poly1305_encrypt(
+ key=b"satoshi_nakamoto_wanted_32_bytes",
+ nonce=b"thetimes2009",
+ associated_data=b"loremipsum",
+ data=b"The quick brown fox jumps over the lazy dog").hex() \
+ == "10861cab9d587356da776c6bca2320f39f6f0a5111a74929108ba27fda87e8777dcf6416a4ca9d443dba94b9891e48a1618a7347ac648970a38265"
+assert chacha20_poly1305_decrypt(
+ key=b"satoshi_nakamoto_wanted_32_bytes",
+ nonce=b"thetimes2009",
+ associated_data=b"loremipsum",
+ data=bytes.fromhex("10861cab9d587356da776c6bca2320f39f6f0a5111a74929108ba27fda87e8777dcf6416a4ca9d443dba94b9891e48a1618a7347ac648970a38265")) \
+ == b"The quick brown fox jumps over the lazy dog"Why this scored 27/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.