logos-execution-zone/python/keycard_wallet.py

125 lines
3.8 KiB
Python
Raw Permalink Normal View History

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
DEFAULT_PAIRING_PASSWORD = "KeycardDefaultPairing"
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
2026-04-30 19:02:33 -04:00
def setup_communication(self, pin: str, password = DEFAULT_PAIRING_PASSWORD) -> bool:
try:
self.card.select()
if not self.card.is_initialized:
return False
2026-04-30 19:02:33 -04:00
pairing_index, pairing_key = self.card.pair(password)
self.pairing_index = pairing_index
2026-04-22 21:23:33 -04:00
self.card.open_secure_channel(pairing_index, pairing_key)
2026-04-22 21:23:33 -04:00
self.card.verify_pin(pin)
return True
except Exception as e:
print(f"Error: {e}")
return False
2026-04-30 19:02:33 -04:00
def load_mnemonic(self, mnemonic: str) -> bool:
try:
2026-04-22 21:23:33 -04:00
# Convert mnemonic to seed
mnemo = Mnemonic("english")
2026-04-30 19:02:33 -04:00
seed = mnemo.to_seed(mnemonic)
2026-04-22 21:23:33 -04:00
# Load the LEE seed onto the card
result = self.card.load_key(
key_type = constants.LoadKeyType.LEE_SEED,
2026-04-23 09:47:09 -04:00
lee_seed = seed
2026-04-22 21:23:33 -04:00
)
2026-04-30 19:02:33 -04:00
#TODO: this appears to be the issue.
2026-04-22 21:23:33 -04:00
return True
except Exception as e:
print(f"Error during disconnect: {e}")
return False
def disconnect(self) -> bool:
try:
2026-04-22 21:23:33 -04:00
if not self.card.is_secure_channel_open:
return None
self.card.unpair(self.pairing_index)
2026-04-22 21:23:33 -04:00
return True
except Exception as e:
2026-04-22 21:23:33 -04:00
print(f"Error during unpair: {e}")
return False
2026-04-22 21:23:33 -04:00
2026-04-23 17:45:43 -04:00
def get_public_key_for_path(self, path: str = "m/44'/60'/0'/0/0") -> bytes | None:
2026-04-22 21:23:33 -04:00
try:
if not self.card.is_secure_channel_open or not self.card.is_pin_verified:
return None
2026-04-22 21:23:33 -04:00
public_key = self.card.export_key(
derivation_option = constants.DerivationOption.DERIVE,
public_only = True,
keypath = path
)
2026-04-23 17:45:43 -04:00
public_key = public_key.public_key
public_key = VerifyingKey.from_string(public_key[1:], curve=SECP256k1)
public_key = public_key.to_string("compressed")[1:]
2026-04-23 17:45:43 -04:00
return public_key
2026-04-22 21:23:33 -04:00
except Exception as e:
print(f"Error getting public key: {e}")
return None
2026-04-30 19:02:33 -04:00
def sign_message_for_path(self, message: bytes, path: str = "m/44'/60'/0'/0/0") -> bytes | None:
2026-04-22 21:23:33 -04:00
try:
if not self.card.is_secure_channel_open or not self.card.is_pin_verified:
return None
2026-04-22 21:23:33 -04:00
signature = self.card.sign_with_path(
digest = message,
2026-04-23 09:47:09 -04:00
path = path,
algorithm = constants.SigningAlgorithm.SCHNORR_BIP340,
2026-04-22 21:23:33 -04:00
make_current = False
)
2026-04-23 17:45:43 -04:00
return signature.signature
2026-04-22 21:23:33 -04:00
except Exception as e:
print(f"Error signing message: {e}")
return None