diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d09edb7..b0a29c70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Changelog 24.11.1 - November 2024 + +### Security Fix +This release addresses a vulnerability affecting AES-CBC encrypted mnemonics stored on flash storage, SD cards, and QR codes. Due to an implementation error, the Initialization Vector (IV) in our CBC encryption, which used camera-generated entropy, was not being correctly utilized, which meant it did not provide the intended additional entropy. + # Changelog 24.11.0 - November 2024 ### Tamper Check Flash Hash and Tamper Check Code (Experimental) diff --git a/mkdocs.yml b/mkdocs.yml index 8e9de2b6..ee9c12f3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,7 +51,7 @@ edit_uri: edit/main/docs docs_dir: docs site_dir: public extra: - latest_krux: krux-v24.11.0 + latest_krux: krux-v24.11.1 latest_installer: v0.0.20-beta latest_installer_rpm: krux-installer-0.0.20_beta-1.x86_64.rpm latest_installer_deb: krux-installer_0.0.20-beta_amd64.deb diff --git a/pyproject.toml b/pyproject.toml index ba665229..5395d128 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ [tool.poetry] name = "krux" -version = "24.11.0" +version = "24.11.1" description = "Open-source signing device firmware for Bitcoin" authors = ["Jeff S "] diff --git a/src/krux/metadata.py b/src/krux/metadata.py index 4103829f..14f9715c 100644 --- a/src/krux/metadata.py +++ b/src/krux/metadata.py @@ -19,5 +19,5 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -VERSION = "24.11.0" +VERSION = "24.11.1" SIGNER_PUBKEY = "03339e883157e45891e61ca9df4cd3bb895ef32d475b8e793559ea10a36766689b" diff --git a/tests/pages/test_encryption_ui.py b/tests/pages/test_encryption_ui.py index e0adc892..d48f3ce6 100644 --- a/tests/pages/test_encryption_ui.py +++ b/tests/pages/test_encryption_ui.py @@ -20,7 +20,7 @@ } }""" ENCRYPTED_QR_TITLE_CBC = "353175d8" -ENCRYPTED_QR_DATA_CBC = b"\x08353175d8\x01\x00\x00\n!\xa1\xf3\x8b\x9e\xa1^\x8d\xab\x08\xf7\t\xf3\x94\x06\x89Q\x15]\xe0\xc6\xabf\x9c\x12E\xbcw\xcaa\x14\xfc\xa5\x16\x15\x0f;\x88\xbc\xb4H\xbe_\xf3\xf1b\x1e\x02\xff\xea\x9a\xe9z\xfd\xc9\xef\xcd\xa0A\x0c\xd1:a\x08" +ENCRYPTED_QR_DATA_CBC = b"\x08353175d8\x01\x00\x00\nOR\xa1\x93l>2q \x9e\x9dd\x05\x9e\xd7\x8e\xa5\x95(IzR\x81\xabI:\x1e\x8a\x1d\xe7|O\xac\x9c\xe8.\x8cc\xc0\x93\x0e\xe67vpO#i\x99\xd1.\x85\xf7\x00\xfez\xadN\x9d7\xaex\xa6\xd3" ENCRYPTED_QR_TITLE_ECB = "06b79aa2" ENCRYPTED_QR_DATA_ECB = b"\x0806b79aa2\x00\x00\x00\n\xa4\xaaa\xb9h\x0c\xdc-i\x85\x83.9,\x91\xf1\x19E,\xc9\xf0'\xb1b7\x91mo\xa2-\xb6\x16\xac\x04-2F\x10\xda\xd1\xdb,\x85\x9fr\x1c\x8aH" diff --git a/tests/test_encryption.py b/tests/test_encryption.py index 0f08307d..0ac9775e 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -13,10 +13,10 @@ CBC_WORDS = "dog guitar hotel random owner gadget salute riot patrol work advice panic erode leader pass cross section laundry elder asset soul scale immune scatter" ECB_ENCRYPTED_WORDS = "1NV55l0ny9vkFV6s4MnDvDlpiWUJo35sv5hs6ZKp4T0zVrOxXft8E/RLX9unZJJwii2/crVgr+XE/lAgWhL7YoKYtimDmbpdOFK9U84+3bE=" -CBC_ENCRYPTED_WORDS = "pJy/goOD11Nulfzd07PPKCOuPWsy2/tONwHrpY/AihVDcGxmIgzasyhs3fY90E0khrCqqgCvzjukMCdxif2OljKDxZQPGoVNeJKqE4nu5fq5023WhO1yKtAcPt3mML6Q" +CBC_ENCRYPTED_WORDS = "T1Khk2w+MnEgnp1kBZ7XjpLaWFYlK9sShaYMyO7BPmjUacEhco2X1LFWDC8YRP+Z2GG4hYEoCMoHyOAoBPXh8P3Z1D5F34pWtBlgEOpGAwFBc+VeDWFRhDLWLjl6pXia" ECB_ENCRYPTED_QR = b"\x07test ID\x00\x00\x00\n*\xe1\x9d\xc5\x82\xc1\x19\x9b\xb7&\xf2?\x03\xc7o\xf6\xaf\x9e\x81#F,Qs\xe6\x1d\xeb\xd1Y\xa0/\xcf" -CBC_ENCRYPTED_QR = b"\x07test ID\x01\x00\x00\n\xf32q \x9e\x9dd\x05\x9e\xd7\x8e\x01\x03`u_\xd7\xab/N\xbc@\x19\xcc\n"\xc5\x8a^3xt\xa4\xb3\x0bK\xca\x8a@\x82\xdaz\xd3' ECB_QR_PUBLIC_DATA = ( "Encrypted QR Code:\nID: test ID\nVersion: AES-ECB\nKey iter.: 100000" @@ -34,12 +34,12 @@ "cbcID": { "version": 1, "key_iterations": 100000, - "data": "GpNxj9kzdiTuIf1UYC6R0FHoUokBhiNLkxWgSOHBhmBHb0Ew8wk1M+VlsR4v/koCfSGOTkgjFshC36+n7mx0W0PI6NizAoPClO8DUVamd5hS6irS+Lfff0//VJWK1BcdvOJjzYw8TBiVaL1swAEEySjn5GsqF1RaJXzAMMgu03Kq32iDIDy7h/jHJTiIPCoVQAle/C9vXq2HQeVx43c0LhGXTZmIhhkHPMgDzFTsMGM=" + "data": "T1Khk2w+MnEgnp1kBZ7Xjp+66c9sy20J39ffK11XvVAaDSyQybsM6txAwKy/U1iU4KKYRu3ywDDN9q9sWAi1R+y7x4aHwQd0C0rRcW0iDxvWtFyWMKilA0AsDQwvBSgkhf5PQnQ1rfjnKVF75rTrG5vUNF01FRwa9PoM5cq30Yki/hFnWj/4niaeXqgQvIwjSzBNbXgaRLjfoaUyHiu8+zBX25rkpI0PW243fgDEfqI=" } }""" ECB_ONLY_JSON = """{"ecbID": {"version": 0, "key_iterations": 100000, "data": "sMCvAUvVpGSCsXsBl7EBNGPZLymZoyB8eAUHb2TMbarhqD4GJga/SW/AstxIvZz6MR1opXLfF7Pyd+IJBe3E0lDQCkvqytSQfVGnVSeYz+sNfd5T1CXS0/C2zYKTKFL7RTpHd0IXHZ+GQuzX1hoJMHkh0sx0VgorVdDj87ykUQIeC95MS98y/ha2q/vWfLyIZU1hc5VcehzmTA1B6ExMGA=="}}""" -CBC_ONLY_JSON = """{"cbcID": {"version": 1, "key_iterations": 100000, "data": "GpNxj9kzdiTuIf1UYC6R0FHoUokBhiNLkxWgSOHBhmBHb0Ew8wk1M+VlsR4v/koCfSGOTkgjFshC36+n7mx0W0PI6NizAoPClO8DUVamd5hS6irS+Lfff0//VJWK1BcdvOJjzYw8TBiVaL1swAEEySjn5GsqF1RaJXzAMMgu03Kq32iDIDy7h/jHJTiIPCoVQAle/C9vXq2HQeVx43c0LhGXTZmIhhkHPMgDzFTsMGM="}}""" +CBC_ONLY_JSON = """{"cbcID": {"version": 1, "key_iterations": 100000, "data": "T1Khk2w+MnEgnp1kBZ7Xjp+66c9sy20J39ffK11XvVAaDSyQybsM6txAwKy/U1iU4KKYRu3ywDDN9q9sWAi1R+y7x4aHwQd0C0rRcW0iDxvWtFyWMKilA0AsDQwvBSgkhf5PQnQ1rfjnKVF75rTrG5vUNF01FRwa9PoM5cq30Yki/hFnWj/4niaeXqgQvIwjSzBNbXgaRLjfoaUyHiu8+zBX25rkpI0PW243fgDEfqI="}}""" I_VECTOR = b"OR\xa1\x93l>2q \x9e\x9dd\x05\x9e\xd7\x8e" @@ -70,7 +70,7 @@ def test_cbc_encryption(m5stickv): from Crypto.Random import get_random_bytes encryptor = AESCipher(TEST_KEY, TEST_MNEMONIC_ID, ITERATIONS) - iv = get_random_bytes(AES.block_size) + iv = I_VECTOR encrypted = encryptor.encrypt(TEST_WORDS, AES.MODE_CBC, iv).decode("utf-8") assert encrypted == CBC_ENCRYPTED_WORDS data = base64.b64decode(encrypted) @@ -80,6 +80,35 @@ def test_cbc_encryption(m5stickv): assert decrypted == TEST_WORDS +def test_cbc_iv_use(m5stickv): + from krux.encryption import AESCipher + from Crypto.Random import get_random_bytes + + SECOND_IV = b"\x8e\xc8b\x8f\xe2\xa8`\xaa\x06d\xe8\xe7\xaa.0\x03" + CBC_ENCRYPTED_WORDS_SECOND_IV = "jshij+KoYKoGZOjnqi4wA8BON70ndcVal949EOpP9Hi15hBsz3XvnpQDTC3c/6Nt8GnU4gp7OUcXvy6WuhoHrGtLOZAttnNmMQFZK6aAYy95T5MnZIsNbnJ15xewAZqb" + + encryptor = AESCipher(TEST_KEY, TEST_MNEMONIC_ID, ITERATIONS) + iv = I_VECTOR + encrypted = encryptor.encrypt(TEST_WORDS, AES.MODE_CBC, iv).decode("utf-8") + assert encrypted == CBC_ENCRYPTED_WORDS + data = base64.b64decode(encrypted) + encrypted_mnemonic = data[AES.block_size :] + i_vector = data[: AES.block_size] + decrypted = encryptor.decrypt(encrypted_mnemonic, AES.MODE_CBC, i_vector) + assert decrypted == TEST_WORDS + # Encrypt again with same data except for the IV + iv = SECOND_IV + print(iv) + encrypted = encryptor.encrypt(TEST_WORDS, AES.MODE_CBC, iv).decode("utf-8") + print(encrypted) + assert encrypted == CBC_ENCRYPTED_WORDS_SECOND_IV + data = base64.b64decode(encrypted) + encrypted_mnemonic = data[AES.block_size :] + i_vector = data[: AES.block_size] + decrypted = encryptor.decrypt(encrypted_mnemonic, AES.MODE_CBC, i_vector) + assert decrypted == TEST_WORDS + + def test_list_mnemonic_storage(m5stickv, mock_file_operations): from krux.encryption import MnemonicStorage