bhwi/common.rs
1//! Device-independent commands, responses, and message routing.
2//!
3//! Command support and required context vary by device. Interpreters perform no I/O:
4//! callers route [`Transmit`] messages to the device, PIN server, or host and return
5//! the resulting bytes to [`crate::Interpreter::exchange`].
6
7#[cfg(feature = "bitbox")]
8use crate::bitbox;
9#[cfg(feature = "coldcard")]
10use crate::coldcard;
11#[cfg(feature = "jade")]
12use crate::jade;
13#[cfg(feature = "keepkey")]
14use crate::keepkey;
15#[cfg(feature = "ledger")]
16use crate::ledger;
17use crate::miniscript::descriptor::{DescriptorPublicKey, WalletPolicy};
18#[cfg(feature = "specter")]
19use crate::specter;
20#[cfg(feature = "trezor")]
21use crate::trezor;
22use bitcoin::Network;
23use bitcoin::address::AddressType;
24use bitcoin::bip32::{DerivationPath, Fingerprint, Xpub};
25use bitcoin::psbt::Psbt;
26use bitcoin::secp256k1::ecdsa::Signature;
27
28mod adapters;
29
30/// Options for starting a device session or unlocking a wallet.
31#[derive(Default)]
32pub struct UnlockOptions {
33 /// Requested network, where supported by the backend.
34 ///
35 /// Ledger requires a network to select its Bitcoin application. Trezor and
36 /// KeepKey use it to select the session network; other common adapters ignore it.
37 pub network: Option<Network>,
38}
39
40/// Options for initializing a new device wallet.
41#[derive(Clone, Debug, Default)]
42pub struct SetupOptions {
43 /// Requested device label; empty labels are omitted by Trezor and KeepKey.
44 pub label: String,
45 /// Backup passphrase supplied by the caller.
46 ///
47 /// BitBox rejects nonempty values; the Trezor and KeepKey adapters do not use it.
48 pub backup_passphrase: String,
49}
50
51/// Options for restoring a device wallet.
52#[derive(Clone, Debug)]
53pub struct RestoreOptions {
54 /// Requested device label; empty labels are omitted by Trezor and KeepKey.
55 pub label: String,
56 /// Number of recovery words.
57 ///
58 /// Defaults to 24. Trezor and KeepKey accept 12, 18, or 24; BitBox ignores this field.
59 pub word_count: i32,
60}
61
62impl Default for RestoreOptions {
63 fn default() -> Self {
64 Self {
65 label: String::new(),
66 word_count: 24,
67 }
68 }
69}
70
71/// Inputs for deriving or displaying an address.
72///
73/// Display flags request device confirmation where supported. Some backends ignore
74/// the flag or always display; Specter-DIY rejects requests for undisplayed addresses.
75#[derive(Clone, Debug)]
76pub enum DisplayAddress {
77 /// Derives an address from a device key path.
78 ByPath {
79 /// Key derivation path for the address.
80 path: DerivationPath,
81 /// Whether to request address confirmation on the device.
82 display: bool,
83 /// Requested script format, or the backend's default or path-inferred format.
84 address_format: Option<AddressType>,
85 },
86 /// Derives an address from a registered descriptor or wallet policy.
87 ///
88 /// Ledger, BitBox, and Specter-DIY require their corresponding [`DeviceContext`].
89 /// Trezor and KeepKey do not support this form.
90 ByDescriptor {
91 /// Address index within the selected descriptor branch.
92 index: u32,
93 /// Whether to select the change branch rather than the receiving branch.
94 change: bool,
95 /// Whether to request address confirmation on the device.
96 display: bool,
97 /// Registered descriptor name used by Jade and Coldcard.
98 ///
99 /// Policy-based backends use the supplied context instead.
100 descriptor_name: String,
101 },
102 /// Displays a multisig address using Python HWI-compatible inputs.
103 ByMultisig(
104 /// Multisig script wrapper, threshold, and concrete keys.
105 MultisigDisplayAddress,
106 ),
107}
108
109/// Sans-I/O inputs for Python HWI's `display_multisig_address` operation.
110///
111/// `threshold`, `sorted`, and `keys` correspond to HWI's
112/// `MultisigDescriptor.thresh`, `is_sorted`, and `pubkeys`, respectively.
113#[derive(Clone, Debug)]
114pub struct MultisigDisplayAddress {
115 /// Number of keys required to authorize a spend.
116 pub threshold: u8,
117 /// Script wrapper used to derive the address.
118 pub address_type: MultisigAddressType,
119 /// Whether keys use BIP67 sorting (`sortedmulti` rather than `multi`).
120 pub sorted: bool,
121 /// Descriptor keys, including origins and concrete address derivations.
122 pub keys: Vec<DescriptorPublicKey>,
123}
124
125/// A script wrapper for a multisig address.
126#[derive(Clone, Copy, Debug)]
127pub enum MultisigAddressType {
128 /// Legacy P2SH multisig.
129 Legacy,
130 /// P2SH-wrapped P2WSH multisig.
131 ShWit,
132 /// Native P2WSH multisig.
133 Wit,
134}
135
136/// A device-independent hardware-wallet operation.
137///
138/// Commands are not supported uniformly. Conversion to a device command can fail
139/// for unsupported operations or missing or mismatched [`DeviceContext`].
140#[allow(clippy::large_enum_variant)]
141pub enum Command {
142 /// Creates a backup on the device or retrieves a backup file.
143 Backup,
144 /// Initializes a device wallet using caller-supplied management data.
145 Setup(
146 /// Requested label and backup options.
147 SetupOptions,
148 /// Device-specific setup inputs, such as host entropy.
149 Option<DeviceContext>,
150 ),
151 /// Erases the device's wallet material.
152 Wipe,
153 /// Starts wallet recovery using caller-supplied management data.
154 Restore(
155 /// Requested label and recovery word count.
156 RestoreOptions,
157 /// Device-specific recovery inputs, such as a U2F counter.
158 Option<DeviceContext>,
159 ),
160 /// Toggles the device's BIP-39 passphrase-protection setting.
161 TogglePassphrase,
162 /// Retrieves the wallet's master key fingerprint.
163 GetMasterFingerprint,
164 /// Retrieves backend-specific firmware or application information.
165 GetVersion,
166 /// Retrieves an extended public key.
167 GetXpub {
168 /// Key derivation path.
169 path: DerivationPath,
170 /// Whether to request confirmation on the device, where supported.
171 display: bool,
172 },
173 /// Derives or displays an address.
174 DisplayAddress(
175 /// Address derivation inputs and requested display behavior.
176 DisplayAddress,
177 /// Device-specific policy context required by some address forms.
178 Option<DeviceContext>,
179 ),
180 /// Registers a named wallet policy on a supporting device.
181 ///
182 /// Trezor and KeepKey do not support wallet registration.
183 RegisterWallet {
184 /// Wallet name shown or stored by the device.
185 name: String,
186 /// Wallet policy, including its descriptor keys and origins.
187 policy: WalletPolicy,
188 },
189 /// Signs the inputs of a partially signed Bitcoin transaction.
190 ///
191 /// Ledger requires policy context even when no wallet HMAC is needed. BitBox
192 /// accepts optional policy context; Coldcard, Trezor, and KeepKey require `None`.
193 SignTx(
194 /// Transaction and metadata to sign.
195 Psbt,
196 /// Optional device-specific policy inputs.
197 Option<DeviceContext>,
198 ),
199 /// Signs a message with the key at a derivation path.
200 SignMessage {
201 /// Message bytes in the form accepted by the device protocol.
202 message: Vec<u8>,
203 /// Signing key derivation path.
204 path: DerivationPath,
205 },
206 /// Starts a session, authenticates, pairs, or opens an app, depending on the backend.
207 ///
208 /// This does not uniformly mean that a device's PIN lock is cleared.
209 Unlock {
210 /// Requested session network and unlock options.
211 options: UnlockOptions,
212 },
213 /// Requests a scrambled PIN-entry matrix from a supporting device.
214 PromptPin,
215 /// Submits scrambled keypad positions, not literal PIN digits.
216 SendPin(
217 /// Management context containing the caller-supplied PIN positions.
218 Option<DeviceContext>,
219 ),
220}
221
222/// Device-specific inputs required by certain shared commands.
223///
224/// Management context carries caller-provided entropy, PIN positions, timestamps,
225/// or counters. Interpreters neither collect these inputs nor perform transport I/O.
226#[allow(clippy::large_enum_variant)]
227#[derive(Clone, Debug)]
228pub enum DeviceContext {
229 /// Wallet policy for Ledger signing and descriptor-based address display.
230 #[cfg(feature = "ledger")]
231 Ledger {
232 /// Policy used to derive addresses or identify signing inputs.
233 wallet_policy: ledger::LedgerWalletPolicy,
234 /// Registration HMAC, or `None` for a policy used without a token.
235 ///
236 /// The Ledger context itself is still required when this is `None`.
237 wallet_hmac: Option<[u8; 32]>,
238 },
239 /// Wallet policy for BitBox descriptor display or policy-based signing.
240 #[cfg(feature = "bitbox")]
241 BitBox {
242 /// Descriptor policy with the key origins required by the device.
243 policy: WalletPolicy,
244 },
245 /// Required context for BitBox02 setup and restore operations.
246 #[cfg(feature = "bitbox")]
247 BitBoxManagement(
248 /// Caller-supplied setup mode and time information.
249 bitbox::ManagementContext,
250 ),
251 /// Caller-supplied inputs for Trezor setup, restore, or PIN submission.
252 #[cfg(feature = "trezor")]
253 TrezorManagement(
254 /// Host entropy, U2F counter, or scrambled keypad positions.
255 trezor::ManagementContext,
256 ),
257 /// Caller-supplied inputs for KeepKey setup, restore, or PIN submission.
258 #[cfg(feature = "keepkey")]
259 KeepKeyManagement(
260 /// Host entropy, U2F counter, or scrambled keypad positions.
261 keepkey::ManagementContext,
262 ),
263 /// Wallet policy required for Specter-DIY descriptor address display.
264 #[cfg(feature = "specter")]
265 Specter {
266 /// Descriptor policy used to prepare the address display request.
267 policy: WalletPolicy,
268 },
269}
270
271/// The result of a shared hardware-wallet command.
272pub enum Response {
273 /// The outcome of a backup operation.
274 Backup(
275 /// Backup completion or downloaded file contents.
276 DeviceBackup,
277 ),
278 /// Backend-specific outcome of a management or PIN request.
279 ///
280 /// Trezor PIN prompting and KeepKey passphrase toggling can return `true`
281 /// while PIN entry is still pending.
282 DeviceAction(
283 /// Whether the backend accepted or completed the request.
284 bool,
285 ),
286 /// A terminal outcome without another result.
287 ///
288 /// Ledger also uses this for failed or unsupported signing; it does not
289 /// guarantee success.
290 TaskDone,
291 /// The device reports that an operation is still busy.
292 TaskBusy,
293 /// Backend-specific device or application information.
294 Info(
295 /// Information reported or derived by the backend.
296 Info,
297 ),
298 /// The wallet's master key fingerprint.
299 MasterFingerprint(
300 /// Four-byte BIP-32 fingerprint.
301 Fingerprint,
302 ),
303 /// An extended public key.
304 Xpub(
305 /// Derived BIP-32 extended public key.
306 Xpub,
307 ),
308 /// Coldcard peer public-key material for establishing encryption.
309 EncryptionKey(
310 /// Uncompressed secp256k1 public key without the `0x04` prefix.
311 [u8; 64],
312 ),
313 /// An ECDSA message signature and its backend-specific header.
314 Signature(
315 /// Compact-message signature header, not normalized across backends.
316 u8,
317 /// ECDSA signature, excluding the header byte.
318 Signature,
319 ),
320 /// A PSBT containing signatures returned by the device.
321 SignedPsbt(
322 /// Partially signed transaction; not necessarily finalized or fully signed.
323 Psbt,
324 ),
325 /// An address returned by the device.
326 Address(
327 /// Address in the device protocol's string encoding.
328 String,
329 ),
330 /// The outcome of registering a wallet policy.
331 WalletRegistration(
332 /// Registration state and any authentication token.
333 WalletRegistration,
334 ),
335}
336
337/// Completion state of a wallet-registration request.
338#[derive(Clone, Copy, Debug, Eq, PartialEq)]
339pub enum WalletRegistration {
340 /// Registration completed, with or without an authentication token.
341 Complete {
342 /// Device-issued wallet HMAC, when the backend provides one.
343 hmac: Option<[u8; 32]>,
344 },
345 /// The request was submitted but still requires confirmation on the device.
346 PendingUserConfirmation,
347}
348
349impl WalletRegistration {
350 /// Returns the registration HMAC, if available.
351 ///
352 /// Returns `None` both for completion without a token and for pending user
353 /// confirmation. Inspect the variant to distinguish those states.
354 pub fn hmac(self) -> Option<[u8; 32]> {
355 match self {
356 Self::Complete { hmac } => hmac,
357 Self::PendingUserConfirmation => None,
358 }
359 }
360}
361
362/// Completion or file contents returned by a backup operation.
363#[derive(Clone, Debug, Eq, PartialEq)]
364pub enum DeviceBackup {
365 /// The device completed its backup without returning a downloaded file.
366 Complete,
367 /// A backup file downloaded from the device.
368 File(
369 /// File bytes in the device's backup format.
370 Vec<u8>,
371 ),
372}
373
374/// Backend-specific device, firmware, and session information.
375///
376/// Optional fields may be unreported, not `false`. The meaning and availability of
377/// names and network lists depend on the backend.
378#[derive(Debug, Clone, Default)]
379pub struct Info {
380 /// Firmware or application version string reported by the backend.
381 pub version: String,
382 /// Networks reported by the backend, or an empty list when unreported.
383 ///
384 /// This may describe the active application or selected session network rather
385 /// than every network supported by the hardware.
386 pub networks: Vec<Network>,
387 /// Backend-specific device, model, or application name, when reported.
388 pub firmware: Option<String>,
389 /// Whether the device has initialized wallet material, when reported by the firmware.
390 pub initialized: Option<bool>,
391 /// User-set device name, when the device protocol reports one.
392 pub label: Option<String>,
393 /// Whether the device can take a passphrase on its own screen, when it reports the capability.
394 pub on_device_passphrase_entry: Option<bool>,
395 /// Whether the device is waiting for a PIN from the host, when it reports a lock state.
396 pub needs_pin_sent: Option<bool>,
397 /// Whether the BIP-39 passphrase is expected from the host, when reported or derived.
398 pub needs_passphrase_sent: Option<bool>,
399}
400
401/// A device request for input that the caller must collect from the user.
402#[derive(Clone, Debug, Eq, PartialEq)]
403pub enum HostRequest {
404 /// Requests positions on the scrambled keypad displayed by the device.
405 PinMatrix {
406 /// Whether the device asks for the current PIN or a new PIN confirmation.
407 kind: PinMatrixRequestKind,
408 },
409 /// Requests a character or control action during cipher-based recovery.
410 RecoveryCharacter {
411 /// Zero-based recovery word position reported by the device.
412 word_position: u32,
413 /// Zero-based character position within the current recovery word.
414 character_position: u32,
415 },
416}
417
418/// The purpose of a scrambled PIN-matrix request.
419#[derive(Clone, Copy, Debug, Eq, PartialEq)]
420pub enum PinMatrixRequestKind {
421 /// Entry of the current PIN.
422 Current,
423 /// First entry of a new PIN.
424 NewFirst,
425 /// Confirmation of the new PIN.
426 NewSecond,
427 /// A request kind not recognized by this library.
428 Unknown(
429 /// Raw protocol value.
430 i32,
431 ),
432}
433
434/// A typed answer to a [`HostRequest`].
435///
436/// Encode the answer with [`into_bytes_for`](Self::into_bytes_for) before returning
437/// it to the interpreter. `Debug` output redacts PIN positions and recovery characters.
438#[derive(Eq, PartialEq)]
439pub enum HostResponse {
440 /// Scrambled keypad positions, never the literal PIN digits.
441 PinPositions(
442 /// Nonempty ASCII digits representing the selected positions.
443 String,
444 ),
445 /// A recovery-cipher character selected using the device's displayed mapping.
446 RecoveryCharacter(
447 /// One lowercase ASCII character.
448 char,
449 ),
450 /// Deletes the previous recovery character.
451 RecoveryDelete,
452 /// Advances to the next recovery word.
453 RecoveryNextWord,
454 /// Completes recovery after the last word.
455 RecoveryDone,
456}
457
458impl core::fmt::Debug for HostResponse {
459 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
460 match self {
461 Self::PinPositions(_) => f.write_str("PinPositions(<redacted>)"),
462 Self::RecoveryCharacter(_) => f.write_str("RecoveryCharacter(<redacted>)"),
463 Self::RecoveryDelete => f.write_str("RecoveryDelete"),
464 Self::RecoveryNextWord => f.write_str("RecoveryNextWord"),
465 Self::RecoveryDone => f.write_str("RecoveryDone"),
466 }
467 }
468}
469
470fn zeroize_string(value: &mut String) {
471 zeroize::Zeroize::zeroize(value);
472}
473
474impl HostResponse {
475 /// Validates and encodes this answer for the given host request.
476 ///
477 /// PIN positions encode as ASCII bytes. Validation requires nonempty ASCII
478 /// digits but does not restrict them to the range 1–9. Recovery characters
479 /// must be lowercase ASCII. Delete encodes as `0x08` and is allowed unless both
480 /// positions are zero; next-word encodes as a space and requires a character
481 /// position of at least 3. Done encodes as a newline and requires a word position
482 /// of 11, 17, or 23 and a character position of at least 3.
483 ///
484 /// # Errors
485 ///
486 /// Returns an [`ErrorKind::InvalidInput`] error if the answer does not match the request,
487 /// its contents are invalid, or a recovery action is invalid at that position.
488 ///
489 /// # Examples
490 ///
491 /// Encode synthetic keypad positions, not literal PIN digits:
492 ///
493 /// ```
494 /// use bhwi::common::{ErrorKind, HostRequest, HostResponse, PinMatrixRequestKind};
495 ///
496 /// let request = HostRequest::PinMatrix {
497 /// kind: PinMatrixRequestKind::Current,
498 /// };
499 /// let bytes = HostResponse::PinPositions("123".into())
500 /// .into_bytes_for(&request)
501 /// .unwrap();
502 /// assert_eq!(bytes, b"123");
503 /// let error = HostResponse::PinPositions(String::new())
504 /// .into_bytes_for(&request)
505 /// .unwrap_err();
506 /// assert_eq!(error.kind(), ErrorKind::InvalidInput);
507 /// ```
508 pub fn into_bytes_for(self, request: &HostRequest) -> Result<Vec<u8>, Error> {
509 match (request, self) {
510 (HostRequest::PinMatrix { .. }, Self::PinPositions(mut positions)) => {
511 if positions.is_empty() || !positions.bytes().all(|byte| byte.is_ascii_digit()) {
512 zeroize_string(&mut positions);
513 return Err(Error::new(
514 ErrorKind::InvalidInput,
515 "PIN positions must contain ASCII digits",
516 ));
517 }
518 Ok(positions.into_bytes())
519 }
520 (HostRequest::RecoveryCharacter { .. }, Self::RecoveryCharacter(character)) => {
521 if !character.is_ascii_lowercase() {
522 return Err(Error::new(
523 ErrorKind::InvalidInput,
524 "recovery cipher response must be one lowercase ASCII character",
525 ));
526 }
527 Ok(vec![character as u8])
528 }
529 (
530 HostRequest::RecoveryCharacter {
531 word_position,
532 character_position,
533 },
534 action,
535 ) => {
536 let byte = match action {
537 Self::RecoveryDelete if *word_position != 0 || *character_position != 0 => 0x08,
538 Self::RecoveryNextWord if *character_position >= 3 => b' ',
539 Self::RecoveryDone
540 if matches!(*word_position, 11 | 17 | 23) && *character_position >= 3 =>
541 {
542 b'\n'
543 }
544 Self::RecoveryDelete | Self::RecoveryNextWord | Self::RecoveryDone => {
545 return Err(Error::new(
546 ErrorKind::InvalidInput,
547 "recovery action is not valid at this position",
548 ));
549 }
550 Self::PinPositions(mut positions) => {
551 zeroize_string(&mut positions);
552 return Err(Error::new(
553 ErrorKind::InvalidInput,
554 "host response does not match request",
555 ));
556 }
557 Self::RecoveryCharacter(_) => {
558 return Err(Error::new(
559 ErrorKind::InvalidInput,
560 "host response does not match request",
561 ));
562 }
563 };
564 Ok(vec![byte])
565 }
566 _ => Err(Error::new(
567 ErrorKind::InvalidInput,
568 "host response does not match request",
569 )),
570 }
571 }
572}
573
574/// The destination of a shared interpreter's next request.
575pub enum Recipient {
576 /// The hardware wallet or its emulator.
577 Device,
578 /// An external PIN-server HTTP endpoint.
579 PinServer {
580 /// URL to which the caller sends the transmission payload.
581 url: String,
582 },
583 /// User interaction handled by the caller rather than a transport.
584 Host(
585 /// Request to answer with [`HostResponse::into_bytes_for`].
586 HostRequest,
587 ),
588}
589
590/// A routed request produced by a shared interpreter.
591///
592/// Send device or PIN-server payloads to their recipient and return the response
593/// bytes to [`crate::Interpreter::exchange`]. For host requests, collect a typed
594/// [`HostResponse`] and encode it with [`HostResponse::into_bytes_for`].
595///
596/// Converting a byte vector targets the device with `encrypted` set to `false`.
597/// Converting a [`HostRequest`] targets the host with an empty payload.
598pub struct Transmit {
599 /// Destination of the payload or host-interaction request.
600 pub recipient: Recipient,
601 /// Encoded protocol payload, ready to route; empty for a converted host request.
602 pub payload: Vec<u8>,
603 /// Whether the payload is already encrypted.
604 ///
605 /// Transports may use this flag for framing; callers must not encrypt the
606 /// payload again.
607 pub encrypted: bool,
608}
609
610/// The kind of failure an error reports, shared by every device.
611#[non_exhaustive]
612#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
613pub enum ErrorKind {
614 /// The user declined on the device or dismissed a prompt.
615 UserCancelled,
616 /// The device's login step failed without saying why.
617 AuthenticationRefused,
618 /// The host could not get an answer from the user.
619 HostUnavailable,
620 /// The device needs its PIN before it will answer.
621 Locked,
622 /// The device is reachable but not ready to run commands.
623 NotReady,
624 /// A PIN step was requested but the device is already unlocked.
625 AlreadyUnlocked,
626 /// The device has no wallet.
627 NotInitialized,
628 /// The device already has a wallet.
629 AlreadyInitialized,
630 /// The PIN was wrong.
631 WrongPin,
632 /// A key or address belongs to another network.
633 WrongNetwork,
634 /// The item being created already exists on the device.
635 Duplicate,
636 /// The device or firmware cannot run this command.
637 Unsupported,
638 /// The device cannot display this address type.
639 UnsupportedDisplayAddress,
640 /// The command needs data the caller did not supply.
641 MissingContext,
642 /// The caller's data is malformed or was rejected as invalid.
643 InvalidInput,
644 /// The device refused for a reason no other kind covers.
645 Rejected,
646 /// The device reported an internal failure.
647 DeviceFailure,
648 /// Host and device disagreed on the protocol.
649 Protocol,
650 /// A valid reply that is not the one expected.
651 UnexpectedResponse,
652 /// Bytes could not be encoded or decoded.
653 Serialization,
654 /// Channel encryption or decryption failed.
655 Encryption,
656 /// I/O with the device or a helper service failed.
657 Transport,
658 /// The device went away.
659 Disconnected,
660 /// No other kind applies.
661 Other,
662}
663
664/// A device's own error code, in that vendor's numbering.
665#[non_exhaustive]
666#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
667pub enum DeviceCode {
668 /// A Ledger status word.
669 Ledger(u16),
670 /// A Trezor failure code.
671 Trezor(i32),
672 /// A KeepKey failure code.
673 KeepKey(i32),
674 /// A Jade RPC error code.
675 Jade(i32),
676 /// A BitBox02 error code.
677 BitBox(i32),
678}
679
680/// A failure with its kind, the device's code and message, and any data the device sent.
681#[derive(Debug, Clone, PartialEq, Eq)]
682pub struct Error {
683 kind: ErrorKind,
684 device_code: Option<DeviceCode>,
685 message: String,
686 data: Option<Vec<u8>>,
687}
688
689impl Error {
690 /// Creates an error of `kind` with `message`.
691 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
692 Self {
693 kind,
694 device_code: None,
695 message: message.into(),
696 data: None,
697 }
698 }
699
700 /// Sets the device's own error code.
701 pub fn with_device_code(mut self, code: DeviceCode) -> Self {
702 self.device_code = Some(code);
703 self
704 }
705
706 /// Sets data the device sent with the error; it is never printed.
707 pub fn with_data(mut self, data: Vec<u8>) -> Self {
708 self.data = Some(data);
709 self
710 }
711
712 /// Returns the failure kind.
713 pub fn kind(&self) -> ErrorKind {
714 self.kind
715 }
716
717 /// Returns the device's own error code, if it sent one.
718 pub fn device_code(&self) -> Option<DeviceCode> {
719 self.device_code
720 }
721
722 /// Returns the message without the kind.
723 pub fn message(&self) -> &str {
724 &self.message
725 }
726
727 /// Returns data the device sent with the error.
728 pub fn data(&self) -> Option<&[u8]> {
729 self.data.as_deref()
730 }
731}
732
733impl core::fmt::Display for Error {
734 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
735 if self.message.is_empty() {
736 write!(f, "[{:?}]", self.kind)
737 } else {
738 write!(f, "[{:?}] {}", self.kind, self.message)
739 }
740 }
741}
742
743impl std::error::Error for Error {}
744
745/// The BitBox interpreter using this module's shared command and result types.
746#[cfg(feature = "bitbox")]
747pub type BitBoxInterpreter<'a> = bitbox::BitBoxInterpreter<'a, Command, Transmit, Response, Error>;
748/// The Coldcard interpreter using this module's shared command and result types.
749#[cfg(feature = "coldcard")]
750pub type ColdcardInterpreter<'a> =
751 coldcard::ColdcardInterpreter<'a, Command, Transmit, Response, Error>;
752/// The Jade interpreter using this module's shared command and result types.
753#[cfg(feature = "jade")]
754pub type JadeInterpreter = jade::JadeInterpreter<Command, Transmit, Response, Error>;
755/// The Ledger interpreter using this module's shared command and result types.
756#[cfg(feature = "ledger")]
757pub type LedgerInterpreter = ledger::LedgerInterpreter<Command, Transmit, Response, Error>;
758/// The Trezor interpreter using this module's shared command and result types.
759#[cfg(feature = "trezor")]
760pub type TrezorInterpreter = trezor::TrezorInterpreter<Command, Transmit, Response, Error>;
761/// The KeepKey interpreter using this module's shared command and result types.
762#[cfg(feature = "keepkey")]
763pub type KeepKeyInterpreter = keepkey::KeepKeyInterpreter<Command, Transmit, Response, Error>;
764/// The Specter-DIY interpreter using this module's shared command and result types.
765#[cfg(feature = "specter")]
766pub type SpecterInterpreter = specter::SpecterInterpreter<Command, Transmit, Response, Error>;
767
768impl From<Vec<u8>> for Transmit {
769 fn from(payload: Vec<u8>) -> Transmit {
770 Transmit {
771 recipient: Recipient::Device,
772 payload,
773 encrypted: false,
774 }
775 }
776}
777
778impl From<HostRequest> for Transmit {
779 fn from(request: HostRequest) -> Self {
780 Self {
781 recipient: Recipient::Host(request),
782 payload: Vec::new(),
783 encrypted: false,
784 }
785 }
786}
787
788#[cfg(test)]
789mod tests {
790 use super::*;
791 use crate::Interpreter;
792
793 fn assert_interpreter<I>()
794 where
795 I: Interpreter<
796 Command = super::Command,
797 Transmit = super::Transmit,
798 Response = super::Response,
799 Error = super::Error,
800 >,
801 {
802 }
803
804 #[test]
805 fn common_interpreters_are_satisfied() {
806 #[cfg(feature = "bitbox")]
807 assert_interpreter::<BitBoxInterpreter<'static>>();
808 #[cfg(feature = "coldcard")]
809 assert_interpreter::<ColdcardInterpreter<'static>>();
810 #[cfg(feature = "jade")]
811 assert_interpreter::<JadeInterpreter>();
812 #[cfg(feature = "keepkey")]
813 assert_interpreter::<KeepKeyInterpreter>();
814 #[cfg(feature = "specter")]
815 assert_interpreter::<SpecterInterpreter>();
816 #[cfg(feature = "ledger")]
817 assert_interpreter::<LedgerInterpreter>();
818 #[cfg(feature = "trezor")]
819 assert_interpreter::<TrezorInterpreter>();
820 }
821
822 #[test]
823 fn host_response_validation_and_encoding() {
824 let pin = HostRequest::PinMatrix {
825 kind: PinMatrixRequestKind::Current,
826 };
827 assert_eq!(
828 HostResponse::PinPositions("123".into())
829 .into_bytes_for(&pin)
830 .unwrap(),
831 b"123"
832 );
833 assert!(matches!(
834 HostResponse::PinPositions("".into()).into_bytes_for(&pin),
835 Err(e) if e.kind() == ErrorKind::InvalidInput
836 && e.message() == "PIN positions must contain ASCII digits"
837 ));
838 assert!(matches!(
839 HostResponse::PinPositions("123".into()).into_bytes_for(&pin),
840 Err(e) if e.kind() == ErrorKind::InvalidInput
841 && e.message() == "PIN positions must contain ASCII digits"
842 ));
843
844 let first = HostRequest::RecoveryCharacter {
845 word_position: 0,
846 character_position: 0,
847 };
848 assert_eq!(
849 HostResponse::RecoveryCharacter('a')
850 .into_bytes_for(&first)
851 .unwrap(),
852 b"a"
853 );
854 assert!(matches!(
855 HostResponse::RecoveryCharacter('A').into_bytes_for(&first),
856 Err(e) if e.kind() == ErrorKind::InvalidInput
857 && e.message()
858 == "recovery cipher response must be one lowercase ASCII character"
859 ));
860 assert!(matches!(
861 HostResponse::RecoveryDelete.into_bytes_for(&first),
862 Err(e) if e.kind() == ErrorKind::InvalidInput
863 && e.message() == "recovery action is not valid at this position"
864 ));
865 for request in [
866 HostRequest::RecoveryCharacter {
867 word_position: 0,
868 character_position: 1,
869 },
870 HostRequest::RecoveryCharacter {
871 word_position: 1,
872 character_position: 0,
873 },
874 ] {
875 assert_eq!(
876 HostResponse::RecoveryDelete
877 .into_bytes_for(&request)
878 .unwrap(),
879 [0x08]
880 );
881 }
882
883 let middle = HostRequest::RecoveryCharacter {
884 word_position: 1,
885 character_position: 3,
886 };
887 assert_eq!(
888 HostResponse::RecoveryDelete
889 .into_bytes_for(&middle)
890 .unwrap(),
891 [0x08]
892 );
893 assert_eq!(
894 HostResponse::RecoveryNextWord
895 .into_bytes_for(&middle)
896 .unwrap(),
897 b" "
898 );
899 assert!(HostResponse::RecoveryDone.into_bytes_for(&middle).is_err());
900 let too_early = HostRequest::RecoveryCharacter {
901 word_position: 1,
902 character_position: 2,
903 };
904 assert!(matches!(
905 HostResponse::RecoveryNextWord.into_bytes_for(&too_early),
906 Err(e) if e.kind() == ErrorKind::InvalidInput
907 && e.message() == "recovery action is not valid at this position"
908 ));
909
910 let last = HostRequest::RecoveryCharacter {
911 word_position: 11,
912 character_position: 3,
913 };
914 assert_eq!(
915 HostResponse::RecoveryDone.into_bytes_for(&last).unwrap(),
916 b"\n"
917 );
918 assert!(matches!(
919 HostResponse::PinPositions("1".into()).into_bytes_for(&last),
920 Err(e) if e.kind() == ErrorKind::InvalidInput
921 && e.message() == "host response does not match request"
922 ));
923 }
924
925 #[test]
926 fn host_response_debug_is_redacted() {
927 assert_eq!(
928 format!("{:?}", HostResponse::PinPositions("8675309".into())),
929 "PinPositions(<redacted>)"
930 );
931 assert_eq!(
932 format!("{:?}", HostResponse::RecoveryCharacter('q')),
933 "RecoveryCharacter(<redacted>)"
934 );
935 }
936}