From d8dd62732ce7a2f7e4b710e78040b651c2cc00fc Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Wed, 10 Jan 2024 16:33:51 +0100 Subject: [PATCH] HW Accelerated AES CTR for BSMS and passphrase saver --- releases/EdgeChangeLog.md | 1 + shared/bsms.py | 6 +- shared/pwsave.py | 8 +-- shared/utils.py | 4 +- testing/devtest/proof_hw_accel_aes.py | 27 +++++++++ testing/devtest/unit_aes_compat.py | 84 +++++++++++++++++++++++++++ testing/test_unit.py | 4 ++ 7 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 testing/devtest/proof_hw_accel_aes.py create mode 100644 testing/devtest/unit_aes_compat.py diff --git a/releases/EdgeChangeLog.md b/releases/EdgeChangeLog.md index 724f6ae7..a281d950 100644 --- a/releases/EdgeChangeLog.md +++ b/releases/EdgeChangeLog.md @@ -15,6 +15,7 @@ imports that have no file, in which case name was created from descriptor checksum. - Enhancement: Allow keys with same origin, differentiated only by change index derivation in miniscript descriptor. +- Enhancement: HW Accelerated AES CTR for BSMS and passphrase saver - Bugfix: Do not allow to import duplicate miniscript wallets (thanks to [AnchorWatch](https://www.anchorwatch.com/)) ## 6.2.1X - 2023-10-26 diff --git a/shared/bsms.py b/shared/bsms.py index 41da8b80..1e770710 100644 --- a/shared/bsms.py +++ b/shared/bsms.py @@ -5,7 +5,7 @@ # For faster testing... # ./simulator.py --seq 99y3y4y # -import ngu, os, stash, chains +import ngu, os, stash, chains, aes256ctr from ubinascii import b2a_base64, a2b_base64 from ubinascii import unhexlify as a2b_hex from ubinascii import hexlify as b2a_hex @@ -82,7 +82,7 @@ def msg_auth_code(key, token_hex, data): def bsms_decrypt(key, data_bytes): mac, ciphertext = data_bytes[:32], data_bytes[32:] iv = mac[:16] - decrypt = ngu.aes.CTR(key, iv) + decrypt = aes256ctr.new(key, iv) decrypted = decrypt.cipher(ciphertext) try: plaintext = decrypted.decode() @@ -98,7 +98,7 @@ def bsms_encrypt(key, token_hex, data_str): hmac_k = hmac_key(key) mac = msg_auth_code(hmac_k, token_hex, data_str) iv = mac[:16] - encrypt = ngu.aes.CTR(key, iv) + encrypt = aes256ctr.new(key, iv) ciphertext = encrypt.cipher(data_str) return mac + ciphertext diff --git a/shared/pwsave.py b/shared/pwsave.py index c2159da6..e8597d32 100644 --- a/shared/pwsave.py +++ b/shared/pwsave.py @@ -2,7 +2,7 @@ # # pwsave.py - Save bip39 passphrases into encrypted file on MicroSD (if desired) # -import stash, ujson, ngu, pyb, os +import stash, ujson, ngu, pyb, os, aes256ctr from files import CardSlot, CardMissingError, needs_microsd from ux import ux_dramatic_pause, ux_confirm, ux_show_story from utils import xfp2str @@ -36,7 +36,7 @@ class PassphraseSaver: def _read(self, card): # Return a list of saved passphrases, or empty list if fail. # Fail silently in all cases. Expect to see lots of noise here. - decrypt = ngu.aes.CTR(self.key) + decrypt = aes256ctr.new(self.key) try: fname = self.filename(card) @@ -60,7 +60,7 @@ class PassphraseSaver: data = self._read(card) if self.key else [] yield data # yield data that can be modified - encrypt = ngu.aes.CTR(self.key) + encrypt = aes256ctr.new(self.key) msg = encrypt.cipher(ujson.dumps(data)) @@ -326,7 +326,7 @@ class MicroSD2FA(PassphraseSaver): data = dict(nonce=nonce) - encrypt = ngu.aes.CTR(self.key) + encrypt = aes256ctr.new(self.key) msg = encrypt.cipher(ujson.dumps(data)) with open(self.filename(card), 'wb') as fd: diff --git a/shared/utils.py b/shared/utils.py index 863d602d..d1e399ef 100644 --- a/shared/utils.py +++ b/shared/utils.py @@ -2,7 +2,7 @@ # # utils.py - Misc utils. My favourite kind of source file. # -import gc, sys, ustruct, ngu, chains, ure, uos, uio, time +import gc, sys, ustruct, chains, ure, uos, uio, time, aes256ctr from ubinascii import unhexlify as a2b_hex from ubinascii import hexlify as b2a_hex from ubinascii import a2b_base64, b2a_base64 @@ -524,7 +524,7 @@ def chunk_writer(fd, body): def decrypt_tapsigner_backup(backup_key, data): try: backup_key = a2b_hex(backup_key) - decrypt = ngu.aes.CTR(backup_key, bytes(16)) # IV 0 + decrypt = aes256ctr.new(backup_key, bytes(16)) # IV 0 decrypted = decrypt.cipher(data).decode().strip() # format of TAPSIGNER backup is known in advance # extended private key is expected at the beginning of the first line diff --git a/testing/devtest/proof_hw_accel_aes.py b/testing/devtest/proof_hw_accel_aes.py new file mode 100644 index 00000000..024a09db --- /dev/null +++ b/testing/devtest/proof_hw_accel_aes.py @@ -0,0 +1,27 @@ +import utime, ngu, aes256ctr, math + +# Cifra +start = utime.ticks_ms() +for i in range(100): + enc = ngu.aes.CTR(b"a" * 32, "b"*16) + dec = ngu.aes.CTR(b"a" * 32, "b"*16) + em = enc.cipher(b"msg" * i) + dm = dec.cipher(em) + assert dm == b"msg" * i +end = utime.ticks_ms() +cifra_res = utime.ticks_diff(end, start) + + +# aes256ctr +start = utime.ticks_ms() +for i in range(100): + enc = aes256ctr.new(b"a" * 32, "b"*16) + dec = aes256ctr.new(b"a" * 32, "b"*16) + em = enc.cipher(b"msg" * i) + dm = dec.cipher(em) + assert dm == b"msg" * i +end = utime.ticks_ms() +hwa_res = utime.ticks_diff(end, start) + +r = math.ceil(cifra_res / hwa_res) +print("Hardware accelerated AES is approximatelly %dX faster than Cifra AES." % r) \ No newline at end of file diff --git a/testing/devtest/unit_aes_compat.py b/testing/devtest/unit_aes_compat.py new file mode 100644 index 00000000..f6186570 --- /dev/null +++ b/testing/devtest/unit_aes_compat.py @@ -0,0 +1,84 @@ +import ngu, aes256ctr, ujson, ustruct + +key = b"a" * 32 + +bsms_signer = b"""BSMS 1.0 +a54044308ceac9b7 +[eedff89a/48'/0'/0'/2']xpub6EhJvMneoLWAf8cuyLBLQiKiwh89RAmqXEqYeFuaCEHdHwxSRfzLrUxKXEBap7nZSHAYP7Jfq6gZmucotNzpMQ9Sb1nTqerqW8hrtmx6Y6o +Signer 2 key +H/IHW5dMGYsrRdYEz3ux+kKnkWBtxHzfYkREpnYbco38VnMvIxCbDuf7iu6960qDhBLR/RLjlb9UPtLmCMbczDE=""" + +bsms_coord = b"""BSMS 1.0 +wsh(sortedmulti(2,[b7868815/48'/0'/0'/2']xpub6FA5rfxJc94K1kNtxRby1hoHwi7YDyTWwx1KUR3FwskaF6HzCbZMz3zQwGnCqdiFeMTPV3YneTGS2YQPiuNYsSvtggWWMQpEJD4jXU7ZzEh/**,[eedff89a/48'/0'/0'/2']xpub6EhJvMneoLWAf8cuyLBLQiKiwh89RAmqXEqYeFuaCEHdHwxSRfzLrUxKXEBap7nZSHAYP7Jfq6gZmucotNzpMQ9Sb1nTqerqW8hrtmx6Y6o/**)) +/0/*,/1/* +bc1qhs4u273g4azq7kqqpe6vh5wfhasfmrq7nheyzsnq77humd7rwtkqagvakf""" + +pws = [dict(xfp=0x4369050f, pw=pw) for pw in ["about abandon about", "@#$%^&*()", "ksjdfh78$%"]] +pws_ser = ujson.dumps(pws).encode() + +# mimic real data that we use +TEST_CASES = [ + b'Hello World!', + pws_ser, + bsms_coord, + bsms_signer +] + + +def secret_msg_exchange(alice, bob, msg): + e_msg = alice.cipher(msg) + assert bob.cipher(e_msg) == msg + return_msg = msg + b"\x00ACK" + e_msg = bob.cipher(return_msg) + assert alice.cipher(e_msg) == return_msg + + +for i, msg in enumerate(TEST_CASES): + # 16 bytes random IV + # encrypt with Cifra, decrypt with HW accelerated AES + iv = ngu.random.bytes(16) + encrypt = ngu.aes.CTR(key, iv) + decrypt = aes256ctr.new(key, iv) + secret_msg_exchange(encrypt, decrypt, msg) + print("Cifra AES --> HW AES\tIV=0b16\t\tOK") + + # encrypt with HW accelerated AES, decrypt with Cifra + encrypt = aes256ctr.new(key, iv) + decrypt = ngu.aes.CTR(key, iv) + secret_msg_exchange(encrypt, decrypt, msg) + print("HW AES --> Cifra AES\tIV=0b16\t\tOK") + + # empty IV + # encrypt with Cifra, decrypt with HW accelerated AES + encrypt = ngu.aes.CTR(key) + decrypt = aes256ctr.new(key) + secret_msg_exchange(encrypt, decrypt, msg) + print("Cifra AES --> HW AES\tIV=NONE\t\tOK") + + # encrypt with HW accelerated AES, decrypt with Cifra + encrypt = aes256ctr.new(key) + decrypt = ngu.aes.CTR(key) + secret_msg_exchange(encrypt, decrypt, msg) + print("HW AES --> Cifra AES\tIV=NONE\t\tOK") + + +print("RANDOM TEST CASES") +for i in range(10): + key = ngu.random.bytes(32) + iv = ngu.random.bytes(16) + + msg = (key + iv) + if i: + msg = msg * i + + # encrypt with Cifra, decrypt with HW accelerated AES + encrypt = ngu.aes.CTR(key, iv) + decrypt = aes256ctr.new(key, iv) + secret_msg_exchange(encrypt, decrypt, msg) + print("Cifra AES --> HW AES\tIV=0b16\t\tOK") + + # encrypt with HW accelerated AES, decrypt with Cifra + encrypt = aes256ctr.new(key, iv) + decrypt = ngu.aes.CTR(key, iv) + secret_msg_exchange(encrypt, decrypt, msg) + print("HW AES --> Cifra AES\tIV=0b16\t\tOK") diff --git a/testing/test_unit.py b/testing/test_unit.py index 929618c3..d6310c4d 100644 --- a/testing/test_unit.py +++ b/testing/test_unit.py @@ -274,4 +274,8 @@ def test_is_dir(microsd_path, sim_exec): shutil.rmtree(microsd_path("my_dir")) +def test_aes_compatibility(sim_execfile): + res = sim_execfile('devtest/unit_aes_compat.py') + assert res == "" + # EOF