From 096522ebb98704b54ba1707155fe9ccbf2f59157 Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:23:33 -0400 Subject: [PATCH] fixes --- python/keycard_test.py | 15 +++ python/keycard_wallet.py | 181 +++++++++++-------------------- wallet/src/cli/keycard.rs | 113 ------------------- wallet/src/cli/keycard_wallet.rs | 57 +++------- 4 files changed, 94 insertions(+), 272 deletions(-) create mode 100644 python/keycard_test.py diff --git a/python/keycard_test.py b/python/keycard_test.py new file mode 100644 index 00000000..ca66aa6a --- /dev/null +++ b/python/keycard_test.py @@ -0,0 +1,15 @@ +import keycard_wallet as keycard_wallet +import time # For testing + +pin = '111111' + +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()) + +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 index 146cba00..df968aec 100644 --- a/python/keycard_wallet.py +++ b/python/keycard_wallet.py @@ -10,7 +10,10 @@ 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): @@ -46,144 +49,88 @@ class KeycardWallet: self.card.select() if not self.card.is_initialized: - # TODO: need to be able to initialize a card. return False if self.pairing_index is None: - pairing_index, pairing_key = self.card.pair(password) #Testing + 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) + self.card.verify_pin(pin) + return True except Exception as e: print(f"Error: {e}") return False - """ - # Needs to be more robust to handle card removal and reinsertion - def is_selected_card_available(self) -> bool: - if self.transport.connection is None: - return False - + def load_mnemonic(self, mnemonic = DEFAULT_MNEMONIC, passphrase = DEFAULT_PASSPHRASE) -> bool: try: - #TODO: fix this up Try a lightweight operation - # Card is present - self.card.send_apdu(cla=0x00, ins=0xA4, p1=0x04, p2=0x00, data=b'') - # return True - except Exception: - return False - - # TODO: attempt to prevent a new card from being inserted - return self.card.is_selected + # Convert mnemonic to seed + mnemo = Mnemonic("english") + seed = mnemo.to_seed(mnemonic, passphrase) - """ + print(f"PIN verified: {self.card.is_pin_verified}") + print(f"Secure channel open: {self.card.is_secure_channel_open}") + print(f"Card initialized: {self.card.status.get('initialized', False)}") + print(f"Seed length: {len(seed)}") + + # Load the LEE seed onto the card + result = self.card.load_key( + key_type = constants.LoadKeyType.BIP39_SEED, + bip39_seed = seed + ) - # Wrapped - def disconnect(self) -> bool: - try: - self.card.unpair(self.pairing_index) - self.pairing_index = None - self.pairing_key = None return True except Exception as e: print(f"Error during disconnect: {e}") return False - # TODO: add path? - # Wrapped - def get_public_signing_key(self): - uncompressed_pub_key = self.card.export_current_key(public_only=True).public_key - - # Convert to VerifyingKey object - vk = VerifyingKey.from_string(uncompressed_pub_key, curve=SECP256k1) - - return vk.to_string("compressed")[1:] - - """ - # TODO: don't think this possible; blocked by firmware - def get_private_signing_key(self): + def disconnect(self) -> bool: try: - exported = self.card.export_current_key(public_only=False) - print(f"Exported key: {exported}") - print(f"Public key: {exported.public_key.hex() if exported.public_key else 'None'}") - print(f"Private key: {exported.private_key.hex() if exported.private_key else 'None'}") - print(f"Chain code: {exported.chain_code.hex() if exported.chain_code else 'None'}") + if not self.card.is_secure_channel_open: + return None - if exported.private_key is None: - raise ValueError("No private key returned - key may not be loaded on card") - - return exported.private_key - except Exception as e: - print(f"Error exporting key: {e}") - raise - """ - # TODO: delete this function - def debug_key_export(self): - """Debug why key export fails with SW=6985""" - - # 1. Check if a key exists - try: - status = self.card.status - print(f"Status: {status}") - except Exception as e: - print(f"Cannot get status: {e}") - - # 2. Try public key export first - try: - exported = self.card.export_current_key(public_only=True) - print(f"Public key export: {exported.public_key.hex() if exported.public_key else 'None'}") - except Exception as e: - print(f"Public key export failed: {e}") - - # 3. Check if key needs to be generated - try: - key_uid = self. card.generate_key() - print(f"Generated key UID: {key_uid.hex()}") - except Exception as e: - print(f"Key generation failed: {e}") - - # 4. Try private export again - try: - exported = self.card.export_current_key(public_only=False) - if exported.private_key: - print(f"Private key: {exported.private_key.hex()}") - else: - print("Private key is None - key may not allow export") - except Exception as e: - print(f"Private key export failed: {e}") - - #TODO: check well formed? - # Wrapped - def change_path(self, path): - self.card.derive_key(path) + self.card.unpair(self.pairing_index) + self.pairing_index = None + self.pairing_key = None - # Message must be 32 bytes - # TODO: rename to current_path - # Wrapped - def sign_message_current_key(self, message = b"TestMessageMustBe32Bytes!\x00\x00\x00\x00\x00\x00\x00"): - # Message must be sent bytes - return self.card.sign(message, constants.SigningAlgorithm.SCHNORR_BIP340) - - - # Does not update the path - # Wrapped - def sign_message_with_path(self, path, message = b"TestMessageMustBe32Bytes!\x00\x00\x00\x00\x00\x00\x00"): - # must be sent bytes - return self.card.sign_with_path(message, path, False, constants.SigningAlgorithm.SCHNORR_BIP340) - - # Wrapped - def remove_account_keys(self): - self.card.remove_key() - - # TODO: update to accept a different language? - def load_account_keys(self, mnemonic) : - mnemo = Mnemonic("english") - seed = mnemo.to_seed(mnemonic, passphrase="") + return True + except Exception as e: + print(f"Error during unpair: {e}") + return False - # Load the seed onto the card - result = self.card.load_key( - key_type= constants.LoadKeyType.BIP39_SEED, - lee_seed=seed - ) \ No newline at end of file + def get_public_key_for_path(self, path: str = "m/44'/60'/0'/0/0") -> str | 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 + ) + + return public_key.public_key.hex() + + 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") -> str | 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, + make_current = False + ) + + return signature.signature.hex() + + except Exception as e: + print(f"Error signing message: {e}") + return None \ No newline at end of file diff --git a/wallet/src/cli/keycard.rs b/wallet/src/cli/keycard.rs index c2f2b034..bc76763f 100644 --- a/wallet/src/cli/keycard.rs +++ b/wallet/src/cli/keycard.rs @@ -25,121 +25,8 @@ pub enum KeycardSubcommand { )] mnemonic: Option, }, - Remove, } -/// Represents generic register CLI subcommand. -/* -#[derive(Subcommand, Debug, Clone)] -pub enum NewSubcommand { - /// Register new public account. - Public { - #[arg(long)] - /// Chain index of a parent node. - cci: Option, - #[arg(short, long)] - /// Label to assign to the new account. - label: Option, - }, - /// Register new private account. - Private { - #[arg(long)] - /// Chain index of a parent node. - cci: Option, - #[arg(short, long)] - /// Label to assign to the new account. - label: Option, - }, -} -*/ -/* -impl WalletSubcommand for NewSubcommand { - async fn handle_subcommand( - self, - wallet_core: &mut WalletCore, - ) -> Result { - match self { - Self::Public { cci, label } => { - if let Some(label) = &label - && wallet_core - .storage - .labels - .values() - .any(|l| l.to_string() == *label) - { - anyhow::bail!("Label '{label}' is already in use by another account"); - } - - let (account_id, chain_index) = wallet_core.create_new_account_public(cci); - - let private_key = wallet_core - .storage - .user_data - .get_pub_account_signing_key(account_id) - .unwrap(); - - let public_key = PublicKey::new_from_private_key(private_key); - - if let Some(label) = label { - wallet_core - .storage - .labels - .insert(account_id.to_string(), Label::new(label)); - } - - println!( - "Generated new account with account_id Public/{account_id} at path {chain_index}" - ); - println!("With pk {}", hex::encode(public_key.value())); - - wallet_core.store_persistent_data().await?; - - Ok(SubcommandReturnValue::RegisterAccount { account_id }) - } - Self::Private { cci, label } => { - if let Some(label) = &label - && wallet_core - .storage - .labels - .values() - .any(|l| l.to_string() == *label) - { - anyhow::bail!("Label '{label}' is already in use by another account"); - } - - let (account_id, chain_index) = wallet_core.create_new_account_private(cci); - - if let Some(label) = label { - wallet_core - .storage - .labels - .insert(account_id.to_string(), Label::new(label)); - } - - let (key, _) = wallet_core - .storage - .user_data - .get_private_account(account_id) - .unwrap(); - - println!( - "Generated new account with account_id Private/{account_id} at path {chain_index}", - ); - println!("With npk {}", hex::encode(key.nullifier_public_key.0)); - println!( - "With vpk {}", - hex::encode(key.viewing_public_key.to_bytes()) - ); - - wallet_core.store_persistent_data().await?; - - Ok(SubcommandReturnValue::RegisterAccount { account_id }) - } - } - } -} - */ - impl WalletSubcommand for KeycardSubcommand { #[expect(clippy::cognitive_complexity, reason = "TODO: fix later")] async fn handle_subcommand( diff --git a/wallet/src/cli/keycard_wallet.rs b/wallet/src/cli/keycard_wallet.rs index 20590d25..f70495c0 100644 --- a/wallet/src/cli/keycard_wallet.rs +++ b/wallet/src/cli/keycard_wallet.rs @@ -23,7 +23,7 @@ impl KeycardWallet { /// Calls Python: is_unpaired_keycard_available() pub fn is_unpaired_keycard_available(&self, py: Python) -> PyResult { self.instance - .bind(py) // replaces as_ref(py) + .bind(py) .call_method0("is_unpaired_keycard_available")? .extract() } @@ -44,22 +44,6 @@ impl KeycardWallet { .extract() } - pub fn get_public_signing_key(&self, py: Python) -> PyResult<[u8; 32]> { - self.instance - .bind(py) - .call_method0("get_public_signing_key")? - .extract() - } - - pub fn derive_path(&self, py: Python, path: Vec) -> PyResult<()> { - let path = Self::convert_path_to_string(path); - - self.instance - .bind(py) - .call_method1("change_path", (path,))?; - Ok(()) - } - fn convert_path_to_string(path: Vec) -> String { format!( "m/{}", @@ -70,23 +54,19 @@ impl KeycardWallet { ) } - pub fn sign_message_current_key(&self, py: Python, message: &[u8; 32]) -> PyResult<[u8; 64]> { - let py_message = pyo3::types::PyBytes::new_bound(py, message); - - let py_signature: Vec = self.instance + pub fn get_public_key_for_path( + &self, + py: Python, + path: &str, + ) -> PyResult> { + let public_key: Vec = self.instance .bind(py) - .call_method1("sign_message_current_key", (py_message,))? - .getattr("signature")? // or "bytes", "data", "value", etc. + .call_method1("get_public_key_for_path", (py_message, path))? + .getattr("public_key")? .extract()?; - let signature: [u8; 64] = py_signature - .try_into() - .map_err(|_| PyErr::new::( - "Expected signature of exactly 64 bytes" - ))?; - - Ok(signature) - } + Ok(Some(public_key.bytes())) + } pub fn sign_message_with_path(&self, py: Python, path: Vec, message: &[u8; 32]) -> PyResult<[u8; 64]> { let py_message = pyo3::types::PyBytes::new_bound(py, message); @@ -94,8 +74,8 @@ impl KeycardWallet { let py_signature: Vec = self.instance .bind(py) - .call_method1("sign_message_with_path", (path, py_message))? - .getattr("signature")? // or "bytes", "data", "value", etc. + .call_method1("sign_message_with_path", (py_message, path))? + .getattr("signature")? .extract()?; let signature: [u8; 64] = py_signature @@ -107,17 +87,10 @@ impl KeycardWallet { Ok(signature) } - pub fn remove_account_keys(&self, py: Python) -> PyResult<()> { + pub fn load_mnemonic(&self, py: Python, mnemonic: &str) -> PyResult<()> { self.instance .bind(py) - .call_method0("remove_account_keys")?; - Ok(()) - } - - pub fn load_account_keys(&self, py: Python, mnemonic: &str) -> PyResult<()> { - self.instance - .bind(py) - .call_method1("load_account_keys", (mnemonic,))?; + .call_method1("load_mnemonic", (mnemonic,))?; Ok(()) } } \ No newline at end of file