diff --git a/Cargo.lock b/Cargo.lock index 84b5e1d6..7227deda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,16 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + [[package]] name = "aes" version = "0.8.4" @@ -56,16 +66,27 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.0", +] + [[package]] name = "aes-gcm" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ - "aead", - "aes", + "aead 0.5.2", + "aes 0.8.4", "cipher 0.4.4", - "ctr", + "ctr 0.9.2", "ghash", "subtle", ] @@ -268,7 +289,7 @@ dependencies = [ "digest 0.10.7", "fnv", "merlin", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -716,7 +737,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ - "base64", + "base64 0.22.1", "http 1.4.1", "log", "url", @@ -813,7 +834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core 0.5.6", - "base64", + "base64 0.22.1", "bytes", "form_urlencoded", "futures-util", @@ -910,6 +931,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base256emoji" version = "1.0.2" @@ -926,6 +953,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -1048,6 +1081,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bollard" version = "0.20.2" @@ -1055,7 +1097,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ "async-stream", - "base64", + "base64 0.22.1", "bitflags 2.12.1", "bollard-buildkit-proto", "bollard-stubs", @@ -1112,7 +1154,7 @@ version = "1.52.1-rc.29.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" dependencies = [ - "base64", + "base64 0.22.1", "bollard-buildkit-proto", "bytes", "prost 0.14.3", @@ -1279,6 +1321,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher 0.5.2", +] + [[package]] name = "cbindgen" version = "0.29.3" @@ -1310,6 +1361,18 @@ dependencies = [ "shlex 2.0.1", ] +[[package]] +name = "ccm" +version = "0.6.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4edea5ea70a1285565ac264767613d6c88351a9a0557e7af793a0942590baaed" +dependencies = [ + "aead 0.6.1", + "cipher 0.5.2", + "ctr 0.10.1", + "subtle", +] + [[package]] name = "cesu8" version = "1.1.0" @@ -1541,7 +1604,7 @@ version = "0.1.0" dependencies = [ "anyhow", "authenticated_transfer_core", - "base64", + "base64 0.22.1", "borsh", "clock_core", "hex", @@ -1552,7 +1615,7 @@ dependencies = [ "programs", "serde", "serde_with", - "sha2", + "sha2 0.10.9", "system_accounts", "thiserror 2.0.18", ] @@ -1765,6 +1828,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1885,6 +1954,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.2", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -1926,6 +2011,15 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher 0.5.2", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -1933,6 +2027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" dependencies = [ "cmov", + "subtle", ] [[package]] @@ -2231,7 +2326,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", + "const-oid 0.10.2", "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -2310,7 +2407,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d" dependencies = [ - "base64", + "base64 0.22.1", "serde", "serde_json", ] @@ -2361,13 +2458,28 @@ checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der 0.7.10", "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "serdect", - "signature", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "serdect 0.2.0", + "signature 2.2.0", "spki 0.7.3", ] +[[package]] +name = "ecdsa" +version = "0.17.0-rc.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c72d1455753a703ad4b90ed2a759f2bc4562024a303176439cf6e593b5ade4" +dependencies = [ + "der 0.8.0", + "digest 0.11.3", + "elliptic-curve 0.14.0", + "rfc6979 0.6.0-pre.0", + "signature 3.0.0", + "spki 0.8.0", + "zeroize", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -2376,7 +2488,7 @@ checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8 0.10.2", "serde", - "signature", + "signature 2.2.0", ] [[package]] @@ -2389,7 +2501,7 @@ dependencies = [ "ed25519", "rand_core 0.6.4", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -2434,17 +2546,38 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", - "crypto-bigint", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", "digest 0.10.7", - "ff", + "ff 0.13.1", "generic-array 0.14.7", - "group", + "group 0.13.0", "pem-rfc7468", "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", - "serdect", + "sec1 0.7.3", + "serdect 0.2.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3273f1195b6f6253ebda493d6742c8baa9b26a291674cd96d92a0f09e90e9b46" +dependencies = [ + "base16ct 1.0.0", + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff 0.14.0", + "group 0.14.0", + "hkdf 0.13.0", + "hybrid-array", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sec1 0.8.1", "subtle", "zeroize", ] @@ -2724,6 +2857,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "ff_derive" version = "0.13.1" @@ -3137,11 +3280,22 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "guardian" version = "1.3.0" @@ -3349,7 +3503,16 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", ] [[package]] @@ -3361,6 +3524,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "hmac-sha512" version = "1.1.12" @@ -3481,7 +3653,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ "ctutils", + "subtle", "typenum", + "zeroize", ] [[package]] @@ -3589,7 +3763,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -3909,7 +4083,7 @@ version = "0.1.0" dependencies = [ "anyhow", "base58", - "base64", + "base64 0.22.1", "common", "hex", "lee", @@ -3981,6 +4155,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ + "block-padding", "hybrid-array", ] @@ -4266,7 +4441,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf36eb27f8e13fa93dcb50ccb44c417e25b818cfa1a481b5470cd07b19c60b98" dependencies = [ - "base64", + "base64 0.22.1", "futures-channel", "futures-util", "gloo-net", @@ -4319,7 +4494,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" dependencies = [ - "base64", + "base64 0.22.1", "http-body", "hyper", "hyper-rustls", @@ -4421,12 +4596,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "once_cell", - "serdect", - "sha2", - "signature", + "serdect 0.2.0", + "sha2 0.10.9", + "signature 2.2.0", +] + +[[package]] +name = "k256" +version = "0.14.0-rc.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905d38bdbb43bb506efa0a428b3e969ff244549832a86b18591492f503adfe37" +dependencies = [ + "cpubits", + "ecdsa 0.17.0-rc.22", + "elliptic-curve 0.14.0", + "primeorder", + "sha2 0.11.0", + "signature 3.0.0", + "wnaf", ] [[package]] @@ -4471,25 +4661,53 @@ dependencies = [ "hex", "hmac-sha512", "itertools 0.14.0", - "k256", + "k256 0.13.4", "lee", "lee_core", "ml-kem", "rand 0.8.6", "serde", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", ] +[[package]] +name = "keycard-rs" +version = "0.1.0" +source = "git+https://github.com/keycard-tech/keycard-rs?rev=98abc3ff1b26785a5631634d5bf24da54da541bf#98abc3ff1b26785a5631634d5bf24da54da541bf" +dependencies = [ + "aes 0.9.1", + "base64 0.21.7", + "cbc", + "ccm", + "generic-array 0.14.7", + "getrandom 0.2.17", + "getrandom 0.4.2", + "hkdf 0.13.0", + "hmac 0.13.0", + "k256 0.14.0-rc.14", + "pbkdf2", + "pcsc", + "rand_core 0.6.4", + "sha2 0.11.0", + "sha3 0.12.0", + "thiserror 1.0.69", + "typenum", + "zeroize", +] + [[package]] name = "keycard_wallet" version = "0.1.0" dependencies = [ + "bip39", + "keycard-rs", "lee", "log", - "pyo3", + "rand 0.8.6", "serde", "serde_json", + "thiserror 2.0.18", "zeroize", ] @@ -4556,7 +4774,7 @@ dependencies = [ "env_logger", "hex", "hex-literal 1.1.0", - "k256", + "k256 0.13.4", "lee_core", "log", "rand 0.8.6", @@ -4564,7 +4782,7 @@ dependencies = [ "risc0-zkvm", "serde", "serde_with", - "sha2", + "sha2 0.10.9", "test-case", "test_methods", "thiserror 2.0.18", @@ -4595,7 +4813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa3982e7fe36c1de68f91f3c9083124f389a975523881f3d7e3363362feda41" dependencies = [ "any_spawner", - "base64", + "base64 0.22.1", "cfg-if", "either_of", "futures", @@ -4797,7 +5015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da974775c5ccbb6bd64be7f53f75e8321542e28f21563a416574dbe4d5447eae" dependencies = [ "any_spawner", - "base64", + "base64 0.22.1", "codee", "futures", "hydration_context", @@ -4988,7 +5206,7 @@ checksum = "d558548fa3b5a8e9b66392f785921e363c57c05dcadfda4db0d41ae82d313e4a" dependencies = [ "async-channel", "asynchronous-codec", - "base64", + "base64 0.22.1", "byteorder", "bytes", "either", @@ -5007,7 +5225,7 @@ dependencies = [ "rand 0.8.6", "regex", "serde", - "sha2", + "sha2 0.10.9", "tracing", "web-time", ] @@ -5042,13 +5260,13 @@ dependencies = [ "asn1_der", "bs58", "ed25519-dalek", - "hkdf", - "k256", + "hkdf 0.12.4", + "k256 0.13.4", "multihash", "prost 0.14.3", "rand 0.8.6", "serde", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", "tracing", "zeroize", @@ -5074,7 +5292,7 @@ dependencies = [ "quick-protobuf-codec", "rand 0.8.6", "serde", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.18", "tracing", @@ -7109,13 +7327,42 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac 0.13.0", +] + +[[package]] +name = "pcsc" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd833ecf8967e65934c49d3521a175929839bf6d0e497f3bd0d3a2ca08943da" +dependencies = [ + "bitflags 2.12.1", + "pcsc-sys", +] + +[[package]] +name = "pcsc-sys" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ef017e15d2e5592a9e39a346c1dbaea5120bab7ed7106b210ef58ebd97003" +dependencies = [ + "pkg-config", +] + [[package]] name = "pem" version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -7330,6 +7577,32 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0-rc.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e56e6d67fdf5744e9e245ae571450fe584b91f5af261d0e40163b618e53a1f6" +dependencies = [ + "elliptic-curve 0.14.0", + "once_cell", + "primefield", + "serdect 0.4.3", +] + [[package]] name = "privacy_preserving_circuit_program" version = "0.1.0" @@ -7575,63 +7848,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "pyo3" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" -dependencies = [ - "libc", - "once_cell", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", -] - -[[package]] -name = "pyo3-build-config" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" -dependencies = [ - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "quick-protobuf" version = "0.8.1" @@ -8012,7 +8228,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -8065,10 +8281,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] +[[package]] +name = "rfc6979" +version = "0.6.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9935425142ac6e252364413291d96c8bc9898d0876a801824c7af4eae397b689" +dependencies = [ + "ctutils", + "hmac 0.13.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -8145,7 +8371,7 @@ dependencies = [ "directories", "hex", "rayon", - "sha2", + "sha2 0.10.9", "tempfile", ] @@ -8205,7 +8431,7 @@ dependencies = [ "risc0-sys", "risc0-zkp", "serde", - "sha2", + "sha2 0.10.9", "tracing", "zip", ] @@ -8342,7 +8568,7 @@ dependencies = [ "bytemuck", "cfg-if", "digest 0.10.7", - "ff", + "ff 0.13.1", "hex", "hex-literal 0.4.1", "metal", @@ -8356,7 +8582,7 @@ dependencies = [ "risc0-sys", "risc0-zkvm-platform", "serde", - "sha2", + "sha2 0.10.9", "stability", "tracing", ] @@ -8403,7 +8629,7 @@ dependencies = [ "rzup", "semver", "serde", - "sha2", + "sha2 0.10.9", "stability", "tempfile", "tracing", @@ -8507,7 +8733,7 @@ dependencies = [ "pkcs1", "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", + "signature 2.2.0", "spki 0.7.3", "subtle", "zeroize", @@ -8751,7 +8977,7 @@ dependencies = [ "semver", "serde", "serde_with", - "sha2", + "sha2 0.10.9", "strum", "tempfile", "thiserror 2.0.18", @@ -8835,11 +9061,25 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", + "base16ct 0.2.0", "der 0.7.10", "generic-array 0.14.7", "pkcs8 0.10.2", - "serdect", + "serdect 0.2.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct 1.0.0", + "ctutils", + "der 0.8.0", + "hybrid-array", "subtle", "zeroize", ] @@ -9109,7 +9349,7 @@ version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ - "base64", + "base64 0.22.1", "bs58", "chrono", "hex", @@ -9154,7 +9394,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" dependencies = [ - "base16ct", + "base16ct 0.2.0", + "serde", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct 1.0.0", "serde", ] @@ -9165,7 +9415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d60e4c1dfccd91fe0990141f69f1d5cf5679797ad53aa1b45e5bd658eb119f0" dependencies = [ "axum 0.8.9", - "base64", + "base64 0.22.1", "bytes", "const-str 1.1.0", "const_format", @@ -9245,6 +9495,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sha3" version = "0.10.9" @@ -9265,6 +9526,17 @@ dependencies = [ "keccak 0.2.0", ] +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -9306,6 +9578,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -9375,7 +9657,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures", "http 1.4.1", @@ -9414,6 +9696,12 @@ dependencies = [ "der 0.8.0", ] +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "spongefish" version = "0.2.0" @@ -9682,12 +9970,6 @@ dependencies = [ "xattr", ] -[[package]] -name = "target-lexicon" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" - [[package]] name = "tempfile" version = "3.27.0" @@ -10200,7 +10482,7 @@ checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum 0.8.9", - "base64", + "base64 0.22.1", "bytes", "h2", "http 1.4.1", @@ -10651,7 +10933,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ - "base64", + "base64 0.22.1", "flate2", "log", "percent-encoding", @@ -10668,7 +10950,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ - "base64", + "base64 0.22.1", "http 1.4.1", "httparse", "log", @@ -10858,13 +11140,12 @@ dependencies = [ "log", "optfield", "programs", - "pyo3", "rand 0.8.6", "rpassword", "sequencer_service_rpc", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "system_accounts", "tempfile", "testnet_initial_state", @@ -11050,7 +11331,7 @@ checksum = "d0a659ffe5c7f4538aa6357c07e3d73221cc61eba03bd9a081e14bc91ed09b8c" dependencies = [ "base16", "quote", - "sha2", + "sha2 0.10.9", "syn 2.0.117", ] @@ -11550,6 +11831,17 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wnaf" +version = "0.14.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86421f2a70c9e6cab8d84c99fb62d8761d355bd1285443a7e7ccad15aa515f2" +dependencies = [ + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 3e71f293..2d269ab0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,6 +163,8 @@ logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/lo logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" } logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" } +keycard-rs = { git = "https://github.com/keycard-tech/keycard-rs", rev = "98abc3ff1b26785a5631634d5bf24da54da541bf" } + rocksdb = { version = "0.24.0", default-features = false, features = [ "snappy", "bindgen-runtime", @@ -183,7 +185,6 @@ actix-web = { version = "4.13.0", default-features = false, features = [ ] } clap = { version = "4.5.42", features = ["derive", "env"] } reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] } -pyo3 = { version = "0.29", features = ["auto-initialize"] } zeroize = "1" criterion = { version = "0.8", features = ["html_reports"] } diff --git a/lez/keycard_wallet/Cargo.toml b/lez/keycard_wallet/Cargo.toml index 4abff1f1..7a062d8b 100644 --- a/lez/keycard_wallet/Cargo.toml +++ b/lez/keycard_wallet/Cargo.toml @@ -9,8 +9,11 @@ workspace = true [dependencies] lee.workspace = true -pyo3.workspace = true +keycard-rs.workspace = true +bip39.workspace = true log.workspace = true +rand.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true +thiserror.workspace = true zeroize.workspace = true diff --git a/lez/keycard_wallet/python/keycard_wallet.py b/lez/keycard_wallet/python/keycard_wallet.py deleted file mode 100644 index 21e966cb..00000000 --- a/lez/keycard_wallet/python/keycard_wallet.py +++ /dev/null @@ -1,221 +0,0 @@ -from smartcard.System import readers -from keycard.exceptions import APDUError, TransportError -from ecdsa import VerifyingKey, SECP256k1 - -from keycard.keycard import KeyCard -from keycard.commands.export_lee_key import export_lee_key -from mnemonic import Mnemonic -from keycard import constants - -import os -import secrets - -DEFAULT_PAIRING_PASSWORD = "KeycardDefaultPairing" - -def _pairing_password() -> str: - return os.environ.get("KEYCARD_PAIRING_PASSWORD", DEFAULT_PAIRING_PASSWORD) - -class KeycardWallet: - def __init__(self): - self.card = KeyCard() - - 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 - - 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 - - def initialize(self, pin: str, pairing_password: str | None = None) -> bool: - try: - self.card.select() - - if self.card.is_initialized: - raise RuntimeError("Card is already initialized") - - puk = ''.join(secrets.choice('0123456789') for _ in range(12)) - self.card.init(pin, puk, pairing_password or _pairing_password()) - print(f"Keycard PUK: {puk}") - print("Record this PUK and store it somewhere safe. It cannot be recovered.") - return True - except Exception as e: - raise RuntimeError(f"Error initializing keycard: {e}") from e - - def _reconnect(self) -> None: - self.card = KeyCard() - self.card.select() - - def _pair(self, pin: str, password: str) -> tuple[int, bytes]: - self.card.select() - - if not self.card.is_initialized: - raise RuntimeError("Card is not initialized — run 'wallet keycard init' first") - - pairing_index, pairing_key = self.card.pair(password) - self.pairing_index = pairing_index - self.pairing_key = pairing_key - - try: - self.card.open_secure_channel(pairing_index, pairing_key) - self.card.verify_pin(pin) - except Exception as e: - try: - self.card.unpair(pairing_index) - except Exception: - pass - raise RuntimeError(f"Error opening secure channel after fresh pair: {e}") from e - - return pairing_index, pairing_key - - def pair(self, pin: str, password: str | None = None) -> tuple[int, bytes]: - password = password or _pairing_password() - try: - return self._pair(pin, password) - except TransportError as e: - print(f"Transport error during fresh pair ({e}), attempting card reset and retry...") - try: - self._reconnect() - result = self._pair(pin, password) - print("Retry succeeded after card reset.") - return result - except TransportError as e2: - raise RuntimeError( - "Card lost power and did not recover after reset. " - "Try reseating the card in the reader." - ) from e2 - - def _setup_communication_with_pairing(self, pin: str, pairing_index: int, pairing_key: bytes) -> bool: - self.card.select() - - if not self.card.is_initialized: - raise RuntimeError("Card is not initialized — run 'wallet keycard init' first") - - self.pairing_index = pairing_index - self.pairing_key = pairing_key - - try: - self.card.open_secure_channel(pairing_index, pairing_key) - self.card.verify_pin(pin) - except Exception as e: - raise RuntimeError(f"Error setting up communication with stored pairing: {e}") from e - - return True - - def setup_communication_with_pairing(self, pin: str, pairing_index: int, pairing_key: bytes) -> bool: - try: - return self._setup_communication_with_pairing(pin, pairing_index, pairing_key) - except TransportError as e: - print(f"Transport error during stored pairing ({e}), attempting card reset and retry...") - try: - self._reconnect() - result = self._setup_communication_with_pairing(pin, pairing_index, pairing_key) - print("Retry succeeded after card reset.") - return result - except TransportError as e2: - raise RuntimeError( - "Card lost power and did not recover after reset. " - "Try reseating the card in the reader." - ) from e2 - - def close_session(self) -> bool: - return True - - def load_mnemonic(self, mnemonic: str) -> bool: - try: - # Convert mnemonic to seed - mnemo = Mnemonic("english") - if not mnemo.check(mnemonic): - raise RuntimeError("Invalid mnemonic phrase — check spelling and word count") - seed = mnemo.to_seed(mnemonic) - - # Load the LEE seed onto the card - result = self.card.load_key( - key_type = constants.LoadKeyType.LEE_SEED, - lee_seed = seed - ) - return True - except Exception as e: - raise RuntimeError(f"Error loading mnemonic: {e}") from e - - def disconnect(self) -> bool: - try: - if not self.card.is_secure_channel_open: - return False - - self.card.unpair(self.pairing_index) - - return True - except Exception as e: - raise RuntimeError(f"Error during disconnect: {e}") from e - - 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: - raise RuntimeError(f"Error getting public key: {e}") from e - - - def sign_message_for_path(self, message: bytes, 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: - raise RuntimeError(f"Error signing message: {e}") from e - - def get_private_keys_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 - - private_keys = export_lee_key( - self.card, - constants.DerivationOption.DERIVE, - path - ) - - nsk = private_keys.lee_nsk - vsk = private_keys.lee_vsk - - return (nsk, vsk) - - except Exception as e: - raise RuntimeError(f"Error getting private keys: {e}") from e - diff --git a/lez/keycard_wallet/src/lib.rs b/lez/keycard_wallet/src/lib.rs index a3ecd140..6d3f9777 100644 --- a/lez/keycard_wallet/src/lib.rs +++ b/lez/keycard_wallet/src/lib.rs @@ -1,16 +1,48 @@ -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr as _}; +use keycard_rs::{ + KeycardCommandSet, PcscChannel, + constants::sign_p2, + parsing::Bip32KeyPair, + secure_channel::Pairing, + tlv::{BerTlvReader, TLV_KEY_TEMPLATE, TLV_PUB_KEY, TLV_SIGNATURE_TEMPLATE}, +}; use lee::{AccountId, PublicKey, Signature}; -use pyo3::{prelude::*, types::PyAny}; +use rand::Rng as _; use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; -pub mod python_path; +const DEFAULT_PAIRING_PASSWORD: &str = "KeycardDefaultPairing"; + +/// LEE-applet extension tags. These aren't part of upstream `keycard-rs`'s `tlv` module (which +/// only knows the standard Status Keycard tags) since they're specific to the LEE-flavored +/// applet (`LEE_keycard.cap`). +const TLV_LEE_NSK: u8 = 0x83; +const TLV_LEE_VSK: u8 = 0x84; +/// Raw Schnorr signature (64 bytes: `r || s`, no ASN.1/DER wrapping) nested inside the standard +/// `TLV_SIGNATURE_TEMPLATE` alongside the usual `TLV_PUB_KEY`. Confirmed against real hardware — +/// the LEE applet's `SIGN` response for `sign_p2::BIP340_SCHNORR` is +/// `0xA0 { 0x80 <65-byte pubkey>, 0x88 <64-byte r||s> }`. +const TLV_LEE_RAW_SIGNATURE: u8 = 0x88; /// NSK (32 bytes) and VSK (64 bytes, the ML-KEM-768 seed `d || z`) as fixed-length zeroizing byte /// arrays. type PrivateKeyPair = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 64]>); +#[derive(Debug, thiserror::Error)] +pub enum KeycardWalletError { + #[error(transparent)] + Keycard(#[from] keycard_rs::Error), + #[error("keycard is already initialized")] + AlreadyInitialized, + #[error("invalid mnemonic phrase: {0}")] + InvalidMnemonic(String), + #[error("invalid key material from keycard: {0}")] + InvalidKeyMaterial(String), + #[error("keycard returned a signature that does not verify against its own public key")] + SignatureVerificationFailed, +} + // TODO: encrypt at rest alongside broader wallet storage encryption work. #[derive(Serialize, Deserialize)] pub struct KeycardPairingData { @@ -24,136 +56,195 @@ impl KeycardPairingData { } } -/// Rust wrapper around the Python `KeycardWallet` class. +/// Rust wrapper around `keycard-rs`, talking to the LEE-flavored Keycard applet over PC/SC. pub struct KeycardWallet { - instance: Py, + command_set: KeycardCommandSet, } impl KeycardWallet { - /// Create a new Python `KeycardWallet` instance. - pub fn new(py: Python) -> PyResult { - let module = py.import("keycard_wallet")?; - let class = module.getattr("KeycardWallet")?; - - let instance = class.call0()?; - + /// Connects to the first available PC/SC reader. Does not select the applet yet — callers + /// that need application info (`initialize`, `pair`, `connect`, ...) do that themselves. + pub fn new() -> Result { + let channel = PcscChannel::connect()?; Ok(Self { - instance: instance.into(), + command_set: KeycardCommandSet::new(channel), }) } - pub fn is_unpaired_keycard_available(&self, py: Python) -> PyResult { - self.instance - .bind(py) - .call_method0("is_unpaired_keycard_available")? - .extract() + /// Returns whether a smart card reader and a selectable Keycard are both present. + #[must_use] + pub fn is_unpaired_keycard_available() -> bool { + let Ok(channel) = PcscChannel::connect() else { + return false; + }; + KeycardCommandSet::new(channel) + .select() + .is_ok_and(|resp| resp.is_ok()) } - pub fn initialize(&self, py: Python<'_>, pin: &str) -> PyResult { - self.instance - .bind(py) - .call_method1("initialize", (pin,))? - .extract() + fn select(&mut self) -> Result<(), KeycardWalletError> { + self.command_set.select()?.check_ok()?; + Ok(()) } - pub fn pair(&self, py: Python<'_>, pin: &str) -> PyResult<(u8, Vec)> { - self.instance - .bind(py) - .call_method1("pair", (pin,))? - .extract() + fn open_and_verify(&mut self, pin: &str) -> Result<(), KeycardWalletError> { + self.command_set.auto_open_secure_channel()?; + self.command_set.verify_pin(pin)?.check_auth_ok()?; + Ok(()) + } + + /// Rebuilds the PC/SC channel and command set, then retries `op` once, if `op` failed with a + /// transport-level error (e.g. the card lost power mid-session). Mirrors the reconnect-once + /// behavior the previous `keycard-py`-based wallet had for `TransportError`. + fn with_reconnect_on_transport_error( + &mut self, + op: impl Fn(&mut Self) -> Result, + ) -> Result { + match op(self) { + Err(KeycardWalletError::Keycard(keycard_rs::Error::Io(io_err))) => { + log::warn!( + "transport error during keycard operation ({io_err}), reconnecting and retrying once" + ); + *self = Self::new()?; + op(self) + } + result => result, + } + } + + /// Initializes an uninitialized card, returning the generated PUK. The caller is responsible + /// for surfacing the PUK to the operator — it cannot be recovered afterward. + pub fn initialize(&mut self, pin: &str) -> Result { + self.select()?; + let already_initialized = self + .command_set + .app_info() + .expect("select() populates app_info on success") + .is_initialized(); + if already_initialized { + return Err(KeycardWalletError::AlreadyInitialized); + } + + let puk = generate_puk(); + self.command_set + .init(pin, &puk, &pairing_password())? + .check_ok()?; + Ok(puk) + } + + pub fn pair(&mut self, pin: &str) -> Result<(u8, [u8; 32]), KeycardWalletError> { + self.select()?; + self.command_set.auto_pair(&pairing_password())?; + let pairing = self + .command_set + .pairing() + .expect("auto_pair sets pairing data on success") + .clone(); + + if let Err(err) = self.open_and_verify(pin) { + drop(self.command_set.auto_unpair()); + return Err(err); + } + + Ok((pairing.pairing_index(), *pairing.pairing_key())) } pub fn setup_communication_with_pairing( - &self, - py: Python<'_>, + &mut self, pin: &str, index: u8, - key: &[u8], - ) -> PyResult { - self.instance - .bind(py) - .call_method1( - "setup_communication_with_pairing", - (pin, index, key.to_vec()), - )? - .extract() - } - - pub fn close_session(&self, py: Python<'_>) -> PyResult { - self.instance - .bind(py) - .call_method0("close_session")? - .extract() + key: &[u8; 32], + ) -> Result<(), KeycardWalletError> { + self.select()?; + self.command_set.set_pairing(Pairing::new(*key, index)); + self.open_and_verify(pin) } /// Connect using a stored pairing if available, falling back to a fresh pair. /// Saves any newly established pairing to disk. - pub fn connect(&self, py: Python<'_>, pin: &str) -> PyResult<()> { - if let Some(pairing) = load_pairing().filter(KeycardPairingData::is_valid) - && self - .setup_communication_with_pairing(py, pin, pairing.index, &pairing.key) - .is_ok() - { - return Ok(()); + pub fn connect(&mut self, pin: &str) -> Result<(), KeycardWalletError> { + if let Some(pairing) = load_pairing().filter(KeycardPairingData::is_valid) { + let key: [u8; 32] = pairing + .key + .clone() + .try_into() + .expect("KeycardPairingData::is_valid checked the key is 32 bytes"); + let reconnected = self.with_reconnect_on_transport_error(|wallet| { + wallet.setup_communication_with_pairing(pin, pairing.index, &key) + }); + if reconnected.is_ok() { + return Ok(()); + } } - let (index, key) = self.pair(py, pin)?; - save_pairing(&KeycardPairingData { index, key }); + + let (index, key) = self.with_reconnect_on_transport_error(|wallet| wallet.pair(pin))?; + save_pairing(&KeycardPairingData { + index, + key: key.to_vec(), + }); Ok(()) } - pub fn disconnect(&self, py: Python) -> PyResult { - self.instance.bind(py).call_method0("disconnect")?.extract() + /// Unpairs the current session. Returns `false` if there was nothing to unpair. + pub fn disconnect(&mut self) -> Result { + if self.command_set.pairing().is_none() { + return Ok(false); + } + self.command_set.auto_unpair()?; + Ok(true) } - pub fn get_public_key_for_path(&self, py: Python, path: &str) -> PyResult { - let public_key: Vec = self - .instance - .bind(py) - .call_method1("get_public_key_for_path", (path,))? - .extract()?; + pub fn get_public_key_for_path(&mut self, path: &str) -> Result { + let resp = self.command_set.export_key(path, false, true)?; + resp.check_ok()?; + let keypair = Bip32KeyPair::from_tlv(resp.data())?; - let public_key: [u8; 32] = public_key.try_into().map_err(|vec: Vec| { - PyErr::new::(format!( - "expected 32-byte public key from keycard, got {} bytes", - vec.len() - )) - })?; + // Uncompressed SEC1 point (0x04 || X || Y); the BIP340 x-only public key is just its X + // coordinate, since secp256k1 (what the card signs with) and k256's Schnorr verifying key + // are the same curve. + let public_key = keypair.public_key(); + let x_only: [u8; 32] = match public_key.split_first() { + Some((&0x04, xy)) if xy.len() == 64 => xy + .split_at(32) + .0 + .try_into() + .expect("split_at(32) of a 64-byte slice"), + _ => { + return Err(KeycardWalletError::InvalidKeyMaterial(format!( + "expected a 65-byte uncompressed secp256k1 public key from keycard, got {} bytes", + public_key.len() + ))); + } + }; - PublicKey::try_new(public_key) - .map_err(|e| PyErr::new::(e.to_string())) + PublicKey::try_new(x_only).map_err(|e| KeycardWalletError::InvalidKeyMaterial(e.to_string())) } - pub fn get_public_key_for_path_with_connect(pin: &str, path: &str) -> PyResult { - Python::attach(|py| { - python_path::add_python_path(py)?; - let wallet = Self::new(py)?; - wallet.connect(py, pin)?; - let pub_key = wallet.get_public_key_for_path(py, path); - drop(wallet.close_session(py)); - pub_key - }) + pub fn get_public_key_for_path_with_connect( + pin: &str, + path: &str, + ) -> Result { + let mut wallet = Self::new()?; + wallet.connect(pin)?; + wallet.get_public_key_for_path(path) } pub fn sign_message_for_path( - &self, - py: Python, + &mut self, path: &str, message: &[u8; 32], - ) -> PyResult<(Signature, PublicKey)> { - let py_signature: Vec = self - .instance - .bind(py) - .call_method1("sign_message_for_path", (message, path))? - .extract()?; + ) -> Result<(Signature, PublicKey), KeycardWalletError> { + let resp = + self.command_set + .sign_with_path_and_algo(message, path, sign_p2::BIP340_SCHNORR, false)?; + resp.check_ok()?; let sig = Signature { - value: normalize_keycard_signature(py_signature)?, + value: parse_schnorr_signature(resp.data())?, }; - let pub_key = self.get_public_key_for_path(py, path)?; + let pub_key = self.get_public_key_for_path(path)?; if !sig.is_valid_for(message, &pub_key) { - return Err(PyErr::new::( - "keycard returned a signature that does not verify against its own public key", - )); + return Err(KeycardWalletError::SignatureVerificationFailed); } Ok((sig, pub_key)) } @@ -162,39 +253,37 @@ impl KeycardWallet { pin: &str, path: &str, message: &[u8; 32], - ) -> PyResult<(Signature, PublicKey)> { - Python::attach(|py| { - python_path::add_python_path(py)?; - let wallet = Self::new(py)?; - wallet.connect(py, pin)?; - let result = wallet.sign_message_for_path(py, path, message); - drop(wallet.close_session(py)); - result - }) + ) -> Result<(Signature, PublicKey), KeycardWalletError> { + let mut wallet = Self::new()?; + wallet.connect(pin)?; + wallet.sign_message_for_path(path, message) } - pub fn load_mnemonic(&self, py: Python, mnemonic: &str) -> PyResult<()> { - self.instance - .bind(py) - .call_method1("load_mnemonic", (mnemonic,))?; + pub fn load_mnemonic(&mut self, mnemonic: &str) -> Result<(), KeycardWalletError> { + let mnemonic = bip39::Mnemonic::from_str(mnemonic) + .map_err(|e| KeycardWalletError::InvalidMnemonic(e.to_string()))?; + let seed = mnemonic.to_seed(""); + self.command_set.load_lee_key(&seed)?.check_ok()?; Ok(()) } pub fn get_public_account_id_for_path_with_connect( pin: &str, key_path: &str, - ) -> PyResult { + ) -> Result { let public_key = Self::get_public_key_for_path_with_connect(pin, key_path)?; Ok(format!("Public/{}", AccountId::from(&public_key))) } - pub fn get_private_keys_for_path(&self, py: Python, path: &str) -> PyResult { - let (raw_nsk, raw_vsk): (Vec, Vec) = self - .instance - .bind(py) - .call_method1("get_private_keys_for_path", (path,))? - .extract()?; + pub fn get_private_keys_for_path(&mut self, path: &str) -> Result { + let resp = self.command_set.export_lee_key(path)?; + resp.check_ok()?; + + let mut reader = BerTlvReader::new(resp.data()); + reader.enter_constructed(TLV_KEY_TEMPLATE)?; + let raw_nsk = reader.read_primitive(TLV_LEE_NSK)?; + let raw_vsk = reader.read_primitive(TLV_LEE_VSK)?; let nsk = zeroizing_fixed_bytes::<32>("nullifier secret key", Zeroizing::new(raw_nsk))?; let vsk = zeroizing_fixed_bytes::<64>("view secret key", Zeroizing::new(raw_vsk))?; @@ -205,46 +294,54 @@ impl KeycardWallet { pub fn get_private_keys_for_path_with_connect( pin: &str, path: &str, - ) -> PyResult { - Python::attach(|py| { - python_path::add_python_path(py)?; - let wallet = Self::new(py)?; - wallet.connect(py, pin)?; - let result = wallet.get_private_keys_for_path(py, path); - drop(wallet.disconnect(py)); - result - }) + ) -> Result { + let mut wallet = Self::new()?; + wallet.connect(pin)?; + let result = wallet.get_private_keys_for_path(path); + drop(wallet.disconnect()); + result } } -/// The keycard Python library strips leading zeros from S when S < 2^(8k) for some k. -/// Left-pad S back to 32 bytes so the full signature is always 64 bytes (R || S). -#[expect( - clippy::arithmetic_side_effects, - reason = "64 - s_stripped.len() is safe: s_stripped.len() ≤ 31 because py_signature.len() is in [32, 63]" -)] -fn normalize_keycard_signature(py_signature: Vec) -> PyResult<[u8; 64]> { - if py_signature.len() < 64 { - if py_signature.len() < 32 { - return Err(PyErr::new::(format!( - "signature from keycard too short: {} bytes", - py_signature.len() - ))); - } - let s_stripped = &py_signature[32..]; - let mut padded = [0_u8; 64]; - padded[..32].copy_from_slice(&py_signature[..32]); - padded[(64 - s_stripped.len())..].copy_from_slice(s_stripped); - Ok(padded) - } else { - py_signature.try_into().map_err(|vec: Vec| { - PyErr::new::(format!( - "Invalid signature length: expected 64 bytes, got {} (bytes: {:02x?})", - vec.len(), - vec - )) - }) +fn generate_puk() -> String { + let mut rng = rand::rngs::OsRng; + std::iter::repeat_with(|| char::from(rng.gen_range(b'0'..=b'9'))) + .take(12) + .collect() +} + +fn pairing_password() -> String { + std::env::var("KEYCARD_PAIRING_PASSWORD").unwrap_or_else(|_| DEFAULT_PAIRING_PASSWORD.to_owned()) +} + +/// Parses a BIP340 Schnorr signature from a LEE `SIGN` response. +/// +/// Confirmed against real hardware: `TLV_SIGNATURE_TEMPLATE` (0xA0) contains the usual +/// `TLV_PUB_KEY` (0x80, 65 bytes, unused here — the caller fetches the pubkey separately via +/// `export_key`) followed by `TLV_LEE_RAW_SIGNATURE` (0x88, 64 bytes: `r || s` with no ASN.1/DER +/// wrapping). `keycard-rs`'s own `RecoverableSignature` parser doesn't apply — it's ECDSA-only +/// (attempts point recovery, which Schnorr doesn't have). +fn parse_schnorr_signature(data: &[u8]) -> Result<[u8; 64], KeycardWalletError> { + parse_schnorr_signature_inner(data).map_err(|e| { + KeycardWalletError::InvalidKeyMaterial(format!( + "failed to parse schnorr signature response ({e}); raw response bytes: {data:02x?}" + )) + }) +} + +fn parse_schnorr_signature_inner(data: &[u8]) -> Result<[u8; 64], KeycardWalletError> { + let mut reader = BerTlvReader::new(data); + reader.enter_constructed(TLV_SIGNATURE_TEMPLATE)?; + if reader.next_tag_is(TLV_PUB_KEY) { + reader.read_primitive(TLV_PUB_KEY)?; } + let sig = reader.read_primitive(TLV_LEE_RAW_SIGNATURE)?; + sig.try_into().map_err(|v: Vec| { + KeycardWalletError::InvalidKeyMaterial(format!( + "expected a 64-byte raw schnorr signature, got {} bytes", + v.len() + )) + }) } #[expect( @@ -254,9 +351,9 @@ fn normalize_keycard_signature(py_signature: Vec) -> PyResult<[u8; 64]> { fn zeroizing_fixed_bytes( label: &str, raw: Zeroizing>, -) -> PyResult> { +) -> Result, KeycardWalletError> { if raw.len() != N { - return Err(PyErr::new::(format!( + return Err(KeycardWalletError::InvalidKeyMaterial(format!( "expected {N}-byte {label} from keycard, got {} bytes", raw.len() ))); diff --git a/lez/keycard_wallet/src/python_path.rs b/lez/keycard_wallet/src/python_path.rs deleted file mode 100644 index 61196ad5..00000000 --- a/lez/keycard_wallet/src/python_path.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::{env, path::PathBuf}; - -use pyo3::{prelude::*, types::PyList}; - -fn collect_python_paths() -> Vec { - let current_dir = env::current_dir().expect("Failed to get current working directory"); - - let python_base = env::var("VIRTUAL_ENV") - .ok() - .and_then(|v| PathBuf::from(v).parent().map(PathBuf::from)) - .unwrap_or_else(|| current_dir.clone()); - - let mut paths = vec![ - python_base - .join("lez") - .join("keycard_wallet") - .join("python"), - python_base - .join("lez") - .join("keycard_wallet") - .join("python") - .join("keycard-py"), - ]; - - // pyo3's embedded interpreter does not inherit sys.path from the shell, - // so venv site-packages must be added explicitly. - if let Ok(venv) = env::var("VIRTUAL_ENV") { - let lib = PathBuf::from(&venv).join("lib"); - if let Ok(entries) = std::fs::read_dir(&lib) { - for entry in entries.flatten() { - let site_packages = entry.path().join("site-packages"); - if site_packages.exists() { - paths.push(site_packages); - } - } - } - } - - paths -} - -/// Adds the project's `python/` directory and venv site-packages to Python's sys.path. -pub fn add_python_path(py: Python<'_>) -> PyResult<()> { - let paths = collect_python_paths(); - - for path in &paths { - if !path.exists() { - log::info!("Warning: Python path does not exist: {}", path.display()); - } - } - - let sys = PyModule::import(py, "sys")?; - let binding = sys.getattr("path")?; - let sys_path = binding.cast::()?; - - for path in &paths { - let path_str = path.to_str().expect("Invalid path"); - - let already_present = sys_path - .iter() - .any(|p| p.extract::<&str>().is_ok_and(|s| s == path_str)); - - if !already_present { - sys_path.insert(0, path_str)?; - } - } - - Ok(()) -} diff --git a/lez/keycard_wallet/tests/force_unpower.py b/lez/keycard_wallet/tests/force_unpower.py index 427d2028..c6789015 100755 --- a/lez/keycard_wallet/tests/force_unpower.py +++ b/lez/keycard_wallet/tests/force_unpower.py @@ -7,8 +7,9 @@ the power-loss condition reported on some USB reader/driver combinations. Either: - pcscd re-powers the card on the next SCardConnect, so wallet commands will succeed without triggering the retry path. -- the card stays unpowered, triggering TransportError -and exercising the retry wrapper in pair() / setup_communication_with_pairing(). +- the card stays unpowered, triggering a PC/SC transport error +(keycard_rs::Error::Io) and exercising the reconnect-and-retry wrapper in +KeycardWallet::pair() / KeycardWallet::setup_communication_with_pairing(). """ import sys from smartcard.scard import ( diff --git a/lez/keycard_wallet/tests/keycard_test_3.sh b/lez/keycard_wallet/tests/keycard_test_3.sh index d80e2aca..f4aa57fe 100755 --- a/lez/keycard_wallet/tests/keycard_test_3.sh +++ b/lez/keycard_wallet/tests/keycard_test_3.sh @@ -5,8 +5,6 @@ # 1. Run wallet_with_keycard.sh once to install dependencies. # 2. Keycard reader inserted with card loaded (wallet keycard load has been run). -source venv/bin/activate - cargo install --path lez/wallet --force --features keycard-debug export KEYCARD_PIN=111111 diff --git a/lez/keycard_wallet/tests/keycard_tests.sh b/lez/keycard_wallet/tests/keycard_tests.sh index dfa30461..3faa48c9 100755 --- a/lez/keycard_wallet/tests/keycard_tests.sh +++ b/lez/keycard_wallet/tests/keycard_tests.sh @@ -1,8 +1,6 @@ #!/bin/bash # Run wallet_with_keycard.sh first -source venv/bin/activate # Load the appropriate virtual environment - export KEYCARD_PIN=111111 # Tests wallet keycard available diff --git a/lez/keycard_wallet/tests/keycard_tests_2.sh b/lez/keycard_wallet/tests/keycard_tests_2.sh index cbff19fe..9804fecb 100755 --- a/lez/keycard_wallet/tests/keycard_tests_2.sh +++ b/lez/keycard_wallet/tests/keycard_tests_2.sh @@ -23,7 +23,6 @@ # amm-lee-fund → public LEE holding used to seed the AMM pool # (LP holding for amm new is created fresh each run — no persistent label) -source venv/bin/activate export KEYCARD_PIN=111111 # ============================================================================= diff --git a/lez/keycard_wallet/wallet_with_keycard.sh b/lez/keycard_wallet/wallet_with_keycard.sh index 7a43bb57..f77fd790 100755 --- a/lez/keycard_wallet/wallet_with_keycard.sh +++ b/lez/keycard_wallet/wallet_with_keycard.sh @@ -2,11 +2,9 @@ cargo install --path lez/wallet --force -# Install appropriate version of `keycard-py`. -git clone --branch lee-schnorr --single-branch https://github.com/bitgamma/keycard-py.git lez/keycard_wallet/python/keycard-py - -# Set up virtual environment. +# `pyscard` is only needed by tests/force_unpower.py, a test-only helper that simulates a card +# power loss via PC/SC — the wallet CLI itself is pure Rust and talks to the card directly via +# `keycard-rs`. python3 -m venv venv source venv/bin/activate -pip install pyscard mnemonic ecdsa pyaes -pip install -e lez/keycard_wallet/python/keycard-py \ No newline at end of file +pip install pyscard \ No newline at end of file diff --git a/lez/wallet/Cargo.toml b/lez/wallet/Cargo.toml index 974d6a71..c03c1505 100644 --- a/lez/wallet/Cargo.toml +++ b/lez/wallet/Cargo.toml @@ -25,7 +25,6 @@ system_accounts.workspace = true associated_token_account_core.workspace = true bip39.workspace = true -pyo3.workspace = true rpassword = "7" zeroize.workspace = true diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index dae8fd31..0be192c5 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -2,7 +2,7 @@ use core::fmt; use anyhow::Result; use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; -use keycard_wallet::{KeycardWallet, python_path}; +use keycard_wallet::KeycardWallet; use lee::{AccountId, PrivateKey, PublicKey, Signature}; use lee_core::{ CommitmentSetDigest, DUMMY_COMMITMENT_HASH, Identifier, InputAccountIdentity, MembershipProof, @@ -213,14 +213,7 @@ impl AccountManager { if pin.is_none() { pin = Some( crate::helperfunctions::read_pin() - .map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >( - e.to_string() - )) - })? + .map_err(ExecutionFailureKind::SignError)? .as_str() .to_owned(), ); @@ -438,17 +431,11 @@ impl AccountManager { .collect(); if let Some(pin) = self.pin.clone() { - pyo3::Python::attach(|py| -> pyo3::PyResult<()> { - python_path::add_python_path(py)?; - let wallet = KeycardWallet::new(py)?; - wallet.connect(py, &pin)?; - for path in keycard_paths { - sigs.push(wallet.sign_message_for_path(py, path, &message_hash)?); - } - let _res = wallet.close_session(py); - Ok(()) - }) - .map_err(anyhow::Error::from)?; + let mut wallet = KeycardWallet::new()?; + wallet.connect(&pin)?; + for path in keycard_paths { + sigs.push(wallet.sign_message_for_path(path, &message_hash)?); + } } Ok(sigs) diff --git a/lez/wallet/src/cli/keycard.rs b/lez/wallet/src/cli/keycard.rs index 8c42bfd1..65ad0b77 100644 --- a/lez/wallet/src/cli/keycard.rs +++ b/lez/wallet/src/cli/keycard.rs @@ -5,8 +5,7 @@ use anyhow::Result; use clap::Subcommand; -use keycard_wallet::{KeycardWallet, clear_pairing, python_path}; -use pyo3::prelude::*; +use keycard_wallet::{KeycardWallet, clear_pairing}; use crate::{ WalletCore, @@ -39,22 +38,11 @@ pub enum KeycardSubcommand { impl KeycardSubcommand { fn handle_available(_wallet_core: &mut WalletCore) -> SubcommandReturnValue { - Python::attach(|py| { - python_path::add_python_path(py) - .expect("`wallet::keycard::available`: unable to setup python path"); - - let wallet = KeycardWallet::new(py) - .expect("`wallet::keycard::available`: invalid data received for pin"); - let available = wallet - .is_unpaired_keycard_available(py) - .expect("`wallet::keycard::available`: received invalid data from Keycard wrapper"); - - if available { - println!("\u{2705} Keycard is available."); - } else { - println!("\u{274c} Keycard is not available."); - } - }); + if KeycardWallet::is_unpaired_keycard_available() { + println!("\u{2705} Keycard is available."); + } else { + println!("\u{274c} Keycard is not available."); + } SubcommandReturnValue::Empty } @@ -62,20 +50,9 @@ impl KeycardSubcommand { fn handle_connect(_wallet_core: &mut WalletCore) -> Result { let pin = read_pin()?; - Python::attach(|py| { - python_path::add_python_path(py) - .expect("`wallet::keycard::connect`: unable to setup python path"); - - let wallet = KeycardWallet::new(py) - .expect("`wallet::keycard::connect`: invalid keycard wallet provided"); - - wallet - .connect(py, &pin) - .expect("`wallet::keycard::connect`: failed to connect to keycard"); - - println!("\u{2705} Keycard paired and ready."); - drop(wallet.close_session(py)); - }); + let mut wallet = KeycardWallet::new()?; + wallet.connect(&pin)?; + println!("\u{2705} Keycard paired and ready."); Ok(SubcommandReturnValue::Empty) } @@ -83,24 +60,12 @@ impl KeycardSubcommand { fn handle_disconnect(_wallet_core: &mut WalletCore) -> Result { let pin = read_pin()?; - Python::attach(|py| { - python_path::add_python_path(py) - .expect("`wallet::keycard::disconnect`: unable to setup python path"); + let mut wallet = KeycardWallet::new()?; + wallet.connect(&pin)?; + wallet.disconnect()?; - let wallet = KeycardWallet::new(py) - .expect("`wallet::keycard::disconnect`: invalid keycard wallet provided"); - - wallet - .connect(py, &pin) - .expect("`wallet::keycard::disconnect`: failed to open session"); - - wallet - .disconnect(py) - .expect("`wallet::keycard::disconnect`: failed to unpair keycard"); - - clear_pairing(); - println!("\u{2705} Keycard unpaired and pairing cleared."); - }); + clear_pairing(); + println!("\u{2705} Keycard unpaired and pairing cleared."); Ok(SubcommandReturnValue::Empty) } @@ -108,22 +73,13 @@ impl KeycardSubcommand { fn handle_init(_wallet_core: &mut WalletCore) -> Result { let pin = read_pin()?; - Python::attach(|py| { - python_path::add_python_path(py) - .expect("`wallet::keycard::init`: unable to setup python path"); + let mut wallet = KeycardWallet::new()?; + let puk = wallet.initialize(&pin)?; - let wallet = KeycardWallet::new(py) - .expect("`wallet::keycard::init`: invalid keycard wallet provided"); - - let initialized = wallet - .initialize(py, &pin) - .expect("`wallet::keycard::init`: failed to initialize keycard"); - - if initialized { - clear_pairing(); - println!("\u{2705} Keycard initialized successfully."); - } - }); + clear_pairing(); + println!("Keycard PUK: {puk}"); + println!("Record this PUK and store it somewhere safe. It cannot be recovered."); + println!("\u{2705} Keycard initialized successfully."); Ok(SubcommandReturnValue::Empty) } @@ -132,25 +88,15 @@ impl KeycardSubcommand { let pin = read_pin()?; let mnemonic = read_mnemonic()?; - Python::attach(|py| { - python_path::add_python_path(py) - .expect("`wallet::keycard::load`: unable to setup python path"); + let mut wallet = KeycardWallet::new()?; + wallet.connect(&pin)?; + println!("\u{2705} Keycard is now connected to wallet."); - let wallet = KeycardWallet::new(py) - .expect("`wallet::keycard::load`: invalid keycard wallet provided"); - - wallet - .connect(py, &pin) - .expect("`wallet::keycard::load`: failed to connect to keycard"); - - println!("\u{2705} Keycard is now connected to wallet."); - if wallet.load_mnemonic(py, &mnemonic).is_ok() { - println!("\u{2705} Mnemonic phrase loaded successfully."); - } else { - println!("\u{274c} Failed to load mnemonic phrase."); - } - drop(wallet.close_session(py)); - }); + if wallet.load_mnemonic(&mnemonic).is_ok() { + println!("\u{2705} Mnemonic phrase loaded successfully."); + } else { + println!("\u{274c} Failed to load mnemonic phrase."); + } Ok(SubcommandReturnValue::Empty) } diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 923f55ae..2306511f 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -79,8 +79,6 @@ pub enum ExecutionFailureKind { TransactionBuildError(#[from] lee::error::LeeError), #[error("Failed to sign transaction: {0}")] SignError(anyhow::Error), - #[error(transparent)] - KeycardError(#[from] pyo3::PyErr), } #[expect(clippy::partial_pub_fields, reason = "TODO: make all fields private")] diff --git a/lez/wallet/src/signing.rs b/lez/wallet/src/signing.rs index 505dedd9..2ee67ae7 100644 --- a/lez/wallet/src/signing.rs +++ b/lez/wallet/src/signing.rs @@ -1,5 +1,4 @@ -use keycard_wallet::{KeycardWallet, python_path}; -use pyo3::Python; +use keycard_wallet::{KeycardWallet, KeycardWalletError}; /// Lazily opens and reuses a single Keycard session for all keycard signers in one transaction. pub struct KeycardSessionContext { @@ -15,19 +14,18 @@ impl KeycardSessionContext { } } - pub fn get_or_connect(&mut self, py: Python<'_>) -> pyo3::PyResult<&KeycardWallet> { + pub fn get_or_connect(&mut self) -> Result<&mut KeycardWallet, KeycardWalletError> { if self.wallet.is_none() { - python_path::add_python_path(py)?; - let wallet = KeycardWallet::new(py)?; - wallet.connect(py, &self.pin)?; + let mut wallet = KeycardWallet::new()?; + wallet.connect(&self.pin)?; self.wallet = Some(wallet); } - Ok(self.wallet.as_ref().expect("wallet was just inserted")) + Ok(self.wallet.as_mut().expect("wallet was just inserted")) } - pub fn close(self, py: Python<'_>) { - if let Some(w) = self.wallet { - let _res = w.close_session(py); + pub fn close(mut self) { + if let Some(wallet) = self.wallet.as_mut() { + drop(wallet.disconnect()); } } }