From 5bcf1a253b2492b77ec215df3a16d388fdaa4b31 Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:28:11 -0400 Subject: [PATCH] Revert "fixes" This reverts commit 41f34f4ff4145b7abb60fd9bec168ae4b60f23b4. --- Cargo.lock | 158 +---- Cargo.toml | 2 - .../src/bin/run_hello_world_private.rs | 2 - ...n_hello_world_through_tail_call_private.rs | 2 - .../bin/run_hello_world_with_move_function.rs | 4 - integration_tests/tests/amm.rs | 8 - integration_tests/tests/ata.rs | 6 - .../tests/auth_transfer/private.rs | 20 - .../tests/auth_transfer/public.rs | 16 - integration_tests/tests/indexer.rs | 6 - integration_tests/tests/keys_restoration.rs | 14 - integration_tests/tests/pinata.rs | 12 - integration_tests/tests/token.rs | 24 - keycard_tests.sh | 61 -- nssa/src/encoding/public_transaction.rs | 5 +- .../witness_set.rs | 22 +- nssa/src/public_transaction/witness_set.rs | 2 +- python/keycard-py/.gitignore | 194 ------ python/keycard-py/CHANGELOG.md | 61 -- python/keycard-py/LICENSE | 21 - python/keycard-py/README.md | 27 - python/keycard-py/docs/conf.py | 36 - python/keycard-py/docs/index.rst | 21 - python/keycard-py/docs/keycard.commands.rst | 77 --- python/keycard-py/docs/keycard.crypto.rst | 29 - python/keycard-py/docs/keycard.parsing.rst | 45 -- python/keycard-py/docs/keycard.rst | 63 -- python/keycard-py/docs/modules.rst | 7 - python/keycard-py/example/example.py | 196 ------ python/keycard-py/keycard/__init__.py | 4 - python/keycard-py/keycard/apdu.py | 48 -- python/keycard-py/keycard/card_interface.py | 38 -- .../keycard-py/keycard/commands/__init__.py | 49 -- .../keycard/commands/change_secret.py | 46 -- .../keycard-py/keycard/commands/derive_key.py | 25 - .../keycard-py/keycard/commands/export_key.py | 80 --- .../keycard/commands/export_lee_key.py | 82 --- .../keycard/commands/factory_reset.py | 15 - .../keycard/commands/generate_key.py | 27 - .../keycard/commands/generate_mnemonic.py | 41 -- .../keycard-py/keycard/commands/get_data.py | 35 - .../keycard-py/keycard/commands/get_status.py | 63 -- python/keycard-py/keycard/commands/ident.py | 34 - python/keycard-py/keycard/commands/init.py | 79 --- .../keycard-py/keycard/commands/load_key.py | 65 -- .../keycard/commands/mutually_authenticate.py | 43 -- .../keycard/commands/open_secure_channel.py | 68 -- python/keycard-py/keycard/commands/pair.py | 75 --- .../keycard-py/keycard/commands/remove_key.py | 11 - python/keycard-py/keycard/commands/select.py | 33 - .../keycard/commands/set_pinless_path.py | 25 - python/keycard-py/keycard/commands/sign.py | 136 ---- .../keycard-py/keycard/commands/store_data.py | 30 - .../keycard/commands/unblock_pin.py | 32 - python/keycard-py/keycard/commands/unpair.py | 31 - .../keycard-py/keycard/commands/verify_pin.py | 49 -- python/keycard-py/keycard/constants.py | 90 --- python/keycard-py/keycard/crypto/__init__.py | 0 python/keycard-py/keycard/crypto/aes.py | 32 - .../keycard/crypto/generate_pairing_token.py | 15 - python/keycard-py/keycard/crypto/padding.py | 9 - python/keycard-py/keycard/exceptions.py | 41 -- python/keycard-py/keycard/keycard.py | 630 ------------------ python/keycard-py/keycard/parsing/__init__.py | 0 .../keycard/parsing/application_info.py | 119 ---- .../keycard/parsing/capabilities.py | 44 -- .../keycard/parsing/exported_key.py | 17 - python/keycard-py/keycard/parsing/identity.py | 43 -- python/keycard-py/keycard/parsing/keypath.py | 87 --- .../keycard/parsing/signature_result.py | 82 --- python/keycard-py/keycard/parsing/tlv.py | 102 --- python/keycard-py/keycard/preconditions.py | 48 -- python/keycard-py/keycard/secure_channel.py | 143 ---- python/keycard-py/keycard/transport.py | 46 -- python/keycard-py/mypy.ini | 12 - python/keycard-py/pyproject.toml | 35 - python/keycard-py/tasks.py | 123 ---- python/keycard-py/tests/__init__.py | 0 python/keycard-py/tests/commands/__init__.py | 0 .../tests/commands/test_change_secret.py | 95 --- .../tests/commands/test_derive_key.py | 14 - .../tests/commands/test_export_key.py | 100 --- .../tests/commands/test_factory_reset.py | 25 - .../tests/commands/test_generate_key.py | 20 - .../tests/commands/test_generate_mnemonic.py | 40 -- .../tests/commands/test_get_data.py | 31 - .../tests/commands/test_get_status.py | 24 - python/keycard-py/tests/commands/test_init.py | 85 --- .../keycard-py/tests/commands/test_keypath.py | 57 -- .../tests/commands/test_load_key.py | 89 --- .../commands/test_mutually_authenticate.py | 48 -- .../commands/test_open_secure_channel.py | 109 --- python/keycard-py/tests/commands/test_pair.py | 106 --- .../tests/commands/test_remove_key.py | 11 - .../keycard-py/tests/commands/test_select.py | 41 -- .../tests/commands/test_set_pinless_path.py | 24 - python/keycard-py/tests/commands/test_sign.py | 101 --- .../tests/commands/test_store_data.py | 29 - .../tests/commands/test_unblock_pin.py | 43 -- .../keycard-py/tests/commands/test_unpair.py | 25 - .../tests/commands/test_verify_pin.py | 29 - python/keycard-py/tests/conftest.py | 9 - python/keycard-py/tests/crypto/__init__.py | 0 python/keycard-py/tests/crypto/test_aes.py | 55 -- .../crypto/test_generate_pairing_token.py | 28 - python/keycard-py/tests/parsing/__init__.py | 0 .../tests/parsing/test_application_info.py | 162 ----- .../keycard-py/tests/parsing/test_identity.py | 87 --- .../tests/parsing/test_signature_result.py | 117 ---- python/keycard-py/tests/parsing/test_tlv.py | 122 ---- python/keycard-py/tests/test_apdu.py | 51 -- python/keycard-py/tests/test_keycard.py | 531 --------------- python/keycard-py/tests/test_preconditions.py | 56 -- .../keycard-py/tests/test_secure_channel.py | 132 ---- python/keycard-py/tests/test_transport.py | 87 --- python/keycard-py/tests/test_vectors.py | 50 -- python/keycard-py/tox.ini | 48 -- python/keycard_test.py | 24 - python/keycard_wallet.py | 137 ---- wallet-ffi/src/transfer.rs | 4 +- wallet/Cargo.toml | 2 - wallet/src/cli/account.rs | 19 +- wallet/src/cli/keycard.rs | 73 -- wallet/src/cli/mod.rs | 7 - wallet/src/cli/programs/amm.rs | 22 - .../src/cli/programs/native_token_transfer.rs | 99 +-- wallet/src/cli/programs/pinata.rs | 21 +- wallet/src/cli/programs/token.rs | 66 +- wallet/src/helperfunctions.rs | 16 +- wallet/src/lib.rs | 146 +--- wallet/src/program_facades/ata.rs | 24 +- .../native_token_transfer/deshielded.rs | 2 - .../native_token_transfer/private.rs | 6 - .../native_token_transfer/public.rs | 51 +- .../native_token_transfer/shielded.rs | 8 - wallet/src/program_facades/pinata.rs | 2 - wallet/src/program_facades/token.rs | 109 +-- wallet_with_keycard.sh | 21 - 138 files changed, 125 insertions(+), 7538 deletions(-) delete mode 100644 keycard_tests.sh delete mode 100644 python/keycard-py/.gitignore delete mode 100644 python/keycard-py/CHANGELOG.md delete mode 100644 python/keycard-py/LICENSE delete mode 100644 python/keycard-py/README.md delete mode 100644 python/keycard-py/docs/conf.py delete mode 100644 python/keycard-py/docs/index.rst delete mode 100644 python/keycard-py/docs/keycard.commands.rst delete mode 100644 python/keycard-py/docs/keycard.crypto.rst delete mode 100644 python/keycard-py/docs/keycard.parsing.rst delete mode 100644 python/keycard-py/docs/keycard.rst delete mode 100644 python/keycard-py/docs/modules.rst delete mode 100644 python/keycard-py/example/example.py delete mode 100644 python/keycard-py/keycard/__init__.py delete mode 100644 python/keycard-py/keycard/apdu.py delete mode 100644 python/keycard-py/keycard/card_interface.py delete mode 100644 python/keycard-py/keycard/commands/__init__.py delete mode 100644 python/keycard-py/keycard/commands/change_secret.py delete mode 100644 python/keycard-py/keycard/commands/derive_key.py delete mode 100644 python/keycard-py/keycard/commands/export_key.py delete mode 100644 python/keycard-py/keycard/commands/export_lee_key.py delete mode 100644 python/keycard-py/keycard/commands/factory_reset.py delete mode 100644 python/keycard-py/keycard/commands/generate_key.py delete mode 100644 python/keycard-py/keycard/commands/generate_mnemonic.py delete mode 100644 python/keycard-py/keycard/commands/get_data.py delete mode 100644 python/keycard-py/keycard/commands/get_status.py delete mode 100644 python/keycard-py/keycard/commands/ident.py delete mode 100644 python/keycard-py/keycard/commands/init.py delete mode 100644 python/keycard-py/keycard/commands/load_key.py delete mode 100644 python/keycard-py/keycard/commands/mutually_authenticate.py delete mode 100644 python/keycard-py/keycard/commands/open_secure_channel.py delete mode 100644 python/keycard-py/keycard/commands/pair.py delete mode 100644 python/keycard-py/keycard/commands/remove_key.py delete mode 100644 python/keycard-py/keycard/commands/select.py delete mode 100644 python/keycard-py/keycard/commands/set_pinless_path.py delete mode 100644 python/keycard-py/keycard/commands/sign.py delete mode 100644 python/keycard-py/keycard/commands/store_data.py delete mode 100644 python/keycard-py/keycard/commands/unblock_pin.py delete mode 100644 python/keycard-py/keycard/commands/unpair.py delete mode 100644 python/keycard-py/keycard/commands/verify_pin.py delete mode 100644 python/keycard-py/keycard/constants.py delete mode 100644 python/keycard-py/keycard/crypto/__init__.py delete mode 100644 python/keycard-py/keycard/crypto/aes.py delete mode 100644 python/keycard-py/keycard/crypto/generate_pairing_token.py delete mode 100644 python/keycard-py/keycard/crypto/padding.py delete mode 100644 python/keycard-py/keycard/exceptions.py delete mode 100644 python/keycard-py/keycard/keycard.py delete mode 100644 python/keycard-py/keycard/parsing/__init__.py delete mode 100644 python/keycard-py/keycard/parsing/application_info.py delete mode 100644 python/keycard-py/keycard/parsing/capabilities.py delete mode 100644 python/keycard-py/keycard/parsing/exported_key.py delete mode 100644 python/keycard-py/keycard/parsing/identity.py delete mode 100644 python/keycard-py/keycard/parsing/keypath.py delete mode 100644 python/keycard-py/keycard/parsing/signature_result.py delete mode 100644 python/keycard-py/keycard/parsing/tlv.py delete mode 100644 python/keycard-py/keycard/preconditions.py delete mode 100644 python/keycard-py/keycard/secure_channel.py delete mode 100644 python/keycard-py/keycard/transport.py delete mode 100644 python/keycard-py/mypy.ini delete mode 100644 python/keycard-py/pyproject.toml delete mode 100644 python/keycard-py/tasks.py delete mode 100644 python/keycard-py/tests/__init__.py delete mode 100644 python/keycard-py/tests/commands/__init__.py delete mode 100644 python/keycard-py/tests/commands/test_change_secret.py delete mode 100644 python/keycard-py/tests/commands/test_derive_key.py delete mode 100644 python/keycard-py/tests/commands/test_export_key.py delete mode 100644 python/keycard-py/tests/commands/test_factory_reset.py delete mode 100644 python/keycard-py/tests/commands/test_generate_key.py delete mode 100644 python/keycard-py/tests/commands/test_generate_mnemonic.py delete mode 100644 python/keycard-py/tests/commands/test_get_data.py delete mode 100644 python/keycard-py/tests/commands/test_get_status.py delete mode 100644 python/keycard-py/tests/commands/test_init.py delete mode 100644 python/keycard-py/tests/commands/test_keypath.py delete mode 100644 python/keycard-py/tests/commands/test_load_key.py delete mode 100644 python/keycard-py/tests/commands/test_mutually_authenticate.py delete mode 100644 python/keycard-py/tests/commands/test_open_secure_channel.py delete mode 100644 python/keycard-py/tests/commands/test_pair.py delete mode 100644 python/keycard-py/tests/commands/test_remove_key.py delete mode 100644 python/keycard-py/tests/commands/test_select.py delete mode 100644 python/keycard-py/tests/commands/test_set_pinless_path.py delete mode 100644 python/keycard-py/tests/commands/test_sign.py delete mode 100644 python/keycard-py/tests/commands/test_store_data.py delete mode 100644 python/keycard-py/tests/commands/test_unblock_pin.py delete mode 100644 python/keycard-py/tests/commands/test_unpair.py delete mode 100644 python/keycard-py/tests/commands/test_verify_pin.py delete mode 100644 python/keycard-py/tests/conftest.py delete mode 100644 python/keycard-py/tests/crypto/__init__.py delete mode 100644 python/keycard-py/tests/crypto/test_aes.py delete mode 100644 python/keycard-py/tests/crypto/test_generate_pairing_token.py delete mode 100644 python/keycard-py/tests/parsing/__init__.py delete mode 100644 python/keycard-py/tests/parsing/test_application_info.py delete mode 100644 python/keycard-py/tests/parsing/test_identity.py delete mode 100644 python/keycard-py/tests/parsing/test_signature_result.py delete mode 100644 python/keycard-py/tests/parsing/test_tlv.py delete mode 100644 python/keycard-py/tests/test_apdu.py delete mode 100644 python/keycard-py/tests/test_keycard.py delete mode 100644 python/keycard-py/tests/test_preconditions.py delete mode 100644 python/keycard-py/tests/test_secure_channel.py delete mode 100644 python/keycard-py/tests/test_transport.py delete mode 100644 python/keycard-py/tests/test_vectors.py delete mode 100644 python/keycard-py/tox.ini delete mode 100644 python/keycard_test.py delete mode 100644 python/keycard_wallet.py delete mode 100644 wallet/src/cli/keycard.rs delete mode 100644 wallet_with_keycard.sh diff --git a/Cargo.lock b/Cargo.lock index 78b97777..ca46abde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1303,7 +1303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799" dependencies = [ "clap", - "heck 0.5.0", + "heck", "indexmap 2.13.0", "log", "proc-macro2", @@ -1450,7 +1450,7 @@ version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.117", @@ -2183,7 +2183,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "proc-macro2-diagnostics", ] @@ -3007,12 +3007,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -3566,15 +3560,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "inout" version = "0.1.4" @@ -3894,7 +3879,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da3f8ab5ce1bb124b6d082e62dffe997578ceaf0aeb9f3174a214589dc00f07" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro-crate", "proc-macro2", "quote", @@ -4011,43 +3996,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "keycard_wallet" -version = "0.1.0" -dependencies = [ - "amm_core", - "anyhow", - "async-stream", - "ata_core", - "base58", - "bip39", - "clap", - "common", - "env_logger", - "futures", - "hex", - "humantime", - "humantime-serde", - "indicatif", - "itertools 0.14.0", - "key_protocol", - "log", - "nssa", - "nssa_core", - "optfield", - "pyo3", - "rand 0.8.5", - "sequencer_service_rpc", - "serde", - "serde_json", - "sha2", - "testnet_initial_state", - "thiserror 2.0.18", - "token_core", - "tokio", - "url", -] - [[package]] name = "lazy-regex" version = "3.6.0" @@ -5166,15 +5114,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "mempool" version = "0.1.0" @@ -5449,7 +5388,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6195,7 +6134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.117", @@ -6208,7 +6147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.117", @@ -6246,69 +6185,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "pyo3" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" -dependencies = [ - "cfg-if", - "indoc", - "libc", - "memoffset", - "parking_lot", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" -dependencies = [ - "once_cell", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn 2.0.117", -] - [[package]] name = "quinn" version = "0.11.9" @@ -7996,7 +7872,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.117", @@ -8121,12 +7997,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" version = "3.26.0" @@ -8975,12 +8845,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "unit-prefix" version = "0.5.2" @@ -9144,12 +9008,10 @@ dependencies = [ "indicatif", "itertools 0.14.0", "key_protocol", - "keycard_wallet", "log", "nssa", "nssa_core", "optfield", - "pyo3", "rand 0.8.5", "sequencer_service_rpc", "serde", @@ -9689,7 +9551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "wit-parser", ] @@ -9700,7 +9562,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "indexmap 2.13.0", "prettyplease", "syn 2.0.117", diff --git a/Cargo.toml b/Cargo.toml index 5621b827..5514c300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ members = [ "examples/program_deployment/methods/guest", "bedrock_client", "testnet_initial_state", - "keycard_wallet", ] [workspace.dependencies] @@ -68,7 +67,6 @@ ata_program = { path = "programs/associated_token_account" } test_program_methods = { path = "test_program_methods" } bedrock_client = { path = "bedrock_client" } testnet_initial_state = { path = "testnet_initial_state" } -keycard_wallet = { path = "keycard_wallet" } tokio = { version = "1.50", features = [ "net", diff --git a/examples/program_deployment/src/bin/run_hello_world_private.rs b/examples/program_deployment/src/bin/run_hello_world_private.rs index 78c11656..27ac2079 100644 --- a/examples/program_deployment/src/bin/run_hello_world_private.rs +++ b/examples/program_deployment/src/bin/run_hello_world_private.rs @@ -52,8 +52,6 @@ async fn main() { accounts, Program::serialize_instruction(greeting).unwrap(), &program.into(), - &None, - &None, ) .await .unwrap(); diff --git a/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs b/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs index e25c4b0e..4fac3eec 100644 --- a/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs +++ b/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs @@ -60,8 +60,6 @@ async fn main() { accounts, Program::serialize_instruction(instruction).unwrap(), &program_with_dependencies, - &None, - &None, ) .await .unwrap(); diff --git a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs index 89763bd7..a1c2517e 100644 --- a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs +++ b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs @@ -106,8 +106,6 @@ async fn main() { accounts, Program::serialize_instruction(instruction).unwrap(), &program.into(), - &None, - &None, ) .await .unwrap(); @@ -149,8 +147,6 @@ async fn main() { accounts, Program::serialize_instruction(instruction).unwrap(), &program.into(), - &None, - &None, ) .await .unwrap(); diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index 2c3c404e..dde9e7f5 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -134,8 +134,6 @@ async fn amm_public() -> Result<()> { to_npk: None, to_vpk: None, amount: 7, - from_pin: None, - from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -165,8 +163,6 @@ async fn amm_public() -> Result<()> { to_npk: None, to_vpk: None, amount: 7, - from_pin: None, - from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -555,8 +551,6 @@ async fn amm_new_pool_using_labels() -> Result<()> { to_npk: None, to_vpk: None, amount: 5, - from_pin: None, - from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -581,8 +575,6 @@ async fn amm_new_pool_using_labels() -> Result<()> { to_npk: None, to_vpk: None, amount: 5, - from_pin: None, - from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index 608b15d4..c0918635 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -269,8 +269,6 @@ async fn transfer_and_burn_via_ata() -> Result<()> { to_npk: None, to_vpk: None, amount: fund_amount, - from_pin: None, - from_key_path: None, }), ) .await?; @@ -503,8 +501,6 @@ async fn transfer_via_ata_private_owner() -> Result<()> { to_npk: None, to_vpk: None, amount: fund_amount, - from_pin: None, - from_key_path: None, }), ) .await?; @@ -619,8 +615,6 @@ async fn burn_via_ata_private_owner() -> Result<()> { to_npk: None, to_vpk: None, amount: fund_amount, - from_pin: None, - from_key_path: None, }), ) .await?; diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 2661084b..cf02d0ac 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -31,8 +31,6 @@ async fn private_transfer_to_owned_account() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -74,8 +72,6 @@ async fn private_transfer_to_foreign_account() -> Result<()> { to_npk: Some(to_npk_string), to_vpk: Some(hex::encode(to_vpk.0)), amount: 100, - pin: None, - key_path: None, }); let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -126,8 +122,6 @@ async fn deshielded_transfer_to_public_account() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -193,8 +187,6 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> { to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)), to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)), amount: 100, - pin: None, - key_path: None, }); let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -245,8 +237,6 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -291,8 +281,6 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> { to_npk: Some(to_npk_string), to_vpk: Some(hex::encode(to_vpk.0)), amount: 100, - pin: None, - key_path: None, }); let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -365,8 +353,6 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> { to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)), to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)), amount: 100, - pin: None, - key_path: None, }); let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -413,8 +399,6 @@ async fn initialize_private_account() -> Result<()> { let command = Command::AuthTransfer(AuthTransferSubcommand::Init { account_id: Some(format_private_account_id(account_id)), account_label: None, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -472,8 +456,6 @@ async fn private_transfer_using_from_label() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -517,8 +499,6 @@ async fn initialize_private_account_using_label() -> Result<()> { let command = Command::AuthTransfer(AuthTransferSubcommand::Init { account_id: None, account_label: Some(label), - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; diff --git a/integration_tests/tests/auth_transfer/public.rs b/integration_tests/tests/auth_transfer/public.rs index 617bce53..416c4490 100644 --- a/integration_tests/tests/auth_transfer/public.rs +++ b/integration_tests/tests/auth_transfer/public.rs @@ -24,8 +24,6 @@ async fn successful_transfer_to_existing_account() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -84,8 +82,6 @@ pub async fn successful_transfer_to_new_account() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -124,8 +120,6 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> { to_npk: None, to_vpk: None, amount: 1_000_000, - pin: None, - key_path: None, }); let failed_send = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await; @@ -166,8 +160,6 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -202,8 +194,6 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -248,8 +238,6 @@ async fn initialize_public_account() -> Result<()> { let command = Command::AuthTransfer(AuthTransferSubcommand::Init { account_id: Some(format_public_account_id(account_id)), account_label: None, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -291,8 +279,6 @@ async fn successful_transfer_using_from_label() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -340,8 +326,6 @@ async fn successful_transfer_using_to_label() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; diff --git a/integration_tests/tests/indexer.rs b/integration_tests/tests/indexer.rs index 0e3e4d4d..0aef4a42 100644 --- a/integration_tests/tests/indexer.rs +++ b/integration_tests/tests/indexer.rs @@ -113,8 +113,6 @@ async fn indexer_state_consistency() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -151,8 +149,6 @@ async fn indexer_state_consistency() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -239,8 +235,6 @@ async fn indexer_state_consistency_with_labels() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; diff --git a/integration_tests/tests/keys_restoration.rs b/integration_tests/tests/keys_restoration.rs index 60c10cbe..8dca027c 100644 --- a/integration_tests/tests/keys_restoration.rs +++ b/integration_tests/tests/keys_restoration.rs @@ -76,8 +76,6 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> { to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)), to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)), amount: 100, - pin: None, - key_path: None, }); let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -154,8 +152,6 @@ async fn restore_keys_from_seed() -> Result<()> { to_npk: None, to_vpk: None, amount: 100, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -168,8 +164,6 @@ async fn restore_keys_from_seed() -> Result<()> { to_npk: None, to_vpk: None, amount: 101, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -210,8 +204,6 @@ async fn restore_keys_from_seed() -> Result<()> { to_npk: None, to_vpk: None, amount: 102, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -224,8 +216,6 @@ async fn restore_keys_from_seed() -> Result<()> { to_npk: None, to_vpk: None, amount: 103, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -291,8 +281,6 @@ async fn restore_keys_from_seed() -> Result<()> { to_npk: None, to_vpk: None, amount: 10, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -304,8 +292,6 @@ async fn restore_keys_from_seed() -> Result<()> { to_npk: None, to_vpk: None, amount: 11, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; diff --git a/integration_tests/tests/pinata.rs b/integration_tests/tests/pinata.rs index 82873843..77c4a646 100644 --- a/integration_tests/tests/pinata.rs +++ b/integration_tests/tests/pinata.rs @@ -54,8 +54,6 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()> Command::Pinata(PinataProgramAgnosticSubcommand::Claim { to: Some(winner_account_id_formatted), to_label: None, - pin: None, - key_path: None, }), ) .await; @@ -111,8 +109,6 @@ async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<() Command::Pinata(PinataProgramAgnosticSubcommand::Claim { to: Some(winner_account_id_formatted), to_label: None, - pin: None, - key_path: None, }), ) .await; @@ -145,8 +141,6 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> { let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { to: Some(format_public_account_id(ctx.existing_public_accounts()[0])), to_label: None, - pin: None, - key_path: None, }); let pinata_balance_pre = ctx @@ -188,8 +182,6 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> { ctx.existing_private_accounts()[0], )), to_label: None, - pin: None, - key_path: None, }); let pinata_balance_pre = ctx @@ -255,8 +247,6 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { let command = Command::AuthTransfer(AuthTransferSubcommand::Init { account_id: Some(winner_account_id_formatted.clone()), account_label: None, - pin: None, - key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -273,8 +263,6 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { to: Some(winner_account_id_formatted), to_label: None, - pin: None, - key_path: None, }); let pinata_balance_pre = ctx diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index 45e9a639..e40e27c8 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -135,8 +135,6 @@ async fn create_and_transfer_public_token() -> Result<()> { to_npk: None, to_vpk: None, amount: transfer_amount, - from_pin: None, - from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -182,8 +180,6 @@ async fn create_and_transfer_public_token() -> Result<()> { holder: Some(format_public_account_id(recipient_account_id)), holder_label: None, amount: burn_amount, - holder_pin: None, - holder_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -232,8 +228,6 @@ async fn create_and_transfer_public_token() -> Result<()> { holder_npk: None, holder_vpk: None, amount: mint_amount, - holder_pin: None, - holder_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -379,8 +373,6 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { to_npk: None, to_vpk: None, amount: transfer_amount, - from_pin: None, - from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -408,8 +400,6 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { holder: Some(format_private_account_id(recipient_account_id)), holder_label: None, amount: burn_amount, - holder_pin: None, - holder_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -577,8 +567,6 @@ async fn create_token_with_private_definition() -> Result<()> { holder_npk: None, holder_vpk: None, amount: mint_amount_public, - holder_pin: None, - holder_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -627,8 +615,6 @@ async fn create_token_with_private_definition() -> Result<()> { holder_npk: None, holder_vpk: None, amount: mint_amount_private, - holder_pin: None, - holder_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -771,8 +757,6 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { to_npk: None, to_vpk: None, amount: transfer_amount, - from_pin: None, - from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -904,8 +888,6 @@ async fn shielded_token_transfer() -> Result<()> { to_npk: None, to_vpk: None, amount: transfer_amount, - from_pin: None, - from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -1032,8 +1014,6 @@ async fn deshielded_token_transfer() -> Result<()> { to_npk: None, to_vpk: None, amount: transfer_amount, - from_pin: None, - from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -1169,8 +1149,6 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)), holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.0)), amount: mint_amount, - holder_pin: None, - holder_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -1374,8 +1352,6 @@ async fn transfer_token_using_from_label() -> Result<()> { to_npk: None, to_vpk: None, amount: transfer_amount, - from_pin: None, - from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; diff --git a/keycard_tests.sh b/keycard_tests.sh deleted file mode 100644 index bd06168a..00000000 --- a/keycard_tests.sh +++ /dev/null @@ -1,61 +0,0 @@ -# Run wallet_with_keycard.sh first - -source venv/bin/activate # Load the appropriate virtual environment - -# Tests wallet keycard available -# - Checks whether smart reader and keycard are both available. -echo "Test: wallet keycard available" -wallet keycard available - -echo 'Test: wallet keycard load --pin 111111 --mnemonic "final empty hair duty next drastic normal miss wreck wreck strategy omit"' -# Install a new mnemonic phrase to keycard -wallet keycard load --pin 111111 --mnemonic "fashion degree mountain wool question damp current pond grow dolphin chronic then" -# Commented out to avoid resetting card constantly - -echo "Test: wallet auth-transfer --pin 111111 --key-path \"m/44'/60/0\'/0/0\"" -wallet auth-transfer init --pin 111111 --key-path "m/44'/60'/0'/0/0" - -echo "Test: wallet account get --pin 111111 --key-path \"m/44'/60'/0'/0/0\"" -wallet account get --pin 111111 --key-path "m/44'/60'/0'/0/0" - - -echo "Test: wallet pinata claim --pin 111111 --key-path \"m/44'/60'/0'/0/0\"" -wallet pinata claim --pin 111111 --key-path "m/44'/60'/0'/0/0" - - -echo "Test: wallet account get --pin 111111 --key-path \"m/44'/60'/0'/0/0\"" -wallet account get --pin 111111 --key-path "m/44'/60'/0'/0/0" - -echo "Initialize new account (auth-transfer init) and send" -wallet auth-transfer init --pin 111111 --key-path "m/44'/60'/0'/0/1" -wallet auth-transfer send --amount 40 --pin 111111 --from-key-path "m/44'/60'/0'/0/0" --to-key-path "m/44'/60'/0'/0/1" - -echo "Test: wallet account get --pin 111111 --key-path \"m/44'/60'/0'/0/0\"" -wallet account get --pin 111111 --key-path "m/44'/60'/0'/0/0" - -echo "Test: wallet account get --pin 111111 --key-path \"m/44'/60'/0'/0/1\"" -wallet account get --pin 111111 --key-path "m/44'/60'/0'/0/1" - - -# initialize account keys (outside of keycard) -# Eventually use for tokens and shielded -wallet account new private -wallet account new public -wallet account new public -wallet account new public -wallet account new public - -# Initialize Token A -wallet token new --definition-account-id "Public/4rXJzAEVn9Av1bK1RR4orTJP8dJDzRuoTBRsXVn1pwcK" --supply-account-id "Public/3PfkXqePVRnet5H1PbnfgeWykBrqX3KPPeMBESJt4QEd" --total-supply 1000 --name LEZT - -# Initialize Token B -wallet token new --definition-account-id "Public/DjJx9ccoRyv1xxmHmpFy8mATeKq3Es1DnobjT4EZ4ab2" --supply-account-id "Public/EKgmwG9n7jMYkKaTYdZa7ELyYZq5f43oBKuCiu3t3Tm8" --total-supply 1000 --name LEET - -# Send Token A to a new wallet account - -# Send from non keycard account to an account owned by keycard. -wallet token send --from "Public/3PfkXqePVRnet5H1PbnfgeWykBrqX3KPPeMBESJt4QEd" --to "Public/6iYPF671bMDEkADFvHgcJDrYHJMqZv6cYbxVMsUU7LFE" --amount 400 -# This fails due to lack of initialization for Token Account - - -wallet auth-transfer send --amount 40 --pin 111111 --from-key-path "m/44'/60'/0'/0/0" --to-npk "55204e2934045b044f06d8222b454d46b54788f33c7dec4f6733d441703bb0e6" --to-vpk "02a8626b0c0ad9383c5678dad48c3969b4174fb377cdb03a6259648032c774cec8" diff --git a/nssa/src/encoding/public_transaction.rs b/nssa/src/encoding/public_transaction.rs index b689e403..2549cf27 100644 --- a/nssa/src/encoding/public_transaction.rs +++ b/nssa/src/encoding/public_transaction.rs @@ -1,8 +1,7 @@ use crate::{PublicTransaction, error::NssaError, public_transaction::Message}; impl Message { - #[must_use] - pub fn to_bytes(&self) -> Vec { + pub(crate) fn to_bytes(&self) -> Vec { borsh::to_vec(&self).expect("Autoderived borsh serialization failure") } } @@ -14,6 +13,6 @@ impl PublicTransaction { } pub fn from_bytes(bytes: &[u8]) -> Result { - Ok(borsh::from_slice(bytes).expect("Autoderived borsh serialization failure")) + Ok(borsh::from_slice(bytes)?) } } diff --git a/nssa/src/privacy_preserving_transaction/witness_set.rs b/nssa/src/privacy_preserving_transaction/witness_set.rs index 8d1ea033..86e3fff9 100644 --- a/nssa/src/privacy_preserving_transaction/witness_set.rs +++ b/nssa/src/privacy_preserving_transaction/witness_set.rs @@ -13,6 +13,8 @@ pub struct WitnessSet { impl WitnessSet { #[must_use] + // TODO: this generates signatures. + // However. we may need to get signatures from Keycard. pub fn for_message(message: &Message, proof: Proof, private_keys: &[&PrivateKey]) -> Self { let message_hash = message.hash_message(); let signatures_and_public_keys = private_keys @@ -30,26 +32,6 @@ impl WitnessSet { } } - #[must_use] - pub fn from_list( - proof: Proof, - signatures: &[Signature], - public_keys: &[PublicKey], - ) -> Self { - assert_eq!(signatures.len(), public_keys.len()); - - let signatures_and_public_keys = signatures - .iter() - .zip(public_keys.iter()) - .map(|(sig, key)| (sig.clone(), key.clone())) - .collect(); - - Self { - signatures_and_public_keys, - proof, - } - } - #[must_use] pub fn signatures_are_valid_for(&self, message: &Message) -> bool { let message_hash = message.hash_message(); diff --git a/nssa/src/public_transaction/witness_set.rs b/nssa/src/public_transaction/witness_set.rs index f9692068..8222c5ed 100644 --- a/nssa/src/public_transaction/witness_set.rs +++ b/nssa/src/public_transaction/witness_set.rs @@ -90,7 +90,7 @@ mod tests { assert_eq!(witness_set.signatures_and_public_keys.len(), 2); - let message_bytes = message.to_bytes(); + let message_bytes = message.hash_message(); for ((signature, public_key), expected_public_key) in witness_set .signatures_and_public_keys .into_iter() diff --git a/python/keycard-py/.gitignore b/python/keycard-py/.gitignore deleted file mode 100644 index fea70884..00000000 --- a/python/keycard-py/.gitignore +++ /dev/null @@ -1,194 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv* -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the enitre vscode folder -# .vscode/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# Cursor -# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to -# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data -# refer to https://docs.cursor.com/context/ignore-files -.cursorignore -.cursorindexingignore \ No newline at end of file diff --git a/python/keycard-py/CHANGELOG.md b/python/keycard-py/CHANGELOG.md deleted file mode 100644 index 3a76f92b..00000000 --- a/python/keycard-py/CHANGELOG.md +++ /dev/null @@ -1,61 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added - -- Support Pairing Mode for Keycard version 3.2 - -### Fixed - -- Older versions not supported. Python 3.10+ now supported - -## [0.3.0] - 2025-08-24 - -### Changed - -- Open Secure Channel also mutually authenticates unless specified otherwise. -- SignatureResult object returned by sign methods -- Identity returns public key. - -## [0.2.0] - 2025-08-06 - -### Added - -- LOAD KEY command -- SET PINLESS PATH command -- GENERATE MNEMONIC command -- DERIVE KEY command - -## [0.1.0] - 2025-08-05 - -### Added - -- INIT command -- IDENT command -- OPEN SECURE CHANNEL command -- MUTUALLY AUTHENTICATE command -- PAIR command -- UNPAIR command -- GET STATUS command -- VERIFY PIN command -- CHANGE PIN command -- UNBLOCK PIN command -- REMOVE KEY command -- GENERATE KEY command -- SIGN command -- EXPORT KEY command -- GET_DATA command -- STORE DATA command -- FACTORY RESET command - - -[unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.3.0...HEAD -[0.3.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.2.0...v0.3.0 -[0.2.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.1.0...v0.2.0 -[0.1.0]: https://github.com/mmlado/keycard-py/releases/tag/v0.1.0 \ No newline at end of file diff --git a/python/keycard-py/LICENSE b/python/keycard-py/LICENSE deleted file mode 100644 index a49f04cf..00000000 --- a/python/keycard-py/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 mmlado - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -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. diff --git a/python/keycard-py/README.md b/python/keycard-py/README.md deleted file mode 100644 index 2ad7df90..00000000 --- a/python/keycard-py/README.md +++ /dev/null @@ -1,27 +0,0 @@ -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) [![codecov](https://codecov.io/gh/mmlado/keycard-py/branch/main/graph/badge.svg)](https://codecov.io/gh/mmlado/keycard-py) [![PyPI version](https://img.shields.io/pypi/v/keycard.svg)](https://pypi.org/project/keycard/) [![Build status](https://github.com/mmlado/keycard-py/actions/workflows/publish.yml/badge.svg)](https://github.com/mmlado/keycard-py/actions/workflows/publish.yml) [![Documentation](https://img.shields.io/badge/docs-gh--pages-blue.svg)](https://mmlado.github.io/keycard-py/) ![stars](https://img.shields.io/github/stars/mmlado/keycard-py.svg?style=social) ![lastcommit](https://img.shields.io/github/last-commit/mmlado/keycard-py.svg) ![numcontributors](https://img.shields.io/github/contributors-anon/mmlado/keycard-py.svg) - -A minimal, clean, fully native Python SDK for communicating with [Keycard](https://keycard.tech) smart cards. - -## Requirements - -- Python 3.10 or higher -- The library is tested on Python 3.10, 3.11, 3.12, and 3.13 - -## Installation - -```bash -git clone https://github.com/mmlado/keycard-py.git -cd keycard-py -python -m venv venv -source venv/bin/activate -pip install -e . -pytest -``` - -## License - -MIT - -## Contributions - -Contributions are welcome as this SDK grows. \ No newline at end of file diff --git a/python/keycard-py/docs/conf.py b/python/keycard-py/docs/conf.py deleted file mode 100644 index c11d3d65..00000000 --- a/python/keycard-py/docs/conf.py +++ /dev/null @@ -1,36 +0,0 @@ - -import os -import sys -sys.path.insert(0, os.path.abspath('../../')) - -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -project = 'KeyCard.py' -copyright = '2025, mmlado' -author = 'mmlado' - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx_autodoc_typehints', - 'sphinx.ext.napoleon', # Google/NumPy style docstrings -] - -templates_path = ['_templates'] -exclude_patterns = [] - - - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = 'alabaster' -html_static_path = ['_static'] diff --git a/python/keycard-py/docs/index.rst b/python/keycard-py/docs/index.rst deleted file mode 100644 index cd2e7345..00000000 --- a/python/keycard-py/docs/index.rst +++ /dev/null @@ -1,21 +0,0 @@ -.. KeyCard.py documentation master file, created by - sphinx-quickstart on Thu Jun 26 13:32:43 2025. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -KeyCard.py documentation -======================== - -Add your content using ``reStructuredText`` syntax. See the -`reStructuredText `_ -documentation for details. - - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - modules - -.. automodule:: keycard.keycard - :members: \ No newline at end of file diff --git a/python/keycard-py/docs/keycard.commands.rst b/python/keycard-py/docs/keycard.commands.rst deleted file mode 100644 index d99d666c..00000000 --- a/python/keycard-py/docs/keycard.commands.rst +++ /dev/null @@ -1,77 +0,0 @@ -keycard.commands package -======================== - -Submodules ----------- - -keycard.commands.ident module ------------------------------ - -.. automodule:: keycard.commands.ident - :members: - :undoc-members: - :show-inheritance: - -keycard.commands.init module ------------------------------ - -.. automodule:: keycard.commands.init - :members: - :undoc-members: - :show-inheritance: - -keycard.commands.mutually_authenticate module ------------------------------ - -.. automodule:: keycard.commands.mutually_authenticate - :members: - :undoc-members: - :show-inheritance: - -keycard.commands.open_secure_channel module ------------------------------ - -.. automodule:: keycard.commands.open_secure_channel - :members: - :undoc-members: - :show-inheritance: - -keycard.commands.pair module ------------------------------ - -.. automodule:: keycard.commands.pair - :members: - :undoc-members: - :show-inheritance: - -keycard.commands.select module ------------------------------ - -.. automodule:: keycard.commands.select - :members: - :undoc-members: - :show-inheritance: - -keycard.commands.unpair module ------------------------------ - -.. automodule:: keycard.commands.unpair - :members: - :undoc-members: - :show-inheritance: - -keycard.commands.verify_pin module ------------------------------ - -.. automodule:: keycard.commands.verify_pin - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: keycard.commands - :members: - :show-inheritance: - :undoc-members: diff --git a/python/keycard-py/docs/keycard.crypto.rst b/python/keycard-py/docs/keycard.crypto.rst deleted file mode 100644 index 1725b670..00000000 --- a/python/keycard-py/docs/keycard.crypto.rst +++ /dev/null @@ -1,29 +0,0 @@ -keycard.crypto package -====================== - -Submodules ----------- - -keycard.crypto.aes module -------------------------- - -.. automodule:: keycard.crypto.aes - :members: - :show-inheritance: - :undoc-members: - -keycard.crypto.padding module ------------------------------ - -.. automodule:: keycard.crypto.padding - :members: - :show-inheritance: - :undoc-members: - -Module contents ---------------- - -.. automodule:: keycard.crypto - :members: - :show-inheritance: - :undoc-members: diff --git a/python/keycard-py/docs/keycard.parsing.rst b/python/keycard-py/docs/keycard.parsing.rst deleted file mode 100644 index 2252c043..00000000 --- a/python/keycard-py/docs/keycard.parsing.rst +++ /dev/null @@ -1,45 +0,0 @@ -keycard.parsing package -======================= - -Submodules ----------- - -keycard.parsing.application\_info module ----------------------------------------- - -.. automodule:: keycard.parsing.application_info - :members: - :show-inheritance: - :undoc-members: - -keycard.parsing.capabilities module ------------------------------------ - -.. automodule:: keycard.parsing.capabilities - :members: - :show-inheritance: - :undoc-members: - -keycard.parsing.identity module -------------------------------- - -.. automodule:: keycard.parsing.identity - :members: - :show-inheritance: - :undoc-members: - -keycard.parsing.tlv module --------------------------- - -.. automodule:: keycard.parsing.tlv - :members: - :show-inheritance: - :undoc-members: - -Module contents ---------------- - -.. automodule:: keycard.parsing - :members: - :show-inheritance: - :undoc-members: diff --git a/python/keycard-py/docs/keycard.rst b/python/keycard-py/docs/keycard.rst deleted file mode 100644 index 2d743859..00000000 --- a/python/keycard-py/docs/keycard.rst +++ /dev/null @@ -1,63 +0,0 @@ -keycard package -=============== - -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - - keycard.commands - keycard.crypto - keycard.parsing - -Submodules ----------- - -keycard.apdu module -------------------- - -.. automodule:: keycard.apdu - :members: - :show-inheritance: - :undoc-members: - -keycard.constants module ------------------------- - -.. automodule:: keycard.constants - :members: - :show-inheritance: - :undoc-members: - -keycard.exceptions module -------------------------- - -.. automodule:: keycard.exceptions - :members: - :show-inheritance: - :undoc-members: - -keycard.keycard module ----------------------- - -.. automodule:: keycard.keycard - :members: - :show-inheritance: - :undoc-members: - -keycard.transport module ------------------------- - -.. automodule:: keycard.transport - :members: - :show-inheritance: - :undoc-members: - -Module contents ---------------- - -.. automodule:: keycard - :members: - :show-inheritance: - :undoc-members: diff --git a/python/keycard-py/docs/modules.rst b/python/keycard-py/docs/modules.rst deleted file mode 100644 index e50e4a79..00000000 --- a/python/keycard-py/docs/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -keycard -======= - -.. toctree:: - :maxdepth: 4 - - keycard diff --git a/python/keycard-py/example/example.py b/python/keycard-py/example/example.py deleted file mode 100644 index e3237df4..00000000 --- a/python/keycard-py/example/example.py +++ /dev/null @@ -1,196 +0,0 @@ -import hashlib -import hmac -import os - -from ecdsa import SigningKey, VerifyingKey, SECP256k1, util -from hashlib import sha256 -from mnemonic import Mnemonic - -from keycard import constants -from keycard.exceptions import APDUError -from keycard.keycard import KeyCard - -PIN = '123456' -PUK = '123456123456' -PAIRING_PASSWORD = 'KeycardTest' - -def bip32_master_key(seed: bytes): - I = hmac.new(b"Bitcoin seed", seed, hashlib.sha512).digest() - master_priv_key = I[:32] - master_chain_code = I[32:] - return master_priv_key, master_chain_code - - -def get_uncompressed_pubkey(priv_key_bytes: bytes): - sk = SigningKey.from_string(priv_key_bytes, curve=SECP256k1) - vk = sk.verifying_key - return b'\x04' + vk.to_string() - - -with KeyCard() as card: - card.select() - print('Retrieving data...') - retrieved_data = card.get_data(slot=constants.StorageSlot.PUBLIC) - print(f'Retrieved data: {retrieved_data}') - try: - print('Factory resetting card...') - card.factory_reset() - except APDUError as e: - print(f'Factory reset failed: {e}') - else: - print(card.select()) - - card.init(PIN, PUK, PAIRING_PASSWORD) - print('Card initialized.') - print(card.select()) - - print('Identifying...') - ident_public_key = card.ident() - print(f'Identity public key: {ident_public_key.hex()}') - - print('Pairing...') - pairing_index, pairing_key = card.pair(PAIRING_PASSWORD) - print(f'Paired. Index: {pairing_index}') - print(f'{pairing_key.hex()=}') - - card.open_secure_channel(pairing_index, pairing_key) - print('Secure channel established.') - - print(card.status) - - print("Generating mnemonic") - indexes = card.generate_mnemonic() - print("Generated list: ", ", ".join(str(m) for m in indexes)) - mnemo = Mnemonic("english") - words = [mnemo.wordlist[i] for i in indexes] - print("Mnemonic: ", " ".join(words)) - - print('Unblocking PIN...') - card.verify_pin('654321') - card.verify_pin('654321') - try: - card.verify_pin('654321') - except RuntimeError as e: - print(f'PIN verification failed: {e}') - card.unblock_pin(PUK, PIN) - print('PIN unblocked.') - - card.verify_pin(PIN) - print('PIN verified.') - - print('Generating key...') - key = b'0x04' + card.generate_key() - print(f'Generated key: {key.hex()}') - - print('Exporting key...') - exported_key = card.export_current_key(True) - print(f'Exported key: {exported_key.public_key.hex()}') - if exported_key.private_key: - print(f'Private key: {exported_key.private_key.hex()}') - if exported_key.chain_code: - print(f'Chain code: {exported_key.chain_code.hex()}') - - digest = sha256(b'This is a test message.').digest() - print(f'Digest: {digest.hex()}') - signature = card.sign(digest) - print(f'Signature: {signature}') - - vk = VerifyingKey.from_string(exported_key.public_key, curve=SECP256k1) - try: - vk.verify_digest( - signature.signature_der, digest, sigdecode=util.sigdecode_der) - print('Signature verified successfully.') - except Exception as e: - print(f"Signature verification failed: {e}") - - print("Set pinless path...") - card.set_pinless_path("m/44'/60'/0'/0/0") - - print("Sign with pinless path...") - print(f'Digest: {digest.hex()}') - signature = card.sign_pinless(digest) - print(f'Signature: {signature}') - - exported_key = card.export_key( - derivation_option=constants.DerivationOption.DERIVE, - public_only=True, - keypath="m/44'/60'/0'/0/0" - ) - - vk = VerifyingKey.from_string(exported_key.public_key, curve=SECP256k1) - try: - vk.verify_digest( - signature.signature_der, digest, sigdecode=util.sigdecode_der) - print('Signature verified successfully.') - except Exception as e: - print(f"Signature verification failed: {e}") - - - print("Load key...") - sk = SigningKey.generate(curve=SECP256k1) - vk = sk.verifying_key - public_key = b'\x04' + vk.to_string() - - result = card.load_key( - key_type=constants.LoadKeyType.ECC, - public_key=public_key, - private_key=sk.to_string() - ) - - uid = sha256(public_key).digest() - if (result == uid): - print("Received public key hash is the same") - else: - print("Received public key hash is not the same") - - print("Loading key from mnemonic...") - mnemonic = ( - "gravity machine north sort system female " - "filter attitude volume fold club stay" - ) - passphrase = "" - mnemo = Mnemonic("english") - seed = mnemo.to_seed(mnemonic, passphrase) - - master_priv_key, master_chain_code = bip32_master_key(seed) - pubkey = get_uncompressed_pubkey(master_priv_key) - uid = hashlib.sha256(pubkey).digest() - - result = card.load_key( - key_type=constants.LoadKeyType.BIP39_SEED, - bip39_seed=seed - ) - - if (result == uid): - print("Received public key hash is the same") - else: - print("Received public key hash is not the same") - - print("Deriving key...") - card.derive_key("m/44'/60'/0'/0/0") - - card.change_pin(PIN) - print('PIN changed.') - - card.change_puk(PUK) - print('PUK changed.') - - card.change_pairing_secret(PAIRING_PASSWORD) - print('Pairing secret changed.') - - print('Storing data...') - data = b'This is some test data.' - card.store_data(data, slot=constants.StorageSlot.PUBLIC) - print('Data stored.') - - print('Retrieving data...') - retrieved_data = card.get_data(slot=constants.StorageSlot.PUBLIC) - print(f'Retrieved data: {retrieved_data}') - - print('Removing key...') - card.remove_key() - print('Key removed.') - - print('Unpairing...') - card.unpair(pairing_index) - print(f'Unpaired index {pairing_index}.') diff --git a/python/keycard-py/keycard/__init__.py b/python/keycard-py/keycard/__init__.py deleted file mode 100644 index e20f382a..00000000 --- a/python/keycard-py/keycard/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""KeyCard Python SDK - APDU communication and cryptographic utilities.""" - -__version__ = "0.3.0" -__doc__ = "Python SDK for interacting with Status Keycard." diff --git a/python/keycard-py/keycard/apdu.py b/python/keycard-py/keycard/apdu.py deleted file mode 100644 index 05a9d8ff..00000000 --- a/python/keycard-py/keycard/apdu.py +++ /dev/null @@ -1,48 +0,0 @@ -''' -This module provides classes and functions for handling APDU (Application -Protocol Data Unit) responses and encoding data in LV (Length-Value) format. -''' - -from dataclasses import dataclass - - -@dataclass -class APDUResponse: - ''' - Represents a response to an APDU (Application Protocol Data Unit) command. - - Attributes: - data (bytes): The response data returned from the APDU command. - status_word (int): The status word indicating the result of the APDU - command. - ''' - data: bytes - status_word: int - - def __str__(self) -> str: - return ( - f'APDUResponse(data={bytes(self.data).hex()}, ' - f'status_word={hex(self.status_word)})' - ) - - -def encode_lv(value: bytes) -> bytes: - ''' - Encodes the given bytes using LV (Length-Value) encoding. - - The function prepends the length of the input bytes as a single byte, - followed by the value itself. The maximum supported length is 255 bytes. - - Args: - value (bytes): The data to encode. - - Returns: - bytes: The LV-encoded bytes. - - Raises: - ValueError: If the input exceeds 255 bytes in length. - ''' - if len(value) > 255: - raise ValueError('LV encoding supports up to 255 bytes') - - return bytes([len(value)]) + value diff --git a/python/keycard-py/keycard/card_interface.py b/python/keycard-py/keycard/card_interface.py deleted file mode 100644 index f699d8f3..00000000 --- a/python/keycard-py/keycard/card_interface.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Optional, Protocol, runtime_checkable - - -@runtime_checkable -class CardInterface(Protocol): - ''' - Abstract base class representing a Keycard interface for command functions. - ''' - card_public_key: Optional[bytes] - - @property - def is_initialized(self) -> bool: ... - - @property - def is_secure_channel_open(self) -> bool: ... - - @property - def is_pin_verified(self) -> bool: ... - - @property - def is_selected(self) -> bool: ... - - def send_apdu( - self, - ins: int, - p1: int = 0x00, - p2: int = 0x00, - data: bytes = b'', - cla: Optional[int] = None - ) -> bytes: ... - - def send_secure_apdu( - self, - ins: int, - p1: int = 0x00, - p2: int = 0x00, - data: bytes = b'' - ) -> bytes: ... diff --git a/python/keycard-py/keycard/commands/__init__.py b/python/keycard-py/keycard/commands/__init__.py deleted file mode 100644 index 359388d6..00000000 --- a/python/keycard-py/keycard/commands/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -from .change_secret import change_secret -from .derive_key import derive_key -from .export_key import export_key -from .export_lee_key import export_lee_key -from .factory_reset import factory_reset -from .generate_key import generate_key -from .generate_mnemonic import generate_mnemonic -from .get_data import get_data -from .ident import ident -from .init import init -from .get_status import get_status -from .load_key import load_key -from .mutually_authenticate import mutually_authenticate -from .open_secure_channel import open_secure_channel -from .pair import pair -from .remove_key import remove_key -from .select import select -from .set_pinless_path import set_pinless_path -from .sign import sign -from .store_data import store_data -from .unblock_pin import unblock_pin -from .unpair import unpair -from .verify_pin import verify_pin - -__all__ = [ - 'change_secret', - 'derive_key', - 'export_key', - 'export_lee_key', - 'factory_reset', - 'generate_key', - 'generate_mnemonic', - 'get_data', - 'ident', - 'init', - 'get_status', - 'load_key', - 'mutually_authenticate', - 'open_secure_channel', - 'pair', - 'remove_key', - 'select', - 'set_pinless_path', - 'sign', - 'store_data', - 'unblock_pin', - 'unpair', - 'verify_pin', -] diff --git a/python/keycard-py/keycard/commands/change_secret.py b/python/keycard-py/keycard/commands/change_secret.py deleted file mode 100644 index 97cd3799..00000000 --- a/python/keycard-py/keycard/commands/change_secret.py +++ /dev/null @@ -1,46 +0,0 @@ -from .. import constants -from ..card_interface import CardInterface -from ..preconditions import require_pin_verified -from ..crypto.generate_pairing_token import generate_pairing_token - - -@require_pin_verified -def change_secret( - card: CardInterface, - new_value: bytes | str, - pin_type: constants.PinType -) -> None: - """ - Changes the specified secret (PIN, PUK, PAIRING) or secret on the card. - - Preconditions: - - Secure Channel must be opened - - User PIN must be verified - - Args: - card: The card session object. - new_value (bytes | str): The new PIN/PUK/secret. - pin_type (PinType): Type of PIN (USER, PUK, or PAIRING) - - Raises: - ValueError: If input format is invalid. - APDUError: If the card returns an error status word. - """ - if pin_type == constants.PinType.PAIRING: - if isinstance(new_value, str): - new_value = generate_pairing_token(new_value) - elif len(new_value) != 32: - raise ValueError("Pairing secret must be 32 bytes.") - elif isinstance(new_value, str): - new_value = new_value.encode("utf-8") - - if pin_type == constants.PinType.USER and len(new_value) != 6: - raise ValueError("User PIN must be exactly 6 digits.") - elif pin_type == constants.PinType.PUK and len(new_value) != 12: - raise ValueError("PUK must be exactly 12 digits.") - - card.send_secure_apdu( - ins=constants.INS_CHANGE_SECRET, - p1=pin_type.value, - data=new_value - ) diff --git a/python/keycard-py/keycard/commands/derive_key.py b/python/keycard-py/keycard/commands/derive_key.py deleted file mode 100644 index ba4ef93a..00000000 --- a/python/keycard-py/keycard/commands/derive_key.py +++ /dev/null @@ -1,25 +0,0 @@ -from ..card_interface import CardInterface -from ..constants import INS_DERIVE_KEY -from ..parsing.keypath import KeyPath -from ..preconditions import require_pin_verified - - -@require_pin_verified -def derive_key(card: CardInterface, path: str = '') -> None: - """ - Set the derivation path for subsequent SIGN and EXPORT KEY commands. - - Args: - card (CardInterface): The card interface. - path (str): BIP-32-style path (e.g., "m/44'/60'/0'/0/0") or - "../0/1" (parent) or "./0" (current). - - Raises: - APDUError: if the derivation fails or the format is invalid. - """ - keypath = KeyPath(path) - card.send_secure_apdu( - ins=INS_DERIVE_KEY, - p1=keypath.source, - data=keypath.data - ) diff --git a/python/keycard-py/keycard/commands/export_key.py b/python/keycard-py/keycard/commands/export_key.py deleted file mode 100644 index 3a72f572..00000000 --- a/python/keycard-py/keycard/commands/export_key.py +++ /dev/null @@ -1,80 +0,0 @@ -from typing import Optional, Union - -from .. import constants -from ..card_interface import CardInterface -from ..constants import DerivationOption, KeyExportOption, DerivationSource -from ..parsing import tlv -from ..parsing.exported_key import ExportedKey -from ..parsing.keypath import KeyPath -from ..preconditions import require_pin_verified - - -@require_pin_verified -def export_key( - card: CardInterface, - derivation_option: constants.DerivationOption, - public_only: bool, - keypath: Optional[Union[str, bytes, bytearray]] = None, - make_current: bool = False, - source: DerivationSource = DerivationSource.MASTER -) -> ExportedKey: - """ - Export a key (public or private) from the card using an optional keypath. - - If derivation_option == CURRENT, keypath can be omitted or empty. - - Args: - card: The card object - derivation_option: e.g. DERIVE, CURRENT, DERIVE_AND_MAKE_CURRENT - public_only: If True, export only public key - keypath: BIP32-style string or packed bytes, or None if CURRENT - make_current: Whether to update the card's current path - source: MASTER (0x00), PARENT (0x40), CURRENT (0x80) - - Returns: - dict with optional 'public_key', 'private_key', 'chain_code' - """ - if keypath is None: - if derivation_option != constants.DerivationOption.CURRENT: - raise ValueError( - "Keypath required unless using CURRENT derivation") - data = b"" - elif isinstance(keypath, str): - data = KeyPath(keypath).data - elif isinstance(keypath, (bytes, bytearray)): - if len(keypath) % 4 != 0: - raise ValueError("Byte keypath must be a multiple of 4") - data = bytes(keypath) - else: - raise TypeError("Keypath must be a string or bytes") - - if make_current: - p1 = DerivationOption.DERIVE_AND_MAKE_CURRENT - else: - p1 = derivation_option - p1 |= source - - if public_only: - p2 = KeyExportOption.PUBLIC_ONLY - else: - p2 = KeyExportOption.PRIVATE_AND_PUBLIC - - response = card.send_secure_apdu( - ins=constants.INS_EXPORT_KEY, - p1=p1, - p2=p2, - data=data - ) - - outer = tlv.parse_tlv(response) - tpl = outer.get(0xA1) - if not tpl: - raise ValueError("Missing keypair template (tag 0xA1)") - - inner = tlv.parse_tlv(tpl[0]) - - return ExportedKey( - public_key=inner.get(0x80, [None])[0], - private_key=inner.get(0x81, [None])[0], - chain_code=inner.get(0x82, [None])[0], - ) diff --git a/python/keycard-py/keycard/commands/export_lee_key.py b/python/keycard-py/keycard/commands/export_lee_key.py deleted file mode 100644 index 4d532360..00000000 --- a/python/keycard-py/keycard/commands/export_lee_key.py +++ /dev/null @@ -1,82 +0,0 @@ -from dataclasses import dataclass -from typing import Optional, Union - -from .. import constants -from ..card_interface import CardInterface -from ..constants import DerivationOption, DerivationSource -from ..parsing import tlv -from ..preconditions import require_pin_verified - - -@dataclass -class ExportedLeeKey: - """Represents a LEE key template containing LEE_NSK and LEE_VSK.""" - lee_nsk: Optional[bytes] = None - lee_vsk: Optional[bytes] = None - - -@require_pin_verified -def export_lee_key( - card: CardInterface, - derivation_option: constants.DerivationOption, - keypath: Optional[Union[str, bytes, bytearray]] = None, - make_current: bool = False, - source: DerivationSource = DerivationSource.MASTER -) -> ExportedLeeKey: - """ - Export a LEE key template from the card. - - The output is a key template (tag 0xA1) containing LEE_NSK (tag 0x83) - and LEE_VSK (tag 0x84). - - If derivation_option == CURRENT, keypath can be omitted or empty. - - Args: - card: The card object - derivation_option: e.g. DERIVE, CURRENT, DERIVE_AND_MAKE_CURRENT - keypath: BIP32-style string or packed bytes, or None if CURRENT - make_current: Whether to update the card's current path - source: MASTER (0x00), PARENT (0x40), CURRENT (0x80) - - Returns: - ExportedLeeKey with lee_nsk and lee_vsk fields - """ - if keypath is None: - if derivation_option != constants.DerivationOption.CURRENT: - raise ValueError( - "Keypath required unless using CURRENT derivation") - data = b"" - elif isinstance(keypath, str): - from ..parsing.keypath import KeyPath - data = KeyPath(keypath).data - elif isinstance(keypath, (bytes, bytearray)): - if len(keypath) % 4 != 0: - raise ValueError("Byte keypath must be a multiple of 4") - data = bytes(keypath) - else: - raise TypeError("Keypath must be a string or bytes") - - if make_current: - p1 = DerivationOption.DERIVE_AND_MAKE_CURRENT - else: - p1 = derivation_option - p1 |= source - - response = card.send_secure_apdu( - ins=constants.INS_EXPORT_LEE_KEY, - p1=p1, - p2=0x00, - data=data - ) - - outer = tlv.parse_tlv(response) - tpl = outer.get(0xA1) - if not tpl: - raise ValueError("Missing keypair template (tag 0xA1)") - - inner = tlv.parse_tlv(tpl[0]) - - return ExportedLeeKey( - lee_nsk=inner.get(0x83, [None])[0], - lee_vsk=inner.get(0x84, [None])[0], - ) diff --git a/python/keycard-py/keycard/commands/factory_reset.py b/python/keycard-py/keycard/commands/factory_reset.py deleted file mode 100644 index 8a4a296a..00000000 --- a/python/keycard-py/keycard/commands/factory_reset.py +++ /dev/null @@ -1,15 +0,0 @@ -from .. import constants -from ..card_interface import CardInterface -from ..preconditions import require_selected - - -@require_selected -def factory_reset(card: CardInterface) -> None: - ''' - Sends the FACTORY_RESET command to the card. - ''' - card.send_apdu( - ins=constants.INS_FACTORY_RESET, - p1=0xAA, - p2=0x55 - ) diff --git a/python/keycard-py/keycard/commands/generate_key.py b/python/keycard-py/keycard/commands/generate_key.py deleted file mode 100644 index 8afbe295..00000000 --- a/python/keycard-py/keycard/commands/generate_key.py +++ /dev/null @@ -1,27 +0,0 @@ -from .. import constants -from ..card_interface import CardInterface -from ..preconditions import require_secure_channel - - -@require_secure_channel -def generate_key(card: CardInterface) -> bytes: - ''' - Generates a new key on the card and returns the key UID. - - Preconditions: - - Secure Channel must be opened - - PIN must be verified - - Args: - transport: Transport instance for APDU communication - session: SecureChannel instance for wrapping/unwrapping - - Returns: - bytes: Key UID (SHA-256 of the public key) - - Raises: - APDUError: If the response status word is not 0x9000 - ''' - return card.send_secure_apdu( - ins=constants.INS_GENERATE_KEY - ) diff --git a/python/keycard-py/keycard/commands/generate_mnemonic.py b/python/keycard-py/keycard/commands/generate_mnemonic.py deleted file mode 100644 index 1d00210c..00000000 --- a/python/keycard-py/keycard/commands/generate_mnemonic.py +++ /dev/null @@ -1,41 +0,0 @@ -from ..card_interface import CardInterface -from ..constants import INS_GENERATE_MNEMONIC -from ..preconditions import require_secure_channel - - -@require_secure_channel -def generate_mnemonic( - card: CardInterface, - checksum_size: int = 6 -) -> list[int]: - """ - Generate a BIP39 mnemonic using the card's RNG. - - Args: - card (CardInterface): The card interface. - checksum_size (int): Number of checksum bits - (between 4 and 8 inclusive). - - Returns: - List[int]: List of integers (0-2047) corresponding to wordlist - indexes. - - Raises: - ValueError: If checksum size is outside the allowed range. - APDUError: If the card rejects the request. - """ - if not (4 <= checksum_size <= 8): - raise ValueError("Checksum size must be between 4 and 8") - - response = card.send_secure_apdu( - ins=INS_GENERATE_MNEMONIC, - p1=checksum_size - ) - - if len(response) % 2 != 0: - raise ValueError("Response must contain an even number of bytes") - - return [ - (response[i] << 8) | response[i + 1] - for i in range(0, len(response), 2) - ] diff --git a/python/keycard-py/keycard/commands/get_data.py b/python/keycard-py/keycard/commands/get_data.py deleted file mode 100644 index 6478773c..00000000 --- a/python/keycard-py/keycard/commands/get_data.py +++ /dev/null @@ -1,35 +0,0 @@ -from .. import constants -from ..card_interface import CardInterface -from ..preconditions import require_selected - - -@require_selected -def get_data( - card: CardInterface, - slot: constants.StorageSlot = constants.StorageSlot.PUBLIC -) -> bytes: - """ - Gets the data on the card previously stored with the store data command - in the specified slot. - - If the secure channel is open, it uses the secure APDU command. - Otherwise, it uses the proprietary APDU command. - - Args: - card: The card session object. - slot (StorageSlot): Where to store the data (PUBLIC, NDEF, CASH) - - Raises: - ValueError: If slot is invalid or data is too long. - """ - if card.is_secure_channel_open: - return card.send_secure_apdu( - ins=constants.INS_GET_DATA, - p1=slot.value - ) - - return card.send_apdu( - cla=constants.CLA_PROPRIETARY, - ins=constants.INS_GET_DATA, - p1=slot.value - ) diff --git a/python/keycard-py/keycard/commands/get_status.py b/python/keycard-py/keycard/commands/get_status.py deleted file mode 100644 index e887a604..00000000 --- a/python/keycard-py/keycard/commands/get_status.py +++ /dev/null @@ -1,63 +0,0 @@ -from .. import constants -from ..card_interface import CardInterface -from ..parsing import tlv -from ..preconditions import require_secure_channel - - -@require_secure_channel -def get_status( - card: CardInterface, - key_path: bool = False -) -> dict[str, int | bool] | list[int]: - ''' - Query the application status or key path from the Keycard. - - Requires an open Secure Channel. - - Args: - transport: Transport instance used to send APDU bytes. - session: An established SecureChannel instance. - key_path (bool): If True, returns the current key path. - If False (default), returns application status. - - Returns: - If key_path is False: - dict with keys: - - pin_retry_count (int) - - puk_retry_count (int) - - initialized (bool) - - If key_path is True: - List of 32-bit integers representing the current key path. - - Raises: - APDUError: If the response status word is not 0x9000. - ValueError: If the application status template (tag 0xA3) is missing. - ''' - response: bytes = card.send_secure_apdu( - ins=constants.INS_GET_STATUS, - p1=0x01 if key_path else 0x00, - ) - - if key_path: - return [ - int.from_bytes(response[i:i+4], 'big') - for i in range(0, len(response), 4) - ] - - outer = tlv.parse_tlv(response) - - if 0xA3 not in outer: - raise ValueError('Missing tag 0xA3 (Application Status Template)') - - inner = tlv.parse_tlv(outer[0xA3][0]) - - pin_retry = inner[0x02][0] or b'\xff' - puk_retry = inner[0x02][1] or b'\xff' - initialized = inner[0x01][0] != b'\x00' - - return { - 'pin_retry_count': pin_retry[0] if pin_retry else 0xff, - 'puk_retry_count': puk_retry[0] if puk_retry else 0xff, - 'initialized': initialized - } diff --git a/python/keycard-py/keycard/commands/ident.py b/python/keycard-py/keycard/commands/ident.py deleted file mode 100644 index aaa72d33..00000000 --- a/python/keycard-py/keycard/commands/ident.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -from typing import Optional - -from .. import constants -from ..card_interface import CardInterface -from ..parsing.identity import parse -from ..preconditions import require_selected - - -@require_selected -def ident(card: CardInterface, challenge: Optional[bytes]) -> bytes: - ''' - Sends a challenge to the card to receive a signed identity response. - - Args: - transport: An instance of the Transport class to communicate with - the card. - challenge (bytes): A challenge (nonce or data) to send to the card. - If None, a random 32-byte challenge is generated. - - Returns: - bytes: The public key extracted from the card's identity response. - - Raises: - APDUError: If the response status word is not successful (0x9000). - ''' - challenge = challenge or os.urandom(32) - - response: bytes = card.send_apdu( - ins=constants.INS_IDENT, - data=challenge - ) - - return parse(challenge, response) diff --git a/python/keycard-py/keycard/commands/init.py b/python/keycard-py/keycard/commands/init.py deleted file mode 100644 index 921d0cb3..00000000 --- a/python/keycard-py/keycard/commands/init.py +++ /dev/null @@ -1,79 +0,0 @@ -from os import urandom -from ecdsa import SigningKey, VerifyingKey, ECDH, SECP256k1 - -from .. import constants -from ..card_interface import CardInterface -from ..crypto.aes import aes_cbc_encrypt -from ..crypto.generate_pairing_token import generate_pairing_token -from ..exceptions import NotSelectedError -from ..preconditions import require_selected - - -@require_selected -def init( - card: CardInterface, - pin: str | bytes, - puk: str | bytes, - pairing_secret: str | bytes -) -> None: - ''' - Initializes a Keycard device with PIN, PUK, and pairing secret. - - Establishes an ephemeral ECDH key exchange and sends encrypted - credentials to the card. - - Args: - transport: The transport used to send APDU commands to the card. - card_public_key (bytes): The card's ECC public key, usually - retrieved via select(). - pin (bytes): The personal identification number (PIN) as bytes. - puk (bytes): The personal unblocking key (PUK) as bytes. - pairing_secret (bytes): A 32-byte shared secret or a passphrase that - will be converted into one. - - Raises: - NotSelectedError: If no card public key is provided. - ValueError: If the encrypted data exceeds a single APDU length. - APDUError: If the card returns a failure status word. - ''' - if card.card_public_key is None: - raise NotSelectedError('Card not selected. Call select() first.') - - if not isinstance(pin, bytes): - pin = pin.encode('ascii') - if not isinstance(puk, bytes): - puk = puk.encode('ascii') - if not isinstance(pairing_secret, bytes): - pairing_secret = generate_pairing_token(pairing_secret) - - ephemeral_key = SigningKey.generate(curve=SECP256k1) - our_pubkey_bytes: bytes = \ - ephemeral_key.verifying_key.to_string('uncompressed') - card_pubkey = VerifyingKey.from_string( - card.card_public_key, - curve=SECP256k1 - ) - ecdh = ECDH( - curve=SECP256k1, - private_key=ephemeral_key, - public_key=card_pubkey - ) - shared_secret = ecdh.generate_sharedsecret_bytes() - - plaintext: bytes = pin + puk + pairing_secret - iv: bytes = urandom(16) - ciphertext: bytes = aes_cbc_encrypt(shared_secret, iv, plaintext) - data: bytes = ( - bytes([len(our_pubkey_bytes)]) - + our_pubkey_bytes - + iv - + ciphertext - ) - - if len(data) > 255: - raise ValueError('Data too long for single APDU') - - card.send_apdu( - ins=constants.INS_INIT, - data=data - ) diff --git a/python/keycard-py/keycard/commands/load_key.py b/python/keycard-py/keycard/commands/load_key.py deleted file mode 100644 index 8e09a255..00000000 --- a/python/keycard-py/keycard/commands/load_key.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import Optional - -from .. import constants -from ..card_interface import CardInterface -from ..parsing import tlv -from ..preconditions import require_pin_verified - - -@require_pin_verified -def load_key( - card: CardInterface, - key_type: constants.LoadKeyType, - public_key: Optional[bytes] = None, - private_key: Optional[bytes] = None, - chain_code: Optional[bytes] = None, - bip39_seed: Optional[bytes] = None, - lee_seed: Optional[bytes] = None -) -> bytes: - """ - Load a key into the card for signing purposes. - - Args: - card: The card interface. - key_type: Key type - public_key: Optional ECC public key (tag 0x80). - private_key: ECC private key (tag 0x81). - chain_code: Optional chain code (tag 0x82, only for extended key). - bip39_seed: 64-byte BIP39 seed (only for key_type=BIP39_SEED). - lee_seed: 64-byte LEE seed (only for key_type=BIP39_SEED). - - Returns: - UID of the loaded key (SHA-256 of public key). - """ - if key_type == constants.LoadKeyType.BIP39_SEED: - if bip39_seed is None and lee_seed is None: - raise ValueError( - "Either bip39_seed or lee_seed must be provided for key_type = BIP39_SEED") - data = bip39_seed if bip39_seed is not None else lee_seed - - if data is not None and len(data) > 64 or len(data) < 16: - raise ValueError( - "BIP39/LEE seed must be 16-64 bytes") - else: - inner_tlv = [] - if public_key is not None: - inner_tlv.append(tlv.encode_tlv(0x80, public_key)) - if private_key is None: - raise ValueError("Private key (tag 0x81) is required") - inner_tlv.append(tlv.encode_tlv(0x81, private_key)) - if ( - key_type == constants.LoadKeyType.EXTENDED_ECC and - chain_code is not None - ): - inner_tlv.append(tlv.encode_tlv(0x82, chain_code)) - tpl = tlv.encode_tlv(0xA1, b''.join(inner_tlv)) - data = tpl - - response = card.send_secure_apdu( - ins=constants.INS_LOAD_KEY, - p1=key_type, - p2=1 if lee_seed is not None else 0, - data=data - ) - - return response diff --git a/python/keycard-py/keycard/commands/mutually_authenticate.py b/python/keycard-py/keycard/commands/mutually_authenticate.py deleted file mode 100644 index 84379d86..00000000 --- a/python/keycard-py/keycard/commands/mutually_authenticate.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -from ..card_interface import CardInterface -from .. import constants -from ..preconditions import require_secure_channel -from typing import Optional - - -@require_secure_channel -def mutually_authenticate( - card: CardInterface, - client_challenge: Optional[bytes] = None -) -> None: - ''' - Performs mutual authentication between the client and the Keycard. - - Preconditions: - - Secure Channel must be opened - - The card will respond with a cryptographic challenge. The secure - session will verify the response. If the response is not exactly - 32 bytes, or if the response has an unexpected status word, the - function raises an error. - - Args: - transport: A Transport instance for sending APDUs. - session: A SecureChannel instance used for wrapping/unwrapping. - client_challenge (bytes, optional): Optional challenge bytes. - If not provided, a random 32-byte value will be generated. - - Raises: - APDUError: If the response status word is not 0x9000. - ValueError: If the decrypted response is not exactly 32 bytes. - ''' - client_challenge = client_challenge or os.urandom(32) - - response: bytes = card.send_secure_apdu( - ins=constants.INS_MUTUALLY_AUTHENTICATE, - data=client_challenge - ) - - if len(response) != 32: - raise ValueError( - 'Response to MUTUALLY AUTHENTICATE is not 32 bytes') diff --git a/python/keycard-py/keycard/commands/open_secure_channel.py b/python/keycard-py/keycard/commands/open_secure_channel.py deleted file mode 100644 index 6ce0fdee..00000000 --- a/python/keycard-py/keycard/commands/open_secure_channel.py +++ /dev/null @@ -1,68 +0,0 @@ -from ecdsa import SigningKey, VerifyingKey, SECP256k1, ECDH - -from .. import constants -from ..card_interface import CardInterface -from ..exceptions import NotSelectedError -from ..secure_channel import SecureChannel -from ..preconditions import require_initialized - - -@require_initialized -def open_secure_channel( - card: CardInterface, - pairing_index: int, - pairing_key: bytes -) -> SecureChannel: - ''' - Opens a secure session with the Keycard using ECDH and a pairing key. - - This function performs an ephemeral ECDH key exchange with the card, - sends the ephemeral public key, and receives cryptographic material - from the card to derive a secure session. - - Args: - transport: The transport used to communicate with the card. - card_public_key (bytes): The ECC public key of the card, retrieved - via select(). - pairing_index (int): The index of the previously established - pairing slot. - pairing_key (bytes): The shared 32-byte pairing key. - - Returns: - SecureChannel: A newly established secure session with the card. - - Raises: - NotSelectedError: If no card public key is provided. - APDUError: If the card returns a failure status word. - ''' - if not card.card_public_key: - raise NotSelectedError('Card not selected or missing public key') - - ephemeral_key = SigningKey.generate(curve=SECP256k1) - eph_pub_bytes = ephemeral_key.verifying_key.to_string('uncompressed') - response: bytes = card.send_apdu( - ins=constants.INS_OPEN_SECURE_CHANNEL, - p1=pairing_index, - data=eph_pub_bytes - ) - - salt = bytes(response[:32]) - seed_iv = bytes(response[32:]) - - public_key = VerifyingKey.from_string( - card.card_public_key, - curve=SECP256k1 - ) - ecdh = ECDH( - curve=SECP256k1, - private_key=ephemeral_key, - public_key=public_key - ) - shared_secret = ecdh.generate_sharedsecret_bytes() - - return SecureChannel.open( - shared_secret, - pairing_key, - salt, - seed_iv, - ) diff --git a/python/keycard-py/keycard/commands/pair.py b/python/keycard-py/keycard/commands/pair.py deleted file mode 100644 index 5f3dec4f..00000000 --- a/python/keycard-py/keycard/commands/pair.py +++ /dev/null @@ -1,75 +0,0 @@ -import hashlib -from os import urandom -from typing import Optional - -from .. import constants -from ..card_interface import CardInterface -from ..crypto.generate_pairing_token import generate_pairing_token -from ..exceptions import InvalidResponseError -from ..preconditions import require_initialized - - -@require_initialized -def pair( - card: CardInterface, - shared_secret: str | bytes, - pairing_mode: Optional[constants.PairingMode] = constants.PairingMode.ANY -) -> tuple[int, bytes]: - ''' - Performs an ECDH-based pairing handshake with the card. - - Args: - card: The keycard interface. - shared_secret: A 32-byte secret or a passphrase convertible to one. - pairing_mode: Mode for pairing: ANY, EPHEMERAL, PERSISTENT - - Returns: - tuple[int, bytes]: Pairing index and derived 32-byte pairing key. - - Raises: - ValueError: If the shared secret is not 32 bytes. - APDUError: If the card returns a non-success status word. - InvalidResponseError: If response lengths or values are unexpected. - ''' - if isinstance(shared_secret, str): - shared_secret = generate_pairing_token(shared_secret) - - if len(shared_secret) != 32: - raise ValueError('Shared secret must be 32 bytes') - - client_challenge = urandom(32) - - response = card.send_apdu( - ins=constants.INS_PAIR, - p2=pairing_mode, - data=client_challenge - ) - - if len(response) != 64: - raise InvalidResponseError('Unexpected response length') - - card_cryptogram = response[:32] - card_challenge = response[32:] - - expected = hashlib.sha256(shared_secret + client_challenge).digest() - - if card_cryptogram != expected: - raise InvalidResponseError('Card cryptogram mismatch') - - client_cryptogram = hashlib.sha256(shared_secret + card_challenge).digest() - - response = card.send_apdu( - ins=constants.INS_PAIR, - p1=0x01, - data=client_cryptogram - ) - - if len(response) != 33: - raise InvalidResponseError('Unexpected response length') - - pairing_index = response[0] - salt = response[1:] - - pairing_key = hashlib.sha256(shared_secret + salt).digest() - - return pairing_index, pairing_key diff --git a/python/keycard-py/keycard/commands/remove_key.py b/python/keycard-py/keycard/commands/remove_key.py deleted file mode 100644 index fb08698d..00000000 --- a/python/keycard-py/keycard/commands/remove_key.py +++ /dev/null @@ -1,11 +0,0 @@ -from ..card_interface import CardInterface -from ..preconditions import require_pin_verified - - -@require_pin_verified -def remove_key(card: CardInterface) -> None: - ''' - Removes the key from the card, returning it to an uninitialized state. - - ''' - card.send_secure_apdu(ins=0xD3) diff --git a/python/keycard-py/keycard/commands/select.py b/python/keycard-py/keycard/commands/select.py deleted file mode 100644 index 17b64125..00000000 --- a/python/keycard-py/keycard/commands/select.py +++ /dev/null @@ -1,33 +0,0 @@ -from .. import constants -from ..card_interface import CardInterface -from ..parsing.application_info import ApplicationInfo - - -def select(card: CardInterface) -> ApplicationInfo: - ''' - Selects the Keycard application on the smart card and retrieves - application information. - - Sends a SELECT APDU command using the Keycard AID, checks for a - successful response, parses the returned application information, - and returns it. - - Args: - transport: The transport instance used to send the APDU command. - - Returns: - ApplicationInfo: Parsed information about the selected Keycard - application. - - Raises: - APDUError: If the card returns a status word indicating failure. - ''' - result = card.send_apdu( - cla=constants.CLAISO7816, - ins=constants.INS_SELECT, - p1=0x04, - p2=0x00, - data=constants.KEYCARD_AID - ) - - return ApplicationInfo.parse(result) diff --git a/python/keycard-py/keycard/commands/set_pinless_path.py b/python/keycard-py/keycard/commands/set_pinless_path.py deleted file mode 100644 index 729afdf3..00000000 --- a/python/keycard-py/keycard/commands/set_pinless_path.py +++ /dev/null @@ -1,25 +0,0 @@ -from ..card_interface import CardInterface -from ..constants import INS_SET_PINLESS_PATH -from ..parsing.keypath import KeyPath -from ..preconditions import require_pin_verified - - -@require_pin_verified -def set_pinless_path(card: CardInterface, path: str) -> None: - """ - Set a PIN-less path on the card. Allows signing without PIN/auth if the - current derived key matches this path. - - Args: - card (CardInterface): The card interface. - path (str): BIP-32-style path (e.g., "m/44'/60'/0'/0/0"). An empty - string disables the pinless path. - - Raises: - APDUError: if the card rejects the input (invalid path) - """ - keypath = KeyPath(path).data if path else b"" - card.send_secure_apdu( - ins=INS_SET_PINLESS_PATH, - data=keypath - ) diff --git a/python/keycard-py/keycard/commands/sign.py b/python/keycard-py/keycard/commands/sign.py deleted file mode 100644 index 17993612..00000000 --- a/python/keycard-py/keycard/commands/sign.py +++ /dev/null @@ -1,136 +0,0 @@ -from typing import Optional - -from ecdsa.util import sigdecode_der - - -from .. import constants -from ..constants import DerivationOption, DerivationSource, SigningAlgorithm -from ..card_interface import CardInterface -from ..exceptions import InvalidStateError -from ..parsing import tlv -from ..parsing.keypath import KeyPath -from ..parsing.signature_result import SignatureResult - - -def sign( - card: CardInterface, - digest: bytes, - p1: DerivationOption = DerivationOption.CURRENT, - p2: SigningAlgorithm = SigningAlgorithm.ECDSA_SECP256K1, - derivation_path: Optional[str] = None -) -> SignatureResult: - """ - Sign a 32-byte digest using the specified key and signing algorithm. - - This command sends the SIGN APDU to the Keycard and parses the response, - returning a structured `SignatureResult` object. The signature may be - returned as a DER-encoded structure, a raw 65-byte format including - the recovery ID, or an ECDSA template depending on card behavior. - - Preconditions: - - Secure Channel must be opened (unless using PINLESS) - - PIN must be verified (unless using PINLESS) - - A valid keypair must be loaded on the card - - If P1=PINLESS, a PIN-less path must be configured - - Args: - card (CardInterface): Active Keycard transport session. - digest (bytes): 32-byte hash to be signed. - p1 (DerivationOption): Key derivation option. One of: - - CURRENT: Sign with the currently loaded key - - DERIVE: Derive key for signing without changing current - - DERIVE_AND_MAKE_CURRENT: Derive and load for future use - - PINLESS: Use pre-defined PIN-less key without SC/PIN - p2 (SigningAlgorithm): Signing algorithm. Defaults to - ECDSA_SECP256K1. Other options include SCHNORR_BIP340. - derivation_path (Optional[str]): String-formatted BIP32 path - (e.g. "m/44'/60'/0'/0/0"). Required if `p1` uses derivation. - The source (master/parent/current) is inferred from the path - prefix. - - Returns: - SignatureResult: Parsed signature result, including the signature - (DER or raw), algorithm, and optional recovery ID or public key. - - Raises: - ValueError: If the digest is not 32 bytes or path is invalid. - InvalidStateError: If preconditions (PIN, SC) are not met. - APDUError: If the card returns an error (e.g., SW=0x6985). - """ - if p2 not in ( - SigningAlgorithm.ECDSA_SECP256K1, - SigningAlgorithm.SCHNORR_BIP340 - ): - raise NotImplementedError( - f"Signature algorithm {p2} not supported" - ) - - if len(digest) != 32: - raise ValueError("Digest must be exactly 32 bytes") - - if p1 != DerivationOption.PINLESS and not card.is_pin_verified: - raise InvalidStateError( - "PIN must be verified to sign with this derivation option") - - data = digest - source = DerivationSource.MASTER - if p1 in ( - DerivationOption.DERIVE, - DerivationOption.DERIVE_AND_MAKE_CURRENT - ): - if not derivation_path: - raise ValueError("Derivation path cannot be empty") - key_path = KeyPath(derivation_path) - data += key_path.data - source = key_path.source - - response = card.send_secure_apdu( - ins=constants.INS_SIGN, - p1=p1 | source, - p2=p2, - data=data - ) - - if response.startswith(b'\xA0'): - outer = tlv.parse_tlv(response) - inner = tlv.parse_tlv(outer[0xA0][0]) - pub = inner.get(0x80, [None])[0] - - if len(inner.get(0x80, [])) > 1: - return SignatureResult( - algo=p2, - digest=digest, - r=int.from_bytes(inner[0x80][1][:32], "big"), - s=int.from_bytes(inner[0x80][1][32:64], "big"), - recovery_id=-1, - public_key=pub - ) - else: - der_bytes = ( - b'\x30' + - len(inner[0x30][0]).to_bytes(1, 'big') + - inner[0x30][0] - ) - signature = sigdecode_der(der_bytes, 0) - r, s = signature - return SignatureResult( - algo=p2, - digest=digest, - r=r, - s=s, - public_key=pub - ) - elif response.startswith(b'\x80'): - outer = tlv.parse_tlv(response) - raw = outer[0x80][0] - if len(raw) != 65: - raise ValueError("Expected 65-byte raw signature (r||s||recId)") - return SignatureResult( - algo=p2, - digest=digest, - r=int.from_bytes(raw[:32], "big"), - s=int.from_bytes(raw[32:64], "big"), - recovery_id=int(raw[64]) - ) - - raise ValueError("Unexpected SIGN response format") diff --git a/python/keycard-py/keycard/commands/store_data.py b/python/keycard-py/keycard/commands/store_data.py deleted file mode 100644 index 672fe7a6..00000000 --- a/python/keycard-py/keycard/commands/store_data.py +++ /dev/null @@ -1,30 +0,0 @@ -from .. import constants -from ..card_interface import CardInterface -from ..preconditions import require_pin_verified - - -@require_pin_verified -def store_data( - card: CardInterface, - data: bytes, - slot: constants.StorageSlot = constants.StorageSlot.PUBLIC -) -> None: - """ - Stores data on the card in the specified slot. - - Args: - card: The card session object. - data (bytes): The data to store (max 127 bytes). - slot (StorageSlot): Where to store the data (PUBLIC, NDEF, CASH) - - Raises: - ValueError: If slot is invalid or data is too long. - """ - if len(data) > 127: - raise ValueError("Data too long. Maximum allowed is 127 bytes.") - - card.send_secure_apdu( - ins=constants.INS_STORE_DATA, - p1=slot.value, - data=data - ) diff --git a/python/keycard-py/keycard/commands/unblock_pin.py b/python/keycard-py/keycard/commands/unblock_pin.py deleted file mode 100644 index c3707410..00000000 --- a/python/keycard-py/keycard/commands/unblock_pin.py +++ /dev/null @@ -1,32 +0,0 @@ -from .. import constants -from ..card_interface import CardInterface -from ..preconditions import require_secure_channel - - -@require_secure_channel -def unblock_pin(card: CardInterface, puk_and_pin: bytes | str) -> None: - """ - Unblocks the user PIN using the provided PUK and sets a new PIN. - - Args: - card: The card session object. - puk_and_pin (bytes | str): Concatenation of PUK (12 digits) + new PIN - (6 digits) - - Raises: - ValueError: If the format is invalid. - APDUError: If the card returns an error. - """ - if isinstance(puk_and_pin, str): - if not puk_and_pin.isdigit(): - raise ValueError("PUK and PIN must be numeric digits.") - puk_and_pin = puk_and_pin.encode("utf-8") - - if len(puk_and_pin) != 18: - raise ValueError( - "Data must be exactly 18 digits (12-digit PUK + 6-digit PIN).") - - card.send_secure_apdu( - ins=constants.INS_UNBLOCK_PIN, - data=puk_and_pin - ) diff --git a/python/keycard-py/keycard/commands/unpair.py b/python/keycard-py/keycard/commands/unpair.py deleted file mode 100644 index 9266eef1..00000000 --- a/python/keycard-py/keycard/commands/unpair.py +++ /dev/null @@ -1,31 +0,0 @@ -from .. import constants -from ..card_interface import CardInterface -from ..preconditions import require_pin_verified - - -@require_pin_verified -def unpair(card: CardInterface, index: int) -> None: - ''' - Sends the UNPAIR command to remove a pairing index from the card. - - Preconditions: - - Secure Channel must be opened - - PIN must be verified - - This function securely communicates with the card using the established - session to instruct it to forget a specific pairing index. - - Args: - transport: The transport interface used to send APDUs. - secure_session: The active SecureChannel object used to wrap APDUs. - index (int): The pairing index (0–15) to unpair from the card. - - Raises: - ValueError: If transport or secure_session is not provided, or if - the session is not authenticated. - APDUError: If the response status word indicates an error. - ''' - card.send_secure_apdu( - ins=constants.INS_UNPAIR, - p1=index, - ) diff --git a/python/keycard-py/keycard/commands/verify_pin.py b/python/keycard-py/keycard/commands/verify_pin.py deleted file mode 100644 index 4a977300..00000000 --- a/python/keycard-py/keycard/commands/verify_pin.py +++ /dev/null @@ -1,49 +0,0 @@ -from .. import constants -from ..card_interface import CardInterface -from ..exceptions import APDUError -from ..preconditions import require_secure_channel - - -@require_secure_channel -def verify_pin(card: CardInterface, pin: str | bytes) -> bool: - ''' - Verifies the user PIN with the card using a secure session. - - Preconditions: - - Secure Channel must be opened - - PIN must be verified - - Sends the VERIFY PIN APDU command through the secure session. Returns - True if the PIN is correct, False if incorrect with remaining attempts, - and raises an error if blocked or another APDU error occurs. - - Args: - transport: The transport instance used to send the command. - session: An established SecureChannel object. - pin (str): The PIN string to be verified. - - Returns: - bool: True if the PIN is correct, False if incorrect but still allowed. - - Raises: - ValueError: If no secure session is provided. - RuntimeError: If the PIN is blocked (no attempts remaining). - APDUError: For other status word errors returned by the card. - ''' - if not isinstance(pin, bytes): - pin = pin.encode('ascii') - - try: - card.send_secure_apdu( - ins=constants.INS_VERIFY_PIN, - data=pin - ) - except APDUError as e: - if (e.sw & 0xFFF0) == 0x63C0: - attempts = e.sw & 0x000F - if attempts == 0: - raise RuntimeError('PIN is blocked') - return False - raise e - - return True diff --git a/python/keycard-py/keycard/constants.py b/python/keycard-py/keycard/constants.py deleted file mode 100644 index f2867358..00000000 --- a/python/keycard-py/keycard/constants.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -This module defines constants used for communication with the Keycard applet -via APDU commands. -""" - -from enum import IntEnum - - -# Applet AID -KEYCARD_AID: bytes = bytes.fromhex('A000000804000101') - -CLAISO7816: int = 0x00 -CLA_PROPRIETARY: int = 0x80 - -# APDU instructions -INS_SELECT: int = 0xA4 -INS_INIT: int = 0xFE -INS_IDENT: int = 0x14 -INS_OPEN_SECURE_CHANNEL: int = 0x10 -INS_MUTUALLY_AUTHENTICATE: int = 0x11 -INS_PAIR: int = 0x12 -INS_UNPAIR: int = 0x13 -INS_VERIFY_PIN: int = 0x20 -INS_GET_STATUS: int = 0xF2 -INS_FACTORY_RESET: int = 0xFD -INS_GENERATE_KEY: int = 0xD4 -INS_CHANGE_SECRET: int = 0x21 -INS_UNBLOCK_PIN: int = 0x22 -INS_STORE_DATA: int = 0xE2 -INS_GET_DATA: int = 0xCA -INS_SIGN: int = 0xC0 -INS_SET_PINLESS_PATH = 0xC1 -INS_EXPORT_KEY: int = 0xC2 -INS_LOAD_KEY: int = 0xD0 -INS_DERIVE_KEY = 0xD1 -INS_GENERATE_MNEMONIC = 0xD2 -INS_EXPORT_LEE_KEY = 0xC3 - -# Status words -SW_SUCCESS: int = 0x9000 - - -class PinType(IntEnum): - USER = 0x00 - PUK = 0x01 - PAIRING = 0x02 - - -class StorageSlot(IntEnum): - PUBLIC = 0x00 - NDEF = 0x01 - CASH = 0x02 - - -class DerivationOption(IntEnum): - CURRENT = 0x00 - DERIVE = 0x01 - DERIVE_AND_MAKE_CURRENT = 0x02 - PINLESS = 0x03 - - -class KeyExportOption(IntEnum): - PRIVATE_AND_PUBLIC = 0x00 - PUBLIC_ONLY = 0x01 - EXTENDED_PUBLIC = 0x02 - - -class DerivationSource(IntEnum): - MASTER = 0x00 - PARENT = 0x40 - CURRENT = 0x80 - - -class SigningAlgorithm(IntEnum): - ECDSA_SECP256K1 = 0x00 - EDDSA_ED25519 = 0x01 - BLS12_381 = 0x02 - SCHNORR_BIP340 = 0x03 - - -class LoadKeyType(IntEnum): - ECC = 0x01 - EXTENDED_ECC = 0x02 - BIP39_SEED = 0x03 - - -class PairingMode(IntEnum): - ANY = 0x00 - EPHEMERAL = 0x01 - PERSISTENT = 0x02 diff --git a/python/keycard-py/keycard/crypto/__init__.py b/python/keycard-py/keycard/crypto/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/python/keycard-py/keycard/crypto/aes.py b/python/keycard-py/keycard/crypto/aes.py deleted file mode 100644 index c78d4029..00000000 --- a/python/keycard-py/keycard/crypto/aes.py +++ /dev/null @@ -1,32 +0,0 @@ -from .padding import iso7816_pad, iso7816_unpad - -import pyaes - - -def aes_cbc_encrypt( - key: bytes, - iv: bytes, - data: bytes, - padding: bool = True -) -> bytes: - if padding: - data = iso7816_pad(data, 16) - aes = pyaes.AESModeOfOperationCBC(key, iv=iv) - - ciphertext = b'' - for i in range(0, len(data), 16): - block = data[i:i+16] - ciphertext += aes.encrypt(block) - - return ciphertext - - -def aes_cbc_decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes: - aes = pyaes.AESModeOfOperationCBC(key, iv=iv) - - decrypted = b'' - for i in range(0, len(ciphertext), 16): - block = ciphertext[i:i+16] - decrypted += aes.decrypt(block) - - return iso7816_unpad(decrypted) diff --git a/python/keycard-py/keycard/crypto/generate_pairing_token.py b/python/keycard-py/keycard/crypto/generate_pairing_token.py deleted file mode 100644 index 3afca8e0..00000000 --- a/python/keycard-py/keycard/crypto/generate_pairing_token.py +++ /dev/null @@ -1,15 +0,0 @@ - -import hashlib -import unicodedata - - -SALT = 'Keycard Pairing Password Salt' -NUMBER_OF_ITERATIONS = 50000 -DKLEN = 32 - - -def generate_pairing_token(passphrase: str) -> bytes: - norm_pass = unicodedata.normalize('NFKD', passphrase).encode('utf-8') - salt = unicodedata.normalize('NFKD', SALT).encode('utf-8') - return hashlib.pbkdf2_hmac( - 'sha256', norm_pass, salt, NUMBER_OF_ITERATIONS, dklen=DKLEN) diff --git a/python/keycard-py/keycard/crypto/padding.py b/python/keycard-py/keycard/crypto/padding.py deleted file mode 100644 index ffdf3d09..00000000 --- a/python/keycard-py/keycard/crypto/padding.py +++ /dev/null @@ -1,9 +0,0 @@ -def iso7816_pad(data: bytes, block_size: int) -> bytes: - pad_len = block_size - (len(data) % block_size) - return data + b'\x80' + b'\x00' * (pad_len - 1) - - -def iso7816_unpad(padded: bytes) -> bytes: - if b'\x80' not in padded: - raise ValueError("Invalid ISO7816 padding") - return padded[:padded.rindex(b'\x80')] diff --git a/python/keycard-py/keycard/exceptions.py b/python/keycard-py/keycard/exceptions.py deleted file mode 100644 index 494d815f..00000000 --- a/python/keycard-py/keycard/exceptions.py +++ /dev/null @@ -1,41 +0,0 @@ -class KeyCardError(Exception): - """Base exception for Keycard SDK""" - - pass - - -class APDUError(KeyCardError): - """Raised when APDU returns non-success status word.""" - - def __init__(self, sw: int): - self.sw = sw - super().__init__(f"APDU failed with SW={sw:04X}") - - -class InvalidResponseError(KeyCardError): - """Raised when response parsing fails.""" - - pass - - -class NotInitializedError(KeyCardError): - """Raised when trying to use card public key before select().""" - - pass - - -class NotSelectedError(KeyCardError): - """Raised when trying to use card before select().""" - - pass - - -class TransportError(KeyCardError): - """Raised there are no readers""" - pass - - -class InvalidStateError(KeyCardError): - """Raised when a precondition is not met.""" - def __init__(self, message: str): - super().__init__(message) diff --git a/python/keycard-py/keycard/keycard.py b/python/keycard-py/keycard/keycard.py deleted file mode 100644 index 5d7f23ac..00000000 --- a/python/keycard-py/keycard/keycard.py +++ /dev/null @@ -1,630 +0,0 @@ -from types import TracebackType -from typing import Optional, Union - -from . import constants -from . import commands -from .apdu import APDUResponse -from .constants import DerivationOption, PairingMode -from .card_interface import CardInterface -from .exceptions import APDUError -from .parsing.application_info import ApplicationInfo -from .parsing.exported_key import ExportedKey -from .parsing.signature_result import SignatureResult -from .transport import Transport -from .secure_channel import SecureChannel - - -class KeyCard(CardInterface): - ''' - High-level interface for interacting with a Keycard device. - - This class provides convenient methods to manage pairing, secure channels, - and card operations. - ''' - - def __init__(self, transport: Optional[Transport] = None): - ''' - Initializes the KeyCard interface. - - Args: - transport (Transport): Instance used for APDU communication. - - Raises: - ValueError: If transport is None. - ''' - self.transport = transport if transport else Transport() - self.card_public_key: Optional[bytes] = None - self.session: Optional[SecureChannel] = None - self._is_pin_verified: bool = False - - def __enter__(self) -> 'KeyCard': - self.transport.connect() - return self - - def __exit__( - self, - type_: type[BaseException] | None, - value: BaseException | None, - traceback: TracebackType | None - ) -> None: - if self.transport: - self.transport.disconnect() - - @property - def is_selected(self) -> bool: - ''' - Checks if a card is selected and has a public key. - - Returns: - bool: True if a card is selected, False otherwise. - ''' - return self.card_public_key is not None - - @property - def is_session_open(self) -> bool: - ''' - Checks if a secure session is currently open. - - Returns: - bool: True if a secure session is established, False otherwise. - ''' - return self.session is not None - - @property - def is_secure_channel_open(self) -> bool: - ''' - Checks if a secure channel is currently open. - - Returns: - bool: True if a secure channel is established, False otherwise. - ''' - return self.session is not None and self.session.authenticated - - @property - def is_initialized(self) -> bool: - ''' - Checks if the Keycard is initialized. - - Returns: - bool: True if the Keycard is initialized, False otherwise. - ''' - return self._is_initialized - - @property - def is_pin_verified(self) -> bool: - ''' - Checks if the user PIN has been verified. - - Returns: - bool: True if the PIN is verified, False otherwise. - ''' - return self._is_pin_verified - - def select(self) -> 'ApplicationInfo': - ''' - Selects the Keycard applet and retrieves application metadata. - - Returns: - ApplicationInfo: Object containing ECC public key and card info. - ''' - info = commands.select(self) - self.card_public_key = info.ecc_public_key - self._is_initialized = info.is_initialized - return info - - def init(self, pin: str, puk: str, pairing_secret: str) -> None: - ''' - Initializes the card with security credentials. - - Args: - pin (bytes): The PIN code in bytes. - puk (bytes): The PUK code in bytes. - pairing_secret (bytes): The shared secret for pairing. - ''' - commands.init( - self, - pin, - puk, - pairing_secret, - ) - - def ident(self, challenge: Optional[bytes] = None) -> bytes: - ''' - Sends an identity challenge to the card. - - Args: - challenge (bytes): A challenge (nonce or data) to send to the - card. If None, a random 32-byte challenge is generated. - - Returns: - bytes: The public key extracted from the card's identity response. - ''' - return commands.ident(self, challenge) - - def open_secure_channel( - self, - pairing_index: int, - pairing_key: bytes, - mutually_authenticate: Optional[bool] = True - ) -> None: - ''' - Opens a secure session with the card. - - Args: - pairing_index (int): Index of the pairing slot to use. - pairing_key (bytes): The shared pairing key (32 bytes). - mutually_authenticate (bool): Execute mutually authenticate when - a secure channel has been opened - ''' - self.session = commands.open_secure_channel( - self, - pairing_index, - pairing_key, - ) - - if mutually_authenticate: - self.mutually_authenticate() - - def mutually_authenticate(self) -> None: - ''' - Performs mutual authentication between host and card. - - Raises: - APDUError: If the authentication fails. - ''' - commands.mutually_authenticate(self) - - def pair( - self, - shared_secret: bytes, - pairing_mode: Optional[PairingMode] = PairingMode.ANY - ) -> tuple[int, bytes]: - ''' - Pairs with the card using an ECDH-derived shared secret. - - Args: - shared_secret (bytes): 32-byte ECDH shared secret. - - Returns: - tuple[int, bytes]: The pairing index and client cryptogram. - ''' - return commands.pair(self, shared_secret, pairing_mode) - - def verify_pin(self, pin: str) -> bool: - ''' - Verifies the user PIN with the card. - - Args: - pin (str): The user-entered PIN. - - Returns: - bool: True if PIN is valid, otherwise False. - ''' - result = commands.verify_pin(self, pin.encode('utf-8')) - self._is_pin_verified = True - return result - - @property - def status(self) -> dict[str, int | bool] | list[int]: - ''' - Retrieves the application status using the secure session. - - Returns: - dict: A dictionary with: - - pin_retry_count (int) - - puk_retry_count (int) - - initialized (bool) - - Raises: - RuntimeError: If the secure session is not open. - ''' - if self.session is None: - raise RuntimeError('Secure session not established') - - return commands.get_status(self) - - @property - def get_key_path(self) -> dict[str, int | bool] | list[int]: - ''' - Returns the current key derivation path from the card. - - Returns: - list of int: List of 32-bit integers representing the key path. - - Raises: - RuntimeError: If the secure session is not open. - ''' - if self.session is None: - raise RuntimeError('Secure session not established') - - return commands.get_status(self, key_path=True) - - def unpair(self, index: int) -> None: - ''' - Removes a pairing slot from the card. - - Args: - index (int): Index of the pairing slot to remove. - ''' - commands.unpair(self, index) - - def factory_reset(self) -> None: - ''' - Sends the FACTORY_RESET command to the card. - - Raises: - APDUError: If the card returns a failure status word. - ''' - commands.factory_reset(self) - - def generate_key(self) -> bytes: - ''' - Generates a new key on the card and returns the key UID. - - Returns: - bytes: Key UID (SHA-256 of the public key) - - Raises: - APDUError: If the response status word is not 0x9000 - ''' - return commands.generate_key(self) - - def change_pin(self, new_value: str) -> None: - ''' - Changes the user PIN on the card. - - Args: - new_value (str): The new PIN value to set. - - Raises: - ValueError: If input format is invalid. - APDUError: If the response status word is not 0x9000. - ''' - commands.change_secret(self, new_value, constants.PinType.USER) - - def change_puk(self, new_value: str) -> None: - ''' - Changes the PUK on the card. - - Args: - new_value (str): The new PUK value to set. - - Raises: - ValueError: If input format is invalid. - APDUError: If the response status word is not 0x9000. - ''' - commands.change_secret(self, new_value, constants.PinType.PUK) - - def change_pairing_secret(self, new_value: str | bytes) -> None: - ''' - Changes the pairing secret on the card. - - Args: - new_value (str): The new pairing secret value to set. - - Raises: - ValueError: If input format is invalid. - APDUError: If the response status word is not 0x9000. - ''' - commands.change_secret(self, new_value, constants.PinType.PAIRING) - - def unblock_pin(self, puk: str | bytes, new_pin: str | bytes) -> None: - ''' - Unblocks the user PIN using the provided PUK and sets a new PIN. - - Args: - puk_and_pin (str | bytes): Concatenation of PUK (12 digits) + - new PIN (6 digits) - - Raises: - ValueError: If the format is invalid. - APDUError: If the card returns an error. - ''' - if isinstance(puk, str): - puk = puk.encode("utf-8") - if isinstance(new_pin, str): - new_pin = new_pin.encode("utf-8") - - commands.unblock_pin(self, puk + new_pin) - - def remove_key(self) -> None: - ''' - Removes the current key from the card. - - Raises: - APDUError: If the response status word is not 0x9000. - ''' - commands.remove_key(self) - - def store_data( - self, - data: bytes, - slot: constants.StorageSlot = constants.StorageSlot.PUBLIC - ) -> None: - """ - Stores data on the card in the specified slot. - - Args: - data (bytes): The data to store (max 127 bytes). - slot (StorageSlot): Where to store the data (PUBLIC, NDEF, CASH) - - Raises: - ValueError: If slot is invalid or data is too long. - """ - commands.store_data(self, data, slot) - - def get_data( - self, - slot: constants.StorageSlot = constants.StorageSlot.PUBLIC - ) -> bytes: - """ - Gets the data on the card previously stored with the store data command - in the specified slot. - - Args: - slot (StorageSlot): Where to retrieve the data (PUBLIC, NDEF, CASH) - - Raises: - ValueError: If slot is invalid or data is too long. - """ - return commands.get_data(self, slot) - - def export_key( - self, - derivation_option: constants.DerivationOption, - public_only: bool, - keypath: Optional[Union[str, bytes, bytearray]] = None, - make_current: bool = False, - source: constants.DerivationSource = constants.DerivationSource.MASTER - ) -> ExportedKey: - """ - Export a key from the card. - - This is a proxy for :func:`keycard.commands.export_key`, provided here - for convenience. - - Args: - derivation_option: One of the derivation options - (CURRENT, DERIVE, DERIVE_AND_MAKE_CURRENT). - public_only: If True, only the public key will be returned. - keypath: BIP32-style string (e.g. "m/44'/60'/0'/0/0") or packed - bytes. If derivation_option is CURRENT, this can be omitted. - make_current: If True, updates the card’s current derivation path. - source: Which node to derive from: MASTER, PARENT, or CURRENT. - - Returns: - ExportedKey: An object containing the public key, and optionally - the private key and chain code. - - See Also: - - :func:`keycard.commands.export_key` - for the lower-level - implementation - - :class:`keycard.types.ExportedKey` - return value - structure - """ - return commands.export_key( - self, - derivation_option=derivation_option, - public_only=public_only, - keypath=keypath, - make_current=make_current, - source=source - ) - - def export_current_key(self, public_only: bool = False) -> ExportedKey: - """ - Exports the current key from the card. - - This is a convenience method that uses the CURRENT derivation option - and does not require a keypath. - - Args: - public_only (bool): If True, only the public key will be returned. - - Returns: - ExportedKey: An object containing the public key, and optionally - the private key and chain code. - """ - return self.export_key( - derivation_option=constants.DerivationOption.CURRENT, - public_only=public_only - ) - - def sign( - self, - digest: bytes, - algorithm: constants.SigningAlgorithm = constants.SigningAlgorithm.ECDSA_SECP256K1, - ) -> SignatureResult: - """ - Sign using the currently loaded keypair. - Requires PIN verification and secure channel. - - Args: - digest (bytes): 32-byte hash to sign - algorithm (SigningAlgorithm): Signing algorithm to use. Defaults to - ECDSA_SECP256K1. Other options include SCHNORR_BIP340. - - Returns: - SignatureResult: Parsed signature result, including the signature - (DER or raw), algorithm, and optional recovery ID or - public key. - """ - return commands.sign(self, digest, DerivationOption.CURRENT, algorithm) - - def sign_with_path( - self, - digest: bytes, - path: str, - make_current: bool = False, - algorithm: constants.SigningAlgorithm = constants.SigningAlgorithm.ECDSA_SECP256K1, - ) -> SignatureResult: - """ - Sign using a derived keypath. Optionally updates the current path. - - Args: - digest (bytes): 32-byte hash to sign - path (str): BIP-32-style path (e.g. "m/44'/60'/0'/0/0") - make_current (bool): whether to update current path on card - algorithm (SigningAlgorithm): Signing algorithm to use. Defaults to - ECDSA_SECP256K1. Other options include SCHNORR_BIP340. - - Returns: - SignatureResult: Parsed signature result, including the signature - (DER or raw), algorithm, and optional recovery ID or - public key. - """ - p1 = ( - DerivationOption.DERIVE_AND_MAKE_CURRENT - if make_current else DerivationOption.DERIVE - ) - return commands.sign(self, digest, p1, algorithm, derivation_path=path) - - def sign_pinless( - self, - digest: bytes, - algorithm: constants.SigningAlgorithm = constants.SigningAlgorithm.ECDSA_SECP256K1, - ) -> SignatureResult: - """ - Sign using the predefined PIN-less path. - Does not require secure channel or PIN. - - Args: - digest (bytes): 32-byte hash to sign - algorithm (SigningAlgorithm): Signing algorithm to use. Defaults to - ECDSA_SECP256K1. Other options include SCHNORR_BIP340. - - Returns: - SignatureResult: Parsed signature result, including the signature - (DER or raw), algorithm, and optional recovery ID or - public key. - - Raises: - APDUError: if no PIN-less path is set - """ - return commands.sign(self, digest, DerivationOption.PINLESS, algorithm) - - def load_key( - self, - key_type: constants.LoadKeyType, - public_key: Optional[bytes] = None, - private_key: Optional[bytes] = None, - chain_code: Optional[bytes] = None, - bip39_seed: Optional[bytes] = None, - lee_seed: Optional[bytes] = None - ) -> bytes: - """ - Load a key into the card for signing purposes. - - Args: - key_type: Key type - public_key: Optional ECC public key (tag 0x80). - private_key: ECC private key (tag 0x81). - chain_code: Optional chain code (tag 0x82, only for extended key). - bip39_seed: 16 to 64-byte BIP39 seed (only for key_type=BIP39_SEED). - lee_seed: 16 to 64-byte LEE seed (only for key_type=BIP39_SEED). - - Returns: - UID of the loaded key (SHA-256 of public key). - """ - return commands.load_key( - self, - key_type=key_type, - public_key=public_key, - private_key=private_key, - chain_code=chain_code, - bip39_seed=bip39_seed, - lee_seed=lee_seed - ) - - def set_pinless_path(self, path: str) -> None: - """ - Set a PIN-less path on the card. Allows signing without PIN/auth if the - current derived key matches this path. - - Args: - path (str): BIP-32-style path (e.g., "m/44'/60'/0'/0/0"). An empty - string disables the pinless path. - """ - commands.set_pinless_path(self, path) - - def generate_mnemonic(self, checksum_size: int = 6) -> list[int]: - """ - Generate a BIP39 mnemonic using the card's RNG. - - Args: - checksum_size (int): Number of checksum bits - (between 4 and 8 inclusive). - - Returns: - List[int]: List of integers (0-2047) corresponding to wordlist - indexes. - """ - return commands.generate_mnemonic(self, checksum_size) - - def derive_key(self, path: str = '') -> None: - """ - Set the derivation path for subsequent SIGN and EXPORT KEY commands. - - Args: - path (str): BIP-32-style path (e.g., "m/44'/60'/0'/0/0") or - "../0/1" (parent) or "./0" (current). - """ - commands.derive_key(self, path) - - def send_apdu( - self, - ins: int, - p1: int = 0x00, - p2: int = 0x00, - data: bytes = b'', - cla: Optional[int] = None - ) -> bytes: - if cla is None: - cla = constants.CLA_PROPRIETARY - - response: APDUResponse = self.transport.send_apdu( - bytes([cla, ins, p1, p2, len(data)]) + data - ) - - if response.status_word != constants.SW_SUCCESS: - raise APDUError(response.status_word) - - return bytes(response.data) - - def send_secure_apdu( - self, - ins: int, - p1: int = 0x00, - p2: int = 0x00, - data: bytes = b'' - ) -> bytes: - if not self.session or not self.session.authenticated: - raise RuntimeError('Secure channel not established') - - encrypted = self.session.wrap_apdu( - cla=constants.CLA_PROPRIETARY, - ins=ins, - p1=p1, - p2=p2, - data=data - ) - - response: APDUResponse = self.transport.send_apdu( - bytes([ - constants.CLA_PROPRIETARY, - ins, - p1, - p2, - len(encrypted) - ]) + encrypted - ) - - if response.status_word != 0x9000: - raise APDUError(response.status_word) - - plaintext, sw = self.session.unwrap_response(response) - - if sw != 0x9000: - raise APDUError(sw) - - return plaintext diff --git a/python/keycard-py/keycard/parsing/__init__.py b/python/keycard-py/keycard/parsing/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/python/keycard-py/keycard/parsing/application_info.py b/python/keycard-py/keycard/parsing/application_info.py deleted file mode 100644 index cc21aefd..00000000 --- a/python/keycard-py/keycard/parsing/application_info.py +++ /dev/null @@ -1,119 +0,0 @@ -from dataclasses import dataclass -from typing import Optional - -from ..exceptions import InvalidResponseError -from .capabilities import Capabilities -from .tlv import parse_tlv - - -@dataclass -class ApplicationInfo: - """ - Represents parsed application information from a TLV-encoded response. - - Attributes: - capabilities (Optional[int]): Parsed capabilities value, if present. - ecc_public_key (Optional[bytes]): ECC public key bytes, if present. - instance_uid (Optional[bytes]): Unique identifier for the application - instance, if present. - key_uid (Optional[bytes]): Unique identifier for the key, if present. - version_major (int): Major version number of the application. - version_minor (int): Minor version number of the application. - """ - capabilities: Optional[int] - ecc_public_key: Optional[bytes] - instance_uid: Optional[bytes] - key_uid: Optional[bytes] - version_major: int - version_minor: int - - @property - def is_initialized(self) -> bool: - """ - Checks if the application is initialized based on the presence of - the key_uid. - - Returns: - bool: True if the key_uid is present, False otherwise. - """ - return self.key_uid is not None - - @staticmethod - def parse(data: bytes) -> "ApplicationInfo": - """ - Parses a byte sequence containing TLV-encoded application information - and returns an ApplicationInfo instance. - - Args: - data (bytes): The TLV-encoded response data to parse. - - Returns: - ApplicationInfo: An instance populated with the parsed application - information fields. - - The function extracts the following fields from the TLV data: - - version_major (int): Major version number (from tag 0x02). - - version_minor (int): Minor version number (from tag 0x02). - - instance_uid (bytes or None): Instance UID (from tag 0x8F). - - key_uid (bytes or None): Key UID (from tag 0x8E). - - ecc_public_key (bytes or None): ECC public key (from tag 0x80). - - capabilities (Capabilities or None): Capabilities object - (from tag 0x8D). - - Raises: - Any exceptions raised by ApplicationInfo._parse_response or - Capabilities.parse. - """ - version_major = version_minor = 0 - instance_uid = None - key_uid = None - ecc_public_key = None - capabilities = 0 - - if data[0] == 0x80: - length = data[1] - pubkey = data[2:2+length] - ecc_public_key = bytes(pubkey) - capabilities += Capabilities.CREDENTIALS_MANAGEMENT - - if pubkey: - capabilities += Capabilities.SECURE_CHANNEL - capabilities = Capabilities.parse(capabilities) - else: - tlv = parse_tlv(data) - if 0xA4 not in tlv: - raise InvalidResponseError( - "Invalid top-level tag, expected 0xA4") - - inner_tlv = parse_tlv(tlv[0xA4][0]) - - instance_uid = bytes(inner_tlv[0x8F][0]) - ecc_public_key = bytes(inner_tlv[0x80][0]) - key_uid = inner_tlv[0x8E][0] - capabilities = Capabilities.parse(inner_tlv[0x8D][0][0]) - for value in inner_tlv[0x02]: - if len(value) == 2: - version_major, version_minor = value[0], value[1] - - return ApplicationInfo( - capabilities=capabilities, - ecc_public_key=ecc_public_key, - instance_uid=instance_uid, - key_uid=key_uid, - version_major=version_major, - version_minor=version_minor, - ) - - def __str__(self) -> str: - return ( - f"ApplicationInfo(version=" - f"{self.version_major}.{self.version_minor}, " - f"instance_uid=" - f"{self.instance_uid.hex() if self.instance_uid else None}, " - f"key_uid=" - f"{self.key_uid.hex() if self.key_uid else None}, " - f"ecc_public_key=" - f"{self.ecc_public_key.hex() if self.ecc_public_key else None}, " - f"capabilities=" - f"{self.capabilities})" - ) diff --git a/python/keycard-py/keycard/parsing/capabilities.py b/python/keycard-py/keycard/parsing/capabilities.py deleted file mode 100644 index 9fc8122e..00000000 --- a/python/keycard-py/keycard/parsing/capabilities.py +++ /dev/null @@ -1,44 +0,0 @@ -from enum import IntFlag - - -class Capabilities(IntFlag): - """ - An enumeration representing the various capabilities supported by a device - or application. - - Attributes: - SECURE_CHANNEL (int): Indicates support for secure channel - communication (0x01). - KEY_MANAGEMENT (int): Indicates support for key management operations - (0x02). - CREDENTIALS_MANAGEMENT (int): Indicates support for credentials - management (0x04). - NDEF (int): Indicates support for NDEF (NFC Data Exchange Format) - operations (0x08). - """ - SECURE_CHANNEL = 0x01 - KEY_MANAGEMENT = 0x02 - CREDENTIALS_MANAGEMENT = 0x04 - NDEF = 0x08 - - @classmethod - def parse(cls, value: int) -> "Capabilities": - """ - Parses an integer value and returns a corresponding Capabilities - instance. - - Args: - value (int): The integer value representing the capabilities. - - Returns: - Capabilities: An instance of the Capabilities class corresponding - to the given value. - """ - return cls(value) - - def __str__(self) -> str: - return " | ".join( - name - for name, member in self.__class__.__members__.items() - if member in self - ) diff --git a/python/keycard-py/keycard/parsing/exported_key.py b/python/keycard-py/keycard/parsing/exported_key.py deleted file mode 100644 index 059ced4f..00000000 --- a/python/keycard-py/keycard/parsing/exported_key.py +++ /dev/null @@ -1,17 +0,0 @@ -from dataclasses import dataclass -from typing import Optional - - -@dataclass -class ExportedKey: - public_key: Optional[bytes] = None - private_key: Optional[bytes] = None - chain_code: Optional[bytes] = None - - @property - def is_extended(self) -> bool: - return self.chain_code is not None - - @property - def has_private(self) -> bool: - return self.private_key is not None diff --git a/python/keycard-py/keycard/parsing/identity.py b/python/keycard-py/keycard/parsing/identity.py deleted file mode 100644 index c67a0f88..00000000 --- a/python/keycard-py/keycard/parsing/identity.py +++ /dev/null @@ -1,43 +0,0 @@ -from hashlib import sha256 -from ecdsa import VerifyingKey, SECP256k1, util - -from ..exceptions import InvalidResponseError -from ..parsing.tlv import parse_tlv - - -def parse(challenge: bytes, data: bytes) -> bytes: - tlvs = parse_tlv(data) - - inner_tlvs = parse_tlv(tlvs[0xA0][0]) - - try: - certificate = inner_tlvs[0x8A][0] - signature = inner_tlvs[0x30][0] - except (IndexError, KeyError): - raise InvalidResponseError('Malformed identity response') - - signature = b'\x30' + len(signature).to_bytes(1, 'big') + signature - if len(certificate) < 95 or len(signature) < 65: - raise InvalidResponseError('Malformed identity response') - - _verify(certificate, signature, challenge) - return _recover_public_key(certificate) - - -def _verify(certificate: bytes, signature: bytes, challenge: bytes) -> None: - pub_key = certificate[:33] - vk = VerifyingKey.from_string(pub_key, curve=SECP256k1) - vk.verify_digest(signature, challenge, sigdecode=util.sigdecode_der) - - -def _recover_public_key(certificate: bytes) -> bytes: - signature = certificate[33:] - v = signature[-1] - digest = sha256(certificate).digest() - - vk = VerifyingKey.from_public_key_recovery_with_digest( - signature[:-1], digest, SECP256k1) - - public_key = vk[v] if isinstance(vk, list) else vk - der: bytes = public_key.to_der('compressed') - return der diff --git a/python/keycard-py/keycard/parsing/keypath.py b/python/keycard-py/keycard/parsing/keypath.py deleted file mode 100644 index a3ff528b..00000000 --- a/python/keycard-py/keycard/parsing/keypath.py +++ /dev/null @@ -1,87 +0,0 @@ -from typing import Union - -from ..constants import DerivationSource - - -class KeyPath: - MAX_COMPONENTS = 10 - - def __init__( - self, - path: Union[str, bytes, bytearray], - source: DerivationSource = DerivationSource.MASTER - ): - if isinstance(path, str): - if path == '': - raise ValueError("Empty path") - self.source, components = self._parse_path_string(path) - if len(components) > self.MAX_COMPONENTS: - raise ValueError("Too many components in derivation path") - self.data = self._encode_components(components) - elif isinstance(path, (bytes, bytearray)): - if len(path) % 4 != 0: - raise ValueError("Byte path must be a multiple of 4") - self.source = source - self.data = bytes(path) - else: - raise TypeError("Path must be a string or bytes") - - def _parse_path_string( - self, - path: str - ) -> tuple[DerivationSource, list[int]]: - tokens = path.split('/') - if not tokens: - raise ValueError("Empty path") - - first = tokens[0] - if first == "m": - source = DerivationSource.MASTER - tokens = tokens[1:] - elif first == "..": - source = DerivationSource.PARENT - tokens = tokens[1:] - elif first == ".": - source = DerivationSource.CURRENT - tokens = tokens[1:] - else: - source = DerivationSource.CURRENT - - components = [self._parse_component(token) for token in tokens] - return source, components - - def _parse_component(self, token: str) -> int: - if token.endswith("'"): - token = token[:-1] - hardened = True - else: - hardened = False - - if not token.isdigit(): - raise ValueError(f"Invalid component: {token}") - - value = int(token) - if hardened: - value |= 0x80000000 - return value - - def _encode_components(self, components: list[int]) -> bytes: - return b''.join(comp.to_bytes(4, 'big') for comp in components) - - def to_string(self) -> str: - prefix = { - DerivationSource.MASTER: 'm', - DerivationSource.PARENT: '..', - DerivationSource.CURRENT: '.' - }.get(self.source, '.') - - components = [] - for i in range(0, len(self.data), 4): - chunk = self.data[i:i+4] - val = int.from_bytes(chunk, 'big') - if val & 0x80000000: - components.append(f"{val & 0x7FFFFFFF}'") - else: - components.append(str(val)) - - return '/'.join([prefix] + components) diff --git a/python/keycard-py/keycard/parsing/signature_result.py b/python/keycard-py/keycard/parsing/signature_result.py deleted file mode 100644 index db5ddfb9..00000000 --- a/python/keycard-py/keycard/parsing/signature_result.py +++ /dev/null @@ -1,82 +0,0 @@ -from dataclasses import dataclass -from ecdsa import VerifyingKey, util, SECP256k1 -from typing import Optional - -from ..constants import SigningAlgorithm - - -@dataclass -class SignatureResult: - algo: SigningAlgorithm - r: bytes - s: bytes - recovery_id: Optional[int] = None - public_key: Optional[bytes] = None - - def __init__( - self, - digest: bytes, - algo: SigningAlgorithm, - r: int, - s: int, - recovery_id: Optional[int] = None, - public_key: Optional[bytes] = None - ) -> None: - self.algo = algo - self.r = r.to_bytes((r.bit_length() + 7) // 8, 'big') - self.s = s.to_bytes((s.bit_length() + 7) // 8, 'big') - if public_key is None and recovery_id is None: - raise ValueError( - "Public key and recovery id not returned from card") - - self.public_key = ( - public_key - if public_key is not None - else self._recover_public_key(digest) - ) - - self.recovery_id = ( - recovery_id - if recovery_id is not None - else self._recover_v(digest) - ) - - @property - def signature(self) -> bytes: - return self.r + self.s - - @property - def signature_der(self) -> bytes: - signature: bytes = util.sigencode_der( - int.from_bytes(self.r, 'big'), - int.from_bytes(self.s, 'big'), - self.recovery_id - ) - return signature - - def _recover_public_key(self, digest: bytes) -> bytes: - if self.recovery_id is None: - raise ValueError("Recovery ID is required for public key recovery") - - public_key = VerifyingKey.from_public_key_recovery_with_digest( - self.signature_der, - digest, - SECP256k1, - sigdecode=util.sigdecode_der) - public_key_bytes: bytes = public_key.to_string() - return public_key_bytes - - def _recover_v(self, digest: bytes) -> int: - if self.public_key is None: - raise ValueError("Public key is required for recovery ID") - - public_keys = VerifyingKey.from_public_key_recovery_with_digest( - self.signature, digest, SECP256k1) - - index = 0 - for public_key in public_keys: - if self.public_key[1:] == public_key.to_string(): - return index - index += 1 - - raise RuntimeError("Recovery ID not found") diff --git a/python/keycard-py/keycard/parsing/tlv.py b/python/keycard-py/keycard/parsing/tlv.py deleted file mode 100644 index 93b97b95..00000000 --- a/python/keycard-py/keycard/parsing/tlv.py +++ /dev/null @@ -1,102 +0,0 @@ -from collections import defaultdict - -from keycard.exceptions import InvalidResponseError - - -def _parse_ber_length(data: bytes, index: int) -> tuple[int, int]: - """ - Parses a BER-encoded length field from a byte sequence starting at the - given index. - - Args: - data (bytes): The byte sequence containing the BER-encoded length. - index (int): The starting index in the byte sequence to parse the - length from. - - Returns: - tuple[int, int]: A tuple containing the parsed length (int) and the - total number of bytes consumed (int). - - Raises: - InvalidResponseError: If the length encoding is unsupported or exceeds - the remaining buffer. - """ - first = data[index] - index += 1 - - if first < 0x80: - return first, 1 - - num_bytes = first & 0x7F - if num_bytes > 4: - raise InvalidResponseError("Unsupported length encoding") - - if index + num_bytes > len(data): - raise InvalidResponseError("Length exceeds remaining buffer") - - length = int.from_bytes(data[index:index+num_bytes], "big") - return length, 1 + num_bytes - - -def parse_tlv(data: bytes) -> defaultdict[int, list[bytes]]: - """ - Parses a byte sequence containing TLV (Tag-Length-Value) encoded data. - - Args: - data (bytes): The byte sequence to parse. - - Returns: - List[Tuple[int, bytes]]: A list of tuples, each containing the tag - (as an int) and the value (as bytes). - - Raises: - InvalidResponseError: If the TLV header is incomplete or the declared - length exceeds the available data. - """ - index = 0 - result = defaultdict(list) - - while index < len(data): - tag = data[index] - index += 1 - - length, length_size = _parse_ber_length(data, index) - index += length_size - - value = data[index:index+length] - - if len(value) < length: - raise InvalidResponseError("Not enough bytes for value") - - index += length - - result[tag].append(value) - - return result - - -def encode_tlv(tag: int, value: bytes) -> bytes: - """ - Encode a tag-length-value (TLV) structure using BER-TLV rules. - - Args: - tag (int): A single-byte tag (0x00 - 0xFF). - value (bytes): Value to encode. - - Returns: - bytes: Encoded TLV. - """ - if not (0 <= tag <= 0xFF): - raise ValueError("Tag must fit in a single byte") - - length = len(value) - - if length < 0x80: - length_bytes = bytes([length]) - else: - len_len = (length.bit_length() + 7) // 8 - length_bytes = ( - bytes([0x80 | len_len]) + length.to_bytes(len_len, 'big') - ) - - return bytes([tag]) + length_bytes + value diff --git a/python/keycard-py/keycard/preconditions.py b/python/keycard-py/keycard/preconditions.py deleted file mode 100644 index edadaa9e..00000000 --- a/python/keycard-py/keycard/preconditions.py +++ /dev/null @@ -1,48 +0,0 @@ -from functools import wraps -from typing import Callable, TypeVar, ParamSpec, cast - -from .exceptions import InvalidStateError -from .card_interface import CardInterface - -P = ParamSpec("P") -R = TypeVar("R") - - -def make_precondition( - attribute_name: str, - display_name: str | None = None -) -> Callable[[Callable[P, R]], Callable[P, R]]: - def decorator(func: Callable[P, R]) -> Callable[P, R]: - @wraps(func) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: - card = args[0] - if not isinstance(card, CardInterface): - raise TypeError("First argument must be a CardInterface") - if not getattr(card, attribute_name, False): - readable = ( - display_name - if display_name is not None - else attribute_name.replace('_', ' ').title() - ) - raise InvalidStateError(f"{readable} must be satisfied.") - return func(*args, **kwargs) - return cast(Callable[P, R], wrapper) - return decorator - - -require_selected = make_precondition( - 'is_selected', - 'Card Selection' -) -require_initialized = make_precondition( - 'is_initialized', - 'Card Initialization' -) -require_secure_channel = make_precondition( - 'is_secure_channel_open', - 'Secure Channel' -) -require_pin_verified = make_precondition( - 'is_pin_verified', - 'PIN verification' -) diff --git a/python/keycard-py/keycard/secure_channel.py b/python/keycard-py/keycard/secure_channel.py deleted file mode 100644 index 2cc9e33a..00000000 --- a/python/keycard-py/keycard/secure_channel.py +++ /dev/null @@ -1,143 +0,0 @@ -# keycard/secure_channel.py - -from hashlib import sha512 - -from .apdu import APDUResponse - -from .crypto.aes import aes_cbc_encrypt, aes_cbc_decrypt - -from dataclasses import dataclass - - -@dataclass -class SecureChannel: - """ - SecureChannel manages a secure communication channel using AES encryption - and MAC authentication. - - Attributes: - enc_key (bytes): The AES encryption key for the session. - mac_key (bytes): The AES MAC key for message authentication. - iv (bytes): The initialization vector for AES operations. - authenticated (bool): Indicates if the session is authenticated. - """ - enc_key: bytes - mac_key: bytes - iv: bytes - authenticated: bool = False - - @classmethod - def open( - cls, - shared_secret: bytes, - pairing_key: bytes, - salt: bytes, - seed_iv: bytes - ) -> "SecureChannel": - """ - Opens a new SecureChannel using the provided cryptographic parameters. - - Args: - shared_secret (bytes): The shared secret used for key derivation. - pairing_key (bytes): The pairing key used for key derivation. - salt (bytes): The salt value used in the key derivation process. - seed_iv (bytes): The initialization vector (IV) to seed the - session. - - Returns: - SecureChannel: An instance of SecureChannel initialized with - derived encryption and MAC keys, and the provided IV. - """ - digest = sha512(shared_secret + pairing_key + salt).digest() - enc_key, mac_key = digest[:32], digest[32:] - return cls( - enc_key=enc_key, - mac_key=mac_key, - iv=seed_iv, - authenticated=True - ) - - def wrap_apdu( - self, - cla: int, - ins: int, - p1: int, - p2: int, - data: bytes - ) -> bytes: - """ - Wraps an APDU command with secure channel encryption and MAC. - - Args: - cla (int): The APDU class byte. - ins (int): The APDU instruction byte. - p1 (int): The APDU parameter 1 byte. - p2 (int): The APDU parameter 2 byte. - data (bytes): The APDU data field to be encrypted. - - Returns: - tuple[int, int, int, int, bytes]: The wrapped APDU as a tuple - containing the class, instruction, parameter 1, parameter 2, - and the concatenated MAC and encrypted data. - - Raises: - ValueError: If the secure channel is not authenticated and the - instruction is not 0x11. - """ - if not self.authenticated and ins != 0x11: - raise ValueError("Secure channel not authenticated") - - encrypted = aes_cbc_encrypt(self.enc_key, self.iv, data) - - lc = 16 + len(encrypted) - mac_input = bytes([cla, ins, p1, p2, lc]) + bytes(11) + encrypted - - enc_data = aes_cbc_encrypt( - self.mac_key, bytes(16), mac_input, padding=False) - - self.iv = enc_data[-16:] - - return self.iv + encrypted - - def unwrap_response(self, response: APDUResponse) -> tuple[bytes, int]: - """ - Unwraps and verifies a secure channel response. - - Args: - response (bytes): The encrypted response bytes to unwrap. - - Returns: - tuple[bytes, int]: A tuple containing the decrypted plaintext - (excluding the status word) and the status word as an integer. - - Raises: - ValueError: If the secure channel is not authenticated. - ValueError: If the response length is invalid. - ValueError: If the MAC verification fails. - ValueError: If the decrypted plaintext is too short to contain a - status word. - """ - if not self.authenticated: - raise ValueError("Secure channel not authenticated") - - if len(response.data) < 18: - raise ValueError("Invalid secure response length") - - received_mac = bytes(response.data[:16]) - encrypted = bytes(response.data[16:]) - - lr = len(response.data) - mac_input = bytes([lr]) + bytes(15) + bytes(encrypted) - expected_mac = aes_cbc_encrypt( - self.mac_key, bytes(16), mac_input, padding=False)[-16:] - if received_mac != expected_mac: - raise ValueError("Invalid MAC") - - plaintext = aes_cbc_decrypt(self.enc_key, self.iv, encrypted) - - self.iv = received_mac - - if len(plaintext) < 2: - raise ValueError("Missing status word in response") - - return plaintext[:-2], int.from_bytes(plaintext[-2:], "big") diff --git a/python/keycard-py/keycard/transport.py b/python/keycard-py/keycard/transport.py deleted file mode 100644 index 9a56e599..00000000 --- a/python/keycard-py/keycard/transport.py +++ /dev/null @@ -1,46 +0,0 @@ -from types import TracebackType -from smartcard.System import readers -from smartcard.pcsc.PCSCReader import PCSCReader - -from .apdu import APDUResponse -from .exceptions import TransportError - - -class Transport: - def __init__(self) -> None: - self.connection: PCSCReader = None - - def __enter__(self) -> 'Transport': - self.connect() - return self - - def __exit__( - self, - type_: type[BaseException] | None, - value: BaseException | None, - traceback: TracebackType | None - ) -> None: - self.disconnect() - - def connect(self, index: int = 0) -> None: - r = readers() - if not r: - raise TransportError('No smart card readers found') - self.connection = r[index].createConnection() - self.connection.connect() - - def disconnect(self) -> None: - if self.connection: - self.connection.disconnect() - self.connection = None - - def send_apdu(self, apdu: bytes) -> APDUResponse: - if not self.connection: - self.connect() - - apdu_list = list(apdu) - - response, sw1, sw2 = self.connection.transmit(apdu_list) - - sw = (sw1 << 8) | sw2 - return APDUResponse(response, sw) diff --git a/python/keycard-py/mypy.ini b/python/keycard-py/mypy.ini deleted file mode 100644 index 462cb50b..00000000 --- a/python/keycard-py/mypy.ini +++ /dev/null @@ -1,12 +0,0 @@ -[mypy] -ignore_missing_imports = True -strict = True - -[mypy-ecdsa.*] -ignore_missing_imports = True - -[mypy-pyaes.*] -ignore_missing_imports = True - -[mypy-smartcard.*] -ignore_missing_imports = True \ No newline at end of file diff --git a/python/keycard-py/pyproject.toml b/python/keycard-py/pyproject.toml deleted file mode 100644 index 8a45f79a..00000000 --- a/python/keycard-py/pyproject.toml +++ /dev/null @@ -1,35 +0,0 @@ -[build-system] -requires = ["flit_core >=3.11,<4"] -build-backend = "flit_core.buildapi" - -[project] -name = "keycard" -authors = [{name = "mmlado", email = "developer@mmlado.com"}] -readme = "README.md" -license = "MIT" -license-files = ["LICENSE"] -dynamic = ["version", "description"] -requires-python = ">=3.10" - -dependencies = [ - "pyscard", - "ecdsa", - "pyaes" -] - -[project.optional-dependencies] -dev = [ - "pytest", - "pytest-cov", - "coverage", - "sphinx", - "sphinx-autodoc-typehints", - "flake8", - "mypy", - "mnemonic", - "tox" -] - -[project.urls] -Homepage = "https://github.com/mmlado/keycard-py" -Documentation = "https://mmlado.github.io/keycard-py/" diff --git a/python/keycard-py/tasks.py b/python/keycard-py/tasks.py deleted file mode 100644 index c2168110..00000000 --- a/python/keycard-py/tasks.py +++ /dev/null @@ -1,123 +0,0 @@ -import os -from pathlib import Path -import shutil - -from invoke import task - - -@task -def venv(c): - if not os.path.exists("venv"): - c.run("python -m venv venv") - print("venv created.") - else: - print("venv already exists.") - -@task -def install(c, dev=False): - """Install dependencies with Flit.""" - pip = "venv/bin/pip" - c.run(f"{pip} install flit") - if dev: - c.run("venv/bin/flit install --symlink --deps develop") - else: - c.run("venv/bin/flit install --deps production") - - -@task -def test(c): - """Run pytest with coverage""" - c.run("coverage run -m pytest", pty=True) - - -@task -def coverage(c): - """ - Runs the coverage report using the coverage tool. - """ - c.run("coverage report", pty=True) - - -@task -def htmlcov(c): - """ - Generates an HTML coverage report using the 'coverage' tool in html - format. - """ - c.run("coverage html", pty=True) - print("Open htmlcov/index.html in your browser") - - -@task -def lint(c): - """Run flake8 linting""" - c.run("flake8 keycard tests", pty=True) - - -@task -def typecheck(c): - """Run mypy type checking.""" - c.run("mypy keycard") - - -@task -def docs(ctx, clean=False, open=False): - """ - Build Sphinx documentation. - - Args: - clean (bool): If True, removes the build directory before building. - open (bool): If True, opens the built docs in a browser. - """ - docs_dir = "docs" - build_dir = os.path.join(docs_dir, "_build") - - if clean and os.path.exists(build_dir): - ctx.run(f"rm -rf {build_dir}") - - ctx.run(f"sphinx-build -b html {docs_dir} {build_dir}/html") - - if open: - index_path = os.path.join(build_dir, "html", "index.html") - ctx.run(f"xdg-open {index_path} || open {index_path}", warn=True) - - -@task -def clean(c): - """Clean artifacts""" - for pycache in Path(".").rglob("__pycache__"): - shutil.rmtree(pycache, ignore_errors=True) - - build_path = Path("docs") / "_build" - if build_path.exists(): - shutil.rmtree(build_path, ignore_errors=True) - - c.run("rm -rf .pytest_cache htmlcov .coverage", warn=True) - - -@task -def cleanall(c): - """Thorough cleanup of all build, cache, and pycache files.""" - patterns = [ - "__pycache__", - ".pytest_cache", - ".coverage", - "htmlcov", - "dist", - "build", - "*.egg-info", - "venv" - ] - - for pattern in patterns: - for path in Path(".").rglob(pattern): - if path.is_dir(): - shutil.rmtree(path, ignore_errors=True) - elif path.is_file(): - path.unlink(missing_ok=True) - - # Sphinx docs - docs_build = Path("docs") / "_build" - if docs_build.exists(): - shutil.rmtree(docs_build, ignore_errors=True) - print("All artifacts cleaned up.") diff --git a/python/keycard-py/tests/__init__.py b/python/keycard-py/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/python/keycard-py/tests/commands/__init__.py b/python/keycard-py/tests/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/python/keycard-py/tests/commands/test_change_secret.py b/python/keycard-py/tests/commands/test_change_secret.py deleted file mode 100644 index 2503ded2..00000000 --- a/python/keycard-py/tests/commands/test_change_secret.py +++ /dev/null @@ -1,95 +0,0 @@ -import sys -import pytest - -from unittest.mock import Mock, patch -from keycard import constants -from keycard.card_interface import CardInterface -from keycard.exceptions import APDUError -from keycard.commands.change_secret import change_secret - - -@pytest.fixture -def mock_card(): - card = Mock(spec=CardInterface) - card.send_secure_apdu = Mock() - return card - - -def test_change_secret_pairing_str_success(mock_card): - change_secret_module = sys.modules['keycard.commands.change_secret'] - with patch.object( - change_secret_module, 'generate_pairing_token' - ) as mock_generate: - mock_generate.return_value = bytes(32) - change_secret(mock_card, 'pairingtoken', constants.PinType.PAIRING) - mock_generate.assert_called_once_with('pairingtoken') - mock_card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_CHANGE_SECRET, - p1=constants.PinType.PAIRING.value, - data=mock_generate.return_value - ) - - -def test_change_secret_user_pin_str_success(mock_card): - pin = '123456' - change_secret(mock_card, pin, constants.PinType.USER) - mock_card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_CHANGE_SECRET, - p1=constants.PinType.USER.value, - data=pin.encode('utf-8') - ) - - -def test_change_secret_user_pin_invalid_length(mock_card): - with pytest.raises(ValueError, match="User PIN must be exactly 6 digits."): - change_secret(mock_card, b'12345', constants.PinType.USER) - with pytest.raises(ValueError, match="User PIN must be exactly 6 digits."): - change_secret(mock_card, '12345', constants.PinType.USER) - - -def test_change_secret_puk_success(mock_card): - puk = b'123456789012' - change_secret(mock_card, puk, constants.PinType.PUK) - mock_card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_CHANGE_SECRET, - p1=constants.PinType.PUK.value, - data=puk - ) - - -def test_change_secret_puk_str_success(mock_card): - puk = '123456789012' - change_secret(mock_card, puk, constants.PinType.PUK) - mock_card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_CHANGE_SECRET, - p1=constants.PinType.PUK.value, - data=puk.encode('utf-8') - ) - - -def test_change_secret_puk_invalid_length(mock_card): - with pytest.raises(ValueError, match="PUK must be exactly 12 digits."): - change_secret(mock_card, b'1234567890', constants.PinType.PUK) - with pytest.raises(ValueError, match="PUK must be exactly 12 digits."): - change_secret(mock_card, '1234567890', constants.PinType.PUK) - - -def test_change_secret_pairing_bytes_success(mock_card): - secret = b'a' * 32 - change_secret(mock_card, secret, constants.PinType.PAIRING) - mock_card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_CHANGE_SECRET, - p1=constants.PinType.PAIRING.value, - data=secret - ) - - -def test_change_secret_pairing_bytes_invalid_length(mock_card): - with pytest.raises(ValueError, match="Pairing secret must be 32 bytes."): - change_secret(mock_card, b'a' * 31, constants.PinType.PAIRING) - - -def test_change_secret_raises_apdu_error(mock_card): - mock_card.send_secure_apdu.side_effect = APDUError(0x6A80) - with pytest.raises(APDUError): - change_secret(mock_card, b'123456', constants.PinType.USER) diff --git a/python/keycard-py/tests/commands/test_derive_key.py b/python/keycard-py/tests/commands/test_derive_key.py deleted file mode 100644 index 85e2646b..00000000 --- a/python/keycard-py/tests/commands/test_derive_key.py +++ /dev/null @@ -1,14 +0,0 @@ -from keycard.commands import derive_key -from keycard.constants import INS_DERIVE_KEY, DerivationSource -from keycard.parsing.keypath import KeyPath - - -def test_derive_key_valid_master(card): - key_path = KeyPath("m/44'/60'/0'/0/0") - derive_key(card, key_path.to_string()) - - card.send_secure_apdu.assert_called_once_with( - ins=INS_DERIVE_KEY, - p1=DerivationSource.MASTER, - data=key_path.data - ) diff --git a/python/keycard-py/tests/commands/test_export_key.py b/python/keycard-py/tests/commands/test_export_key.py deleted file mode 100644 index cf9db5f7..00000000 --- a/python/keycard-py/tests/commands/test_export_key.py +++ /dev/null @@ -1,100 +0,0 @@ -import pytest -from keycard.commands.export_key import export_key -from keycard.constants import DerivationOption, DerivationSource -from keycard.parsing.exported_key import ExportedKey - - -def test_export_key_success_public_only(card): - public_key = b'\x04' + b'\x01' * 64 - inner_tlv = b'\x80' + bytes([len(public_key)]) + public_key - outer_tlv = b'\xA1' + bytes([len(inner_tlv)]) + inner_tlv - card.send_secure_apdu.return_value = outer_tlv - - result = export_key( - card, - derivation_option=DerivationOption.CURRENT, - public_only=True, - keypath=None, - make_current=False, - source=DerivationSource.MASTER - ) - - assert isinstance(result, ExportedKey) - assert result.public_key == public_key - assert result.private_key is None - assert result.chain_code is None - - -def test_export_key_with_path_string(card): - public_key = b'\x04' + b'\x02' * 64 - inner_tlv = b'\x80' + bytes([len(public_key)]) + public_key - outer_tlv = b'\xA1' + bytes([len(inner_tlv)]) + inner_tlv - card.send_secure_apdu.return_value = outer_tlv - - result = export_key( - card, - derivation_option=DerivationOption.DERIVE, - public_only=True, - keypath="m/44'/60'/0'/0/0", - make_current=True, - source=DerivationSource.MASTER - ) - - assert isinstance(result, ExportedKey) - assert result.public_key == public_key - - -def test_export_key_invalid_keypath_length_bytes(card): - with pytest.raises( - ValueError, - match="Byte keypath must be a multiple of 4" - ): - export_key( - card, - derivation_option=DerivationOption.DERIVE, - public_only=True, - keypath=b'\x01\x02\x03', - make_current=False, - source=DerivationSource.PARENT - ) - - -def test_export_key_requires_keypath_if_not_current(card): - with pytest.raises( - ValueError, - match="Keypath required unless using CURRENT derivation" - ): - export_key( - card, - derivation_option=DerivationOption.DERIVE, - public_only=True, - keypath=None, - make_current=False, - source=DerivationSource.CURRENT - ) - - -def test_export_key_invalid_keypath_type(card): - with pytest.raises(TypeError, match="Keypath must be a string or bytes"): - export_key( - card, - derivation_option=DerivationOption.DERIVE, - public_only=True, - keypath=123, - make_current=False, - source=DerivationSource.CURRENT - ) - - -def test_export_key_missing_keypair_template(card): - card.send_secure_apdu.return_value = b'\xA0\x00' - - with pytest.raises(ValueError, match="Missing keypair template"): - export_key( - card, - derivation_option=DerivationOption.CURRENT, - public_only=True, - keypath=None, - make_current=False, - source=DerivationSource.MASTER - ) diff --git a/python/keycard-py/tests/commands/test_factory_reset.py b/python/keycard-py/tests/commands/test_factory_reset.py deleted file mode 100644 index 31d1e530..00000000 --- a/python/keycard-py/tests/commands/test_factory_reset.py +++ /dev/null @@ -1,25 +0,0 @@ -import pytest -from unittest.mock import Mock -from keycard import constants -from keycard.commands.factory_reset import factory_reset -from keycard.exceptions import APDUError - - -def test_factory_reset_success(card): - mock_response = Mock() - mock_response.status_word = 0x9000 - card.send_apdu.return_value = mock_response - - factory_reset(card) - card.send_apdu.assert_called_once_with( - ins=constants.INS_FACTORY_RESET, - p1=0xAA, - p2=0x55 - ) - - -def test_factory_reset_failure(card): - card.send_apdu.side_effect = APDUError(0x6A80) - - with pytest.raises(APDUError): - factory_reset(card) diff --git a/python/keycard-py/tests/commands/test_generate_key.py b/python/keycard-py/tests/commands/test_generate_key.py deleted file mode 100644 index ef2c820e..00000000 --- a/python/keycard-py/tests/commands/test_generate_key.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest -from keycard import constants -from keycard.commands.generate_key import generate_key -from keycard.exceptions import APDUError - - -def test_generate_key_success(card): - mock_id = b'\x01' * 32 - card.send_secure_apdu.return_value = mock_id - result = generate_key(card) - assert result == mock_id - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_GENERATE_KEY) - - -def test_generate_key_apdu_error(card): - card.send_secure_apdu.side_effect = APDUError(0x6A80) - - with pytest.raises(APDUError): - generate_key(card) diff --git a/python/keycard-py/tests/commands/test_generate_mnemonic.py b/python/keycard-py/tests/commands/test_generate_mnemonic.py deleted file mode 100644 index 54160cc4..00000000 --- a/python/keycard-py/tests/commands/test_generate_mnemonic.py +++ /dev/null @@ -1,40 +0,0 @@ -import pytest -from keycard.constants import INS_GENERATE_MNEMONIC -from keycard.commands.generate_mnemonic import generate_mnemonic - - -def test_generate_mnemonic_valid(card): - card.send_secure_apdu.return_value = bytes([ - 0x00, 0x00, - 0x07, 0xFF, - 0x05, 0x39, - 0x00, 0x2A - ]) - - result = generate_mnemonic(card, checksum_size=6) - - card.send_secure_apdu.assert_called_once_with( - ins=INS_GENERATE_MNEMONIC, - p1=6 - ) - - assert result == [0, 2047, 1337, 42] - - -def test_generate_mnemonic_invalid_checksum(card): - with pytest.raises( - ValueError, - match="Checksum size must be between 4 and 8" - ): - generate_mnemonic(card, checksum_size=2) - - -def test_generate_mnemonic_odd_length_response(card): - # Simulate invalid odd-length byte response - card.send_secure_apdu.return_value = b'\x00\x01\x02' - - with pytest.raises( - ValueError, - match="Response must contain an even number of bytes" - ): - generate_mnemonic(card, checksum_size=6) diff --git a/python/keycard-py/tests/commands/test_get_data.py b/python/keycard-py/tests/commands/test_get_data.py deleted file mode 100644 index 2faf5127..00000000 --- a/python/keycard-py/tests/commands/test_get_data.py +++ /dev/null @@ -1,31 +0,0 @@ -import pytest -from keycard.commands.get_data import get_data -from keycard import constants - - -def test_get_data_secure_channel(card): - card.is_secure_channel_open = True - card.send_secure_apdu.return_value = b"secure_data" - result = get_data(card, slot=constants.StorageSlot.PUBLIC) - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_GET_DATA, - p1=constants.StorageSlot.PUBLIC, - ) - assert result == card.send_secure_apdu.return_value - - -def test_get_data_proprietary_channel(card): - card.is_secure_channel_open = False - card.send_apdu.return_value = b"proprietary_data" - result = get_data(card, slot=constants.StorageSlot.NDEF) - card.send_apdu.assert_called_once_with( - ins=constants.INS_GET_DATA, - p1=constants.StorageSlot.NDEF.value, - cla=constants.CLA_PROPRIETARY - ) - assert result == card.send_apdu.return_value - - -def test_get_data_invalid_slot(card): - with pytest.raises(AttributeError): - get_data(card, slot="INVALID_SLOT") diff --git a/python/keycard-py/tests/commands/test_get_status.py b/python/keycard-py/tests/commands/test_get_status.py deleted file mode 100644 index 0464b905..00000000 --- a/python/keycard-py/tests/commands/test_get_status.py +++ /dev/null @@ -1,24 +0,0 @@ -from keycard.commands.get_status import get_status - - -def test_get_application_status(card): - card.send_secure_apdu.return_value = bytes.fromhex( - 'A309020103020102010101') - - result = get_status(card) - - assert result['pin_retry_count'] == 3 - assert result['puk_retry_count'] == 2 - assert result['initialized'] is True - - -def test_get_key_path_status(card): - key_path = [0x8000002C, 0x8000003C] - - card.send_secure_apdu.return_value = b''.join( - i.to_bytes(4, 'big') for i in key_path - ) - - result = get_status(card, key_path=True) - - assert result == key_path diff --git a/python/keycard-py/tests/commands/test_init.py b/python/keycard-py/tests/commands/test_init.py deleted file mode 100644 index d24dd33b..00000000 --- a/python/keycard-py/tests/commands/test_init.py +++ /dev/null @@ -1,85 +0,0 @@ -import sys -import pytest -from unittest.mock import MagicMock, patch -from keycard.commands.init import init -from keycard.exceptions import APDUError -from keycard import constants - - -PIN = b'1234' -PUK = b'5678' -PAIRING_SECRET = b'abcdefgh' -CARD_PUBLIC_KEY = b'\x04' + b'\x00' * 64 # Valid uncompressed pubkey format - - -@pytest.fixture -def ecc_patches(): - init_module = sys.modules['keycard.commands.init'] - with ( - patch.object(init_module, 'urandom', return_value=b'\x00' * 16), - patch.object( - init_module, - 'aes_cbc_encrypt', - side_effect=lambda k, iv, - pt: b'\xAA' * len(pt) - ), - patch.object(init_module, 'SigningKey') as mock_signing_key_cls, - patch.object(init_module, 'VerifyingKey') as mock_verifying_key_cls, - patch.object(init_module, 'ECDH') as mock_ecdh_cls, - ): - mock_gen = mock_signing_key_cls.generate - fake_privkey = MagicMock() - fake_privkey.verifying_key.to_string.return_value = b'\x01' * 65 - mock_gen.return_value = fake_privkey - - mock_parse = mock_verifying_key_cls.from_string - mock_parse.return_value = 'parsed-pubkey' - - ecdh_instance = MagicMock() - ecdh_instance.generate_sharedsecret_bytes.return_value = b'\xBB' * 32 - mock_ecdh_cls.return_value = ecdh_instance - - yield - - -def test_init_success(card, ecc_patches): - card.send_apdu.return_value = b'' - card.card_public_key = CARD_PUBLIC_KEY - - init(card, PIN, PUK, PAIRING_SECRET) - - card.send_apdu.assert_called_once_with( - ins=constants.INS_INIT, - data=bytes.fromhex( - '4101010101010101010101010101010101010101010101010101010' - '1010101010101010101010101010101010101010101010101010101' - '010101010101010101010100000000000000000000000000000000' - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') - ) - - -@pytest.mark.parametrize('secret_length', [10, 240]) -def test_init_data_length(card, ecc_patches, secret_length): - card.send_apdu.return_value = b'' - card.card_public_key = CARD_PUBLIC_KEY - - pairing_secret = b'x' * secret_length - plaintext = PIN + PUK + pairing_secret - total_data_len = 1 + 65 + 16 + len(plaintext) - - if total_data_len > 255: - with pytest.raises(ValueError, match='Data too long'): - init(card, PIN, PUK, pairing_secret) - else: - init(card, PIN, PUK, pairing_secret) - assert card.send_apdu.call_count == 1 - - -def test_init_apdu_error(card, ecc_patches): - card.send_apdu.side_effect = APDUError(0x6A84) - card.card_public_key = CARD_PUBLIC_KEY - - with pytest.raises(APDUError) as excinfo: - init(card, PIN, PUK, PAIRING_SECRET) - - assert excinfo.value.sw == 0x6A84 diff --git a/python/keycard-py/tests/commands/test_keypath.py b/python/keycard-py/tests/commands/test_keypath.py deleted file mode 100644 index a1177493..00000000 --- a/python/keycard-py/tests/commands/test_keypath.py +++ /dev/null @@ -1,57 +0,0 @@ -import pytest -from keycard.parsing.keypath import KeyPath -from keycard.constants import DerivationSource - - -def test_keypath_from_string_master(): - path = KeyPath("m/44'/60'/0'/0/0") - assert path.source == DerivationSource.MASTER - assert path.data == bytes.fromhex( - '8000002c8000003c800000000000000000000000') - assert path.to_string() == "m/44'/60'/0'/0/0" - - -def test_keypath_from_string_parent(): - path = KeyPath('../1/2/3') - assert path.source == DerivationSource.PARENT - assert path.to_string() == '../1/2/3' - - -def test_keypath_from_string_current_default(): - path = KeyPath('1/2/3') - assert path.source == DerivationSource.CURRENT - assert path.to_string() == './1/2/3' - - -def test_keypath_from_bytes(): - data = bytes.fromhex('8000002c00000001') - path = KeyPath(data, source=DerivationSource.PARENT) - assert path.source == DerivationSource.PARENT - assert path.data == data - assert path.to_string() == "../44'/1" - - -def test_keypath_empty_string_raises(): - with pytest.raises(ValueError, match="Empty path"): - KeyPath('') - - -def test_keypath_invalid_component(): - with pytest.raises(ValueError, match="Invalid component: abc"): - KeyPath('m/abc') - - -def test_keypath_too_many_components(): - long_path = 'm/' + '/'.join('0' for _ in range(11)) - with pytest.raises(ValueError, match="Too many components"): - KeyPath(long_path) - - -def test_keypath_invalid_byte_length(): - with pytest.raises(ValueError, match="Byte path must be a multiple of 4"): - KeyPath(b'\x00\x01') - - -def test_keypath_invalid_type(): - with pytest.raises(TypeError, match="Path must be a string or bytes"): - KeyPath(123) diff --git a/python/keycard-py/tests/commands/test_load_key.py b/python/keycard-py/tests/commands/test_load_key.py deleted file mode 100644 index 5f92a196..00000000 --- a/python/keycard-py/tests/commands/test_load_key.py +++ /dev/null @@ -1,89 +0,0 @@ -import pytest -from keycard.commands.load_key import load_key -from keycard import constants -from hashlib import sha256 -from keycard.parsing import tlv - - -def test_load_key_bip39(card): - seed = b"\xAA" * 64 - fake_uid = b"\xBB" * 32 - card.send_secure_apdu.return_value = fake_uid - - result = load_key( - card, - key_type=constants.LoadKeyType.BIP39_SEED, - bip39_seed=seed - ) - - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_LOAD_KEY, - p1=constants.LoadKeyType.BIP39_SEED, - p2=0, - data=seed - ) - assert result == fake_uid - - -def test_load_key_pair(card): - public_key = b'\x04' + b'\x01' * 64 - private_key = b'\x02' * 32 - uid = sha256(public_key).digest() - card.send_secure_apdu.return_value = uid - - encoded = tlv.encode_tlv( - 0xA1, - tlv.encode_tlv(0x80, public_key) + - tlv.encode_tlv(0x81, private_key) - ) - - result = load_key( - card, - key_type=constants.LoadKeyType.ECC, - public_key=public_key, - private_key=private_key - ) - - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_LOAD_KEY, - p1=constants.LoadKeyType.ECC, - p2=0, - data=encoded - ) - assert result == uid - - -def test_bip39_seed_too_short(card): - with pytest.raises(ValueError, match="BIP39/LEE seed must be 16-64 bytes"): - load_key( - card, - key_type=constants.LoadKeyType.BIP39_SEED, - bip39_seed=b"\xAA" * 8 - ) - - -def test_bip39_seed_missing(card): - with pytest.raises(ValueError, match="Either bip39_seed or lee_seed must be provided for key_type = BIP39_SEED"): - load_key( - card, - key_type=constants.LoadKeyType.BIP39_SEED - ) - - -def test_ecc_missing_private_key(card): - with pytest.raises(ValueError, match="Private key.*required"): - load_key( - card, - key_type=constants.LoadKeyType.ECC, - public_key=b"\x04" + b"\x01" * 64 - ) - - -def test_extended_ecc_missing_private_key(card): - with pytest.raises(ValueError, match="Private key.*required"): - load_key( - card, - key_type=constants.LoadKeyType.EXTENDED_ECC, - public_key=b"\x04" + b"\x02" * 64, - chain_code=b"\x00" * 32 - ) diff --git a/python/keycard-py/tests/commands/test_mutually_authenticate.py b/python/keycard-py/tests/commands/test_mutually_authenticate.py deleted file mode 100644 index dec88a24..00000000 --- a/python/keycard-py/tests/commands/test_mutually_authenticate.py +++ /dev/null @@ -1,48 +0,0 @@ -import pytest - -from keycard import constants -from keycard.commands.mutually_authenticate import mutually_authenticate -from keycard.exceptions import APDUError - - -def test_mutually_authenticate_success(card): - client_challenge = bytes(32) - card.send_secure_apdu.return_value = bytes(32) - - mutually_authenticate(card, client_challenge) - - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_MUTUALLY_AUTHENTICATE, - data=client_challenge - ) - - -def test_mutually_authenticate_invalid_status_word(card): - card.send_secure_apdu.side_effect = APDUError(0x6F00) - - with pytest.raises(APDUError, match='APDU failed with SW=6F00'): - mutually_authenticate(card, bytes(32)) - - -def test_mutually_authenticate_invalid_response_length(card): - client_challenge = b'\xAA' * 32 - response = b'\xBB' * 16 # Invalid length - - card.send_secure_apdu.return_value = response - - with pytest.raises( - ValueError, - match='Response to MUTUALLY AUTHENTICATE is not 32 bytes' - ): - mutually_authenticate(card, client_challenge) - - -def test_mutually_authenticate_auto_challenge(card, monkeypatch): - fake_challenge = b'\xCC' * 32 - monkeypatch.setattr('os.urandom', lambda n: fake_challenge) - - card.send_secure_apdu.return_value = fake_challenge - - mutually_authenticate(card) - - card.send_secure_apdu.assert_called_once() diff --git a/python/keycard-py/tests/commands/test_open_secure_channel.py b/python/keycard-py/tests/commands/test_open_secure_channel.py deleted file mode 100644 index c6adab33..00000000 --- a/python/keycard-py/tests/commands/test_open_secure_channel.py +++ /dev/null @@ -1,109 +0,0 @@ -import sys -import pytest -from unittest.mock import MagicMock, patch -from ecdsa import SECP256k1 - -from keycard.card_interface import CardInterface -from keycard.commands.open_secure_channel import open_secure_channel -from keycard.exceptions import APDUError - - -@pytest.fixture -def mock_ecdsa(): - open_secure_channel_module = sys.modules[ - 'keycard.commands.open_secure_channel' - ] - with ( - patch.object( - open_secure_channel_module, - 'SecureChannel' - ) as mock_secure_channel, - patch.object( - open_secure_channel_module, - 'VerifyingKey' - ) as mock_verifying_key, - patch.object( - open_secure_channel_module, - 'ECDH' - ) as mock_ecdh, - patch.object( - open_secure_channel_module, - 'SigningKey' - ) as mock_signing_key, - ): - yield { - 'secure_channel': mock_secure_channel, - 'verifying_key': mock_verifying_key, - 'ecdh': mock_ecdh, - 'signing_key': mock_signing_key, - } - - -def test_open_secure_channel_success(mock_ecdsa): - mock_verifying_key = mock_ecdsa['verifying_key'] - mock_ecdh = mock_ecdsa['ecdh'] - mock_signing_key = mock_ecdsa['signing_key'] - mock_secure_channel = mock_ecdsa['secure_channel'] - - pairing_index = 1 - pairing_key = b'pairing_key' - card = MagicMock(spec=CardInterface) - card.card_public_key = b'\x04' + b'\x01' * 64 - - salt = b'A' * 32 - seed_iv = b'B' * 16 - response_data = salt + seed_iv - card.send_apdu.return_value = response_data - - # Mock SigningKey.generate - mock_signing_key_instance = MagicMock() - mock_signing_key_instance.verifying_key.to_string.return_value = \ - b'\x04' + b'\x02' * 64 - mock_signing_key.generate.return_value = mock_signing_key_instance - - mock_verifying_key.from_string.return_value = MagicMock() - mock_ecdh_instance = MagicMock() - mock_ecdh.return_value = mock_ecdh_instance - mock_ecdh_instance.generate_sharedsecret_bytes.return_value = ( - b'shared_secret' - ) - mock_secure_channel.open.return_value = 'secure_session' - - result = open_secure_channel( - card, - pairing_index, - pairing_key - ) - - card.send_apdu.assert_called_once() - mock_verifying_key.from_string.assert_called_once_with( - card.card_public_key, curve=SECP256k1 - ) - mock_ecdh.assert_called_once() - mock_ecdh_instance.generate_sharedsecret_bytes.assert_called_once() - mock_secure_channel.open.assert_called_once_with( - b'shared_secret', pairing_key, salt, seed_iv - ) - assert result == 'secure_session' - - -def test_open_secure_channel_raises_apdu_error(card, mock_ecdsa): - mock_signing_key = mock_ecdsa['signing_key'] - - # Mock SigningKey.generate - mock_signing_key_instance = MagicMock() - mock_signing_key_instance.verifying_key.to_string.return_value = \ - b'\x04' + b'\x02' * 64 - mock_signing_key.generate.return_value = mock_signing_key_instance - - pairing_index = 1 - pairing_key = b'pairing_key' - card.card_public_key = b'\x04' + b'\x01' * 64 - card.send_apdu.side_effect = APDUError(0x6A80) - - with pytest.raises(APDUError): - open_secure_channel( - card, - pairing_index, - pairing_key - ) diff --git a/python/keycard-py/tests/commands/test_pair.py b/python/keycard-py/tests/commands/test_pair.py deleted file mode 100644 index b43b9269..00000000 --- a/python/keycard-py/tests/commands/test_pair.py +++ /dev/null @@ -1,106 +0,0 @@ -import sys -import pytest -import hashlib - -from unittest.mock import patch - -from keycard.constants import INS_PAIR, PairingMode -from keycard.commands.pair import pair -from keycard.exceptions import APDUError, InvalidResponseError - - -@pytest.fixture -def mock_urandom(): - pair_module = sys.modules['keycard.commands.pair'] - with patch.object(pair_module, 'urandom', return_value=b'\x01' * 32): - yield - - -def test_pair_success(card, mock_urandom): - shared_secret = b'\xAA' * 32 - client_challenge = b'\x01' * 32 - card_challenge = b'\x02' * 32 - expected_card_cryptogram = hashlib.sha256( - shared_secret + client_challenge).digest() - expected_client_cryptogram = hashlib.sha256( - shared_secret + card_challenge).digest() - - first_response = expected_card_cryptogram + card_challenge - second_response = b'\x05' + card_challenge - - card.send_apdu.side_effect = [first_response, second_response] - - result = pair(card, shared_secret) - - assert result == (5, expected_client_cryptogram) - assert card.send_apdu.call_count == 2 - - -def test_pairing_mode(card, mock_urandom): - shared_secret = b'\xAA' * 32 - client_challenge = b'\x01' * 32 - card_challenge = b'\x02' * 32 - expected_card_cryptogram = hashlib.sha256( - shared_secret + client_challenge).digest() - first_response = expected_card_cryptogram + card_challenge - second_response = b'\x05' + card_challenge - - card.send_apdu.side_effect = [first_response, second_response] - - pair(card, shared_secret, PairingMode.EPHEMERAL) - card.send_apdu.assert_any_call( - ins=INS_PAIR, - p2=PairingMode.EPHEMERAL, - data=client_challenge - ) - - -def test_pair_invalid_shared_secret(card, mock_urandom): - with pytest.raises(ValueError, match='Shared secret must be 32 bytes'): - pair(card, b'short') - - -def test_pair_apdu_error_on_first(card, mock_urandom): - card.send_apdu.side_effect = APDUError(0x6A82) - - with pytest.raises(APDUError): - pair(card, b'\x00' * 32) - - -def test_pair_invalid_response_length_first(card, mock_urandom): - card.send_apdu.return_value = bytes(10) - - with pytest.raises( - InvalidResponseError, - match='Unexpected response length' - ): - pair(card, b'\x00' * 32) - - -def test_pair_cryptogram_mismatch(card, mock_urandom): - wrong_card_cryptogram = b'\xAB' * 32 - card_challenge = b'\x02' * 32 - response = wrong_card_cryptogram + card_challenge - - card.send_apdu.side_effect = [response] - - with pytest.raises(InvalidResponseError, match='Card cryptogram mismatch'): - pair(card, b'\xAA' * 32) - - -def test_pair_invalid_response_second_apdu(card, mock_urandom): - shared_secret = b'\xAA' * 32 - client_challenge = b'\x01' * 32 - card_challenge = b'\x02' * 32 - card_cryptogram = hashlib.sha256(shared_secret + client_challenge).digest() - - first_response = card_cryptogram + card_challenge - second_response = b'\x00' * 10 - - card.send_apdu.side_effect = [first_response, second_response] - - with pytest.raises( - InvalidResponseError, - match='Unexpected response length' - ): - pair(card, shared_secret) diff --git a/python/keycard-py/tests/commands/test_remove_key.py b/python/keycard-py/tests/commands/test_remove_key.py deleted file mode 100644 index 20c76d04..00000000 --- a/python/keycard-py/tests/commands/test_remove_key.py +++ /dev/null @@ -1,11 +0,0 @@ -from keycard.commands.remove_key import remove_key - - -def test_remove_key_calls_send_secure_apdu_with_correct_ins(card): - remove_key(card) - card.send_secure_apdu.assert_called_once_with(ins=0xD3) - - -def test_remove_key_returns_none(card): - result = remove_key(card) - assert result is None diff --git a/python/keycard-py/tests/commands/test_select.py b/python/keycard-py/tests/commands/test_select.py deleted file mode 100644 index 75a040b9..00000000 --- a/python/keycard-py/tests/commands/test_select.py +++ /dev/null @@ -1,41 +0,0 @@ -import sys -import pytest - - -from unittest.mock import MagicMock, patch -from keycard.commands.select import select -from keycard.exceptions import APDUError -from keycard import constants - - -def test_select_success(): - select_module = sys.modules['keycard.commands.select'] - dummy_info = MagicMock() - response_data = b'\x01\x02\x03\x04' - - card = MagicMock() - card.send_apdu.return_value = response_data - - with patch.object(select_module, 'ApplicationInfo') as mock_app_info: - mock_app_info.parse.return_value = dummy_info - result = select(card) - - card.send_apdu.assert_called_once_with( - cla=constants.CLAISO7816, - ins=constants.INS_SELECT, - p1=0x04, - p2=0x00, - data=constants.KEYCARD_AID - ) - mock_app_info.parse.assert_called_once_with(response_data) - assert result == dummy_info - - -def test_select_apdu_error(): - card = MagicMock() - card.send_apdu.side_effect = APDUError(0x6A82) - - with pytest.raises(APDUError) as excinfo: - select(card) - - assert excinfo.value.sw == 0x6A82 diff --git a/python/keycard-py/tests/commands/test_set_pinless_path.py b/python/keycard-py/tests/commands/test_set_pinless_path.py deleted file mode 100644 index ff8be399..00000000 --- a/python/keycard-py/tests/commands/test_set_pinless_path.py +++ /dev/null @@ -1,24 +0,0 @@ -from keycard.commands.set_pinless_path import set_pinless_path -from keycard.constants import INS_SET_PINLESS_PATH -from keycard.parsing.keypath import KeyPath - - -def test_set_pinless_path(card): - path = "m/44'/60'/0'/0/0" - expected_data = KeyPath(path).data - - set_pinless_path(card, path) - - card.send_secure_apdu.assert_called_once_with( - ins=INS_SET_PINLESS_PATH, - data=expected_data - ) - - -def test_set_pinless_path_empty(card): - set_pinless_path(card, "") - - card.send_secure_apdu.assert_called_once_with( - ins=INS_SET_PINLESS_PATH, - data=b"" - ) diff --git a/python/keycard-py/tests/commands/test_sign.py b/python/keycard-py/tests/commands/test_sign.py deleted file mode 100644 index 40ae3ee9..00000000 --- a/python/keycard-py/tests/commands/test_sign.py +++ /dev/null @@ -1,101 +0,0 @@ -import sys -import pytest - -from unittest import mock - -from keycard.commands.sign import sign -from keycard import constants -from keycard.exceptions import InvalidStateError -from keycard.parsing.keypath import KeyPath - - -def test_sign_current_key(card): - sign_module = sys.modules['keycard.commands.sign'] - digest = b'\xAA' * 32 - raw = b'\x01' * 64 + b'\x1f' - encoded = b'\x80' + bytes([len(raw)]) + raw - card.send_secure_apdu.return_value = encoded - with mock.patch.object(sign_module, "SignatureResult"): - sign(card, digest) - - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_SIGN, - p1=constants.DerivationOption.CURRENT, - p2=constants.SigningAlgorithm.ECDSA_SECP256K1, - data=digest, - ) - - -def test_sign_with_derivation_path(card): - sign_module = sys.modules['keycard.commands.sign'] - digest = bytes(32) - raw = bytes(65) - encoded = b'\x80' + bytes([len(raw)]) + raw - card.send_secure_apdu.return_value = encoded - key_path = KeyPath("m/44'/60'/0'/0/0") - expected_data = digest + key_path.data - - with mock.patch.object(sign_module, "SignatureResult"): - sign( - card, - digest, - p1=constants.DerivationOption.DERIVE, - derivation_path=key_path.to_string() - ) - - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_SIGN, - p1=constants.DerivationOption.DERIVE, - p2=constants.SigningAlgorithm.ECDSA_SECP256K1, - data=expected_data, - ) - - -def test_sign_requires_pin(card): - card.is_pin_verified = False - digest = b'\xCC' * 32 - - with pytest.raises( - InvalidStateError, - match="PIN must be verified to sign with this derivation option" - ): - sign(card, digest) - - -def test_sign_short_digest(card): - short_digest = b'\xDD' * 10 - - with pytest.raises(ValueError, match="Digest must be exactly 32 bytes"): - sign(card, short_digest) - - -def test_sign_missing_path(card): - digest = b'\xEE' * 32 - - with pytest.raises(ValueError, match="Derivation path cannot be empty"): - sign( - card, - digest, - p1=constants.DerivationOption.DERIVE, - derivation_path=None - ) - - -def test_sign_not_implemented_algo(card): - digest = b'\xAB' * 32 - - with pytest.raises( - NotImplementedError, - match="Signature algorithm 255 not supported" - ): - sign(card, digest, p2=0xFF) - - -def test_sign_raw_signature_wrong_length(card): - digest = b'\xCC' * 32 - raw = b'\x01' * 64 # Should be 65 bytes - encoded = b'\x80' + bytes([len(raw)]) + raw - card.send_secure_apdu.return_value = encoded - card.is_pin_verified = True - with pytest.raises(ValueError, match="Expected 65-byte raw signature"): - sign(card, digest) diff --git a/python/keycard-py/tests/commands/test_store_data.py b/python/keycard-py/tests/commands/test_store_data.py deleted file mode 100644 index f24c8f75..00000000 --- a/python/keycard-py/tests/commands/test_store_data.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest - -from keycard.commands import store_data -from keycard import constants - - -def test_store_data_calls_send_secure_apdu_with_correct_args(card): - store_data(card, b"hello", constants.StorageSlot.PUBLIC) - - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_STORE_DATA, - p1=constants.StorageSlot.PUBLIC.value, - data=b'hello' - ) - - -def test_store_data_uses_default_slot(card): - store_data(card, b'world') - - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_STORE_DATA, - p1=constants.StorageSlot.PUBLIC, - data=b'world' - ) - - -def test_store_data_raises_value_error_on_too_long_data(card): - with pytest.raises(ValueError, match="Data too long"): - store_data(card, b'a' * 128, constants.StorageSlot.PUBLIC) diff --git a/python/keycard-py/tests/commands/test_unblock_pin.py b/python/keycard-py/tests/commands/test_unblock_pin.py deleted file mode 100644 index 8eea2a1d..00000000 --- a/python/keycard-py/tests/commands/test_unblock_pin.py +++ /dev/null @@ -1,43 +0,0 @@ -import pytest -from keycard.commands.unblock_pin import unblock_pin -from keycard import constants - - -def test_unblock_pin_with_valid_str(card): - puk = '123456789012' - pin = '123456' - unblock_pin(card, puk + pin) - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_UNBLOCK_PIN, - data=(puk + pin).encode('utf-8') - ) - - -def test_unblock_pin_with_valid_bytes(card): - data = b'123456789012123456' - unblock_pin(card, data) - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_UNBLOCK_PIN, - data=data - ) - - -@pytest.mark.parametrize('bad_input', [ - '12345678901212345', # Too short - '1234567890121234567', # Too long - b'12345678901212345', # Too short (bytes) - b'1234567890121234567', # Too long (bytes) -]) -def test_unblock_pin_invalid_length(card, bad_input): - with pytest.raises(ValueError, match='exactly 18 digits'): - unblock_pin(card, bad_input) - - -@pytest.mark.parametrize('bad_input', [ - '12345678901A123456', # Non-digit in PUK - '12345678901212345A', # Non-digit in PIN - 'ABCDEFGHIJKL123456', # All non-digits in PUK -]) -def test_unblock_pin_invalid_digits(card, bad_input): - with pytest.raises(ValueError, match='must be numeric digits'): - unblock_pin(card, bad_input) diff --git a/python/keycard-py/tests/commands/test_unpair.py b/python/keycard-py/tests/commands/test_unpair.py deleted file mode 100644 index e65c80cd..00000000 --- a/python/keycard-py/tests/commands/test_unpair.py +++ /dev/null @@ -1,25 +0,0 @@ -import pytest -from keycard.commands.unpair import unpair -from keycard.apdu import APDUResponse -from keycard.exceptions import APDUError -from keycard import constants - - -def test_unpair_success(card): - card.send_secure_apdu.return_value = APDUResponse(b'', 0x9000) - - unpair(card, 1) - - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_UNPAIR, - p1=0x01, - ) - - -def test_unpair_apdu_error(card): - card.send_secure_apdu.side_effect = APDUError(0x6A84) - - with pytest.raises(APDUError) as excinfo: - unpair(card, 1) - - assert excinfo.value.sw == 0x6A84 diff --git a/python/keycard-py/tests/commands/test_verify_pin.py b/python/keycard-py/tests/commands/test_verify_pin.py deleted file mode 100644 index 910b7fea..00000000 --- a/python/keycard-py/tests/commands/test_verify_pin.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest -from keycard.commands.verify_pin import verify_pin -from keycard.exceptions import APDUError -from keycard import constants - - -def test_verify_pin_success(card): - assert verify_pin(card, '1234') is True - card.send_secure_apdu.assert_called_once_with( - ins=constants.INS_VERIFY_PIN, - data=b'1234' - ) - - -def test_verify_pin_incorrect_but_allowed(card): - card.send_secure_apdu.side_effect = APDUError(0x63C2) - assert verify_pin(card, '0000') is False - - -def test_verify_pin_blocked(card): - card.send_secure_apdu.side_effect = APDUError(0x63C0) - with pytest.raises(RuntimeError, match='PIN is blocked'): - verify_pin(card, '0000') - - -def test_verify_pin_other_apdu_error(card): - card.send_secure_apdu.side_effect = APDUError(0x6A80) - with pytest.raises(APDUError): - verify_pin(card, '0000') diff --git a/python/keycard-py/tests/conftest.py b/python/keycard-py/tests/conftest.py deleted file mode 100644 index 81c5a6b1..00000000 --- a/python/keycard-py/tests/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ -import pytest -from unittest.mock import Mock -from keycard.card_interface import CardInterface - - -@pytest.fixture -def card(): - mock = Mock(spec=CardInterface) - return mock diff --git a/python/keycard-py/tests/crypto/__init__.py b/python/keycard-py/tests/crypto/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/python/keycard-py/tests/crypto/test_aes.py b/python/keycard-py/tests/crypto/test_aes.py deleted file mode 100644 index c1b6b70d..00000000 --- a/python/keycard-py/tests/crypto/test_aes.py +++ /dev/null @@ -1,55 +0,0 @@ -import pytest -from keycard.crypto import aes - - -def test_aes_cbc_encrypt_decrypt_roundtrip(): - key = b'0123456789abcdef' - iv = b'abcdef9876543210' - data = b'hello world 1234' - ciphertext = aes.aes_cbc_encrypt(key, iv, data) - decrypted = aes.aes_cbc_decrypt(key, iv, ciphertext) - assert decrypted == data - - -def test_aes_cbc_encrypt_padding(): - key = b'0123456789abcdef' - iv = b'abcdef9876543210' - data = b'abc' - ciphertext = aes.aes_cbc_encrypt(key, iv, data) - assert len(ciphertext) % 16 == 0 - decrypted = aes.aes_cbc_decrypt(key, iv, ciphertext) - assert decrypted == data - - -def test_aes_cbc_encrypt_no_padding(): - key = b'0123456789abcdef' - iv = b'abcdef9876543210' - data = b'1234567890abcdef' - ciphertext = aes.aes_cbc_encrypt(key, iv, data, padding=False) - assert len(ciphertext) == 16 - with pytest.raises(ValueError): - aes.aes_cbc_decrypt(key, iv, ciphertext) - - -def test_aes_cbc_decrypt_invalid_padding(): - key = b'0123456789abcdef' - iv = b'abcdef9876543210' - data = b'1234567890abcdef' - ciphertext = aes.aes_cbc_encrypt(key, iv, data, padding=False) - with pytest.raises(ValueError): - aes.aes_cbc_decrypt(key, iv, ciphertext) - - -@pytest.mark.parametrize('data', [ - b'', - b'a', - b'short', - b'exactly16bytes!!', - b'longer data that is not a multiple of block size', -]) -def test_various_lengths(data): - key = b'0123456789abcdef' - iv = b'abcdef9876543210' - ciphertext = aes.aes_cbc_encrypt(key, iv, data) - decrypted = aes.aes_cbc_decrypt(key, iv, ciphertext) - assert decrypted == data diff --git a/python/keycard-py/tests/crypto/test_generate_pairing_token.py b/python/keycard-py/tests/crypto/test_generate_pairing_token.py deleted file mode 100644 index 67aec706..00000000 --- a/python/keycard-py/tests/crypto/test_generate_pairing_token.py +++ /dev/null @@ -1,28 +0,0 @@ -import hashlib -import unicodedata -from keycard.crypto.generate_pairing_token import generate_pairing_token - - -def test_generate_pairing_token_deterministic(): - passphrase = "correct horse battery staple" - expected = hashlib.pbkdf2_hmac( - 'sha256', - unicodedata.normalize('NFKD', passphrase).encode('utf-8'), - unicodedata.normalize( - 'NFKD', 'Keycard Pairing Password Salt').encode('utf-8'), - 50000, - dklen=32 - ) - assert generate_pairing_token(passphrase) == expected - - -def test_generate_pairing_token_unicode_normalization(): - token_plain = generate_pairing_token("é") - token_composed = generate_pairing_token("é") - assert token_plain == token_composed - - -def test_generate_pairing_token_output_length(): - token = generate_pairing_token("whatever") - assert isinstance(token, bytes) - assert len(token) == 32 diff --git a/python/keycard-py/tests/parsing/__init__.py b/python/keycard-py/tests/parsing/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/python/keycard-py/tests/parsing/test_application_info.py b/python/keycard-py/tests/parsing/test_application_info.py deleted file mode 100644 index 3b666a61..00000000 --- a/python/keycard-py/tests/parsing/test_application_info.py +++ /dev/null @@ -1,162 +0,0 @@ -import pytest -from keycard.exceptions import InvalidResponseError -from keycard.parsing.application_info import ApplicationInfo - - -class DummyCapabilities: - CREDENTIALS_MANAGEMENT = 1 - SECURE_CHANNEL = 2 - - @staticmethod - def parse(val): - return val - - -def test_parse_simple_pubkey(monkeypatch): - monkeypatch.setattr( - 'keycard.parsing.application_info.Capabilities', - DummyCapabilities - ) - data = bytes([0x80, 0x04, 0x01, 0x02, 0x03, 0x04]) - info = ApplicationInfo.parse(data) - - assert info.ecc_public_key == b'\x01\x02\x03\x04' - assert info.capabilities == 3 - assert info.instance_uid is None - assert info.key_uid is None - assert info.version_major == 0 - assert info.version_minor == 0 - - -def test_str_method(): - info = ApplicationInfo( - capabilities=7, - ecc_public_key=b'\x01\x02', - instance_uid=b'\xAA\xBB', - key_uid=b'\xCC\xDD', - version_major=2, - version_minor=5, - ) - s = str(info) - - assert '2.5' in s - assert 'aabb' in s - assert 'ccdd' in s - assert '0102' in s - assert '7' in s - - -def test_parse_tlv_success(monkeypatch): - def dummy_parse_tlv(data): - if data == b'\xA4\x0C' + b'\x01'*12: - return { - 0xA4: [ - b'\x8F\x02\xAA\xBB' - b'\x80\x02\x01\x02' - b'\x8E\x02\xCC\xDD' - b'\x8D\x01\x07' - b'\x02\x02\x02\x05' - ] - } - return { - 0x8F: [b'\xAA\xBB'], - 0x80: [b'\x01\x02'], - 0x8E: [b'\xCC\xDD'], - 0x8D: [b'\x07'], - 0x02: [b'\x02\x05'] - } - - class DummyCapabilities: - @staticmethod - def parse(val): - return val - - monkeypatch.setattr( - 'keycard.parsing.application_info.parse_tlv', - dummy_parse_tlv - ) - monkeypatch.setattr( - 'keycard.parsing.application_info.Capabilities', - DummyCapabilities - ) - - # Simulate TLV-encoded data - data = b'\xA4\x0C' + b'\x01'*12 - info = ApplicationInfo.parse(data) - - assert info.instance_uid == b'\xAA\xBB' - assert info.ecc_public_key == b'\x01\x02' - assert info.key_uid == b'\xCC\xDD' - assert info.capabilities == 7 - assert info.version_major == 2 - assert info.version_minor == 5 - - -def test_parse_tlv_missing_a4(monkeypatch): - def dummy_parse_tlv(data): - # No 0xA4 tag present - return {} - - monkeypatch.setattr( - 'keycard.parsing.application_info.parse_tlv', - dummy_parse_tlv - ) - with pytest.raises(InvalidResponseError): - ApplicationInfo.parse(b'\x00\x01\x02') - - -def test_parse_tlv_missing_fields(monkeypatch): - def dummy_parse_tlv(data): - # Missing some tags - return { - 0xA4: [b''] - } - - class DummyCapabilities: - @staticmethod - def parse(val): - return val - - monkeypatch.setattr( - 'keycard.parsing.application_info.parse_tlv', - dummy_parse_tlv - ) - monkeypatch.setattr( - 'keycard.parsing.application_info.Capabilities', - DummyCapabilities - ) - - # Should raise KeyError due to missing tags in inner_tlv - with pytest.raises(KeyError): - ApplicationInfo.parse(b'\xA4\x01\x00') - - -def test_parse_pubkey_empty(monkeypatch): - monkeypatch.setattr( - 'keycard.parsing.application_info.Capabilities', - DummyCapabilities - ) - # No pubkey bytes - data = bytes([0x80, 0x00]) - info = ApplicationInfo.parse(data) - assert info.ecc_public_key == b'' - assert info.capabilities == 1 # Only CREDENTIALS_MANAGEMENT - assert info.instance_uid is None - assert info.key_uid is None - assert info.version_major == 0 - assert info.version_minor == 0 - - -def test_is_initialized_property(): - info = ApplicationInfo( - capabilities=1, - ecc_public_key=None, - instance_uid=None, - key_uid=None, - version_major=0, - version_minor=0, - ) - assert not info.is_initialized - - info.key_uid = b'\x01' - assert info.is_initialized diff --git a/python/keycard-py/tests/parsing/test_identity.py b/python/keycard-py/tests/parsing/test_identity.py deleted file mode 100644 index f8b16d02..00000000 --- a/python/keycard-py/tests/parsing/test_identity.py +++ /dev/null @@ -1,87 +0,0 @@ -import pytest -from keycard.parsing.identity import parse, InvalidResponseError -from keycard.parsing import identity - - -def make_tlv(tag, value): - return bytes([tag, len(value)]) + value - - -def fake_parse_tlv(data): - if data == b'outer': - return {0xA0: [b'inner']} - elif data == b'inner': - return {0x8A: [b'cert'], 0x30: [b'sig']} - return {} - - -def test_parse_success(monkeypatch): - monkeypatch.setattr( - identity, - "parse_tlv", - lambda data: - {0xA0: [b'inner']} if data == b'data' - else {0x8A: [b'c'*95], 0x30: [b's'*64]}) - monkeypatch.setattr( - identity, - "_verify", - lambda certificate, signature, challenge: None - ) - monkeypatch.setattr( - identity, - "_recover_public_key", - lambda certificate: b'pubkey' - ) - - challenge = b'challenge' - data = b'data' - result = parse(challenge, data) - assert result == b'pubkey' - - -def test_parse_malformed_index(monkeypatch): - monkeypatch.setattr( - identity, - "parse_tlv", - lambda data: {0xA0: [b'inner']} if data == b'data' else {} - ) - challenge = b'challenge' - data = b'data' - with pytest.raises( - InvalidResponseError, - match="Malformed identity response" - ): - parse(challenge, data) - - -def test_parse_certificate_too_short(monkeypatch): - monkeypatch.setattr( - identity, - "parse_tlv", - lambda data: - {0xA0: [b'inner']} if data == b'data' - else {0x8A: [b'c'*10], 0x30: [b's'*64]} - ) - challenge = b'challenge' - data = b'data' - with pytest.raises( - InvalidResponseError, - match="Malformed identity response" - ): - parse(challenge, data) - - -def test_parse_signature_too_short(monkeypatch): - monkeypatch.setattr( - identity, - "parse_tlv", - lambda data: - {0xA0: [b'inner']} if data == b'data' - else {0x8A: [b'c'*95], 0x30: [b's'*10]}) - challenge = b'challenge' - data = b'data' - with pytest.raises( - InvalidResponseError, - match="Malformed identity response" - ): - parse(challenge, data) diff --git a/python/keycard-py/tests/parsing/test_signature_result.py b/python/keycard-py/tests/parsing/test_signature_result.py deleted file mode 100644 index 4f8d4a3c..00000000 --- a/python/keycard-py/tests/parsing/test_signature_result.py +++ /dev/null @@ -1,117 +0,0 @@ -from keycard.parsing.signature_result import SignatureResult -from keycard.constants import SigningAlgorithm -from unittest import mock - - -def test_signature_result_with_minimal_r_s(): - r = 1 - s = 1 - digest = b'\x01' * 32 - public_key = b'\x02' + b'\x01' * 32 - sig = SignatureResult( - digest=digest, - algo=SigningAlgorithm.ECDSA_SECP256K1, - r=r, - s=s, - public_key=public_key, - recovery_id=0 - ) - assert sig.r == b'\x01' - assert sig.s == b'\x01' - assert sig.signature == b'\x01\x01' - - -def test_signature_result_with_large_r_s(): - r = 2**255 - s = 2**255 - 1 - digest = b'\x01' * 32 - public_key = b'\x02' + b'\x01' * 32 - sig = SignatureResult( - digest=digest, - algo=SigningAlgorithm.ECDSA_SECP256K1, - r=r, - s=s, - public_key=public_key, - recovery_id=2 - ) - assert sig.r == r.to_bytes((r.bit_length() + 7) // 8, 'big') - assert sig.s == s.to_bytes((s.bit_length() + 7) // 8, 'big') - assert sig.recovery_id == 2 - - -def test_signature_result_signature_der_property(): - r = 123 - s = 456 - digest = b'\x01' * 32 - public_key = b'\x02' + b'\x01' * 32 - - with mock.patch( - 'keycard.parsing.signature_result.util.sigencode_der' - ) as mock_sigencode_der: - mock_sigencode_der.return_value = b'der' - sig = SignatureResult( - digest=digest, - algo=SigningAlgorithm.ECDSA_SECP256K1, - r=r, - s=s, - public_key=public_key, - recovery_id=3 - ) - der = sig.signature_der - assert der == b'der' - mock_sigencode_der.assert_called_once_with(r, s, 3) - - -def test_signature_result_repr_exists(): - r = int.from_bytes(b'\x01' * 32, 'big') - s = int.from_bytes(b'\x01' * 32, 'big') - digest = b'\x01' * 32 - public_key = b'\x02' + b'\x01' * 32 - sig = SignatureResult( - digest=digest, - algo=SigningAlgorithm.ECDSA_SECP256K1, - r=r, - s=s, - public_key=public_key, - recovery_id=0 - ) - assert isinstance(repr(sig), str) - - -def test_signature_result_public_key_and_recovery_id_priority(): - r = 5 - s = 6 - digest = b'\x01' * 32 - public_key = b'\x02' + b'\x01' * 32 - sig = SignatureResult( - digest=digest, - algo=SigningAlgorithm.ECDSA_SECP256K1, - r=r, - s=s, - public_key=public_key, - recovery_id=7 - ) - assert sig.public_key == public_key - assert sig.recovery_id == 7 - - -def test_signature_result_missing_public_key_calls_recover(): - r = 10 - s = 20 - digest = b'\x01' * 32 - with mock.patch.object( - SignatureResult, - "_recover_public_key", - return_value=b'\x02' + b'\x02' * 32 - ) as mock_pubkey: - sig = SignatureResult( - digest=digest, - algo=SigningAlgorithm.ECDSA_SECP256K1, - r=r, - s=s, - public_key=None, - recovery_id=9 - ) - assert sig.public_key == b'\x02' + b'\x02' * 32 - assert sig.recovery_id == 9 - mock_pubkey.assert_called_once_with(digest) diff --git a/python/keycard-py/tests/parsing/test_tlv.py b/python/keycard-py/tests/parsing/test_tlv.py deleted file mode 100644 index f6e44c80..00000000 --- a/python/keycard-py/tests/parsing/test_tlv.py +++ /dev/null @@ -1,122 +0,0 @@ -import pytest - -from keycard.exceptions import InvalidResponseError -from keycard.parsing import tlv - - -def test_parse_ber_length_short_form(): - data = bytes([0x05]) - length, consumed = tlv._parse_ber_length(data, 0) - assert length == 5 - assert consumed == 1 - - -def test_parse_ber_length_long_form_1byte(): - data = bytes([0x81, 0x10]) - length, consumed = tlv._parse_ber_length(data, 0) - assert length == 0x10 - assert consumed == 2 - - -def test_parse_ber_length_long_form_2bytes(): - data = bytes([0x82, 0x01, 0xF4]) - length, consumed = tlv._parse_ber_length(data, 0) - assert length == 500 - assert consumed == 3 - - -def test_parse_ber_length_unsupported_length(): - data = bytes([0x85, 0, 0, 0, 0, 0]) - with pytest.raises(InvalidResponseError): - tlv._parse_ber_length(data, 0) - - -def test_parse_ber_length_exceeds_buffer(): - data = bytes([0x82, 0x01]) - with pytest.raises(InvalidResponseError): - tlv._parse_ber_length(data, 0) - - -def test_parse_tlv_single(): - data = bytes([0x01, 0x03, ord('a'), ord('b'), ord('c')]) - result = tlv.parse_tlv(data) - assert 0x01 in result - assert result[0x01][0] == b'abc' - - -def test_parse_tlv_multiple_tags(): - data = bytes([ - 0x01, 0x02, ord('h'), ord('i'), - 0x02, 0x01, ord('x')]) - result = tlv.parse_tlv(data) - assert result[0x01][0] == b'hi' - assert result[0x02][0] == b'x' - - -def test_parse_tlv_repeated_tag(): - data = bytes([ - 0x01, 0x01, ord('a'), - 0x01, 0x02, ord('b'), ord('c') - ]) - result = tlv.parse_tlv(data) - assert result[0x01][0] == b'a' - assert result[0x01][1] == b'bc' - - -def test_parse_tlv_long_length(): - data = bytes([0x10, 0x82, 0x01, 0x01]) + b'a' * 257 - result = tlv.parse_tlv(data) - assert result[0x10][0] == b'a' * 257 - - -def test_parse_tlv_incomplete_value(): - data = bytes([0x01, 0x05, ord('a'), ord('b'), ord('c')]) - with pytest.raises(InvalidResponseError): - tlv.parse_tlv(data) - - -def test_encode_tlv_short(): - tag = 0x01 - value = b'\xAB\xCD' - encoded = tlv.encode_tlv(tag, value) - assert encoded == b'\x01\x02\xAB\xCD' - - -def test_encode_tlv_1byte_long_form(): - tag = 0x02 - value = b'\x00' * 130 # >127 triggers long form - encoded = tlv.encode_tlv(tag, value) - assert encoded[:2] == b'\x02\x81' - assert encoded[2] == 130 - assert encoded[3:] == value - - -def test_encode_tlv_2byte_long_form(): - tag = 0x03 - value = b'\xFF' * 300 # >255 triggers 2-byte length - encoded = tlv.encode_tlv(tag, value) - assert encoded[:3] == b'\x03\x82\x01' - assert encoded[3] == 0x2C # 300 = 0x012C - assert encoded[4:] == value - - -def test_encode_tlv_empty_value(): - tag = 0x04 - value = b"" - encoded = tlv.encode_tlv(tag, value) - assert encoded == b'\x04\x00' - - -def test_encode_tlv_max_short_length(): - tag = 0x10 - value = b"A" * 127 - encoded = tlv.encode_tlv(tag, value) - assert encoded[1] == 127 - assert len(encoded) == 2 + 127 - - -def test_encode_tlv_max_1byte_long_form(): - tag = 0x20 - value = b"A" * 255 - encoded = tlv.encode_tlv(tag, value) - assert encoded[1:3] == b'\x81\xFF' diff --git a/python/keycard-py/tests/test_apdu.py b/python/keycard-py/tests/test_apdu.py deleted file mode 100644 index 0d4c83f4..00000000 --- a/python/keycard-py/tests/test_apdu.py +++ /dev/null @@ -1,51 +0,0 @@ -import pytest -from keycard.apdu import encode_lv, APDUResponse - - -def test_encode_lv_valid(): - value = bytes(10) - result = encode_lv(value) - assert result == b"\x0A" + value - - -def test_encode_lv_too_long(): - value = bytes(256) - with pytest.raises(ValueError): - encode_lv(value) - - -def test_encode_lv_empty(): - value = bytes() - result = encode_lv(value) - assert result == b"\x00" - - -def test_encode_lv_single_byte(): - value = bytes([0xFF]) - result = encode_lv(value) - assert result == b"\x01\xFF" - - -def test_encode_lv_max_length(): - value = bytes(255) - result = encode_lv(value) - assert result == b"\xFF" + value - - -def test_apdu_response_success(): - r = APDUResponse([0x01, 0x02], 0x9000) - assert r.data == [0x01, 0x02] - assert r.status_word == 0x9000 - - -def test_apdu_response_error_status(): - r = APDUResponse([], 0x6A82) - assert r.status_word == 0x6A82 - assert isinstance(r.status_word, int) - - -def test_apdu_response_all_status_range(): - for sw in [0x9000, 0x6A80, 0x6A84, 0x6982]: - r = APDUResponse([0x00], sw) - assert r.status_word == sw - assert r.data == [0x00] diff --git a/python/keycard-py/tests/test_keycard.py b/python/keycard-py/tests/test_keycard.py deleted file mode 100644 index d84dcead..00000000 --- a/python/keycard-py/tests/test_keycard.py +++ /dev/null @@ -1,531 +0,0 @@ -import pytest -from unittest.mock import MagicMock, patch - -from keycard import constants -from keycard.apdu import APDUResponse -from keycard.exceptions import APDUError -from keycard.parsing.exported_key import ExportedKey -from keycard.keycard import KeyCard -from keycard.transport import Transport - - -def test_keycard_init_with_transport(): - transport = MagicMock(spec=Transport) - kc = KeyCard(transport) - assert kc.transport == transport - assert kc.card_public_key is None - assert kc.session is None - - -def test_select_sets_card_pubkey(): - mock_info = MagicMock() - mock_info.ecc_public_key = b'pubkey' - with patch('keycard.keycard.commands.select', return_value=mock_info): - kc = KeyCard(MagicMock()) - result = kc.select() - assert kc.card_public_key == b'pubkey' - assert result == mock_info - - -def test_init_calls_command(): - transport = MagicMock() - with patch('keycard.keycard.commands.init') as mock_init: - kc = KeyCard(transport) - kc.card_public_key = b'pub' - kc.init(b'pin', b'puk', b'secret') - mock_init.assert_called_once_with(kc, b'pin', b'puk', b'secret') - - -def test_ident_calls_command(): - with patch('keycard.keycard.commands.ident', return_value='identity') as m: - kc = KeyCard(MagicMock()) - result = kc.ident(b'challenge') - m.assert_called_once() - assert result == 'identity' - - -def test_open_secure_channel_with_mutual_authentication(): - with patch( - 'keycard.keycard.commands.open_secure_channel' - ) as mock_osc: - with patch( - 'keycard.keycard.commands.mutually_authenticate' - ) as mock_ma: - mock_osc.return_value = 'session' - kc = KeyCard(MagicMock()) - kc._card_public_key = b'pub' - kc.open_secure_channel(1, b'pairing_key') - mock_osc.assert_called_once_with(kc, 1, b'pairing_key') - mock_ma.assert_called_once_with(kc) - assert kc.session == 'session' - - -def test_open_secure_channel_without_mutual_authentication(): - with patch( - 'keycard.keycard.commands.open_secure_channel' - ) as mock_osc: - with patch( - 'keycard.keycard.commands.mutually_authenticate' - ) as mock_ma: - mock_osc.return_value = 'session' - kc = KeyCard(MagicMock()) - kc._card_public_key = b'pub' - kc.open_secure_channel(1, b'pairing_key', False) - mock_osc.assert_called_once_with(kc, 1, b'pairing_key') - mock_ma.assert_not_called() - assert kc.session == 'session' - - -def test_mutually_authenticate_calls_command(): - with patch('keycard.keycard.commands.mutually_authenticate') as mock_auth: - kc = KeyCard(MagicMock()) - kc.secure_session = 'sess' - kc.mutually_authenticate() - mock_auth.assert_called_once() - - -def test_pair_returns_expected_tuple(): - with patch('keycard.keycard.commands.pair', return_value=(1, b'crypt')): - kc = KeyCard(MagicMock()) - result = kc.pair(b'shared') - assert result == (1, b'crypt') - - -def test_verify_pin_delegates_call_and_returns_result(): - with patch( - 'keycard.keycard.commands.verify_pin', - return_value=True - ) as mock_cmd: - kc = KeyCard(MagicMock()) - kc.secure_session = 'sess' - result = kc.verify_pin('1234') - mock_cmd.assert_called_once_with(kc, b'1234') - assert result is True - - -def test_unpair_delegates_call(): - transport = MagicMock() - with patch('keycard.keycard.commands.unpair') as mock_unpair: - kc = KeyCard(transport) - kc.secure_session = 'sess' - kc.unpair(2) - mock_unpair.assert_called_once_with(kc, 2) - - -def test_send_secure_apdu_success(): - mock_session = MagicMock() - mock_session.wrap_apdu.return_value = b'encrypted' - mock_session.unwrap_response.return_value = (b'plaintext', 0x9000) - mock_transport = MagicMock() - mock_response = MagicMock() - mock_response.status_word = 0x9000 - mock_response.data = b'ciphertext' - mock_transport.send_apdu.return_value = mock_response - - kc = KeyCard(mock_transport) - kc.session = mock_session - - result = kc.send_secure_apdu(0xA4, 0x01, 0x02, b'data') - - mock_session.wrap_apdu.assert_called_once_with( - cla=kc.transport.send_apdu.call_args[0][0][0], - ins=0xA4, - p1=0x01, - p2=0x02, - data=b'data' - ) - mock_transport.send_apdu.assert_called_once() - mock_session.unwrap_response.assert_called_once_with(mock_response) - assert result == b'plaintext' - - -def test_send_secure_apdu_raises_on_transport_status_word(): - mock_session = MagicMock() - mock_session.wrap_apdu.return_value = b'encrypted' - mock_transport = MagicMock() - mock_transport.send_apdu.return_value = APDUResponse( - b'', status_word=0x6A82) - - kc = KeyCard(mock_transport) - kc.session = mock_session - - with pytest.raises(APDUError) as exc: - kc.send_secure_apdu(0xA4, 0x00, 0x00, b'data') - assert exc.value.args[0] == 'APDU failed with SW=6A82' - - -def test_send_secure_apdu_raises_on_unwrap_status_word(): - mock_session = MagicMock() - mock_session.wrap_apdu.return_value = b'encrypted' - mock_session.unwrap_response.return_value = (b'plaintext', 0x6A84) - mock_transport = MagicMock() - mock_transport.send_apdu.return_value = APDUResponse( - b'', status_word=0x9000) - - kc = KeyCard(mock_transport) - kc.session = mock_session - - with pytest.raises(APDUError) as exc: - kc.send_secure_apdu(0xA4, 0x00, 0x00, b'data') - assert exc.value.args[0] == 'APDU failed with SW=6A84' - - -def test_send_apdu_success(monkeypatch): - mock_transport = MagicMock() - mock_response = MagicMock() - mock_response.status_word = 0x9000 - mock_response.data = b'response' - mock_transport.send_apdu.return_value = mock_response - - kc = KeyCard(mock_transport) - - result = kc.send_apdu(ins=0xA4, p1=0x01, p2=0x02, data=b'data') - expected_apdu = bytes([0x80, 0xA4, 0x01, 0x02, 4]) + b'data' - mock_transport.send_apdu.assert_called_once_with(expected_apdu) - assert result == b'response' - - -def test_send_apdu_raises_on_non_success_status(monkeypatch): - mock_transport = MagicMock() - mock_transport.send_apdu.return_value = APDUResponse(b'', 0x6A82) - - kc = KeyCard(mock_transport) - - with pytest.raises(APDUError) as exc: - kc.send_apdu(ins=0xA4, p1=0x00, p2=0x00, data=b'') - assert exc.value.args[0] == 'APDU failed with SW=6A82' - - -def test_send_apdu_with_custom_cla(monkeypatch): - mock_transport = MagicMock() - mock_response = MagicMock() - mock_response.status_word = 0x9000 - mock_response.data = b'abc' - mock_transport.send_apdu.return_value = mock_response - - kc = KeyCard(mock_transport) - - result = kc.send_apdu(ins=0xA4, p1=0x01, p2=0x02, data=b'data', cla=0x90) - expected_apdu = bytes([0x90, 0xA4, 0x01, 0x02, 4]) + b'data' - mock_transport.send_apdu.assert_called_once_with(expected_apdu) - assert result == b'abc' - - -def test_unblock_pin_calls_command_with_bytes(): - with patch('keycard.keycard.commands.unblock_pin') as mock_unblock: - kc = KeyCard(MagicMock()) - puk = b'123456789012' - new_pin = b'654321' - kc.unblock_pin(puk, new_pin) - mock_unblock.assert_called_once_with(kc, puk + new_pin) - - -def test_unblock_pin_calls_command_with_str(): - with patch('keycard.keycard.commands.unblock_pin') as mock_unblock: - kc = KeyCard(MagicMock()) - puk = '123456789012' - new_pin = '654321' - kc.unblock_pin(puk, new_pin) - mock_unblock.assert_called_once_with( - kc, - (puk + new_pin).encode('utf-8') - ) - - -def test_unblock_pin_calls_command_with_mixed_types(): - with patch('keycard.keycard.commands.unblock_pin') as mock_unblock: - kc = KeyCard(MagicMock()) - puk = '123456789012' - new_pin = b'654321' - kc.unblock_pin(puk, new_pin) - mock_unblock.assert_called_once_with(kc, puk.encode('utf-8') + new_pin) - - -def test_remove_key_calls_command(): - with patch('keycard.keycard.commands.remove_key') as mock_remove_key: - kc = KeyCard(MagicMock()) - kc.remove_key() - mock_remove_key.assert_called_once_with(kc) - - -def test_store_data_calls_command_with_default_slot(): - with patch('keycard.keycard.commands.store_data') as mock_store_data: - kc = KeyCard(MagicMock()) - data = b'testdata' - kc.store_data(data) - mock_store_data.assert_called_once_with( - kc, data, constants.StorageSlot.PUBLIC - ) - - -def test_store_data_calls_command_with_custom_slot(): - with patch('keycard.keycard.commands.store_data') as mock_store_data: - kc = KeyCard(MagicMock()) - data = b'testdata' - slot = MagicMock() - kc.store_data(data, slot) - mock_store_data.assert_called_once_with(kc, data, slot) - - -def test_store_data_raises_value_error_on_invalid_slot(): - with patch( - 'keycard.keycard.commands.store_data', - side_effect=ValueError("Invalid slot") - ): - kc = KeyCard(MagicMock()) - with pytest.raises(ValueError, match="Invalid slot"): - kc.store_data(b'testdata', slot="INVALID") - - -def test_store_data_raises_value_error_on_data_too_long(): - with patch( - 'keycard.keycard.commands.store_data', - side_effect=ValueError("data is too long") - ): - kc = KeyCard(MagicMock()) - long_data = b'a' * 128 - with pytest.raises(ValueError, match="data is too long"): - kc.store_data(long_data) - - -def test_get_data_calls_command_with_default_slot(): - with patch( - 'keycard.keycard.commands.get_data', - return_value=b'data' - ) as mock_get_data: - kc = KeyCard(MagicMock()) - result = kc.get_data() - mock_get_data.assert_called_once_with(kc, constants.StorageSlot.PUBLIC) - assert result == b'data' - - -def test_get_data_calls_command_with_custom_slot(): - with patch( - 'keycard.keycard.commands.get_data', - return_value=b'data' - ) as mock_get_data: - kc = KeyCard(MagicMock()) - slot = MagicMock() - result = kc.get_data(slot) - mock_get_data.assert_called_once_with(kc, slot) - assert result == b'data' - - -def test_export_key_delegates_and_returns_result(): - mock_exported = MagicMock(spec=ExportedKey) - with patch( - 'keycard.keycard.commands.export_key', - return_value=mock_exported - ) as mock_cmd: - kc = KeyCard(MagicMock()) - result = kc.export_key( - derivation_option=constants.DerivationOption.DERIVE, - public_only=True, - keypath="m/44'/60'/0'/0/0", - make_current=True, - source=constants.DerivationSource.PARENT - ) - - mock_cmd.assert_called_once_with( - kc, - derivation_option=constants.DerivationOption.DERIVE, - public_only=True, - keypath="m/44'/60'/0'/0/0", - make_current=True, - source=constants.DerivationSource.PARENT - ) - assert result is mock_exported - - -def test_export_current_key_delegates_and_returns_result(): - mock_exported = MagicMock(spec=ExportedKey) - with patch( - 'keycard.keycard.commands.export_key', - return_value=mock_exported - ) as mock_cmd: - kc = KeyCard(MagicMock()) - result = kc.export_current_key(public_only=False) - - mock_cmd.assert_called_once_with( - kc, - derivation_option=constants.DerivationOption.CURRENT, - public_only=False, - keypath=None, - make_current=False, - source=constants.DerivationSource.MASTER - ) - assert result is mock_exported - - -def test_sign_current_key(): - with patch("keycard.keycard.commands.sign") as mock_sign: - card = KeyCard(MagicMock()) - digest = b"\xAA" * 32 - mock_sign.return_value = "signed" - - result = card.sign(digest) - - mock_sign.assert_called_once_with( - card, - digest, - constants.DerivationOption.CURRENT, - constants.SigningAlgorithm.ECDSA_SECP256K1 - ) - assert result == "signed" - - -def test_sign_with_path(): - with patch("keycard.keycard.commands.sign") as mock_sign: - card = KeyCard(MagicMock()) - digest = b"\xBB" * 32 - path = [0x8000002C, 0x8000003C, 0, 0, 0] # m/44'/60'/0'/0/0 - mock_sign.return_value = "sig" - - result = card.sign_with_path(digest, path) - - mock_sign.assert_called_once_with( - card, - digest, - constants.DerivationOption.DERIVE, - constants.SigningAlgorithm.ECDSA_SECP256K1, - derivation_path=path - ) - assert result == "sig" - - -def test_sign_with_path_make_current(): - with patch("keycard.keycard.commands.sign") as mock_sign: - card = KeyCard(MagicMock()) - digest = b"\xCC" * 32 - path = [0x8000002C, 0x8000003C, 0, 0, 0] - mock_sign.return_value = "sig" - - result = card.sign_with_path(digest, path, make_current=True) - - mock_sign.assert_called_once_with( - card, - digest, - constants.DerivationOption.DERIVE_AND_MAKE_CURRENT, - constants.SigningAlgorithm.ECDSA_SECP256K1, - derivation_path=path - ) - assert result == "sig" - - -def test_sign_pinless(): - with patch("keycard.keycard.commands.sign") as mock_sign: - card = KeyCard(MagicMock()) - digest = b"\xDD" * 32 - mock_sign.return_value = "sig" - - result = card.sign_pinless(digest) - - mock_sign.assert_called_once_with( - card, - digest, - constants.DerivationOption.PINLESS, - constants.SigningAlgorithm.ECDSA_SECP256K1 - ) - assert result == "sig" - - -def test_load_key_bip39_seed(): - with patch("keycard.keycard.commands.load_key") as mock_load_key: - card = KeyCard(MagicMock()) - seed = b"\xAB" * 64 - mock_load_key.return_value = b"uid" - - result = card.load_key( - key_type=constants.LoadKeyType.BIP39_SEED, - bip39_seed=seed - ) - - mock_load_key.assert_called_once_with( - card, - key_type=constants.LoadKeyType.BIP39_SEED, - public_key=None, - private_key=None, - chain_code=None, - bip39_seed=seed, - lee_seed=None - ) - assert result == b"uid" - - -def test_load_key_ecc_pair(): - with patch("keycard.keycard.commands.load_key") as mock_load_key: - card = KeyCard(MagicMock()) - pub = b"\x04" + b"\x01" * 64 - priv = b"\x02" * 32 - mock_load_key.return_value = b"uid" - - result = card.load_key( - key_type=constants.LoadKeyType.ECC, - public_key=pub, - private_key=priv - ) - - mock_load_key.assert_called_once_with( - card, - key_type=constants.LoadKeyType.ECC, - public_key=pub, - private_key=priv, - chain_code=None, - bip39_seed=None, - lee_seed=None - ) - assert result == b"uid" - - -def test_load_key_extended(): - with patch("keycard.keycard.commands.load_key") as mock_load_key: - card = KeyCard(MagicMock()) - pub = b"\x04" + b"\x01" * 64 - priv = b"\x02" * 32 - chain = b"\x00" * 32 - mock_load_key.return_value = b"uid" - - result = card.load_key( - key_type=constants.LoadKeyType.EXTENDED_ECC, - public_key=pub, - private_key=priv, - chain_code=chain - ) - - mock_load_key.assert_called_once_with( - card, - key_type=constants.LoadKeyType.EXTENDED_ECC, - public_key=pub, - private_key=priv, - chain_code=chain, - bip39_seed=None, - lee_seed=None - ) - assert result == b"uid" - - -def test_keycard_set_pinless_path(): - with patch("keycard.keycard.commands.set_pinless_path") as mock_cmd: - card = KeyCard(MagicMock()) - card.set_pinless_path("m/44'/60'/0'/0/0") - - mock_cmd.assert_called_once_with(card, "m/44'/60'/0'/0/0") - - -def test_keycard_generate_mnemonic(): - with patch("keycard.keycard.commands.generate_mnemonic") as mock_cmd: - card = KeyCard(None) - mock_cmd.return_value = [0, 2047, 1337, 42] - - result = card.generate_mnemonic(checksum_size=6) - - mock_cmd.assert_called_once_with(card, 6) - assert result == [0, 2047, 1337, 42] - - -def test_keycard_derive_key(): - with patch("keycard.keycard.commands.derive_key") as mock_cmd: - card = KeyCard(MagicMock()) - card.derive_key("m/44'/60'/0'/0/0") - - mock_cmd.assert_called_once_with(card, "m/44'/60'/0'/0/0") diff --git a/python/keycard-py/tests/test_preconditions.py b/python/keycard-py/tests/test_preconditions.py deleted file mode 100644 index 1c46bd15..00000000 --- a/python/keycard-py/tests/test_preconditions.py +++ /dev/null @@ -1,56 +0,0 @@ -import pytest -from keycard.card_interface import CardInterface -from keycard.preconditions import make_precondition -from keycard.exceptions import InvalidStateError - - -class DummyCard(CardInterface): - def __init__(self, **attrs): - for k, v in attrs.items(): - setattr(self, k, v) - - -def test_precondition_passes_when_attribute_true(): - @make_precondition('is_ready') - def do_something(card): - return "success" - card = DummyCard(is_ready=True) - assert do_something(card) == "success" - - -def test_precondition_raises_when_attribute_false(): - @make_precondition('is_ready') - def do_something(card): - return "should not reach" - card = DummyCard(is_ready=False) - with pytest.raises(InvalidStateError) as exc: - do_something(card) - assert "Is Ready must be satisfied." in str(exc.value) - - -def test_precondition_raises_when_attribute_missing(): - @make_precondition('is_ready') - def do_something(card): - return "should not reach" - card = DummyCard() - with pytest.raises(InvalidStateError) as exc: - do_something(card) - assert "Is Ready must be satisfied." in str(exc.value) - - -def test_precondition_custom_display_name(): - @make_precondition('is_ready', display_name="Custom Name") - def do_something(card): - return "success" - card = DummyCard(is_ready=False) - with pytest.raises(InvalidStateError) as exc: - do_something(card) - assert "Custom Name must be satisfied." in str(exc.value) - - -def test_precondition_passes_args_kwargs(): - @make_precondition('is_ready') - def do_something(card, x, y=2): - return x + y - card = DummyCard(is_ready=True) - assert do_something(card, 3, y=4) == 7 diff --git a/python/keycard-py/tests/test_secure_channel.py b/python/keycard-py/tests/test_secure_channel.py deleted file mode 100644 index 2622f9e8..00000000 --- a/python/keycard-py/tests/test_secure_channel.py +++ /dev/null @@ -1,132 +0,0 @@ -import pytest - -from keycard.apdu import APDUResponse -from keycard.secure_channel import SecureChannel - - -@pytest.fixture -def session_params(): - return { - "shared_secret": bytes(32), - "pairing_key": bytes(32), - "salt": bytes(16), - "seed_iv": bytes(16), - } - - -def test_open_sets_authenticated_and_keys(session_params): - session = SecureChannel.open(**session_params) - assert session.authenticated is True - assert isinstance(session.enc_key, bytes) and len(session.enc_key) == 32 - assert isinstance(session.mac_key, bytes) and len(session.mac_key) == 32 - assert session.iv == session_params['seed_iv'] - - -def test_wrap_apdu_authenticated(session_params): - session = SecureChannel.open(**session_params) - wrapped = session.wrap_apdu( - 0x80, - 0xCA, - 0x00, - 0x00, - b'testdata' - ) - assert isinstance(wrapped, bytes) - assert len(wrapped) > 16 # IV + encrypted data - - -@pytest.mark.parametrize("ins,should_raise", [ - (0x11, False), - (0xCA, True), -]) -def test_wrap_apdu_auth_check(ins, should_raise): - session = SecureChannel( - b'\x01' * 32, - b'\x02' * 32, - bytes(16), - authenticated=False - ) - if should_raise: - with pytest.raises(ValueError, match="not authenticated"): - session.wrap_apdu(0x80, ins, 0x00, 0x00, b'test') - else: - session.wrap_apdu(0x80, ins, 0x00, 0x00, b'test') - - -def test_unwrap_response_authenticated_and_mac(monkeypatch, session_params): - # Patch aes_cbc_encrypt and aes_cbc_decrypt to simulate expected behavior - session = SecureChannel.open(**session_params) - plaintext = b"hello world" + b'\x90\x00' # status word 0x9000 - - # Simulate encryption and MAC - - def fake_decrypt(key, iv, data): - return plaintext - - def fake_encrypt(key, iv, data, padding=True): - # Return 16 bytes MAC for mac_key, else just return dummy - if key == session.mac_key: - return b'Y' * 16 - return b'Z' * (len(data) // 16 * 16) - - monkeypatch.setattr('keycard.secure_channel.aes_cbc_decrypt', fake_decrypt) - monkeypatch.setattr('keycard.secure_channel.aes_cbc_encrypt', fake_encrypt) - - # Compose response: 16 bytes MAC + encrypted data - response = APDUResponse(b'Y' * 16 + b'Z' * 16, 0x900) - out, sw = session.unwrap_response(response) - assert out == plaintext[:-2] - assert sw == 0x9000 - - -def test_unwrap_response_not_authenticated_raises(session_params): - session = SecureChannel.open(**session_params) - session.authenticated = False - response = APDUResponse(bytes(32), 0x900) - with pytest.raises(ValueError, match="not authenticated"): - session.unwrap_response(response) - - -def test_unwrap_response_invalid_length_raises(session_params): - session = SecureChannel.open(**session_params) - session.authenticated = True - response = APDUResponse(bytes(10), 0x900) - with pytest.raises(ValueError, match="Invalid secure response length"): - session.unwrap_response(response) - - -def test_unwrap_response_invalid_mac_raises(monkeypatch, session_params): - session = SecureChannel.open(**session_params) - # Patch aes_cbc_encrypt to return a different MAC - - def fake_encrypt(key, iv, data, padding=True): - return b'X' * 16 - - monkeypatch.setattr( - 'keycard.secure_channel.aes_cbc_encrypt', - fake_encrypt - ) - - response = APDUResponse(b'Y' * 16 + b'Z' * 16, 0x900) - - with pytest.raises(ValueError, match="Invalid MAC"): - session.unwrap_response(response) - - -def test_unwrap_response_missing_status_word(monkeypatch, session_params): - session = SecureChannel.open(**session_params) - - def fake_decrypt(key, iv, data): - return b'\x01' - - monkeypatch.setattr( - 'keycard.secure_channel.aes_cbc_decrypt', - fake_decrypt) - monkeypatch.setattr( - 'keycard.secure_channel.aes_cbc_encrypt', - lambda *a, **k: b'Y' * 16) - - response = APDUResponse(b'Y' * 16 + b'Z' * 16, 0x9000) - - with pytest.raises(ValueError, match="Missing status word"): - session.unwrap_response(response) diff --git a/python/keycard-py/tests/test_transport.py b/python/keycard-py/tests/test_transport.py deleted file mode 100644 index f3ecf6fc..00000000 --- a/python/keycard-py/tests/test_transport.py +++ /dev/null @@ -1,87 +0,0 @@ -import pytest -from unittest.mock import MagicMock, patch -from keycard.transport import Transport -from keycard.apdu import APDUResponse -from keycard.exceptions import TransportError - - -@patch("keycard.transport.readers") -def test_transport_connect_success(mock_readers): - mock_connection = MagicMock() - mock_reader = MagicMock() - mock_reader.createConnection.return_value = mock_connection - mock_readers.return_value = [mock_reader] - - transport = Transport() - transport.connect() - - mock_readers.assert_called_once() - mock_connection.connect.assert_called_once() - assert transport.connection == mock_connection - - -@patch("keycard.transport.readers", return_value=[]) -def test_transport_connect_no_reader(mock_readers): - transport = Transport() - with pytest.raises(TransportError, match="No smart card readers found"): - transport.connect() - - -@patch("keycard.transport.readers") -def test_send_apdu_success(mock_readers): - mock_connection = MagicMock() - mock_connection.transmit.return_value = ([1, 2, 3], 0x90, 0x00) - - mock_reader = MagicMock() - mock_reader.createConnection.return_value = mock_connection - mock_readers.return_value = [mock_reader] - - transport = Transport() - transport.connection = mock_connection - - apdu = b"\x00\xA4\x04\x00" - response = transport.send_apdu(apdu) - - mock_connection.transmit.assert_called_once_with(list(apdu)) - assert isinstance(response, APDUResponse) - assert response.data == [1, 2, 3] - assert response.status_word == 0x9000 - - -@patch("keycard.transport.readers") -def test_send_apdu_auto_connect(mock_readers): - mock_connection = MagicMock() - mock_connection.transmit.return_value = ([0x90], 0x90, 0x00) - - mock_reader = MagicMock() - mock_reader.createConnection.return_value = mock_connection - mock_readers.return_value = [mock_reader] - - transport = Transport() - response = transport.send_apdu(b"\x00") - - assert isinstance(response, APDUResponse) - assert response.status_word == 0x9000 - assert mock_connection.connect.called - - -@patch("keycard.transport.readers") -def test_transport_context_manager(mock_readers): - mock_connection = MagicMock() - mock_reader = MagicMock() - mock_reader.createConnection.return_value = mock_connection - mock_readers.return_value = [mock_reader] - - with Transport() as transport: - assert transport.connection == mock_connection - - mock_connection.disconnect.assert_called_once() - assert transport.connection is None - - -def test_exit_without_connection(): - transport = Transport() - transport.connection = None - - transport.__exit__(None, None, None) - assert transport.connection is None diff --git a/python/keycard-py/tests/test_vectors.py b/python/keycard-py/tests/test_vectors.py deleted file mode 100644 index de390d0a..00000000 --- a/python/keycard-py/tests/test_vectors.py +++ /dev/null @@ -1,50 +0,0 @@ -from ecdsa import ECDH, SigningKey, SECP256k1, VerifyingKey -from keycard.crypto.aes import aes_cbc_encrypt - - -def test_full_crypto_vector(): - card_pubkey_bytes = bytes.fromhex( - '04525481c70263f79c29092e95cfc972e0eb427ea31fe6cc6c96787eb12205737' - 'd431929f0837c66a4ee514578a7d5eb78087927851b15b691a79cdea431bd63d9' - ) - ephemeral_private_bytes = bytes.fromhex( - 'e3b9a83efa7b113bac4562a77c496de21a9f91a17fa8dcb2384ed7154bb43c5c' - ) - iv = bytes.fromhex('d2c5feedf4bdb935057f8c78cf92395e') - expected_ciphertext = bytes.fromhex( - '4707ca7edf4218c416f252967da55f1b6e2e65f0ffa0305f71501f53aa283fd5' - 'aaa8b049e75288c01034f25893db43d4db4bd6dfc4a6546658dd22227082aa58' - ) - - ephemeral_key = SigningKey.from_string( - ephemeral_private_bytes, - curve=SECP256k1 - ) - card_pubkey = VerifyingKey.from_string( - card_pubkey_bytes, - curve=SECP256k1 - ) - - ecdh = ECDH( - curve=SECP256k1, - private_key=ephemeral_key, - public_key=card_pubkey - ) - shared_secret = ecdh.generate_sharedsecret_bytes() - - pin = b'123456' - puk = b'123456789012' - pairing_secret = b'A' * 32 - plaintext = pin + puk + pairing_secret - - ciphertext: bytes = aes_cbc_encrypt(shared_secret, iv, plaintext) - - assert ciphertext == expected_ciphertext, ( - "Ciphertext does not match expected test vector" - ) - - -def test_crypto_vector_fails_on_mismatch(): - bogus = b"\x00" * 48 - actual = b"\x01" * 48 - assert bogus != actual, "Test vector should intentionally fail mismatch" diff --git a/python/keycard-py/tox.ini b/python/keycard-py/tox.ini deleted file mode 100644 index e2a96ef0..00000000 --- a/python/keycard-py/tox.ini +++ /dev/null @@ -1,48 +0,0 @@ -[tox] -envlist = py310,py311,py312,py313,py314 -isolated_build = True -skip_missing_interpreters = True - -[testenv] -description = Run tests with pytest -basepython = - py310: python3.10 - py311: python3.11 - py312: python3.12 - py313: python3.13 - py314: python3.14 -deps = - pytest - pytest-cov - coverage - mnemonic -extras = dev -commands = - pytest --maxfail=1 --disable-warnings {posargs} - -[testenv:lint] -description = Run linting checks -deps = - flake8 - mypy -commands = - flake8 keycard tests - mypy keycard - -[testenv:coverage] -description = Run tests with coverage report -deps = - pytest - pytest-cov - coverage - mnemonic -commands = - pytest --cov=keycard --cov-report=term-missing --cov-report=xml - -[gh-actions] -python = - 3.10: py310 - 3.11: py311 - 3.12: py312 - 3.13: py313 - 3.14: py314 diff --git a/python/keycard_test.py b/python/keycard_test.py deleted file mode 100644 index df4957aa..00000000 --- a/python/keycard_test.py +++ /dev/null @@ -1,24 +0,0 @@ -import keycard_wallet as keycard_wallet -import time # For testing - -pin = '111111' -path0 = "m/44'/60'/0'/0/0" -path1 = "m/44'/61'/0'/0/0" -path2 = "m/44'/62'/0'/0/0" -path3 = "m/44'/63'/0'/0/0" -path4 = "m/44'/64'/0'/0/0" - -my_wallet = keycard_wallet.KeycardWallet() -print("Setup communication with card...", my_wallet.setup_communication(pin)) - -print("Load mnemonic...", my_wallet.load_mnemonic()) - -print("Public key", my_wallet.get_public_key_for_path(path0)) -print("Public key", my_wallet.get_public_key_for_path(path1)) -print("Public key", my_wallet.get_public_key_for_path(path2)) -print("Public key", my_wallet.get_public_key_for_path(path3)) -print("Public key", my_wallet.get_public_key_for_path(path4)) - -print("Signature", my_wallet.sign_message_for_path()) - -print("Disconnection", my_wallet.disconnect()) \ No newline at end of file diff --git a/python/keycard_wallet.py b/python/keycard_wallet.py deleted file mode 100644 index 1a85af06..00000000 --- a/python/keycard_wallet.py +++ /dev/null @@ -1,137 +0,0 @@ -from smartcard.System import readers -from keycard.exceptions import APDUError, TransportError -from ecdsa import VerifyingKey, SECP256k1 - -from keycard.keycard import KeyCard - -from mnemonic import Mnemonic -from keycard import constants - -import keycard - -PIN = '123456' -PUK = '123456123456' -DEFAULT_PAIRING_PASSWORD = "KeycardDefaultPairing" -DEFAULT_MNEMONIC = "fashion degree mountain wool question damp current pond grow dolphin chronic then" -DEFAULT_PASSPHRASE = "" - -class KeycardWallet: - def __init__(self): - self.card = KeyCard() - self.pairing_index = None - self.pairing_key = None - - def _is_smart_card_reader_detected(self) -> bool: - try: - return len(readers()) > 0 - except Exception: - return False - - def _is_keycard_detected(self) -> bool: - try: - KeyCard().select() - return True - except (TransportError, APDUError, Exception): - # No readers, no card, or card doesn't respond. - return False - - # Wrapped - def is_unpaired_keycard_available(self) -> bool: - if not self._is_smart_card_reader_detected(): - return False - elif not self._is_keycard_detected(): - return False - return True - - # Wrapped - def setup_communication(self, pin = PIN, password = DEFAULT_PAIRING_PASSWORD) -> bool: - try: - self.card.select() - - if not self.card.is_initialized: - return False - - if self.pairing_index is None: - pairing_index, pairing_key = self.card.pair(password) - self.pairing_index = pairing_index - self.pairing_key = pairing_key - - - self.card.open_secure_channel(pairing_index, pairing_key) - self.card.verify_pin(pin) - - return True - except Exception as e: - print(f"Error: {e}") - return False - - def load_mnemonic(self, mnemonic = DEFAULT_MNEMONIC, passphrase = DEFAULT_PASSPHRASE) -> bool: - try: - # Convert mnemonic to seed - mnemo = Mnemonic("english") - seed = mnemo.to_seed(mnemonic, passphrase) - - # Load the LEE seed onto the card - result = self.card.load_key( - key_type = constants.LoadKeyType.BIP39_SEED, - lee_seed = seed - ) - - return True - except Exception as e: - print(f"Error during disconnect: {e}") - return False - - def disconnect(self) -> bool: - try: - if not self.card.is_secure_channel_open: - return None - - self.card.unpair(self.pairing_index) - self.pairing_index = None - self.pairing_key = None - - return True - except Exception as e: - print(f"Error during unpair: {e}") - return False - - def get_public_key_for_path(self, path: str = "m/44'/60'/0'/0/0") -> bytes | None: - try: - if not self.card.is_secure_channel_open or not self.card.is_pin_verified: - return None - - public_key = self.card.export_key( - derivation_option = constants.DerivationOption.DERIVE, - public_only = True, - keypath = path - ) - - public_key = public_key.public_key - public_key = VerifyingKey.from_string(public_key[1:], curve=SECP256k1) - public_key = public_key.to_string("compressed")[1:] - - return public_key - - except Exception as e: - print(f"Error getting public key: {e}") - return None - - - def sign_message_for_path(self, message: bytes = b"DefaultMessageTestDefaultMessage", path: str = "m/44'/60'/0'/0/0") -> bytes | None: - try: - if not self.card.is_secure_channel_open or not self.card.is_pin_verified: - return None - - signature = self.card.sign_with_path( - digest = message, - path = path, - algorithm = constants.SigningAlgorithm.SCHNORR_BIP340, - make_current = False - ) - - return signature.signature - - except Exception as e: - print(f"Error signing message: {e}") - return None \ No newline at end of file diff --git a/wallet-ffi/src/transfer.rs b/wallet-ffi/src/transfer.rs index e65fa473..739832ae 100644 --- a/wallet-ffi/src/transfer.rs +++ b/wallet-ffi/src/transfer.rs @@ -72,7 +72,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_public( let transfer = NativeTokenTransfer(&wallet); - match block_on(transfer.send_public_transfer(from_id, to_id, amount, &None, &None)) { + match block_on(transfer.send_public_transfer(from_id, to_id, amount)) { Ok(tx_hash) => { let tx_hash = CString::new(tx_hash.to_string()) .map_or(ptr::null_mut(), std::ffi::CString::into_raw); @@ -566,7 +566,7 @@ pub unsafe extern "C" fn wallet_ffi_register_public_account( let transfer = NativeTokenTransfer(&wallet); - match block_on(transfer.register_account(account_id, &None, &None)) { + match block_on(transfer.register_account(account_id)) { Ok(tx_hash) => { let tx_hash = CString::new(tx_hash.to_string()) .map_or(ptr::null_mut(), std::ffi::CString::into_raw); diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 8e94ec2f..4e98b8ef 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -18,7 +18,6 @@ amm_core.workspace = true testnet_initial_state.workspace = true ata_core.workspace = true bip39.workspace = true -pyo3 = { version = "0.21", features = ["auto-initialize"] } anyhow.workspace = true thiserror.workspace = true @@ -40,4 +39,3 @@ async-stream.workspace = true indicatif = { version = "0.18.3", features = ["improved_unicode"] } optfield = "0.4.0" url.workspace = true -keycard_wallet.workspace = true \ No newline at end of file diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 04050a79..86ae7e35 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -28,15 +28,16 @@ pub enum AccountSubcommand { #[arg(short, long)] keys: bool, /// Valid 32 byte base58 string with privacy prefix. - #[arg(short, long, conflicts_with = "account_label")] + #[arg( + short, + long, + conflicts_with = "account_label", + required_unless_present = "account_label" + )] account_id: Option, /// Account label (alternative to --account-id). #[arg(long, conflicts_with = "account_id")] account_label: Option, - #[arg(long, conflicts_with = "account_id", conflicts_with = "account_id")] - pin: Option, - #[arg(long, conflicts_with = "account_id", conflicts_with = "account_id")] - key_path: Option, }, /// Produce new public or private account. #[command(subcommand)] @@ -190,23 +191,17 @@ impl WalletSubcommand for AccountSubcommand { keys, account_id, account_label, - pin, - key_path, } => { let resolved = resolve_id_or_label( account_id, account_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &pin, - &key_path, )?; let (account_id_str, addr_kind) = parse_addr_with_privacy_prefix(&resolved)?; let account_id: nssa::AccountId = account_id_str.parse()?; - println!("Account Id: {}", resolved); - if let Some(label) = wallet_core.storage.labels.get(&account_id_str) { println!("Label: {label}"); } @@ -412,8 +407,6 @@ impl WalletSubcommand for AccountSubcommand { account_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let (account_id_str, _) = parse_addr_with_privacy_prefix(&resolved)?; diff --git a/wallet/src/cli/keycard.rs b/wallet/src/cli/keycard.rs deleted file mode 100644 index 8c825458..00000000 --- a/wallet/src/cli/keycard.rs +++ /dev/null @@ -1,73 +0,0 @@ -use anyhow::Result; -use clap::Subcommand; -use keycard_wallet::{KeycardWallet, python_path}; -use pyo3::prelude::*; - -use crate::{ - WalletCore, - cli::{SubcommandReturnValue, WalletSubcommand}, -}; - -/// Represents generic chain CLI subcommand. -#[derive(Subcommand, Debug, Clone)] -pub enum KeycardSubcommand { - Available, - Load { - #[arg(short, long)] - mnemonic: Option, - #[arg(short, long)] - pin: Option, - }, -} - -impl WalletSubcommand for KeycardSubcommand { - #[expect(clippy::cognitive_complexity, reason = "TODO: fix later")] - async fn handle_subcommand( - self, - _wallet_core: &mut WalletCore, - ) -> Result { - match self { - Self::Available => { - Python::with_gil(|py| { - python_path::add_python_path(py).expect("keycard_wallet.py not found"); - - let wallet = KeycardWallet::new(py).expect("Expect keycard wallet"); - let available = wallet - .is_unpaired_keycard_available(py) - .expect("Expect a Boolean."); - - if available { - println!("\u{2705} Keycard is available."); - } else { - println!("\u{274c} Keycard is not available."); - } - }); - - Ok(SubcommandReturnValue::Empty) - } - Self::Load { mnemonic, pin } => { - Python::with_gil(|py| { - python_path::add_python_path(py).expect("keycard_wallet.py not found"); - - let wallet = KeycardWallet::new(py).expect("Expect keycard wallet"); - - let is_connected = wallet - .setup_communication(py, &pin.expect("TODO")) - .expect("Expect a Boolean."); - - if is_connected { - println!("\u{2705} Keycard is now connected to wallet."); - } else { - println!("\u{274c} Keycard is not connected to wallet."); - } - - let _ = wallet.load_mnemonic(py, &mnemonic.expect("TODO")); - - let _ = wallet.disconnect(py); - }); - - Ok(SubcommandReturnValue::Empty) - } - } - } -} diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 1948db24..1653e938 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -14,7 +14,6 @@ use crate::{ account::AccountSubcommand, chain::ChainSubcommand, config::ConfigSubcommand, - keycard::KeycardSubcommand, programs::{ amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand, native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, @@ -26,7 +25,6 @@ use crate::{ pub mod account; pub mod chain; pub mod config; -pub mod keycard; pub mod programs; pub(crate) trait WalletSubcommand { @@ -75,8 +73,6 @@ pub enum Command { }, /// Deploy a program. DeployProgram { binary_filepath: PathBuf }, - #[command(subcommand)] - Keycard(KeycardSubcommand), } /// To execute commands, env var `NSSA_WALLET_HOME_DIR` must be set into directory with config. @@ -125,9 +121,6 @@ pub async fn execute_subcommand( Command::Pinata(pinata_subcommand) => { pinata_subcommand.handle_subcommand(wallet_core).await? } - Command::Keycard(keycard_subcommand) => { - keycard_subcommand.handle_subcommand(wallet_core).await? - } Command::CheckHealth => { let remote_program_ids = wallet_core .sequencer_client diff --git a/wallet/src/cli/programs/amm.rs b/wallet/src/cli/programs/amm.rs index aa0c5482..0f8a0fff 100644 --- a/wallet/src/cli/programs/amm.rs +++ b/wallet/src/cli/programs/amm.rs @@ -216,24 +216,18 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { user_holding_a_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let user_holding_b = resolve_id_or_label( user_holding_b, user_holding_b_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let user_holding_lp = resolve_id_or_label( user_holding_lp, user_holding_lp_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; @@ -288,16 +282,12 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { user_holding_a_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let user_holding_b = resolve_id_or_label( user_holding_b, user_holding_b_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; @@ -378,24 +368,18 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { user_holding_a_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let user_holding_b = resolve_id_or_label( user_holding_b, user_holding_b_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let user_holding_lp = resolve_id_or_label( user_holding_lp, user_holding_lp_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; @@ -453,24 +437,18 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { user_holding_a_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let user_holding_b = resolve_id_or_label( user_holding_b, user_holding_b_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let user_holding_lp = resolve_id_or_label( user_holding_lp, user_holding_lp_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index 3bd60464..41008eac 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -1,7 +1,6 @@ use anyhow::Result; use clap::Subcommand; use common::transaction::NSSATransaction; -use keycard_wallet::KeycardWallet; use nssa::AccountId; use crate::{ @@ -24,16 +23,12 @@ pub enum AuthTransferSubcommand { #[arg( long, conflicts_with = "account_label", - // required_unless_present = "account_label" + required_unless_present = "account_label" )] account_id: Option, /// Account label (alternative to --account-id). #[arg(long, conflicts_with = "account_id")] account_label: Option, - #[arg(long)] - pin: Option, - #[arg(long, conflicts_with = "account_id", conflicts_with = "account_label")] - key_path: Option, }, /// Send native tokens from one account to another with variable privacy. /// @@ -43,7 +38,11 @@ pub enum AuthTransferSubcommand { /// First is used for owned accounts, second otherwise. Send { /// from - valid 32 byte base58 string with privacy prefix. - #[arg(long, conflicts_with = "from_label")] + #[arg( + long, + conflicts_with = "from_label", + required_unless_present = "from_label" + )] from: Option, /// From account label (alternative to --from). #[arg(long, conflicts_with = "from")] @@ -63,12 +62,6 @@ pub enum AuthTransferSubcommand { /// amount - amount of balance to move. #[arg(long)] amount: u128, - #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] - pin: Option, - #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] - from_key_path: Option, - #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] - to_key_path: Option, }, } @@ -81,18 +74,13 @@ impl WalletSubcommand for AuthTransferSubcommand { Self::Init { account_id, account_label, - pin, - key_path, } => { let resolved = resolve_id_or_label( account_id, account_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &pin, - &key_path, )?; - let (account_id, addr_privacy) = parse_addr_with_privacy_prefix(&resolved)?; match addr_privacy { @@ -100,7 +88,7 @@ impl WalletSubcommand for AuthTransferSubcommand { let account_id = account_id.parse()?; let tx_hash = NativeTokenTransfer(wallet_core) - .register_account(account_id, &pin, &key_path) + .register_account(account_id) .await?; println!("Transaction hash is {tx_hash}"); @@ -145,37 +133,24 @@ impl WalletSubcommand for AuthTransferSubcommand { to_npk, to_vpk, amount, - pin, - from_key_path, - to_key_path, } => { let from = resolve_id_or_label( from, from_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &pin, - &from_key_path, )?; - - let to = match (to, to_label, to_key_path) { - (v, None, None) => v, - (None, Some(label), None) => Some(resolve_account_label( + let to = match (to, to_label) { + (v, None) => v, + (None, Some(label)) => Some(resolve_account_label( &label, &wallet_core.storage.labels, &wallet_core.storage.user_data, )?), - (None, None, Some(to_key_path)) => { - Some(KeycardWallet::get_account_id_for_path_with_connect( - &pin.as_ref().expect("TODO"), - &to_key_path, - )) - } - _ => { + (Some(_), Some(_)) => { anyhow::bail!("Provide only one of --to or --to-label") } }; - let underlying_subcommand = match (to, to_npk, to_vpk) { (None, None, None) => { anyhow::bail!( @@ -196,13 +171,7 @@ impl WalletSubcommand for AuthTransferSubcommand { match (from_privacy, to_privacy) { (AccountPrivacyKind::Public, AccountPrivacyKind::Public) => { - NativeTokenTransferProgramSubcommand::Public { - from, - to, - amount, - pin, - key_path: from_key_path, - } + NativeTokenTransferProgramSubcommand::Public { from, to, amount } } (AccountPrivacyKind::Private, AccountPrivacyKind::Private) => { NativeTokenTransferProgramSubcommand::Private( @@ -226,8 +195,6 @@ impl WalletSubcommand for AuthTransferSubcommand { from, to, amount, - pin, - key_path: from_key_path, }, ) } @@ -254,8 +221,6 @@ impl WalletSubcommand for AuthTransferSubcommand { to_npk, to_vpk, amount, - pin, - key_path: from_key_path, }, ) } @@ -285,10 +250,6 @@ pub enum NativeTokenTransferProgramSubcommand { /// amount - amount of balance to move. #[arg(long)] amount: u128, - #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] - pin: Option, - #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] - key_path: Option, }, /// Private execution. #[command(subcommand)] @@ -329,10 +290,6 @@ pub enum NativeTokenTransferProgramSubcommandShielded { /// amount - amount of balance to move. #[arg(long)] amount: u128, - #[arg(long)] - pin: Option, - #[arg(long)] - key_path: Option, }, /// Send native token transfer from `from` to `to` for `amount`. /// @@ -350,10 +307,6 @@ pub enum NativeTokenTransferProgramSubcommandShielded { /// amount - amount of balance to move. #[arg(long)] amount: u128, - #[arg(long)] - pin: Option, - #[arg(long)] - key_path: Option, }, } @@ -474,19 +427,13 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { wallet_core: &mut WalletCore, ) -> Result { match self { - Self::ShieldedOwned { - from, - to, - amount, - pin, - key_path, - } => { + Self::ShieldedOwned { from, to, amount } => { let from: AccountId = from.parse().unwrap(); let to: AccountId = to.parse().unwrap(); let (tx_hash, secret) = NativeTokenTransfer(wallet_core) - .send_shielded_transfer(from, to, amount, &pin, &key_path) - .await?; //TODO: here (marvin) + .send_shielded_transfer(from, to, amount) + .await?; println!("Transaction hash is {tx_hash}"); @@ -510,8 +457,6 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { to_npk, to_vpk, amount, - pin, - key_path, } => { let from: AccountId = from.parse().unwrap(); @@ -527,9 +472,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_vpk.to_vec()); let (tx_hash, _) = NativeTokenTransfer(wallet_core) - .send_shielded_transfer_to_outer_account( - from, to_npk, to_vpk, amount, &pin, &key_path, - ) + .send_shielded_transfer_to_outer_account(from, to_npk, to_vpk, amount) .await?; println!("Transaction hash is {tx_hash}"); @@ -579,18 +522,12 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) } - Self::Public { - from, - to, - amount, - pin, - key_path, - } => { + Self::Public { from, to, amount } => { let from: AccountId = from.parse().unwrap(); let to: AccountId = to.parse().unwrap(); let tx_hash = NativeTokenTransfer(wallet_core) - .send_public_transfer(from, to, amount, &pin, &key_path) + .send_public_transfer(from, to, amount) .await?; println!("Transaction hash is {tx_hash}"); diff --git a/wallet/src/cli/programs/pinata.rs b/wallet/src/cli/programs/pinata.rs index e41c0fd8..6171a0f2 100644 --- a/wallet/src/cli/programs/pinata.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -17,15 +17,15 @@ pub enum PinataProgramAgnosticSubcommand { /// Claim pinata. Claim { /// to - valid 32 byte base58 string with privacy prefix. - #[arg(long, conflicts_with = "to_label")] + #[arg( + long, + conflicts_with = "to_label", + required_unless_present = "to_label" + )] to: Option, /// To account label (alternative to --to). #[arg(long, conflicts_with = "to")] to_label: Option, - #[arg(long, conflicts_with = "to", conflicts_with = "to_label")] - pin: Option, - #[arg(long, conflicts_with = "to", conflicts_with = "to_label")] - key_path: Option, }, } @@ -35,22 +35,15 @@ impl WalletSubcommand for PinataProgramAgnosticSubcommand { wallet_core: &mut WalletCore, ) -> Result { let underlying_subcommand = match self { - Self::Claim { - to, - to_label, - pin, - key_path, - } => { + Self::Claim { to, to_label } => { let to = resolve_id_or_label( to, to_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &pin, - &key_path, )?; - let (to, to_addr_privacy) = parse_addr_with_privacy_prefix(&to)?; + match to_addr_privacy { AccountPrivacyKind::Public => { PinataProgramSubcommand::Public(PinataProgramSubcommandPublic::Claim { diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index 436b54a8..0575da09 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -76,10 +76,6 @@ pub enum TokenProgramAgnosticSubcommand { /// amount - amount of balance to move. #[arg(long)] amount: u128, - #[arg(long)] - pin: Option, - #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] - from_key_path: Option, }, /// Burn tokens on `holder`, modify `definition`. /// @@ -99,7 +95,11 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(long, conflicts_with = "definition")] definition_label: Option, /// holder - valid 32 byte base58 string with privacy prefix. - #[arg(long, conflicts_with = "holder_label")] + #[arg( + long, + conflicts_with = "holder_label", + required_unless_present = "holder_label" + )] holder: Option, /// Holder account label (alternative to --holder). #[arg(long, conflicts_with = "holder")] @@ -107,10 +107,6 @@ pub enum TokenProgramAgnosticSubcommand { /// amount - amount of balance to burn. #[arg(long)] amount: u128, - #[arg(long, conflicts_with = "holder", conflicts_with = "holder_label")] - holder_pin: Option, - #[arg(long, conflicts_with = "holder", conflicts_with = "holder_label")] - holder_key_path: Option, }, /// Mint tokens on `holder`, modify `definition`. /// @@ -146,10 +142,6 @@ pub enum TokenProgramAgnosticSubcommand { /// amount - amount of balance to mint. #[arg(long)] amount: u128, - #[arg(long, conflicts_with = "holder", conflicts_with = "holder_label")] - holder_pin: Option, - #[arg(long, conflicts_with = "holder", conflicts_with = "holder_label")] - holder_key_path: Option, }, } @@ -172,16 +164,12 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { definition_account_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let supply_account_id = resolve_id_or_label( supply_account_id, supply_account_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let (definition_account_id, definition_addr_privacy) = parse_addr_with_privacy_prefix(&definition_account_id)?; @@ -241,16 +229,12 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { to_npk, to_vpk, amount, - pin, - from_key_path, } => { let from = resolve_id_or_label( from, from_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &pin, - &from_key_path, )?; let to = match (to, to_label) { (v, None) => v, @@ -288,8 +272,6 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { sender_account_id: from, recipient_account_id: to, balance_to_move: amount, - pin, - sender_key_path: from_key_path, }, ) } @@ -354,29 +336,24 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { holder, holder_label, amount, - holder_pin, - holder_key_path, } => { let definition = resolve_id_or_label( definition, definition_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; let holder = resolve_id_or_label( holder, holder_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &holder_pin, - &holder_key_path, )?; let underlying_subcommand = { let (definition, definition_privacy) = parse_addr_with_privacy_prefix(&definition)?; let (holder, holder_privacy) = parse_addr_with_privacy_prefix(&holder)?; + match (definition_privacy, holder_privacy) { (AccountPrivacyKind::Public, AccountPrivacyKind::Public) => { TokenProgramSubcommand::Public( @@ -427,25 +404,24 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { holder_npk, holder_vpk, amount, - holder_pin, - holder_key_path, } => { let definition = resolve_id_or_label( definition, definition_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, )?; - let holder = Some(resolve_id_or_label( - holder.clone(), - holder_label.clone(), - &wallet_core.storage.labels, - &wallet_core.storage.user_data, - &holder_pin, - &holder_key_path, - )?); + let holder = match (holder, holder_label) { + (v, None) => v, + (None, Some(label)) => Some(resolve_account_label( + &label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?), + (Some(_), Some(_)) => { + anyhow::bail!("Provide only one of --holder or --holder-label") + } + }; let underlying_subcommand = match (holder, holder_npk, holder_vpk) { (None, None, None) => { anyhow::bail!( @@ -566,10 +542,6 @@ pub enum TokenProgramSubcommandPublic { recipient_account_id: String, #[arg(short, long)] balance_to_move: u128, - #[arg(long)] - pin: Option, - #[arg(long)] - sender_key_path: Option, }, // Burn tokens using the token program BurnToken { @@ -802,16 +774,12 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { sender_account_id, recipient_account_id, balance_to_move, - pin, - sender_key_path, } => { Token(wallet_core) .send_transfer_transaction( sender_account_id.parse().unwrap(), recipient_account_id.parse().unwrap(), balance_to_move, - pin, - sender_key_path, ) .await?; Ok(SubcommandReturnValue::Empty) diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index fcf19863..3e304253 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -3,7 +3,6 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr as _}; use anyhow::{Context as _, Result}; use base58::ToBase58 as _; use key_protocol::key_protocol_core::NSSAUserData; -use keycard_wallet::KeycardWallet; use nssa::Account; use nssa_core::account::Nonce; use rand::{RngCore as _, rngs::OsRng}; @@ -61,18 +60,11 @@ pub fn resolve_id_or_label( label: Option, labels: &HashMap, user_data: &NSSAUserData, - pin: &Option, - key_path: &Option, ) -> Result { - match (id, label, pin) { - (Some(id), None, None) => Ok(id), - (None, Some(label), None) => resolve_account_label(&label, labels, user_data), - (None, None, Some(pin)) => Ok(KeycardWallet::get_account_id_for_path_with_connect( - pin, - key_path.as_ref().expect("TODO"), - ) - .to_string()), - _ => anyhow::bail!("provide exactly one of account id, account label or keycard path"), + match (id, label) { + (Some(id), None) => Ok(id), + (None, Some(label)) => resolve_account_label(&label, labels, user_data), + _ => anyhow::bail!("provide exactly one of account id or account label"), } } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 3c82ec92..c9cf57f1 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -16,19 +16,16 @@ use chain_storage::WalletChainStore; use common::{HashType, transaction::NSSATransaction}; use config::WalletConfig; use key_protocol::key_management::key_tree::{chain_index::ChainIndex, traits::KeyNode as _}; -use keycard_wallet::KeycardWallet; use log::info; use nssa::{ - Account, AccountId, PrivacyPreservingTransaction, PublicKey, Signature, + Account, AccountId, PrivacyPreservingTransaction, privacy_preserving_transaction::{ circuit::{ProgramWithDependencies, Proof}, message::EncryptedAccountData, }, }; use nssa_core::{ - Commitment, MembershipProof, SharedSecretKey, - account::{AccountWithMetadata, Nonce}, - program::InstructionData, + Commitment, MembershipProof, SharedSecretKey, account::Nonce, program::InstructionData, }; pub use privacy_preserving_tx::PrivacyPreservingAccount; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; @@ -36,7 +33,7 @@ use tokio::io::AsyncWriteExt as _; use crate::{ config::{PersistentStorage, WalletConfigOverrides}, - helperfunctions::{parse_addr_with_privacy_prefix, produce_data_for_storage}, + helperfunctions::produce_data_for_storage, poller::TxPoller, }; @@ -366,17 +363,10 @@ impl WalletCore { accounts: Vec, instruction_data: InstructionData, program: &ProgramWithDependencies, - pin: &Option, - key_path: &Option, ) -> Result<(HashType, Vec), ExecutionFailureKind> { - self.send_privacy_preserving_tx_with_pre_check( - accounts, - instruction_data, - program, - |_| Ok(()), - pin, - key_path, - ) + self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| { + Ok(()) + }) .await } @@ -386,53 +376,10 @@ impl WalletCore { instruction_data: InstructionData, program: &ProgramWithDependencies, tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, - pin: &Option, - key_path: &Option, ) -> Result<(HashType, Vec), ExecutionFailureKind> { let acc_manager = privacy_preserving_tx::AccountManager::new(self, accounts).await?; - let mut pre_states = acc_manager.pre_states(); - - let keycard_account = if let Some(pin) = pin.as_ref() { - let account_id = KeycardWallet::get_account_id_for_path_with_connect( - pin, - key_path.as_ref().expect("TODO"), - ); - - let (acc_id, _) = - parse_addr_with_privacy_prefix(&account_id).expect("Valid parsing of account id"); - - let account_id = acc_id.parse().expect("Expect a valid Account Id"); - let account = self - .get_account_public(account_id) - .await - .expect("Expect valid account"); - - Some(AccountWithMetadata { - account, - is_authorized: true, - account_id, - }) - } else { - None - }; - - let mut nonces: Vec = acc_manager.public_account_nonces().into_iter().collect(); - - let mut account_ids: Vec = acc_manager.public_account_ids(); - - let mut visibility_mask = acc_manager.visibility_mask().to_vec(); - - if let Some(acc) = keycard_account.as_ref() { - nonces.push(acc.account.nonce); - - account_ids.push(acc.account_id); - - visibility_mask.push(0); - - pre_states.push(acc.clone()); - } - + let pre_states = acc_manager.pre_states(); tx_pre_check( &pre_states .iter() @@ -444,7 +391,7 @@ impl WalletCore { let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove( pre_states, instruction_data, - visibility_mask, + acc_manager.visibility_mask().to_vec(), private_account_keys .iter() .map(|keys| (keys.npk, keys.ssk)) @@ -457,8 +404,8 @@ impl WalletCore { let message = nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( - account_ids, - nonces, + acc_manager.public_account_ids(), + Vec::from_iter(acc_manager.public_account_nonces()), private_account_keys .iter() .map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone())) @@ -467,7 +414,7 @@ impl WalletCore { ) .unwrap(); - let witness_set = Self::sign_privacy_message(&message, &proof, &acc_manager, pin, key_path) + let witness_set = Self::sign_privacy_message(&message, &proof, &acc_manager) .expect("Expect a valid witness set"); let tx = PrivacyPreservingTransaction::new(message, witness_set); @@ -627,76 +574,13 @@ impl WalletCore { message: &nssa::privacy_preserving_transaction::Message, proof: &Proof, acc_manager: &privacy_preserving_tx::AccountManager, - pin: &Option, - key_path: &Option, ) -> Result { - if pin.is_none() { - Ok( - nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( - message, - proof.clone(), - &acc_manager.public_account_auth(), - ), - ) - } else { - let public_key = KeycardWallet::get_public_key_for_path_with_connect( - &pin.as_ref().expect("TODO"), - &key_path.as_ref().expect("TODO"), - ); - let signature = KeycardWallet::sign_message_for_path_with_connect( - &pin.as_ref().expect("TODO"), - &key_path.as_ref().expect("TODO"), - &message.hash_message(), - ) - .expect("Expect a valid signature"); - let mut signatures = Vec::::new(); - signatures.push(signature); - let mut public_keys = Vec::::new(); - public_keys.push(public_key); - Ok( - nssa::privacy_preserving_transaction::witness_set::WitnessSet::from_list( - proof.clone(), - &signatures, - &public_keys, - ), - ) - } - } - - pub fn sign_privacy_message_with_keycard( - message: &nssa::privacy_preserving_transaction::Message, - proof: Proof, - pin: &String, - key_paths: &[String], - ) -> Result - { - let mut signatures = Vec::::new(); - let mut public_keys = Vec::::new(); - - let message_bytes: [u8; 32] = { - let v = message.to_bytes(); - let mut bytes = [0_u8; 32]; - let len = v.len().min(32); - bytes[..len].copy_from_slice(&v[..len]); - bytes - }; - - for path in key_paths.iter() { - public_keys.push(KeycardWallet::get_public_key_for_path_with_connect( - &pin, &path, - )); - signatures.push( - KeycardWallet::sign_message_for_path_with_connect(&pin, &path, &message_bytes) - .expect("Expect a valid signature"), - ); - } - Ok( - nssa::privacy_preserving_transaction::witness_set::WitnessSet::from_list( - proof, - &signatures, - &public_keys, + nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( + message, + proof.clone(), + &acc_manager.public_account_auth(), ), ) } diff --git a/wallet/src/program_facades/ata.rs b/wallet/src/program_facades/ata.rs index a72d50a9..ac60fb63 100644 --- a/wallet/src/program_facades/ata.rs +++ b/wallet/src/program_facades/ata.rs @@ -194,13 +194,7 @@ impl Ata<'_> { ]; self.0 - .send_privacy_preserving_tx( - accounts, - instruction_data, - &ata_with_token_dependency(), - &None, - &None, - ) + .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency()) .await .map(|(hash, mut secrets)| { let secret = secrets.pop().expect("expected owner's secret"); @@ -235,13 +229,7 @@ impl Ata<'_> { ]; self.0 - .send_privacy_preserving_tx( - accounts, - instruction_data, - &ata_with_token_dependency(), - &None, - &None, - ) + .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency()) .await .map(|(hash, mut secrets)| { let secret = secrets.pop().expect("expected owner's secret"); @@ -275,13 +263,7 @@ impl Ata<'_> { ]; self.0 - .send_privacy_preserving_tx( - accounts, - instruction_data, - &ata_with_token_dependency(), - &None, - &None, - ) + .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency()) .await .map(|(hash, mut secrets)| { let secret = secrets.pop().expect("expected owner's secret"); diff --git a/wallet/src/program_facades/native_token_transfer/deshielded.rs b/wallet/src/program_facades/native_token_transfer/deshielded.rs index e7abbb3e..d51f15ce 100644 --- a/wallet/src/program_facades/native_token_transfer/deshielded.rs +++ b/wallet/src/program_facades/native_token_transfer/deshielded.rs @@ -22,8 +22,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - &None, - &None, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/native_token_transfer/private.rs b/wallet/src/program_facades/native_token_transfer/private.rs index dbf9f5bf..c3a2125b 100644 --- a/wallet/src/program_facades/native_token_transfer/private.rs +++ b/wallet/src/program_facades/native_token_transfer/private.rs @@ -19,8 +19,6 @@ impl NativeTokenTransfer<'_> { vec![PrivacyPreservingAccount::PrivateOwned(from)], Program::serialize_instruction(instruction).unwrap(), &Program::authenticated_transfer_program().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -51,8 +49,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -80,8 +76,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - &None, - &None, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/native_token_transfer/public.rs b/wallet/src/program_facades/native_token_transfer/public.rs index 1ee1da6f..7705c268 100644 --- a/wallet/src/program_facades/native_token_transfer/public.rs +++ b/wallet/src/program_facades/native_token_transfer/public.rs @@ -1,5 +1,4 @@ use common::{HashType, transaction::NSSATransaction}; -use keycard_wallet::KeycardWallet; use nssa::{ AccountId, PublicTransaction, program::Program, @@ -16,8 +15,6 @@ impl NativeTokenTransfer<'_> { from: AccountId, to: AccountId, balance_to_move: u128, - pin: &Option, - key_path: &Option, ) -> Result { let balance = self .0 @@ -55,22 +52,8 @@ impl NativeTokenTransfer<'_> { let message = Message::try_new(program_id, account_ids, nonces, balance_to_move).unwrap(); - let witness_set = if pin.is_none() { - WalletCore::sign_public_message(self.0, &message, &sign_ids) - .expect("Expect a valid signature") - } else { - let pub_key = KeycardWallet::get_public_key_for_path_with_connect( - &pin.as_ref().expect("TODO"), - &key_path.as_ref().expect("TODO"), - ); - let signature = KeycardWallet::sign_message_for_path_with_connect( - &pin.as_ref().expect("TODO"), - &key_path.as_ref().expect("TODO"), - &message.hash_message(), - ) - .expect("Expect valid signature"); - WitnessSet::from_list(&[signature], &[pub_key]) - }; + let witness_set = WalletCore::sign_public_message(self.0, &message, &sign_ids) + .expect("Expect a valid signature"); let tx = PublicTransaction::new(message, witness_set); @@ -87,8 +70,6 @@ impl NativeTokenTransfer<'_> { pub async fn register_account( &self, from: AccountId, - pin: &Option, // Used by Keycard. - key_path: &Option, // Used by Keycard. ) -> Result { let nonces = self .0 @@ -99,32 +80,16 @@ impl NativeTokenTransfer<'_> { let instruction: u128 = 0; let account_ids = vec![from]; let program_id = Program::authenticated_transfer_program().id(); - let message = Message::try_new(program_id, account_ids, nonces, instruction) - .expect("Expect a valid Message"); + let message = Message::try_new(program_id, account_ids, nonces, instruction).unwrap(); - let witness_set = if pin.is_none() { - let signing_key = self.0.storage.user_data.get_pub_account_signing_key(from); + let signing_key = self.0.storage.user_data.get_pub_account_signing_key(from); - let Some(signing_key) = signing_key else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - WitnessSet::for_message(&message, &[signing_key]) - } else { - let pub_key = KeycardWallet::get_public_key_for_path_with_connect( - pin.as_ref().expect("TODO"), - key_path.as_ref().expect("TODO"), - ); - - let signature = KeycardWallet::sign_message_for_path_with_connect( - pin.as_ref().as_ref().expect("TODO"), - key_path.as_ref().expect("TODO"), - &message.hash_message(), - ) - .expect("Expect a valid Signature."); - WitnessSet::from_list(&[signature], &[pub_key]) + let Some(signing_key) = signing_key else { + return Err(ExecutionFailureKind::KeyNotFoundError); }; + let witness_set = WitnessSet::for_message(&message, &[signing_key]); + let tx = PublicTransaction::new(message, witness_set); Ok(self diff --git a/wallet/src/program_facades/native_token_transfer/shielded.rs b/wallet/src/program_facades/native_token_transfer/shielded.rs index 48d90820..625e1a8b 100644 --- a/wallet/src/program_facades/native_token_transfer/shielded.rs +++ b/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -11,8 +11,6 @@ impl NativeTokenTransfer<'_> { from: AccountId, to: AccountId, balance_to_move: u128, - pin: &Option, - from_key_path: &Option, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); @@ -25,8 +23,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - pin, - from_key_path, ) .await .map(|(resp, secrets)| { @@ -44,8 +40,6 @@ impl NativeTokenTransfer<'_> { to_npk: NullifierPublicKey, to_vpk: ViewingPublicKey, balance_to_move: u128, - pin: &Option, - from_key_path: &Option, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); @@ -61,8 +55,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - pin, - from_key_path, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/pinata.rs b/wallet/src/program_facades/pinata.rs index f30fcd5c..97118ecd 100644 --- a/wallet/src/program_facades/pinata.rs +++ b/wallet/src/program_facades/pinata.rs @@ -60,8 +60,6 @@ impl Pinata<'_> { ], nssa::program::Program::serialize_instruction(solution).unwrap(), &nssa::program::Program::pinata().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index 36d7557b..1f941c8c 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -1,6 +1,5 @@ use common::{HashType, transaction::NSSATransaction}; -use keycard_wallet::KeycardWallet; -use nssa::{AccountId, program::Program, public_transaction::WitnessSet}; +use nssa::{AccountId, program::Program}; use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey}; use sequencer_service_rpc::RpcClient as _; use token_core::Instruction; @@ -79,8 +78,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -111,8 +108,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -143,8 +138,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -160,8 +153,6 @@ impl Token<'_> { sender_account_id: AccountId, recipient_account_id: AccountId, amount: u128, - pin: Option, - sender_key_path: Option, ) -> Result { let account_ids = vec![sender_account_id, recipient_account_id]; let program_id = nssa::program::Program::token().id(); @@ -173,12 +164,34 @@ impl Token<'_> { .get_accounts_nonces(vec![sender_account_id]) .await .map_err(ExecutionFailureKind::SequencerError)?; - let recipient_nonces = self + + let mut private_keys = Vec::new(); + let sender_sk = self .0 - .get_accounts_nonces(vec![recipient_account_id]) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - nonces.extend(recipient_nonces); + .storage + .user_data + .get_pub_account_signing_key(sender_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + private_keys.push(sender_sk); + + if let Some(recipient_sk) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(recipient_account_id) + { + private_keys.push(recipient_sk); + let recipient_nonces = self + .0 + .get_accounts_nonces(vec![recipient_account_id]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + nonces.extend(recipient_nonces); + } else { + println!( + "Receiver's account ({recipient_account_id}) private key not found in wallet. Proceeding with only sender's key." + ); + } let message = nssa::public_transaction::Message::try_new( program_id, @@ -187,44 +200,8 @@ impl Token<'_> { instruction, ) .unwrap(); - - let witness_set = if pin.is_none() { - let mut private_keys = Vec::new(); - let sender_sk = self - .0 - .storage - .user_data - .get_pub_account_signing_key(sender_account_id) - .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - private_keys.push(sender_sk); - - if let Some(recipient_sk) = self - .0 - .storage - .user_data - .get_pub_account_signing_key(recipient_account_id) - { - private_keys.push(recipient_sk); - } else { - println!( - "Receiver's account ({recipient_account_id}) private key not found in wallet. Proceeding with only sender's key." - ); - } - - nssa::public_transaction::WitnessSet::for_message(&message, &private_keys) - } else { - let sender_public_key = KeycardWallet::get_public_key_for_path_with_connect( - &pin.as_ref().expect("TODO"), - &sender_key_path.as_ref().expect("TODO"), - ); - let signature = KeycardWallet::sign_message_for_path_with_connect( - &pin.expect("TODO"), - &sender_key_path.expect("TODO"), - &message.hash_message(), - ) - .expect("Expect a valid signature"); - WitnessSet::from_list(&[signature], &[sender_public_key]) - }; + let witness_set = + nssa::public_transaction::WitnessSet::for_message(&message, &private_keys); let tx = nssa::PublicTransaction::new(message, witness_set); @@ -255,8 +232,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -291,8 +266,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -323,8 +296,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -356,8 +327,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -393,8 +362,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -468,8 +435,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -500,8 +465,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -533,8 +496,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -630,8 +591,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -666,8 +625,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -698,8 +655,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -731,8 +686,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { @@ -768,8 +721,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, - &None, ) .await .map(|(resp, secrets)| { diff --git a/wallet_with_keycard.sh b/wallet_with_keycard.sh deleted file mode 100644 index 94c10d4c..00000000 --- a/wallet_with_keycard.sh +++ /dev/null @@ -1,21 +0,0 @@ -cargo install --path wallet --force - - -python3 -m venv venv -source venv/bin/activate -python3 -m pip install pyscard -python3 -m pip install mnemonic -python3 -m pip install ecdsa -python3 -m pip install pyaes - -cd python - -# Need to use local version till fix applet -# git clone --branch lee-schnorr --single-branch https://github.com/bitgamma/keycard-py.git -cd keycard-py -python3 -m venv venv -source venv/bin/activate -pip install -e . - - -