mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-07-09 15:29:34 +00:00
Updated to v2 secure communication
This commit is contained in:
parent
763464d27b
commit
42b53c6907
4
.gitignore
vendored
4
.gitignore
vendored
@ -17,9 +17,5 @@ result
|
||||
wallet-ffi/wallet_ffi.h
|
||||
bedrock_signing_key
|
||||
integration_tests/configs/debug/
|
||||
venv/
|
||||
|
||||
keycard_wallet/python/__pycache__/
|
||||
keycard_wallet/python/keycard-py/
|
||||
|
||||
.DS_Store
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -4674,7 +4674,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "keycard-rs"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/keycard-tech/keycard-rs?rev=98abc3ff1b26785a5631634d5bf24da54da541bf#98abc3ff1b26785a5631634d5bf24da54da541bf"
|
||||
source = "git+https://github.com/keycard-tech/keycard-rs?rev=52cb832a6334ab84ea0a0e1f42aeabcf8a3b4337#52cb832a6334ab84ea0a0e1f42aeabcf8a3b4337"
|
||||
dependencies = [
|
||||
"aes 0.9.1",
|
||||
"base64 0.21.7",
|
||||
@ -4701,12 +4701,12 @@ name = "keycard_wallet"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bip39",
|
||||
"hex",
|
||||
"keycard-rs",
|
||||
"lee",
|
||||
"log",
|
||||
"pcsc",
|
||||
"rand 0.8.6",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@ -163,13 +163,14 @@ 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" }
|
||||
keycard-rs = { git = "https://github.com/keycard-tech/keycard-rs", rev = "52cb832a6334ab84ea0a0e1f42aeabcf8a3b4337" } # includes the handshake signature-normalization fix (marvin/normalize-signature, merged)
|
||||
|
||||
rocksdb = { version = "0.24.0", default-features = false, features = [
|
||||
"snappy",
|
||||
"bindgen-runtime",
|
||||
] }
|
||||
rand = { version = "0.8.5", features = ["std", "std_rng", "getrandom"] }
|
||||
pcsc = "2"
|
||||
k256 = { version = "0.13.3", features = [
|
||||
"ecdsa-core",
|
||||
"arithmetic",
|
||||
|
||||
@ -6,46 +6,57 @@ This tutorial walks you through using Keycard with Wallet CLI. Keycard is option
|
||||
### Required hardware
|
||||
- Keycard (Blank) - a Keycard, directly, from Keycard.tech cannot (currently) be updated to support LEE.
|
||||
- Smartcard reader
|
||||
- Applets (`math.cap` and `LEE_keycard.cap`). Eventually, both of these applets will be available in separate repos.
|
||||
- `math.cap` is an applet to speed up computations on Keycard; developed by Bitgamma (Keycard-tech team).
|
||||
- `LEE_keycard.cap` is an applet that contains LEE keycard protocol; developed by Bitgamma (Keycard-tech team)
|
||||
|
||||
### Firmware installation
|
||||
Installation:
|
||||
|
||||
1. Install math applet on your keycard; this process only needs to be done once. In the root of repo:
|
||||
```
|
||||
sudo apt-get install -y default-jdk
|
||||
wget https://github.com/martinpaljak/GlobalPlatformPro/releases/download/v25.10.20/gp.jar -P lez/keycard_wallet/keycard_applets
|
||||
cd lez/keycard_wallet/keycard_applets
|
||||
java -jar gp.jar --key c212e073ff8b4bbfaff4de8ab655221f --load math.cap
|
||||
```
|
||||
2. Install `keycard-desktop` from [github](https://github.com/choppu/keycard-desktop)
|
||||
- Keycard Desktop is used to install the LEE key protocol to a blank keycard.
|
||||
- Select (Re)Install Applet and upload the key binary (`lez/keycard_wallet/keycard_applets/LEE_keycard.cap`).
|
||||

|
||||
- **Important:** keycard can only connect with one application at a time; if Keycard-Desktop is using keycard then Wallet CLI cannot access the same keycard, and vice-versa.
|
||||
The applet (LEE key protocol support, on top of standard Status Keycard commands) is no longer vendored in this repo as a pre-built `.cap` — build it directly from source.
|
||||
|
||||
## Wallet with Keycard
|
||||
Keycard functionality is available to Wallet CLI by setting up the following Python virtual environment. The steps below can also be run via `lez/keycard_wallet/wallet_with_keycard.sh`.
|
||||
**Currently this means building from a fork with a pending fix**, not upstream directly. We found and fixed a bug where the card signed LEE Schnorr signatures over the wrong message entirely (see [PR](https://github.com/jonesmarvin8/status-keycard/pull/new/marvin/fix-schnorr-hash-aliasing), not yet merged into [`keycard-tech/status-keycard`](https://github.com/keycard-tech/status-keycard)):
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install pyscard mnemonic ecdsa pyaes
|
||||
pip install -e lez/keycard_wallet/python/keycard-py
|
||||
git clone --recurse-submodules --branch marvin/fix-schnorr-hash-aliasing https://github.com/jonesmarvin8/status-keycard.git
|
||||
cd status-keycard
|
||||
```
|
||||
|
||||
**Important**: Keycard wallet commands only work within the virtual environment.
|
||||
**Once that PR is merged, switch back to cloning `keycard-tech/status-keycard`'s default branch directly** — this fork/branch pointer is temporary.
|
||||
|
||||
The build requires **OpenJDK 11 specifically** (newer JDKs aren't compatible with its Gradle/plugin versions):
|
||||
|
||||
```bash
|
||||
# In the root of LEE repo:
|
||||
source venv/bin/activate
|
||||
sudo apt-get install -y openjdk-11-jdk
|
||||
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
|
||||
```
|
||||
|
||||
Gradle's default heap is too small for this build and will OOM during `buildSrc` compilation; bump it once:
|
||||
|
||||
```bash
|
||||
echo "org.gradle.jvmargs=-Xmx2g" >> gradle.properties
|
||||
```
|
||||
|
||||
Build and install onto a connected, blank card (disconnect all other card readers first):
|
||||
|
||||
```bash
|
||||
./gradlew install
|
||||
```
|
||||
|
||||
This uses the GlobalPlatform default keys (`404142434445464748494a4b4c4d4e4f`) or the Keycard development-card key (`c212e073ff8b4bbfaff4de8ab655221f`) to load the applet.
|
||||
|
||||
### Personalizing the card
|
||||
|
||||
**Installing the applet alone isn't enough to use the card.** A freshly installed applet has no identity certificate, and refuses every command (including `SELECT`) until one is provisioned via `IdentApplet`. There isn't yet a documented, supported personalization path for production cards — this is a known gap, not something to work around ad hoc.
|
||||
|
||||
For development/testing, the project's own JUnit test suite provisions the card against a fixed test CA as a side effect of running any test — run the narrowest one:
|
||||
|
||||
```bash
|
||||
./gradlew test --tests "im.status.keycard.KeycardTest.selectTest"
|
||||
```
|
||||
|
||||
If the card isn't initialized yet, this also initializes it with fixed test values (not secrets): PIN `000000`, alt-PIN `024680`, PUK `012345678901`. To re-personalize a card that's already initialized with different values, wipe it first with `wallet keycard factory-reset --confirm` (see the Commands table below), then re-run the test above, then `wallet keycard init` with whatever PIN you want.
|
||||
|
||||
Because this personalizes against a throwaway test CA rather than a real production CA, the wallet needs to be told to trust it explicitly — see "CA public key override" below. Real, production-personalized cards need no such override.
|
||||
|
||||
**Important:** keycard can only connect with one application at a time; if another tool is using the keycard then Wallet CLI cannot access the same keycard, and vice-versa.
|
||||
|
||||
## PIN entry
|
||||
|
||||
Each Keycard command prompts for a PIN interactively. To avoid re-entering it across multiple commands, export it as an environment variable:
|
||||
@ -60,43 +71,37 @@ Unset it when done:
|
||||
unset KEYCARD_PIN
|
||||
```
|
||||
|
||||
## Pairing password
|
||||
## CA public key override
|
||||
|
||||
The pairing password is used to establish a secure channel between the wallet and the card. It is set permanently on the card during `wallet keycard init` and must match on every subsequent re-pair.
|
||||
Secure Channel V2 verifies the card's identity certificate against a trusted CA public key. `keycard-rs` ships with the production default baked in, so real, production-personalized cards need no configuration at all.
|
||||
|
||||
The default password (`KeycardDefaultPairing`) is [recommended](https://docs.keycard.tech/en/developers/core) for most users. Wallet CLI allows advance users the flexibility to set their own pairing password.
|
||||
|
||||
To use a custom pairing password, set it before `init`:
|
||||
Cards personalized for development/testing (see "Personalizing the card" above) are signed by a different, throwaway CA, so the wallet needs to be told to trust it explicitly:
|
||||
|
||||
```bash
|
||||
# Note: Keep the leading space before this command.
|
||||
# Leading space prevents this command from being stored in shell history
|
||||
# (when HISTCONTROL=ignorespace is enabled).
|
||||
export KEYCARD_PAIRING_PASSWORD=my-custom-password
|
||||
wallet keycard init
|
||||
export KEYCARD_CA_PUBLIC_KEY=025877220aaae6e54a6f974602d5995c0fe24a3ea7ddabd8644bec795b9da00743
|
||||
```
|
||||
|
||||
After a successful initializaation, subsequent commands (`connect`, transfers) use the cached pairing index and key — the pairing password is not needed again until the pairing is cleared.
|
||||
If this is unset (or set, but doesn't match the card's actual certificate), every keycard command will report the card as unavailable — that's the default-CA check correctly rejecting a card it doesn't recognize, not a bug. If it's set to something that isn't 66 hex characters, commands fail immediately with `invalid KEYCARD_CA_PUBLIC_KEY: ...` instead of a confusing "unavailable".
|
||||
|
||||
**Important:** if you initialized with a custom password, `KEYCARD_PAIRING_PASSWORD` must be set in every session where re-pairing can occur (after `disconnect`, or on a new machine). If the env var is missing then wallet CLI will attempt to use the default password. As a result, pairing will fail.
|
||||
|
||||
Unset the pairing password variable when done:
|
||||
Unset it when you're done testing against a dev card:
|
||||
|
||||
```bash
|
||||
unset KEYCARD_PAIRING_PASSWORD
|
||||
unset KEYCARD_CA_PUBLIC_KEY
|
||||
```
|
||||
|
||||
## Keycard Commands
|
||||
|
||||
Keycard uses Secure Channel V2 (applet version >= 4.0) — the wallet authenticates the card via its identity certificate and opens a fresh ECDHE-derived channel every session. There's no pairing step and nothing cached between commands; you'll enter your PIN each time you connect.
|
||||
|
||||
### Keycard
|
||||
|
||||
| Command | Description |
|
||||
|----------------------------------|-----------------------------------------------------------------------|
|
||||
| `wallet keycard available` | Checks whether a Keycard reader and card are accessible |
|
||||
| `wallet keycard init` | Initializes a blank Keycard with a PIN and a generated PUK |
|
||||
| `wallet keycard connect` | Establishes and saves a pairing with the Keycard |
|
||||
| `wallet keycard disconnect` | Unpairs the Keycard and clears the saved pairing |
|
||||
| `wallet keycard connect` | Opens a secure channel with the Keycard and verifies the PIN |
|
||||
| `wallet keycard load` | Loads a mnemonic phrase onto the Keycard |
|
||||
| `wallet keycard factory-reset` | Wipes PIN/PUK/keys back to uninitialized, for re-`init` — **debug builds only** (see below) |
|
||||
| `wallet keycard get-private-keys`| Prints NSK and VSK for a BIP-32 path — **debug builds only** (see below) |
|
||||
|
||||
1. Check keycard availability
|
||||
@ -118,13 +123,13 @@ Record this PUK and store it somewhere safe. It cannot be recovered.
|
||||
✅ Keycard initialized successfully.
|
||||
```
|
||||
|
||||
3. Connect (pair and save pairing for subsequent commands)
|
||||
3. Connect (open a secure channel and verify the PIN)
|
||||
```bash
|
||||
wallet keycard connect
|
||||
|
||||
# Output:
|
||||
Keycard PIN:
|
||||
✅ Keycard paired and ready.
|
||||
✅ Keycard connected and PIN verified.
|
||||
```
|
||||
|
||||
4. Load a mnemonic phrase
|
||||
@ -140,25 +145,22 @@ Keycard PIN:
|
||||
✅ Mnemonic phrase loaded successfully.
|
||||
```
|
||||
|
||||
5. Disconnect (unpair and clear saved pairing)
|
||||
```bash
|
||||
wallet keycard disconnect
|
||||
5. `factory-reset` and `get-private-keys` (**debug builds only**)
|
||||
|
||||
# Output:
|
||||
Keycard PIN:
|
||||
✅ Keycard unpaired and pairing cleared.
|
||||
```
|
||||
|
||||
6. Get private keys for a BIP-32 path (**debug builds only**)
|
||||
|
||||
`get-private-keys` exports the raw NSK and VSK for a derivation path. NSK gates nullifier creation and VSK gates note decryption — either key is sufficient to fully compromise that account's privacy. The command is only available in debug builds and requires `--reveal` to confirm intent.
|
||||
|
||||
First install the wallet with the `keycard-debug` feature:
|
||||
Both require building the wallet with the `keycard-debug` feature:
|
||||
```bash
|
||||
cargo install --path lez/wallet --force --features keycard-debug
|
||||
```
|
||||
|
||||
Then run the command:
|
||||
`factory-reset` wipes the card's PIN, PUK, and loaded keys back to an uninitialized state, so it can be re-`init`ialized — the counterpart to `init`. It does **not** remove the identity certificate, so the card doesn't need re-personalizing afterward. Irreversibly destroys any keys currently on the card, so it requires `--confirm`:
|
||||
```bash
|
||||
wallet keycard factory-reset --confirm
|
||||
|
||||
# Output:
|
||||
✅ Keycard factory-reset. Run `wallet keycard init` to reinitialize it.
|
||||
```
|
||||
|
||||
`get-private-keys` exports the raw NSK and VSK for a derivation path. NSK gates nullifier creation and VSK gates note decryption — either key is sufficient to fully compromise that account's privacy. Requires `--reveal` to confirm intent:
|
||||
```bash
|
||||
wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/0" --reveal
|
||||
|
||||
@ -515,20 +517,4 @@ bash lez/keycard_wallet/tests/keycard_tests.sh
|
||||
bash lez/keycard_wallet/tests/keycard_tests_2.sh
|
||||
bash lez/keycard_wallet/tests/keycard_test_3.sh
|
||||
bash lez/keycard_wallet/tests/keycard_power_recovery_tests.sh
|
||||
```
|
||||
|
||||
## SigningGroup
|
||||
|
||||
`SigningGroup` (`lez/wallet/src/signing.rs`) partitions a transaction's signers into two buckets — local accounts and Keycard accounts. This ensures that Python GIL is only used at most once per transaction, regardless of how many Keycard accounts are involved.
|
||||
|
||||
Local signers are resolved and signed in pure Rust. Keycard signers store only their BIP32 key path; all of them are signed inside a single Python session (`connect` / `close_session`) when `sign_all` is called. The command calls `needs_pin` to decide whether to prompt for a PIN before signing.
|
||||
|
||||
Foreign recipient accounts — those with no local key and no Keycard path — are silently skipped and require neither a signature nor a nonce.
|
||||
|
||||
```
|
||||
SigningGroup {
|
||||
local: [(AccountId, PrivateKey)], // signed in pure Rust
|
||||
keycard: [(AccountId, BIP32Path)], // signed via a single Python/Keycard session
|
||||
}
|
||||
```
|
||||
```
|
||||
@ -11,9 +11,9 @@ workspace = true
|
||||
lee.workspace = true
|
||||
keycard-rs.workspace = true
|
||||
bip39.workspace = true
|
||||
hex.workspace = true
|
||||
log.workspace = true
|
||||
pcsc.workspace = true
|
||||
rand.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
zeroize.workspace = true
|
||||
|
||||
Binary file not shown.
Binary file not shown.
52
lez/keycard_wallet/src/bin/force_unpower.rs
Normal file
52
lez/keycard_wallet/src/bin/force_unpower.rs
Normal file
@ -0,0 +1,52 @@
|
||||
#![expect(
|
||||
clippy::print_stdout,
|
||||
reason = "This is a CLI test helper, printing to stdout is expected and convenient"
|
||||
)]
|
||||
|
||||
//! Forces the card in the first available reader into the unpowered state via PC/SC
|
||||
//! `SCARD_UNPOWER_CARD`. Run immediately before a wallet command to simulate 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 a PC/SC transport error (`keycard_rs::Error::Io`) and
|
||||
//! exercising the reconnect-and-retry wrapper in `KeycardWallet::connect()`.
|
||||
|
||||
fn main() {
|
||||
let context = match pcsc::Context::establish(pcsc::Scope::User) {
|
||||
Ok(context) => context,
|
||||
Err(e) => {
|
||||
println!("force_unpower: failed to establish PC/SC context ({e}), skipping.");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let readers = match context.list_readers_owned() {
|
||||
Ok(readers) => readers,
|
||||
Err(e) => {
|
||||
println!("force_unpower: failed to list readers ({e}), skipping.");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(reader) = readers.first() else {
|
||||
println!("force_unpower: no readers found, skipping.");
|
||||
return;
|
||||
};
|
||||
|
||||
let card = match context.connect(reader, pcsc::ShareMode::Shared, pcsc::Protocols::ANY) {
|
||||
Ok(card) => card,
|
||||
Err(e) => {
|
||||
println!("force_unpower: connect failed ({e}), skipping.");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err((_card, e)) = card.disconnect(pcsc::Disposition::UnpowerCard) {
|
||||
println!("force_unpower: disconnect failed ({e}), skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
println!("force_unpower: card powered down.");
|
||||
}
|
||||
@ -1,28 +1,28 @@
|
||||
use std::{path::PathBuf, str::FromStr as _};
|
||||
use std::str::FromStr as _;
|
||||
|
||||
use keycard_rs::{
|
||||
KeycardCommandSet, PcscChannel,
|
||||
constants::sign_p2,
|
||||
parsing::Bip32KeyPair,
|
||||
secure_channel::Pairing,
|
||||
secure_channel::SecureChannelVersion,
|
||||
tlv::{BerTlvReader, TLV_KEY_TEMPLATE, TLV_PUB_KEY, TLV_SIGNATURE_TEMPLATE},
|
||||
};
|
||||
use lee::{AccountId, PublicKey, Signature};
|
||||
use rand::Rng as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
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`).
|
||||
/// LEE-applet extension tags. `keycard-rs` implements *sending* the LEE commands
|
||||
/// (`load_lee_key`, `export_lee_key`) but never added parsing for their LEE-specific responses,
|
||||
/// so these tag values — owned by the applet, not either client library — have to be hardcoded
|
||||
/// here. Matches `KeycardApplet.TLV_LEE_NSK`/`TLV_LEE_VSK` in `status-keycard`
|
||||
/// (`KeycardApplet.java:97-98`), the applet `LEE_keycard.cap` was almost certainly built from.
|
||||
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> }`.
|
||||
/// `0xA0 { 0x80 <65-byte pubkey>, 0x88 <64-byte r||s> }` — and matches
|
||||
/// `SECP256k1.TLV_RAW_SIGNATURE` in `status-keycard` (`SECP256k1.java:64`).
|
||||
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
|
||||
@ -35,51 +35,60 @@ pub enum KeycardWalletError {
|
||||
Keycard(#[from] keycard_rs::Error),
|
||||
#[error("keycard is already initialized")]
|
||||
AlreadyInitialized,
|
||||
#[error(
|
||||
"this wallet only supports Secure Channel V2 keycards (applet version >= 4.0); detected {0:?}"
|
||||
)]
|
||||
UnsupportedSecureChannel(Option<SecureChannelVersion>),
|
||||
#[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 {
|
||||
pub index: u8,
|
||||
pub key: Vec<u8>,
|
||||
}
|
||||
|
||||
impl KeycardPairingData {
|
||||
const fn is_valid(&self) -> bool {
|
||||
self.key.len() == 32 && self.index <= 4
|
||||
}
|
||||
#[error("invalid KEYCARD_CA_PUBLIC_KEY: {0}")]
|
||||
InvalidCaPublicKey(String),
|
||||
}
|
||||
|
||||
/// Rust wrapper around `keycard-rs`, talking to the LEE-flavored Keycard applet over PC/SC.
|
||||
/// Only Secure Channel V2 cards are supported — see `require_secure_channel_v2`.
|
||||
pub struct KeycardWallet {
|
||||
command_set: KeycardCommandSet,
|
||||
}
|
||||
|
||||
impl KeycardWallet {
|
||||
/// 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.
|
||||
/// that need application info (`initialize`, `connect`, ...) do that themselves.
|
||||
///
|
||||
/// Verifies the card's identity certificate against `keycard-rs`'s default production CA,
|
||||
/// unless overridden via `KEYCARD_CA_PUBLIC_KEY` — see `ca_public_key_override`.
|
||||
pub fn new() -> Result<Self, KeycardWalletError> {
|
||||
let channel = PcscChannel::connect()?;
|
||||
Ok(Self {
|
||||
command_set: KeycardCommandSet::new(channel),
|
||||
command_set: Self::build_command_set(channel)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether a smart card reader and a selectable Keycard are both present.
|
||||
fn build_command_set(channel: PcscChannel) -> Result<KeycardCommandSet, KeycardWalletError> {
|
||||
Ok(match ca_public_key_override()? {
|
||||
Some(ca) => KeycardCommandSet::new_with_ca(channel, ca),
|
||||
None => KeycardCommandSet::new(channel),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether a smart card reader and a selectable, Secure-Channel-V2 Keycard are both
|
||||
/// present.
|
||||
#[must_use]
|
||||
pub fn is_unpaired_keycard_available() -> bool {
|
||||
pub fn is_keycard_available() -> bool {
|
||||
let Ok(channel) = PcscChannel::connect() else {
|
||||
return false;
|
||||
};
|
||||
KeycardCommandSet::new(channel)
|
||||
.select()
|
||||
.is_ok_and(|resp| resp.is_ok())
|
||||
let Ok(mut command_set) = Self::build_command_set(channel) else {
|
||||
return false;
|
||||
};
|
||||
if !command_set.select().is_ok_and(|resp| resp.is_ok()) {
|
||||
return false;
|
||||
}
|
||||
command_set.secure_channel_version() == Some(SecureChannelVersion::V2)
|
||||
}
|
||||
|
||||
fn select(&mut self) -> Result<(), KeycardWalletError> {
|
||||
@ -87,15 +96,17 @@ impl KeycardWallet {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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(())
|
||||
/// Rejects any card that isn't running Secure Channel V2 (older applets, or a card that
|
||||
/// hasn't advertised a secure channel at all). Call right after `select()`.
|
||||
fn require_secure_channel_v2(&self) -> Result<(), KeycardWalletError> {
|
||||
match self.command_set.secure_channel_version() {
|
||||
Some(SecureChannelVersion::V2) => Ok(()),
|
||||
other => Err(KeycardWalletError::UnsupportedSecureChannel(other)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
/// transport-level error (e.g. the card lost power mid-session).
|
||||
fn with_reconnect_on_transport_error<T>(
|
||||
&mut self,
|
||||
op: impl Fn(&mut Self) -> Result<T, KeycardWalletError>,
|
||||
@ -116,6 +127,7 @@ impl KeycardWallet {
|
||||
/// for surfacing the PUK to the operator — it cannot be recovered afterward.
|
||||
pub fn initialize(&mut self, pin: &str) -> Result<String, KeycardWalletError> {
|
||||
self.select()?;
|
||||
self.require_secure_channel_v2()?;
|
||||
let already_initialized = self
|
||||
.command_set
|
||||
.app_info()
|
||||
@ -125,73 +137,37 @@ impl KeycardWallet {
|
||||
return Err(KeycardWalletError::AlreadyInitialized);
|
||||
}
|
||||
|
||||
// V2's INIT has no shared-secret field (confirmed against real hardware and the
|
||||
// applet's own reference command set: a payload containing one is rejected outright) —
|
||||
// pass an empty secret and let the card default the PIN/PUK retry counts.
|
||||
let puk = generate_puk();
|
||||
self.command_set
|
||||
.init(pin, &puk, &pairing_password())?
|
||||
.init_with_secret(pin, None, &puk, &[], 0, 0)?
|
||||
.check_ok()?;
|
||||
Ok(puk)
|
||||
}
|
||||
|
||||
pub fn pair(&mut self, pin: &str) -> Result<(u8, [u8; 32]), KeycardWalletError> {
|
||||
/// Wipes the card's PIN, PUK, and loaded keys, returning it to an uninitialized state —
|
||||
/// the counterpart to `initialize()`. Does **not** remove the identity certificate
|
||||
/// provisioned via `IdentApplet`, so the card can be re-`initialize()`d afterward without
|
||||
/// re-personalizing it. Irreversibly destroys any keys currently loaded on the card.
|
||||
pub fn factory_reset(&mut self) -> Result<(), 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(
|
||||
&mut self,
|
||||
pin: &str,
|
||||
index: u8,
|
||||
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(&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.with_reconnect_on_transport_error(|wallet| wallet.pair(pin))?;
|
||||
save_pairing(&KeycardPairingData {
|
||||
index,
|
||||
key: key.to_vec(),
|
||||
});
|
||||
self.require_secure_channel_v2()?;
|
||||
self.command_set.factory_reset()?.check_ok()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unpairs the current session. Returns `false` if there was nothing to unpair.
|
||||
pub fn disconnect(&mut self) -> Result<bool, KeycardWalletError> {
|
||||
if self.command_set.pairing().is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
self.command_set.auto_unpair()?;
|
||||
Ok(true)
|
||||
/// Opens the secure channel and verifies the PIN. Secure Channel V2 re-authenticates from
|
||||
/// the card's certificate every session — there's no pairing step and nothing to persist.
|
||||
pub fn connect(&mut self, pin: &str) -> Result<(), KeycardWalletError> {
|
||||
self.with_reconnect_on_transport_error(|wallet| {
|
||||
wallet.select()?;
|
||||
wallet.require_secure_channel_v2()?;
|
||||
wallet.command_set.auto_open_secure_channel()?;
|
||||
wallet.command_set.verify_pin(pin)?.check_auth_ok()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_public_key_for_path(&mut self, path: &str) -> Result<PublicKey, KeycardWalletError> {
|
||||
@ -217,7 +193,8 @@ impl KeycardWallet {
|
||||
}
|
||||
};
|
||||
|
||||
PublicKey::try_new(x_only).map_err(|e| KeycardWalletError::InvalidKeyMaterial(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(
|
||||
@ -234,9 +211,12 @@ impl KeycardWallet {
|
||||
path: &str,
|
||||
message: &[u8; 32],
|
||||
) -> Result<(Signature, PublicKey), KeycardWalletError> {
|
||||
let resp =
|
||||
self.command_set
|
||||
.sign_with_path_and_algo(message, path, sign_p2::BIP340_SCHNORR, false)?;
|
||||
let resp = self.command_set.sign_with_path_and_algo(
|
||||
message,
|
||||
path,
|
||||
sign_p2::BIP340_SCHNORR,
|
||||
false,
|
||||
)?;
|
||||
resp.check_ok()?;
|
||||
|
||||
let sig = Signature {
|
||||
@ -276,7 +256,10 @@ impl KeycardWallet {
|
||||
Ok(format!("Public/{}", AccountId::from(&public_key)))
|
||||
}
|
||||
|
||||
pub fn get_private_keys_for_path(&mut self, path: &str) -> Result<PrivateKeyPair, KeycardWalletError> {
|
||||
pub fn get_private_keys_for_path(
|
||||
&mut self,
|
||||
path: &str,
|
||||
) -> Result<PrivateKeyPair, KeycardWalletError> {
|
||||
let resp = self.command_set.export_lee_key(path)?;
|
||||
resp.check_ok()?;
|
||||
|
||||
@ -297,9 +280,7 @@ impl KeycardWallet {
|
||||
) -> Result<PrivateKeyPair, KeycardWalletError> {
|
||||
let mut wallet = Self::new()?;
|
||||
wallet.connect(pin)?;
|
||||
let result = wallet.get_private_keys_for_path(path);
|
||||
drop(wallet.disconnect());
|
||||
result
|
||||
wallet.get_private_keys_for_path(path)
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,8 +291,22 @@ fn generate_puk() -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn pairing_password() -> String {
|
||||
std::env::var("KEYCARD_PAIRING_PASSWORD").unwrap_or_else(|_| DEFAULT_PAIRING_PASSWORD.to_owned())
|
||||
/// Optional override for the CA public key used to verify a card's identity certificate, read
|
||||
/// from `KEYCARD_CA_PUBLIC_KEY` as 66 hex characters (a 33-byte compressed secp256k1 key).
|
||||
/// Falls back to `keycard-rs`'s production default when unset.
|
||||
///
|
||||
/// This exists purely for testing against cards that weren't personalized through the real
|
||||
/// production process — e.g. `status-keycard`'s own `JUnit` suite signs test cards with a fixed,
|
||||
/// throwaway CA that will never match the production default. Real users' cards should need no
|
||||
/// override at all.
|
||||
fn ca_public_key_override() -> Result<Option<[u8; 33]>, KeycardWalletError> {
|
||||
let Ok(hex_str) = std::env::var("KEYCARD_CA_PUBLIC_KEY") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut bytes = [0_u8; 33];
|
||||
hex::decode_to_slice(hex_str.trim(), &mut bytes)
|
||||
.map_err(|e| KeycardWalletError::InvalidCaPublicKey(e.to_string()))?;
|
||||
Ok(Some(bytes))
|
||||
}
|
||||
|
||||
/// Parses a BIP340 Schnorr signature from a LEE `SIGN` response.
|
||||
@ -362,35 +357,3 @@ fn zeroizing_fixed_bytes<const N: usize>(
|
||||
arr.copy_from_slice(&raw);
|
||||
Ok(arr)
|
||||
}
|
||||
|
||||
fn pairing_file_path() -> Option<PathBuf> {
|
||||
let home = std::env::var("LEE_WALLET_HOME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|_| {
|
||||
std::env::home_dir()
|
||||
.map(|h| h.join(".lee").join("wallet"))
|
||||
.ok_or(())
|
||||
})
|
||||
.ok()?;
|
||||
Some(home.join("keycard_pairing.json"))
|
||||
}
|
||||
|
||||
fn load_pairing() -> Option<KeycardPairingData> {
|
||||
let path = pairing_file_path()?;
|
||||
let file = std::fs::File::open(path).ok()?;
|
||||
serde_json::from_reader(file).ok()
|
||||
}
|
||||
|
||||
fn save_pairing(data: &KeycardPairingData) {
|
||||
if let Some(path) = pairing_file_path()
|
||||
&& let Ok(json) = serde_json::to_vec_pretty(data)
|
||||
{
|
||||
drop(std::fs::write(path, json));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_pairing() {
|
||||
if let Some(path) = pairing_file_path() {
|
||||
drop(std::fs::remove_file(path));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Forces the card in the first available reader into the unpowered state via
|
||||
PC/SC SCARD_UNPOWER_CARD. Run immediately before a wallet command to simulate
|
||||
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 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 (
|
||||
SCardEstablishContext, SCardListReaders, SCardConnect, SCardDisconnect,
|
||||
SCARD_SCOPE_USER, SCARD_SHARE_SHARED,
|
||||
SCARD_PROTOCOL_T0, SCARD_PROTOCOL_T1,
|
||||
SCARD_UNPOWER_CARD,
|
||||
)
|
||||
|
||||
hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER)
|
||||
hresult, reader_list = SCardListReaders(hcontext, [])
|
||||
|
||||
if not reader_list:
|
||||
print("force_unpower: no readers found, skipping.")
|
||||
sys.exit(0)
|
||||
|
||||
hresult, hcard, _ = SCardConnect(
|
||||
hcontext,
|
||||
reader_list[0],
|
||||
SCARD_SHARE_SHARED,
|
||||
SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1,
|
||||
)
|
||||
|
||||
if hresult != 0:
|
||||
print(f"force_unpower: SCardConnect failed (hresult={hresult:#010x}), skipping.")
|
||||
sys.exit(0)
|
||||
|
||||
SCardDisconnect(hcard, SCARD_UNPOWER_CARD)
|
||||
print("force_unpower: card powered down.")
|
||||
@ -4,14 +4,12 @@
|
||||
# Forces a card power cycle before each keycard-backed wallet command to verify
|
||||
# commands survive mid-session power loss.
|
||||
|
||||
source venv/bin/activate
|
||||
|
||||
export KEYCARD_PIN=111111
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
unpower() {
|
||||
python "$SCRIPT_DIR/force_unpower.py"
|
||||
cargo run -q --manifest-path "$SCRIPT_DIR/../Cargo.toml" --bin force_unpower
|
||||
}
|
||||
|
||||
echo "Test: wallet keycard available"
|
||||
|
||||
@ -2,8 +2,7 @@
|
||||
# keycard_test_3.sh — tests for `wallet keycard get-private-keys`.
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Run wallet_with_keycard.sh once to install dependencies.
|
||||
# 2. Keycard reader inserted with card loaded (wallet keycard load has been run).
|
||||
# 1. Keycard reader inserted with card loaded (wallet keycard load has been run).
|
||||
|
||||
cargo install --path lez/wallet --force --features keycard-debug
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Run wallet_with_keycard.sh first
|
||||
# Run `cargo install --path lez/wallet --force` first
|
||||
|
||||
export KEYCARD_PIN=111111
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
# keycard_tests_2.sh — comprehensive token + AMM keycard integration tests.
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Run wallet_with_keycard.sh once to install dependencies.
|
||||
# 1. Run `cargo install --path lez/wallet --force` once to install the wallet CLI.
|
||||
# 2. Reset the local chain so all accounts are uninitialized.
|
||||
# 3. Keycard reader inserted with card loaded.
|
||||
#
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cargo install --path lez/wallet --force
|
||||
|
||||
# `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
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use keycard_wallet::{KeycardWallet, clear_pairing};
|
||||
use keycard_wallet::KeycardWallet;
|
||||
|
||||
use crate::{
|
||||
WalletCore,
|
||||
@ -17,9 +17,16 @@ use crate::{
|
||||
pub enum KeycardSubcommand {
|
||||
Available,
|
||||
Connect,
|
||||
Disconnect,
|
||||
Init,
|
||||
Load,
|
||||
/// Wipes the card's PIN, PUK, and loaded keys back to an uninitialized state, so it can be
|
||||
/// re-initialized with `wallet keycard init`. Irreversibly destroys any keys currently on
|
||||
/// the card. Requires --confirm.
|
||||
FactoryReset {
|
||||
/// Confirm that the card's current keys should be irreversibly destroyed.
|
||||
#[arg(long)]
|
||||
confirm: bool,
|
||||
},
|
||||
/// Retrieve the private keys (NSK, VSK) for a given BIP-32 key path.
|
||||
///
|
||||
/// Prints raw key material to stdout — intended for debugging only.
|
||||
@ -38,7 +45,7 @@ pub enum KeycardSubcommand {
|
||||
|
||||
impl KeycardSubcommand {
|
||||
fn handle_available(_wallet_core: &mut WalletCore) -> SubcommandReturnValue {
|
||||
if KeycardWallet::is_unpaired_keycard_available() {
|
||||
if KeycardWallet::is_keycard_available() {
|
||||
println!("\u{2705} Keycard is available.");
|
||||
} else {
|
||||
println!("\u{274c} Keycard is not available.");
|
||||
@ -52,20 +59,7 @@ impl KeycardSubcommand {
|
||||
|
||||
let mut wallet = KeycardWallet::new()?;
|
||||
wallet.connect(&pin)?;
|
||||
println!("\u{2705} Keycard paired and ready.");
|
||||
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
|
||||
fn handle_disconnect(_wallet_core: &mut WalletCore) -> Result<SubcommandReturnValue> {
|
||||
let pin = read_pin()?;
|
||||
|
||||
let mut wallet = KeycardWallet::new()?;
|
||||
wallet.connect(&pin)?;
|
||||
wallet.disconnect()?;
|
||||
|
||||
clear_pairing();
|
||||
println!("\u{2705} Keycard unpaired and pairing cleared.");
|
||||
println!("\u{2705} Keycard connected and PIN verified.");
|
||||
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
@ -76,7 +70,6 @@ impl KeycardSubcommand {
|
||||
let mut wallet = KeycardWallet::new()?;
|
||||
let puk = wallet.initialize(&pin)?;
|
||||
|
||||
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.");
|
||||
@ -101,6 +94,25 @@ impl KeycardSubcommand {
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
|
||||
fn handle_factory_reset(
|
||||
confirm: bool,
|
||||
_wallet_core: &mut WalletCore,
|
||||
) -> Result<SubcommandReturnValue> {
|
||||
if !confirm {
|
||||
eprintln!(
|
||||
"WARNING: pass --confirm to factory-reset the keycard. \
|
||||
This irreversibly destroys any keys currently loaded on it."
|
||||
);
|
||||
return Ok(SubcommandReturnValue::Empty);
|
||||
}
|
||||
|
||||
let mut wallet = KeycardWallet::new()?;
|
||||
wallet.factory_reset()?;
|
||||
println!("\u{2705} Keycard factory-reset. Run `wallet keycard init` to reinitialize it.");
|
||||
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
|
||||
#[cfg(feature = "keycard-debug")]
|
||||
fn handle_get_private_keys(
|
||||
key_path: &str,
|
||||
@ -135,9 +147,9 @@ impl WalletSubcommand for KeycardSubcommand {
|
||||
match self {
|
||||
Self::Available => Ok(Self::handle_available(wallet_core)),
|
||||
Self::Connect => Self::handle_connect(wallet_core),
|
||||
Self::Disconnect => Self::handle_disconnect(wallet_core),
|
||||
Self::Init => Self::handle_init(wallet_core),
|
||||
Self::Load => Self::handle_load(wallet_core),
|
||||
Self::FactoryReset { confirm } => Self::handle_factory_reset(confirm, wallet_core),
|
||||
#[cfg(feature = "keycard-debug")]
|
||||
Self::GetPrivateKeys { key_path, reveal } => {
|
||||
Self::handle_get_private_keys(&key_path, reveal, wallet_core)
|
||||
|
||||
@ -44,7 +44,6 @@ pub mod config;
|
||||
pub mod helperfunctions;
|
||||
pub mod poller;
|
||||
pub mod program_facades;
|
||||
pub mod signing;
|
||||
pub mod storage;
|
||||
|
||||
pub const HOME_DIR_ENV_VAR: &str = "LEE_WALLET_HOME_DIR";
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
use keycard_wallet::{KeycardWallet, KeycardWalletError};
|
||||
|
||||
/// Lazily opens and reuses a single Keycard session for all keycard signers in one transaction.
|
||||
pub struct KeycardSessionContext {
|
||||
pin: String,
|
||||
wallet: Option<KeycardWallet>,
|
||||
}
|
||||
|
||||
impl KeycardSessionContext {
|
||||
pub fn new(pin: impl Into<String>) -> Self {
|
||||
Self {
|
||||
pin: pin.into(),
|
||||
wallet: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_connect(&mut self) -> Result<&mut KeycardWallet, KeycardWalletError> {
|
||||
if self.wallet.is_none() {
|
||||
let mut wallet = KeycardWallet::new()?;
|
||||
wallet.connect(&self.pin)?;
|
||||
self.wallet = Some(wallet);
|
||||
}
|
||||
Ok(self.wallet.as_mut().expect("wallet was just inserted"))
|
||||
}
|
||||
|
||||
pub fn close(mut self) {
|
||||
if let Some(wallet) = self.wallet.as_mut() {
|
||||
drop(wallet.disconnect());
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user