From 0d8bdee4f3a2e9c10c52a4a5ec4998d425d40ef1 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 18 Aug 2026 09:25:46 -0500 Subject: [PATCH 1/8] Harden CVC authentication flows Represent CVCs as zeroizing, redacted ASCII values and validate foreign and CLI inputs before card commands. Reject unsuccessful wait responses so authentication failures cannot appear as completed delays. --- cktap-ffi/src/error.rs | 39 +++++-- cktap-ffi/src/lib.rs | 6 ++ cktap-ffi/src/sats_card.rs | 9 +- cktap-ffi/src/sats_chip.rs | 7 ++ cktap-ffi/src/tap_signer.rs | 18 ++-- cli/src/main.rs | 74 +++++++++---- lib/Cargo.toml | 1 + lib/src/apdu.rs | 2 +- lib/src/cvc.rs | 161 ++++++++++++++++++++++++++++ lib/src/emulator.rs | 7 +- lib/src/error.rs | 4 - lib/src/lib.rs | 2 + lib/src/sats_card.rs | 28 ++--- lib/src/shared.rs | 204 ++++++++++++++++++++++++++++++++---- lib/src/tap_signer.rs | 33 +++--- 15 files changed, 501 insertions(+), 94 deletions(-) create mode 100644 lib/src/cvc.rs diff --git a/cktap-ffi/src/error.rs b/cktap-ffi/src/error.rs index 25df813..6dda815 100644 --- a/cktap-ffi/src/error.rs +++ b/cktap-ffi/src/error.rs @@ -87,6 +87,15 @@ pub enum CkTapError { UnknownCardType, } +impl From for CkTapError { + fn from(_value: rust_cktap::CvcError) -> Self { + // cvc validation failures are local argument errors + Self::Card { + err: CardError::BadArguments, + } + } +} + impl From for CkTapError { fn from(value: rust_cktap::CkTapError) -> Self { match value { @@ -344,10 +353,6 @@ pub enum ChangeError { #[from] err: CkTapError, }, - #[error("new cvc is too short, must be at least 6 bytes, was only {len} bytes")] - TooShort { len: u32 }, - #[error("new cvc is too long, must be at most 32 bytes, was {len} bytes")] - TooLong { len: u32 }, #[error("new cvc is the same as the old one")] SameAsOld, } @@ -356,8 +361,6 @@ impl From for ChangeError { fn from(value: rust_cktap::ChangeError) -> Self { match value { rust_cktap::ChangeError::CkTap(err) => ChangeError::CkTap { err: err.into() }, - rust_cktap::ChangeError::TooShort(len) => ChangeError::TooShort { len }, - rust_cktap::ChangeError::TooLong(len) => ChangeError::TooLong { len }, rust_cktap::ChangeError::SameAsOld => ChangeError::SameAsOld, } } @@ -385,3 +388,27 @@ impl From for XpubError { } } } + +#[cfg(test)] +mod tests { + use super::*; + use rust_cktap::CvcError; + + #[test] + fn cvc_validation_errors_map_to_bad_arguments() { + let errors = [ + CvcError::TooShort { length: 5 }, + CvcError::TooLong { length: 33 }, + CvcError::NonAsciiDigit { index: 5 }, + ]; + + for error in errors { + assert_eq!( + CkTapError::from(error), + CkTapError::Card { + err: CardError::BadArguments + } + ); + } + } +} diff --git a/cktap-ffi/src/lib.rs b/cktap-ffi/src/lib.rs index 4f284b7..0480af4 100644 --- a/cktap-ffi/src/lib.rs +++ b/cktap-ffi/src/lib.rs @@ -13,6 +13,7 @@ use crate::sats_card::SatsCard; use crate::sats_chip::SatsChip; use crate::tap_signer::TapSigner; use futures::lock::Mutex; +use rust_cktap::Cvc; use rust_cktap::shared::{Certificate, Read}; use std::fmt::Debug; use std::sync::Arc; @@ -69,6 +70,11 @@ async fn read( card: &mut (impl Read + Send + Sync), cvc: Option, ) -> Result { + let cvc = cvc + .map(Cvc::try_from) + .transpose() + .map_err(CkTapError::from)?; + card.read(cvc) .await .map(|pk| pk.to_string()) diff --git a/cktap-ffi/src/sats_card.rs b/cktap-ffi/src/sats_card.rs index 19c6701..6ffb80a 100644 --- a/cktap-ffi/src/sats_card.rs +++ b/cktap-ffi/src/sats_card.rs @@ -8,7 +8,7 @@ use crate::error::{ use futures::lock::Mutex; use rust_cktap::descriptor::Wpkh; use rust_cktap::shared::{Authentication, Nfc, Read, Wait}; -use rust_cktap::{Psbt, rand_chaincode}; +use rust_cktap::{Cvc, Psbt, rand_chaincode}; use std::str::FromStr; #[derive(uniffi::Object)] @@ -81,6 +81,7 @@ impl SatsCard { /// Open a new slot, it will be the current active but must be unused (no address) pub async fn new_slot(&self, cvc: String) -> Result { + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; let (active_slot, _) = card.slots; let new_slot_chain_code = rand_chaincode(); @@ -99,6 +100,7 @@ impl SatsCard { /// Unseal currently active slot pub async fn unseal(&self, cvc: String) -> Result { + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; let active_slot = card.slots.0; let (privkey, pubkey) = card.unseal(active_slot, &cvc).await?; @@ -112,6 +114,10 @@ impl SatsCard { /// This is only needed for debugging, use `sign_psbt` for signing /// If no CVC given only pubkey and pubkey descriptor returned. pub async fn dump(&self, slot: u8, cvc: Option) -> Result { + let cvc = cvc + .map(Cvc::try_from) + .transpose() + .map_err(CkTapError::from)?; let mut card = self.0.lock().await; let (privkey, pubkey) = card.dump(slot, cvc).await?; Ok(SlotDetails { @@ -128,6 +134,7 @@ impl SatsCard { psbt: String, cvc: String, ) -> Result { + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; let psbt = Psbt::from_str(&psbt)?; let signed_psbt = card.sign_psbt(slot, psbt, &cvc).await?; diff --git a/cktap-ffi/src/sats_chip.rs b/cktap-ffi/src/sats_chip.rs index 67f5b59..a217030 100644 --- a/cktap-ffi/src/sats_chip.rs +++ b/cktap-ffi/src/sats_chip.rs @@ -7,6 +7,7 @@ use crate::error::{ use crate::tap_signer::{change, derive, init, sign_psbt}; use crate::{check_cert, read}; use futures::lock::Mutex; +use rust_cktap::Cvc; use rust_cktap::shared::{Authentication, Nfc, Wait}; use rust_cktap::tap_signer::TapSignerShared; @@ -55,23 +56,28 @@ impl SatsChip { } pub async fn init(&self, cvc: String) -> Result<(), CkTapError> { + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; init(&mut *card, cvc).await } pub async fn sign_psbt(&self, psbt: String, cvc: String) -> Result { + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; let psbt = sign_psbt(&mut *card, psbt, cvc).await?; Ok(psbt) } pub async fn derive(&self, path: Vec, cvc: String) -> Result { + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; let pubkey = derive(&mut *card, path, cvc).await?; Ok(pubkey) } pub async fn change(&self, new_cvc: String, cvc: String) -> Result<(), ChangeError> { + let new_cvc = Cvc::try_from(new_cvc).map_err(CkTapError::from)?; + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; change(&mut *card, new_cvc, cvc).await?; Ok(()) @@ -84,6 +90,7 @@ impl SatsChip { } pub async fn xpub(&self, master: bool, cvc: String) -> Result { + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; let xpub = card.xpub(master, &cvc).await?; Ok(xpub.to_string()) diff --git a/cktap-ffi/src/tap_signer.rs b/cktap-ffi/src/tap_signer.rs index 901cdc4..9d28800 100644 --- a/cktap-ffi/src/tap_signer.rs +++ b/cktap-ffi/src/tap_signer.rs @@ -8,7 +8,7 @@ use crate::{check_cert, read}; use futures::lock::Mutex; use rust_cktap::shared::{Authentication, Nfc, Wait}; use rust_cktap::tap_signer::TapSignerShared; -use rust_cktap::{Psbt, rand_chaincode}; +use rust_cktap::{Cvc, Psbt, rand_chaincode}; use std::str::FromStr; #[derive(uniffi::Object)] @@ -58,23 +58,28 @@ impl TapSigner { } pub async fn init(&self, cvc: String) -> Result<(), CkTapError> { + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; init(&mut *card, cvc).await } pub async fn sign_psbt(&self, psbt: String, cvc: String) -> Result { + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; let psbt = sign_psbt(&mut *card, psbt, cvc).await?; Ok(psbt) } pub async fn derive(&self, path: Vec, cvc: String) -> Result { + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; let pubkey = derive(&mut *card, path, cvc).await?; Ok(pubkey) } pub async fn change(&self, new_cvc: String, cvc: String) -> Result<(), ChangeError> { + let new_cvc = Cvc::try_from(new_cvc).map_err(CkTapError::from)?; + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; change(&mut *card, new_cvc, cvc).await?; Ok(()) @@ -87,6 +92,7 @@ impl TapSigner { } pub async fn xpub(&self, master: bool, cvc: String) -> Result { + let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; let mut card = self.0.lock().await; let xpub = card.xpub(master, &cvc).await?; Ok(xpub.to_string()) @@ -96,7 +102,7 @@ impl TapSigner { /// Initialize a new TAPSIGNER card. pub async fn init( card: &mut (impl TapSignerShared + Send + Sync), - cvc: String, + cvc: Cvc, ) -> Result<(), CkTapError> { let chain_code = rand_chaincode(); card.init(chain_code, &cvc).await.map_err(CkTapError::from) @@ -108,7 +114,7 @@ pub async fn init( pub async fn sign_psbt( card: &mut (impl TapSignerShared + Send + Sync), psbt: String, - cvc: String, + cvc: Cvc, ) -> Result { let unsigned_psbt = Psbt::from_str(&psbt)?; let psbt = card.sign_psbt(unsigned_psbt, &cvc).await?; @@ -119,7 +125,7 @@ pub async fn sign_psbt( pub async fn derive( card: &mut (impl TapSignerShared + Send + Sync), path: Vec, - cvc: String, + cvc: Cvc, ) -> Result { let pubkey = card.derive(path, &cvc).await.map(|pk| pk.to_string())?; Ok(pubkey) @@ -127,8 +133,8 @@ pub async fn derive( pub async fn change( card: &mut (impl TapSignerShared + Send + Sync), - new_cvc: String, - cvc: String, + new_cvc: Cvc, + cvc: Cvc, ) -> Result<(), ChangeError> { card.change(&new_cvc, &cvc).await?; Ok(()) diff --git a/cli/src/main.rs b/cli/src/main.rs index 9df3199..456f520 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -12,7 +12,8 @@ use rust_cktap::pcsc; use rust_cktap::shared::{Authentication, Nfc, Read, Wait}; use rust_cktap::tap_signer::TapSignerShared; use rust_cktap::{ - CkTapCard, CkTapError, Psbt, PsbtParseError, SignPsbtError, rand_chaincode, shared::Certificate, + CkTapCard, CkTapError, Cvc, CvcError, Psbt, PsbtParseError, SignPsbtError, rand_chaincode, + shared::Certificate, }; use std::io; use std::io::Write; @@ -35,6 +36,10 @@ pub enum CliError { Dump(#[from] DumpError), #[error(transparent)] CkTap(#[from] CkTapError), + #[error(transparent)] + Cvc(#[from] CvcError), + #[error("unable to read CVC: {0}")] + CvcInput(String), } /// SatsCard CLI @@ -187,26 +192,28 @@ async fn main() -> Result<(), CliError> { SatsCardCommand::New => { let slot = sc.slot().expect("current slot number"); let chain_code = Some(rand_chaincode()); - let response = &sc.new_slot(slot, chain_code, &cvc()).await?; + let cvc = cvc()?; + let response = &sc.new_slot(slot, chain_code, &cvc).await?; println!("chain_code: {chain_code:?}"); println!("{response}") } SatsCardCommand::Unseal => { let slot = sc.slot().expect("current slot number"); - let (privkey, pubkey) = &sc.unseal(slot, &cvc()).await?; + let cvc = cvc()?; + let (privkey, pubkey) = &sc.unseal(slot, &cvc).await?; println!("privkey: {}, pubkey: {pubkey}", privkey.to_wif()) } SatsCardCommand::Sign { slot, psbt } => { let psbt = Psbt::from_str(&psbt)?; - let signed_psbt = sc.sign_psbt(slot, psbt, &cvc()).await?; + let cvc = cvc()?; + let signed_psbt = sc.sign_psbt(slot, psbt, &cvc).await?; println!("signed_psbt: {signed_psbt}"); } SatsCardCommand::Derive => { dbg!(&sc.derive().await); } SatsCardCommand::Dump { slot } => { - let cvc = cvc(); - let cvc = if cvc.is_empty() { None } else { Some(cvc) }; + let cvc = optional_cvc()?; let response = sc.dump(slot, cvc).await?; dbg!(response); } @@ -221,30 +228,36 @@ async fn main() -> Result<(), CliError> { dbg!(&ts); } TapSignerCommand::Certs => check_cert(ts).await, - TapSignerCommand::Read => read(ts, Some(cvc())).await, + TapSignerCommand::Read => read(ts, Some(cvc()?)).await, TapSignerCommand::Init => { let chain_code = rand_chaincode(); - let response = &ts.init(chain_code, &cvc()).await; + let cvc = cvc()?; + let response = &ts.init(chain_code, &cvc).await; dbg!(response); } TapSignerCommand::Derive { path } => { // let test_path:Vec = ts.path.clone().unwrap().iter().map(|p| p ^ (1 << 31)).collect(); // dbg!(test_path); - dbg!(&ts.derive(path.unwrap_or_default(), &cvc()).await); + let cvc = cvc()?; + dbg!(&ts.derive(path.unwrap_or_default(), &cvc).await); } TapSignerCommand::Backup => { - let response = &ts.backup(&cvc()).await; + let cvc = cvc()?; + let response = &ts.backup(&cvc).await; println!("{response:?}"); } TapSignerCommand::Change { new_cvc } => { - let response = &ts.change(&new_cvc, &cvc()).await; + let new_cvc = Cvc::try_from(new_cvc)?; + let cvc = cvc()?; + let response = &ts.change(&new_cvc, &cvc).await; println!("{response:?}"); } TapSignerCommand::Sign { psbt } => { let psbt = Psbt::from_str(&psbt)?; - let signed_psbt = ts.sign_psbt(psbt, &cvc()).await?; + let cvc = cvc()?; + let signed_psbt = ts.sign_psbt(psbt, &cvc).await?; println!("signed_psbt: {signed_psbt}"); } TapSignerCommand::Wait => wait(ts).await, @@ -259,23 +272,28 @@ async fn main() -> Result<(), CliError> { dbg!(&sc); } SatsChipCommand::Certs => check_cert(sc).await, - SatsChipCommand::Read => read(sc, Some(cvc())).await, + SatsChipCommand::Read => read(sc, Some(cvc()?)).await, SatsChipCommand::Init => { let chain_code = rand_chaincode(); - let response = &sc.init(chain_code, &cvc()).await; + let cvc = cvc()?; + let response = &sc.init(chain_code, &cvc).await; dbg!(response); } SatsChipCommand::Derive { path } => { - dbg!(&sc.derive(path.unwrap_or_default(), &cvc()).await); + let cvc = cvc()?; + dbg!(&sc.derive(path.unwrap_or_default(), &cvc).await); } SatsChipCommand::Change { new_cvc } => { - let response = &sc.change(&new_cvc, &cvc()).await; + let new_cvc = Cvc::try_from(new_cvc)?; + let cvc = cvc()?; + let response = &sc.change(&new_cvc, &cvc).await; println!("{response:?}"); } SatsChipCommand::Sign { psbt } => { let psbt = Psbt::from_str(&psbt)?; - let signed_psbt = sc.sign_psbt(psbt, &cvc()).await?; + let cvc = cvc()?; + let signed_psbt = sc.sign_psbt(psbt, &cvc).await?; println!("signed_psbt: {signed_psbt}"); } SatsChipCommand::Wait => wait(sc).await, @@ -304,7 +322,7 @@ where } } -async fn read(card: &mut C, cvc: Option) +async fn read(card: &mut C, cvc: Option) where C: Read + Send, { @@ -317,11 +335,22 @@ where } } -fn cvc() -> String { +fn cvc() -> Result { print!("Enter cvc: "); io::stdout().flush().unwrap(); - let cvc = read_password().unwrap(); - cvc.trim().to_string() + let cvc = read_password().map_err(|error| CliError::CvcInput(error.to_string()))?; + Ok(Cvc::try_from(cvc)?) +} + +fn optional_cvc() -> Result, CliError> { + print!("Enter cvc (leave empty for none): "); + io::stdout().flush().unwrap(); + let cvc = read_password().map_err(|error| CliError::CvcInput(error.to_string()))?; + if cvc.is_empty() { + Ok(None) + } else { + Ok(Some(Cvc::try_from(cvc)?)) + } } async fn wait(card: &mut C) @@ -353,7 +382,8 @@ where C: TapSignerShared + Send, { dbg!(master); - let xpub = card.xpub(master, &cvc()).await.expect("xpub failed"); + let cvc = cvc().expect("valid cvc"); + let xpub = card.xpub(master, &cvc).await.expect("xpub failed"); dbg!(&xpub); println!("{xpub}"); } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index c0bda38..38b5261 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -35,6 +35,7 @@ data-encoding = "2.6" # logging log = "0.4" +zeroize = "1.8" # pcsc as optional pcsc = { version = "2", optional = true } diff --git a/lib/src/apdu.rs b/lib/src/apdu.rs index 73168e8..1fc0065 100644 --- a/lib/src/apdu.rs +++ b/lib/src/apdu.rs @@ -625,7 +625,7 @@ impl CommandApdu for WaitCommand { #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub struct WaitResponse { /// command result - success: bool, + pub(crate) success: bool, /// how much more delay is now required #[serde(default)] pub(crate) auth_delay: u8, diff --git a/lib/src/cvc.rs b/lib/src/cvc.rs new file mode 100644 index 0000000..de098ec --- /dev/null +++ b/lib/src/cvc.rs @@ -0,0 +1,161 @@ +// Copyright (c) 2025 rust-cktap contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::fmt; + +use zeroize::Zeroize; + +/// The shortest CVC accepted by the CkTap protocol +pub const MIN_CVC_LENGTH: usize = 6; + +/// The longest CVC accepted by the CkTap protocol +pub const MAX_CVC_LENGTH: usize = 32; + +/// A numeric secret used to authenticate CkTap commands +/// +/// The protocol specification is internally inconsistent: its CVC content section permits +/// non-ASCII bytes, but its TAPSIGNER `change` command requires numeric digits +/// +/// This type follows factory-card behavior and the `change` command rule by accepting only ASCII +/// digits +/// +/// See [CVC Length & Content] and [`change`] +/// +/// [CVC Length & Content]: https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#cvc-length--content +/// [`change`]: https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#change +#[derive(Clone, PartialEq, Eq)] +pub struct Cvc(String); + +impl Cvc { + /// Return the ASCII bytes in this CVC + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + /// Return the CVC as a string slice + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Return the number of bytes in this CVC + #[allow(clippy::len_without_is_empty)] // a valid CVC can never be empty + pub fn len(&self) -> usize { + self.0.len() + } +} + +impl TryFrom for Cvc { + type Error = CvcError; + + fn try_from(mut value: String) -> Result { + let length = value.len(); + + if length < MIN_CVC_LENGTH { + value.zeroize(); + return Err(CvcError::TooShort { length }); + } + + if length > MAX_CVC_LENGTH { + value.zeroize(); + return Err(CvcError::TooLong { length }); + } + + if let Some(index) = value.bytes().position(|byte| !byte.is_ascii_digit()) { + value.zeroize(); + return Err(CvcError::NonAsciiDigit { index }); + } + + Ok(Self(value)) + } +} + +impl TryFrom<&str> for Cvc { + type Error = CvcError; + + fn try_from(value: &str) -> Result { + Self::try_from(value.to_owned()) + } +} + +impl AsRef for Cvc { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl fmt::Debug for Cvc { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Cvc(REDACTED)") + } +} + +impl Drop for Cvc { + fn drop(&mut self) { + self.0.zeroize(); + } +} + +/// Errors returned when a CVC does not satisfy the protocol constraints +#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] +pub enum CvcError { + /// The CVC contains fewer than [`MIN_CVC_LENGTH`] bytes + #[error("CVC is too short: {length} bytes; minimum is {MIN_CVC_LENGTH}")] + TooShort { length: usize }, + /// The CVC contains more than [`MAX_CVC_LENGTH`] bytes + #[error("CVC is too long: {length} bytes; maximum is {MAX_CVC_LENGTH}")] + TooLong { length: usize }, + /// The CVC contains a byte that is not an ASCII digit + #[error("CVC contains a byte that is not an ASCII digit at byte index {index}")] + NonAsciiDigit { index: usize }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accepts_protocol_boundaries() { + let minimum = Cvc::try_from("0".repeat(MIN_CVC_LENGTH)).expect("minimum length is valid"); + let maximum = Cvc::try_from("9".repeat(MAX_CVC_LENGTH)).expect("maximum length is valid"); + + assert_eq!(minimum.len(), MIN_CVC_LENGTH); + assert_eq!(maximum.len(), MAX_CVC_LENGTH); + } + + #[test] + fn rejects_lengths_outside_protocol_bounds() { + assert_eq!( + Cvc::try_from("0".repeat(MIN_CVC_LENGTH - 1)), + Err(CvcError::TooShort { + length: MIN_CVC_LENGTH - 1 + }) + ); + assert_eq!( + Cvc::try_from("9".repeat(MAX_CVC_LENGTH + 1)), + Err(CvcError::TooLong { + length: MAX_CVC_LENGTH + 1 + }) + ); + } + + #[test] + fn rejects_non_numeric_and_non_ascii_values() { + assert_eq!( + Cvc::try_from("12345a"), + Err(CvcError::NonAsciiDigit { index: 5 }) + ); + assert_eq!( + Cvc::try_from("12345é"), + Err(CvcError::NonAsciiDigit { index: 5 }) + ); + } + + #[test] + fn debug_is_redacted() { + let cvc = Cvc::try_from("123456").expect("valid CVC"); + let debug = format!("{cvc:?}"); + + assert!(!debug.contains("123456")); + assert_eq!(debug, "Cvc(REDACTED)"); + } +} diff --git a/lib/src/emulator.rs b/lib/src/emulator.rs index fd70dec..d417d91 100644 --- a/lib/src/emulator.rs +++ b/lib/src/emulator.rs @@ -4,7 +4,7 @@ use crate::apdu::{AppletSelect, CommandApdu, StatusCommand}; use crate::error::StatusError; use crate::shared::{CkTransport, to_cktap}; -use crate::{CkTapCard, CkTapError}; +use crate::{CkTapCard, CkTapError, Cvc}; use async_trait::async_trait; use std::io::{Read, Write}; use std::os::unix::net::UnixStream; @@ -12,7 +12,10 @@ use std::path::Path; use std::string::ToString; use std::sync::Arc; -pub const CVC: &str = "123456"; +/// Return the default emulator CVC +pub fn emulator_default_cvc() -> Cvc { + Cvc::try_from("123456").expect("the emulator CVC is valid") +} pub async fn find_emulator(pipe_path: &Path) -> Result { if !pipe_path.exists() { diff --git a/lib/src/error.rs b/lib/src/error.rs index 9c8369b..5b70344 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -130,10 +130,6 @@ impl From for StatusError { pub enum ChangeError { #[error(transparent)] CkTap(#[from] CkTapError), - #[error("new cvc is too short, must be at least 6 bytes, was only {0} bytes")] - TooShort(u32), - #[error("new cvc is too long, must be at most 32 bytes, was {0} bytes")] - TooLong(u32), #[error("new cvc is the same as the old one")] SameAsOld, } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b6667d4..eba3ab5 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -11,6 +11,7 @@ pub use bitcoin::secp256k1::{Error as SecpError, rand}; pub use bitcoin_hashes::sha256::Hash; pub use miniscript::descriptor; +pub use cvc::{Cvc, CvcError, MAX_CVC_LENGTH, MIN_CVC_LENGTH}; pub use error::{ CardError, CertsError, ChangeError, CkTapError, DeriveError, DumpError, ReadError, SignPsbtError, StatusError, UnsealError, XpubError, @@ -20,6 +21,7 @@ pub use shared::{CkTransport, card_pubkey_to_ident}; use bitcoin::key::rand::Rng as _; pub(crate) mod apdu; +pub mod cvc; pub mod error; pub mod sats_card; pub mod sats_chip; diff --git a/lib/src/sats_card.rs b/lib/src/sats_card.rs index 22901d0..48f8211 100644 --- a/lib/src/sats_card.rs +++ b/lib/src/sats_card.rs @@ -1,7 +1,6 @@ // Copyright (c) 2025 rust-cktap contributors // SPDX-License-Identifier: MIT OR Apache-2.0 -use crate::CkTapError; use crate::apdu::{ AppletSelect, CommandApdu as _, DeriveCommand, DeriveResponse, DumpCommand, DumpResponse, NewCommand, NewResponse, SignCommand, SignResponse, StatusResponse, UnsealCommand, @@ -12,6 +11,7 @@ use crate::error::{SignPsbtError, StatusError}; use crate::shared::{ Authentication, Certificate, CkTransport, Nfc, Read, Wait, card_pubkey_to_ident, transmit, }; +use crate::{CkTapError, Cvc}; use async_trait::async_trait; use bitcoin::bip32::{ChainCode, DerivationPath, Fingerprint, Xpub}; use bitcoin::secp256k1; @@ -103,7 +103,7 @@ impl SatsCard { &mut self, slot: u8, chain_code: Option, - cvc: &str, + cvc: &Cvc, ) -> Result { let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, NewCommand::name()); let new_command = NewCommand::new(Some(slot), chain_code, epubkey, xcvc); @@ -200,7 +200,7 @@ impl SatsCard { pub async fn unseal( &mut self, slot: u8, - cvc: &str, + cvc: &Cvc, ) -> Result<(PrivateKey, PublicKey), UnsealError> { let (eprivkey, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, UnsealCommand::name()); let unseal_command = UnsealCommand::new(slot, epubkey, xcvc); @@ -237,7 +237,7 @@ impl SatsCard { pub async fn dump( &mut self, slot: u8, - cvc: Option, + cvc: Option, ) -> Result<(Option, PublicKey), DumpError> { let epubkey_eprivkey_xcvc = cvc.map(|cvc| { let (eprivkey, epubkey, xcvc) = self.calc_ekeys_xcvc(&cvc, DumpCommand::name()); @@ -308,7 +308,7 @@ impl SatsCard { &mut self, digest: [u8; 32], slot: u8, - cvc: &str, + cvc: &Cvc, ) -> Result { let (eprivkey, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, SignCommand::name()); @@ -355,7 +355,7 @@ impl SatsCard { &mut self, slot: u8, mut psbt: bitcoin::Psbt, - cvc: &str, + cvc: &Cvc, ) -> Result { use bitcoin::{ secp256k1::ecdsa, @@ -467,11 +467,11 @@ impl core::fmt::Debug for SatsCard { #[cfg(test)] mod test { #![allow(deprecated)] // bdk_wallet::SignOptions is deprecated upstream; tests still rely on it. - use crate::CkTapCard; use crate::emulator::find_emulator; use crate::emulator::test::{CardTypeOption, EcardSubprocess}; use crate::error::DumpError; use crate::shared::Certificate; + use crate::{CkTapCard, Cvc}; use bdk_wallet::chain::{BlockId, ConfirmationBlockTime}; use bdk_wallet::template::P2Wpkh; use bdk_wallet::test_utils::{insert_anchor, insert_checkpoint, insert_tx, new_tx}; @@ -493,7 +493,8 @@ mod test { if let CkTapCard::SatsCard(mut sc) = emulator { let slot_pubkey = sc.slot_pubkey().await.unwrap().unwrap(); let card_address = sc.address().await.unwrap(); - let (_seckey, pubkey) = sc.unseal(0, "123456").await.unwrap(); + let cvc = Cvc::try_from("123456").unwrap(); + let (_seckey, pubkey) = sc.unseal(0, &cvc).await.unwrap(); assert_eq!(pubkey, slot_pubkey); let descriptor = P2Wpkh(pubkey); @@ -551,7 +552,7 @@ mod test { ) .fee_rate(FeeRate::from_sat_per_vb(2).unwrap()); let psbt = builder.finish().unwrap(); - let mut signed_psbt = sc.sign_psbt(0, psbt, "123456").await.unwrap(); + let mut signed_psbt = sc.sign_psbt(0, psbt, &cvc).await.unwrap(); let finalized = wallet .finalize_psbt(&mut signed_psbt, SignOptions::default()) .unwrap(); @@ -573,15 +574,16 @@ mod test { let emulator = find_emulator(pipe_path).await.unwrap(); if let CkTapCard::SatsCard(mut sc) = emulator { // slot 0 is sealed, with cvc return sealed error - let slot_keys = sc.dump(0, Some("123456".to_string())).await; + let cvc = Cvc::try_from("123456").unwrap(); + let slot_keys = sc.dump(0, Some(cvc.clone())).await; assert!(matches!(slot_keys, Err(DumpError::SlotSealed(slot)) if slot == 0)); // slot 0 is sealed, with no cvc return sealed error let slot_keys = sc.dump(0, None).await; assert!(matches!(slot_keys, Err(DumpError::SlotSealed(slot)) if slot == 0)); // unseal slot 0 - sc.unseal(0, "123456").await.unwrap(); + sc.unseal(0, &cvc).await.unwrap(); // slot 0 is unsealed, with cvc return privkey - let slot_keys = sc.dump(0, Some("123456".to_string())).await; + let slot_keys = sc.dump(0, Some(cvc.clone())).await; assert!(slot_keys.is_ok()); assert!(matches!(slot_keys, Ok((Some(_), _)))); let slot_keys = slot_keys.unwrap(); @@ -593,7 +595,7 @@ mod test { assert!(slot_keys.is_ok()); assert!(matches!(slot_keys, Ok((None, _)))); // slot 1 is unused, with cvc return unused error - let dump_response = sc.dump(1, Some("123456".to_string())).await; + let dump_response = sc.dump(1, Some(cvc)).await; assert!(matches!(dump_response, Err(DumpError::SlotUnused(slot)) if slot == 1)); // slot 1 is unused, with no cvc also return unused error let dump_response = sc.dump(1, None).await; diff --git a/lib/src/shared.rs b/lib/src/shared.rs index 3a286f0..821f44f 100644 --- a/lib/src/shared.rs +++ b/lib/src/shared.rs @@ -1,7 +1,7 @@ // Copyright (c) 2025 rust-cktap contributors // SPDX-License-Identifier: MIT OR Apache-2.0 -use crate::{CardError, CkTapCard, CkTapError, SatsCard, TapSigner}; +use crate::{CardError, CkTapCard, CkTapError, Cvc, SatsCard, TapSigner}; use crate::{apdu::*, rand_nonce}; use bitcoin::key::{PublicKey, rand}; @@ -92,7 +92,7 @@ pub trait Authentication { /// ref: ["Authenticating Commands with CVC"](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#authenticating-commands-with-cvc) fn calc_ekeys_xcvc( &self, - cvc: &str, + cvc: &Cvc, command: &str, ) -> (secp256k1::SecretKey, secp256k1::PublicKey, Vec) { let secp = Self::secp(self); @@ -147,6 +147,169 @@ mod card_ident_tests { } } +#[cfg(test)] +mod authentication_tests { + use super::*; + use crate::Cvc; + use async_trait::async_trait; + use bitcoin::key::PublicKey; + use std::collections::VecDeque; + use std::sync::Mutex; + + struct TestTransport; + + #[async_trait] + impl CkTransport for TestTransport { + async fn transmit_apdu(&self, _command_apdu: Vec) -> Result, CkTapError> { + unreachable!("authentication vector tests do not transmit APDUs") + } + } + + struct TestAuthentication { + secp: Secp256k1, + pubkey: PublicKey, + card_nonce: [u8; 16], + auth_delay: Option, + transport: Arc, + } + + impl Authentication for TestAuthentication { + fn secp(&self) -> &Secp256k1 { + &self.secp + } + + fn ver(&self) -> &str { + "test" + } + + fn pubkey(&self) -> &PublicKey { + &self.pubkey + } + + fn card_nonce(&self) -> &[u8; 16] { + &self.card_nonce + } + + fn set_card_nonce(&mut self, new_nonce: [u8; 16]) { + self.card_nonce = new_nonce; + } + + fn auth_delay(&self) -> Option { + self.auth_delay + } + + fn set_auth_delay(&mut self, auth_delay: Option) { + self.auth_delay = auth_delay; + } + + fn transport(&self) -> Arc { + self.transport.clone() + } + } + + impl Wait for TestAuthentication {} + + #[test] + fn xcvc_xors_exact_ascii_cvc_bytes() { + let secp = Secp256k1::new(); + let (_, card_pubkey) = secp.generate_keypair(&mut rand::thread_rng()); + let card_pubkey = PublicKey::new(card_pubkey); + let card_nonce = [0x42; 16]; + let authentication = TestAuthentication { + secp: secp.clone(), + pubkey: card_pubkey, + card_nonce, + auth_delay: None, + transport: Arc::new(TestTransport), + }; + let cvc = Cvc::try_from("907856").expect("test CVC is valid"); + let command = "read"; + + let (ephemeral_private_key, _, encrypted_cvc) = + authentication.calc_ekeys_xcvc(&cvc, command); + let session_key = SharedSecret::new(&authentication.pubkey.inner, &ephemeral_private_key); + let digest_hash = sha256::Hash::hash(&[card_nonce.as_slice(), command.as_bytes()].concat()); + let digest: &[u8; 32] = digest_hash.as_ref(); + let expected: Vec = cvc + .as_bytes() + .iter() + .zip(session_key.as_ref().iter().zip(digest.iter())) + .map(|(cvc_byte, (session_byte, digest_byte))| cvc_byte ^ session_byte ^ digest_byte) + .collect::>(); + + assert_eq!(encrypted_cvc, expected); + } + + struct WaitTransport(Mutex>>); + + #[async_trait] + impl CkTransport for WaitTransport { + async fn transmit_apdu(&self, _command_apdu: Vec) -> Result, CkTapError> { + self.0 + .lock() + .expect("wait response queue lock") + .pop_front() + .ok_or_else(|| CkTapError::Transport("wait response queue is empty".to_string())) + } + } + + #[derive(serde::Serialize)] + struct WaitResponseFixture { + success: bool, + auth_delay: u8, + } + + fn wait_authentication(success: bool, auth_delay: u8) -> TestAuthentication { + let secp = Secp256k1::new(); + let (_, card_pubkey) = secp.generate_keypair(&mut rand::thread_rng()); + let response = WaitResponseFixture { + success, + auth_delay, + }; + let mut response_bytes = Vec::new(); + ciborium::ser::into_writer(&response, &mut response_bytes) + .expect("wait response fixture serializes"); + + TestAuthentication { + secp, + pubkey: PublicKey::new(card_pubkey), + card_nonce: [0x42; 16], + auth_delay: None, + transport: Arc::new(WaitTransport(Mutex::new(VecDeque::from([response_bytes])))), + } + } + + #[tokio::test] + async fn wait_rejects_unsuccessful_response_even_with_zero_delay() { + let mut authentication = wait_authentication(false, 0); + + let error = authentication.wait(None).await.unwrap_err(); + + assert_eq!(error, CkTapError::Card(CardError::BadAuth)); + assert_eq!(authentication.auth_delay(), None); + } + + #[tokio::test] + async fn wait_returns_no_delay_after_success_at_zero() { + let mut authentication = wait_authentication(true, 0); + + let result = authentication.wait(None).await.unwrap(); + + assert_eq!(result, None); + assert_eq!(authentication.auth_delay(), None); + } + + #[tokio::test] + async fn wait_records_delay_before_returning_bad_auth() { + let mut authentication = wait_authentication(false, 3); + + let error = authentication.wait(None).await.unwrap_err(); + + assert_eq!(error, CkTapError::Card(CardError::BadAuth)); + assert_eq!(authentication.auth_delay(), Some(3)); + } +} + /// Trait for exchanging APDU data with cktap cards. #[async_trait] pub trait CkTransport: Sync + Send { @@ -200,7 +363,7 @@ pub trait Read: Authentication { fn slot(&self) -> Option; - async fn read(&mut self, cvc: Option) -> Result { + async fn read(&mut self, cvc: Option) -> Result { let card_nonce = *self.card_nonce(); let app_nonce = rand_nonce(); @@ -249,7 +412,7 @@ pub trait Read: Authentication { #[async_trait] pub trait Wait: Authentication { - async fn wait(&mut self, cvc: Option) -> Result, CkTapError> { + async fn wait(&mut self, cvc: Option) -> Result, CkTapError> { let epubkey_xcvc = cvc.map(|cvc| { let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(&cvc, WaitCommand::name()); (epubkey, xcvc) @@ -262,15 +425,14 @@ pub trait Wait: Authentication { let wait_command = WaitCommand::new(epubkey, xcvc); let wait_response: WaitResponse = transmit(self.transport(), &wait_command).await?; - // TODO throw error if success == false - if wait_response.auth_delay > 0 { - let auth_delay = Some(wait_response.auth_delay); - self.set_auth_delay(auth_delay); - Ok(auth_delay) - } else { - self.set_auth_delay(None); - Ok(None) + let auth_delay = (wait_response.auth_delay > 0).then_some(wait_response.auth_delay); + self.set_auth_delay(auth_delay); + + if !wait_response.success { + return Err(CkTapError::Card(CardError::BadAuth)); } + + Ok(auth_delay) } } @@ -540,7 +702,7 @@ mod tests { use super::*; use std::path::Path; - use crate::emulator::CVC; + use crate::Cvc; use crate::emulator::find_emulator; use crate::emulator::test::{CardTypeOption, EcardSubprocess}; use crate::rand_chaincode; @@ -558,27 +720,31 @@ mod tests { CkTapCard::SatsCard(mut sc) => { assert_eq!(card_type, CardTypeOption::SatsCard); let current_slot = sc.slots.0; - let response = sc.unseal(current_slot, CVC).await; + let cvc = Cvc::try_from("123456").unwrap(); + let response = sc.unseal(current_slot, &cvc).await; assert!(response.is_ok()); - let response = sc.new_slot(current_slot + 1, Some(chain_code), CVC).await; + let response = sc.new_slot(current_slot + 1, Some(chain_code), &cvc).await; assert!(response.is_ok()); assert_eq!(sc.slots.0, current_slot + 1); // test with no new chain_code let current_slot = sc.slots.0; - let response = sc.unseal(current_slot, CVC).await; + let cvc = Cvc::try_from("123456").unwrap(); + let response = sc.unseal(current_slot, &cvc).await; assert!(response.is_ok()); - let response = sc.new_slot(current_slot + 1, None, CVC).await; + let response = sc.new_slot(current_slot + 1, None, &cvc).await; assert!(response.is_ok()); assert_eq!(sc.slots.0, current_slot + 1); } CkTapCard::TapSigner(mut ts) => { assert_eq!(card_type, CardTypeOption::TapSigner); - let response = ts.init(chain_code, CVC).await; + let cvc = Cvc::try_from("123456").unwrap(); + let response = ts.init(chain_code, &cvc).await; assert!(response.is_ok()) } CkTapCard::SatsChip(mut sc) => { assert_eq!(card_type, CardTypeOption::SatsChip); - let response = sc.init(chain_code, CVC).await; + let cvc = Cvc::try_from("123456").unwrap(); + let response = sc.init(chain_code, &cvc).await; assert!(response.is_ok()) } }; diff --git a/lib/src/tap_signer.rs b/lib/src/tap_signer.rs index 2ad50d9..52aec27 100644 --- a/lib/src/tap_signer.rs +++ b/lib/src/tap_signer.rs @@ -11,7 +11,7 @@ use crate::error::{ChangeError, DeriveError, ReadError, SignPsbtError, StatusErr use crate::shared::{ Authentication, Certificate, CkTransport, Nfc, Read, Wait, card_pubkey_to_ident, transmit, }; -use crate::{BIP32_HARDENED_MASK, CkTapError}; +use crate::{BIP32_HARDENED_MASK, CkTapError, Cvc}; use async_trait::async_trait; use bitcoin::PublicKey; use bitcoin::bip32::{ChainCode, Xpub}; @@ -79,7 +79,7 @@ impl Authentication for TapSigner { #[async_trait] pub trait TapSignerShared: Authentication { /// Initialize the tap signer or sats chip, can only be done once - async fn init(&mut self, chain_code: ChainCode, cvc: &str) -> Result<(), CkTapError> { + async fn init(&mut self, chain_code: ChainCode, cvc: &Cvc) -> Result<(), CkTapError> { let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, NewCommand::name()); let new_command = NewCommand::new(Some(0), Some(chain_code), epubkey, xcvc); let new_response: NewResponse = transmit(self.transport(), &new_command).await?; @@ -100,7 +100,7 @@ pub trait TapSignerShared: Authentication { &mut self, digest: [u8; 32], sub_path: Vec, - cvc: &str, + cvc: &Cvc, ) -> Result { let (eprivkey, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, SignCommand::name()); @@ -147,7 +147,7 @@ pub trait TapSignerShared: Authentication { async fn sign_psbt( &mut self, mut psbt: bitcoin::Psbt, - cvc: &str, + cvc: &Cvc, ) -> Result { use bitcoin::{ secp256k1::ecdsa, @@ -246,7 +246,7 @@ pub trait TapSignerShared: Authentication { /// mobile wallet. /// /// Ref: - async fn derive(&mut self, path: Vec, cvc: &str) -> Result { + async fn derive(&mut self, path: Vec, cvc: &Cvc) -> Result { // set most significant bit to 1 to represent hardened path steps let path = path.iter().map(|p| p ^ (1 << 31)).collect::>(); let app_nonce = crate::rand_nonce(); @@ -284,15 +284,7 @@ pub trait TapSignerShared: Authentication { } /// Change the CVC used for card authentication to a new user provided one - async fn change(&mut self, new_cvc: &str, cvc: &str) -> Result<(), ChangeError> { - if new_cvc.len() < 6 { - return Err(ChangeError::TooShort(new_cvc.len() as u32)); - } - - if new_cvc.len() > 32 { - return Err(ChangeError::TooLong(new_cvc.len() as u32)); - } - + async fn change(&mut self, new_cvc: &Cvc, cvc: &Cvc) -> Result<(), ChangeError> { if new_cvc == cvc { return Err(ChangeError::SameAsOld); } @@ -318,7 +310,7 @@ pub trait TapSignerShared: Authentication { Ok(()) } - async fn xpub(&mut self, master: bool, cvc: &str) -> Result { + async fn xpub(&mut self, master: bool, cvc: &Cvc) -> Result { let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, XpubCommand::name()); let xpub_command = XpubCommand::new(master, epubkey, xcvc); let xpub_response: XpubResponse = transmit(self.transport(), &xpub_command).await?; @@ -360,7 +352,7 @@ impl TapSigner { } /// Backup the current card, the backup is encrypted with the "Backup Password" on the back of the card - pub async fn backup(&mut self, cvc: &str) -> Result, ChangeError> { + pub async fn backup(&mut self, cvc: &Cvc) -> Result, ChangeError> { let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, "backup"); let backup_command = BackupCommand::new(epubkey, xcvc); @@ -414,7 +406,7 @@ mod test { use crate::emulator::find_emulator; use crate::emulator::test::{CardTypeOption, EcardSubprocess}; use crate::tap_signer::TapSignerShared; - use crate::{CkTapCard, rand_chaincode}; + use crate::{CkTapCard, Cvc, rand_chaincode}; use std::path::Path; // verify the xpub command works @@ -426,10 +418,11 @@ mod test { let python = EcardSubprocess::new(pipe_path, &card_type).unwrap(); let emulator = find_emulator(pipe_path).await.unwrap(); if let CkTapCard::TapSigner(mut ts) = emulator { - ts.init(rand_chaincode(), "123456").await.unwrap(); - let xpub = ts.xpub(false, "123456").await.unwrap(); + let cvc = Cvc::try_from("123456").unwrap(); + ts.init(rand_chaincode(), &cvc).await.unwrap(); + let xpub = ts.xpub(false, &cvc).await.unwrap(); assert_eq!(xpub.depth, 3); - let master_xpub = ts.xpub(true, "123456").await.unwrap(); + let master_xpub = ts.xpub(true, &cvc).await.unwrap(); assert_eq!(master_xpub.depth, 0); } drop(python); From f12671112f3ca5d83a77f6dae169380eedef5cbf Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 18 Aug 2026 09:26:04 -0500 Subject: [PATCH 2/8] Preserve typed APDU errors Keep unknown status codes and callback error variants intact so callers can classify protocol failures without parsing transport strings. --- cktap-ffi/src/error.rs | 38 ++++++++++++++++++++++ cktap-ffi/src/lib.rs | 73 +++++++++++++++++++++++++++++++++++++++++- lib/src/apdu.rs | 52 ++++++++++++++++++++++++++++-- lib/src/error.rs | 14 ++++++++ 4 files changed, 173 insertions(+), 4 deletions(-) diff --git a/cktap-ffi/src/error.rs b/cktap-ffi/src/error.rs index 6dda815..3098726 100644 --- a/cktap-ffi/src/error.rs +++ b/cktap-ffi/src/error.rs @@ -72,6 +72,24 @@ impl From for CardError { } } +impl From for rust_cktap::CardError { + fn from(value: CardError) -> Self { + match value { + CardError::UnluckyNumber => Self::UnluckyNumber, + CardError::BadArguments => Self::BadArguments, + CardError::BadAuth => Self::BadAuth, + CardError::NeedsAuth => Self::NeedsAuth, + CardError::UnknownCommand => Self::UnknownCommand, + CardError::InvalidCommand => Self::InvalidCommand, + CardError::InvalidState => Self::InvalidState, + CardError::WeakNonce => Self::WeakNonce, + CardError::BadCBOR => Self::BadCBOR, + CardError::BackupFirst => Self::BackupFirst, + CardError::RateLimited => Self::RateLimited, + } + } +} + /// Errors returned by the card, CBOR deserialization or value encoding, or the APDU transport. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error)] pub enum CkTapError { @@ -83,6 +101,8 @@ pub enum CkTapError { CborValue { msg: String }, #[error("APDU transport error: {msg}")] Transport { msg: String }, + #[error("Unknown APDU status word ({code}): {message}")] + UnknownStatusWord { code: u16, message: String }, #[error("Unknown card type")] UnknownCardType, } @@ -103,11 +123,29 @@ impl From for CkTapError { rust_cktap::CkTapError::CborDe(msg) => CkTapError::CborDe { msg }, rust_cktap::CkTapError::CborValue(msg) => CkTapError::CborValue { msg }, rust_cktap::CkTapError::Transport(msg) => CkTapError::Transport { msg }, + rust_cktap::CkTapError::UnknownStatusWord { code, message } => { + CkTapError::UnknownStatusWord { code, message } + } rust_cktap::CkTapError::UnknownCardType => CkTapError::UnknownCardType, } } } +impl From for rust_cktap::CkTapError { + fn from(value: CkTapError) -> Self { + match value { + CkTapError::Card { err } => Self::Card(err.into()), + CkTapError::CborDe { msg } => Self::CborDe(msg), + CkTapError::CborValue { msg } => Self::CborValue(msg), + CkTapError::Transport { msg } => Self::Transport(msg), + CkTapError::UnknownStatusWord { code, message } => { + Self::UnknownStatusWord { code, message } + } + CkTapError::UnknownCardType => Self::UnknownCardType, + } + } +} + /// Errors returned by the `status` command. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error)] pub enum StatusError { diff --git a/cktap-ffi/src/lib.rs b/cktap-ffi/src/lib.rs index 0480af4..9b7f1d6 100644 --- a/cktap-ffi/src/lib.rs +++ b/cktap-ffi/src/lib.rs @@ -35,7 +35,78 @@ impl rust_cktap::CkTransport for CkTransportWrapper { self.0 .transmit_apdu(command_apdu) .await - .map_err(|e| rust_cktap::CkTapError::Transport(e.to_string())) + .map_err(rust_cktap::CkTapError::from) + } +} + +#[cfg(test)] +mod transport_tests { + use super::*; + use crate::error::CardError; + use rust_cktap::CkTransport as _; + + struct ErrorTransport(CkTapError); + + #[async_trait::async_trait] + impl CkTransport for ErrorTransport { + async fn transmit_apdu(&self, _command_apdu: Vec) -> Result, CkTapError> { + Err(self.0.clone()) + } + } + + fn transmit_error(error: CkTapError) -> rust_cktap::CkTapError { + futures::executor::block_on( + CkTransportWrapper(Box::new(ErrorTransport(error))).transmit_apdu(Vec::new()), + ) + .expect_err("error transport must fail") + } + + #[test] + fn callback_errors_keep_their_typed_protocol_variants() { + let cases = [ + ( + CkTapError::Card { + err: CardError::BadAuth, + }, + rust_cktap::CkTapError::Card(rust_cktap::CardError::BadAuth), + ), + ( + CkTapError::CborDe { + msg: "invalid response".to_string(), + }, + rust_cktap::CkTapError::CborDe("invalid response".to_string()), + ), + ( + CkTapError::CborValue { + msg: "invalid value".to_string(), + }, + rust_cktap::CkTapError::CborValue("invalid value".to_string()), + ), + ( + CkTapError::Transport { + msg: "link lost".to_string(), + }, + rust_cktap::CkTapError::Transport("link lost".to_string()), + ), + ( + CkTapError::UnknownStatusWord { + code: 499, + message: "future status".to_string(), + }, + rust_cktap::CkTapError::UnknownStatusWord { + code: 499, + message: "future status".to_string(), + }, + ), + ( + CkTapError::UnknownCardType, + rust_cktap::CkTapError::UnknownCardType, + ), + ]; + + for (callback_error, expected) in cases { + assert_eq!(transmit_error(callback_error), expected); + } } } diff --git a/lib/src/apdu.rs b/lib/src/apdu.rs index 1fc0065..614ee7f 100644 --- a/lib/src/apdu.rs +++ b/lib/src/apdu.rs @@ -5,8 +5,8 @@ /// reader and a smart card. This file defines the Coinkite APDU and set of command/responses. pub mod tap_signer; +use crate::CkTapError; use crate::error::{ErrorResponse, ReadError}; -use crate::{CardError, CkTapError}; use bitcoin::bip32::ChainCode; use bitcoin::secp256k1::{self, ecdh::SharedSecret, ecdsa::Signature}; use bitcoin::{Network, PrivateKey, PublicKey}; @@ -45,8 +45,10 @@ pub trait ResponseApdu { let cbor_struct: Result = cbor_value.deserialized(); if let Ok(error_resp) = cbor_struct { - let error = CardError::error_from_code(error_resp.code).unwrap_or(CardError::BadCBOR); - return Err(CkTapError::Card(error)); + return Err(CkTapError::from_status_word( + error_resp.code, + error_resp.error, + )); } let cbor_struct: Self = cbor_value.deserialized()?; @@ -874,3 +876,47 @@ pub struct DumpResponse { } impl ResponseApdu for DumpResponse {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CardError, CkTapError}; + + #[derive(Serialize)] + struct ErrorResponseFixture { + error: String, + code: u16, + } + + fn encode_error_response(code: u16, message: &str) -> Vec { + let response = ErrorResponseFixture { + error: message.to_string(), + code, + }; + let mut cbor = Vec::new(); + into_writer(&response, &mut cbor).expect("error response fixture serializes"); + cbor + } + + #[test] + fn known_status_word_maps_to_card_error() { + let error = StatusResponse::from_cbor(encode_error_response(400, "bad arguments")) + .expect_err("error response must fail to deserialize as status"); + + assert_eq!(error, CkTapError::Card(CardError::BadArguments)); + } + + #[test] + fn unknown_status_word_preserves_code_and_message() { + let error = StatusResponse::from_cbor(encode_error_response(499, "future protocol error")) + .expect_err("error response must fail to deserialize as status"); + + assert_eq!( + error, + CkTapError::UnknownStatusWord { + code: 499, + message: "future protocol error".to_string(), + } + ); + } +} diff --git a/lib/src/error.rs b/lib/src/error.rs index 5b70344..d240fdb 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -15,10 +15,24 @@ pub enum CkTapError { CborValue(String), #[error("APDU transport error: {0}")] Transport(String), + #[error("Unknown APDU status word ({code}): {message}")] + UnknownStatusWord { code: u16, message: String }, #[error("Unknown card type")] UnknownCardType, } +impl CkTapError { + pub(crate) fn from_status_word(code: u16, message: impl Into) -> Self { + match CardError::error_from_code(code) { + Some(error) => Self::Card(error), + None => Self::UnknownStatusWord { + code, + message: message.into(), + }, + } + } +} + /// Errors returned by the CkTap card. #[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] pub enum CardError { From 0a75dd40f1d388bed936311069f7c68404a15e0e Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 18 Aug 2026 09:26:18 -0500 Subject: [PATCH 3/8] Refresh Swift bindings for UniFFI 0.32 Regenerate the Swift bindings for the updated error model and handle UniFFI's module-based artifact names in the XCFramework build. --- cktap-swift/Sources/CKTap/cktap_ffi.swift | 1063 +++++++++++---------- cktap-swift/build-xcframework.sh | 9 +- 2 files changed, 557 insertions(+), 515 deletions(-) diff --git a/cktap-swift/Sources/CKTap/cktap_ffi.swift b/cktap-swift/Sources/CKTap/cktap_ffi.swift index f117c34..f2ff1d1 100644 --- a/cktap-swift/Sources/CKTap/cktap_ffi.swift +++ b/cktap-swift/Sources/CKTap/cktap_ffi.swift @@ -7,8 +7,8 @@ import Foundation // Depending on the consumer's build setup, the low-level FFI code // might be in a separate module, or it might be compiled inline into // this module. This is a bit of light hackery to work with both. -#if canImport(cktap_ffiFFI) -import cktap_ffiFFI +#if canImport(CKTapFFI) +import CKTapFFI #endif fileprivate extension RustBuffer { @@ -39,6 +39,52 @@ fileprivate extension ForeignBytes { init(bufferPointer: UnsafeBufferPointer) { self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) } + + init(rawBufferPointer: UnsafeRawBufferPointer) { + self.init( + len: Int32(rawBufferPointer.count), + data: rawBufferPointer.baseAddress?.assumingMemoryBound(to: UInt8.self) + ) + } +} + +// Converter for `&[u8]` / `[ByRef] bytes` arguments. +// +// Conforms to `FfiConverter` so the compiler enforces the full converter +// method set. Only the scope-bound `lower(_:_body:)` overload is sound — +// zero-copy byte buffers only flow foreign -> Rust, and only in argument +// position. The four protocol-witness methods (`lift`, `lower`, `read`, +// `write`) `fatalError` at runtime if anyone reaches them. +// +// The scope-bound `lower` takes a closure because the `ForeignBytes` +// pointer is only guaranteed valid for the duration of +// `Data.withUnsafeBytes`. Callers must run the full FFI call inside +// the closure body. +fileprivate enum FfiConverterByRefBytes: FfiConverter { + typealias SwiftType = Data + typealias FfiType = ForeignBytes + + static func lower(_ value: Data, _ body: (ForeignBytes) throws -> R) rethrows -> R { + return try value.withUnsafeBytes { rawBuf in + try body(ForeignBytes(rawBufferPointer: rawBuf)) + } + } + + static func lower(_ value: Data) -> ForeignBytes { + fatalError("ByRef bytes cannot use the plain lower: returning ForeignBytes escapes the Data.withUnsafeBytes scope. Use the scope-bound lower(_:_body:) overload instead.") + } + + static func lift(_ value: ForeignBytes) throws -> Data { + fatalError("ByRef bytes cannot be lifted: zero-copy &[u8] only flows foreign->Rust") + } + + static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Data { + fatalError("ByRef bytes cannot be read from a buffer: zero-copy &[u8] is only supported in argument position, not nested in records/options/etc.") + } + + static func write(_ value: Data, into buf: inout [UInt8]) { + fatalError("ByRef bytes cannot be written to a buffer: zero-copy &[u8] is only supported in argument position, not nested in records/options/etc.") + } } // For every type used in the interface, we provide helper methods for conveniently @@ -352,7 +398,7 @@ private func uniffiTraitInterfaceCallWithError( callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) } } -// Initial value and increment amount for handles. +// Initial value and increment amount for handles. // These ensure that SWIFT handles always have the lowest bit set fileprivate let UNIFFI_HANDLEMAP_INITIAL: UInt64 = 1 fileprivate let UNIFFI_HANDLEMAP_DELTA: UInt64 = 2 @@ -438,6 +484,22 @@ fileprivate struct FfiConverterUInt8: FfiConverterPrimitive { } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterUInt16: FfiConverterPrimitive { + typealias FfiType = UInt16 + typealias SwiftType = UInt16 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt16 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -493,7 +555,11 @@ fileprivate struct FfiConverterString: FfiConverter { return String() } let bytes = UnsafeBufferPointer(start: value.data!, count: Int(value.len)) - return String(bytes: bytes, encoding: String.Encoding.utf8)! + // Use Swift's native UTF-8 decoder; `String(bytes:encoding:.utf8)` goes + // through Foundation's NSString and silently strips a leading U+FEFF BOM. + // Invalid UTF-8 substitutes U+FFFD instead of trapping (unreachable + // given Rust's `String` invariant). + return String(decoding: bytes, as: UTF8.self) } public static func lower(_ value: String) -> RustBuffer { @@ -509,7 +575,8 @@ fileprivate struct FfiConverterString: FfiConverter { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> String { let len: Int32 = try readInt(&buf) - return String(bytes: try readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)! + // See `lift` above for why we avoid Foundation's NSString-backed decoder here. + return String(decoding: try readBytes(&buf, count: Int(len)), as: UTF8.self) } public static func write(_ value: String, into buf: inout [UInt8]) { @@ -541,55 +608,55 @@ fileprivate struct FfiConverterData: FfiConverterRustBuffer { public protocol SatsCardProtocol: AnyObject, Sendable { - + /** * Get the current active slot's receive address */ func address() async throws -> String - + /** * Verify the card has authentic Coinkite root certificate */ - func checkCert() async throws - + func checkCert() async throws + /** * This is only needed for debugging, use `sign_psbt` for signing * If no CVC given only pubkey and pubkey descriptor returned. */ func dump(slot: UInt8, cvc: String?) async throws -> SlotDetails - + /** * Open a new slot, it will be the current active but must be unused (no address) */ func newSlot(cvc: String) async throws -> UInt8 - + /** * Return the same URL as given with a NFC tap. */ func nfc() async throws -> String - + /** * Get the current active slot's wpkh public key descriptor */ func read() async throws -> String - + /** * Sign PSBT, base64 encoded */ func signPsbt(slot: UInt8, psbt: String, cvc: String) async throws -> String - + func status() async -> SatsCardStatus - + /** * Unseal currently active slot */ func unseal(cvc: String) async throws -> SlotDetails - + /** * Wait one second of auth delay and return the remaining delay, if any. */ func wait() async throws -> UInt8? - + } open class SatsCard: SatsCardProtocol, @unchecked Sendable { fileprivate let handle: UInt64 @@ -641,9 +708,9 @@ open class SatsCard: SatsCardProtocol, @unchecked Sendable { try! rustCall { uniffi_cktap_ffi_fn_free_satscard(handle, $0) } } - - + + /** * Get the current active slot's receive address */ @@ -652,8 +719,7 @@ open func address()async throws -> String { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satscard_address( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -663,7 +729,7 @@ open func address()async throws -> String { errorHandler: FfiConverterTypeReadError_lift ) } - + /** * Verify the card has authentic Coinkite root certificate */ @@ -672,8 +738,7 @@ open func checkCert()async throws { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satscard_check_cert( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_void, @@ -683,7 +748,7 @@ open func checkCert()async throws { errorHandler: FfiConverterTypeCertsError_lift ) } - + /** * This is only needed for debugging, use `sign_psbt` for signing * If no CVC given only pubkey and pubkey descriptor returned. @@ -693,8 +758,7 @@ open func dump(slot: UInt8, cvc: String?)async throws -> SlotDetails { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satscard_dump( - self.uniffiCloneHandle(), - FfiConverterUInt8.lower(slot),FfiConverterOptionString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterUInt8.lower(slot),FfiConverterOptionString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -704,7 +768,7 @@ open func dump(slot: UInt8, cvc: String?)async throws -> SlotDetails { errorHandler: FfiConverterTypeDumpError_lift ) } - + /** * Open a new slot, it will be the current active but must be unused (no address) */ @@ -713,8 +777,7 @@ open func newSlot(cvc: String)async throws -> UInt8 { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satscard_new_slot( - self.uniffiCloneHandle(), - FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_u8, @@ -724,7 +787,7 @@ open func newSlot(cvc: String)async throws -> UInt8 { errorHandler: FfiConverterTypeDeriveError_lift ) } - + /** * Return the same URL as given with a NFC tap. */ @@ -733,8 +796,7 @@ open func nfc()async throws -> String { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satscard_nfc( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -744,7 +806,7 @@ open func nfc()async throws -> String { errorHandler: FfiConverterTypeCkTapError_lift ) } - + /** * Get the current active slot's wpkh public key descriptor */ @@ -753,8 +815,7 @@ open func read()async throws -> String { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satscard_read( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -764,7 +825,7 @@ open func read()async throws -> String { errorHandler: FfiConverterTypeReadError_lift ) } - + /** * Sign PSBT, base64 encoded */ @@ -773,8 +834,7 @@ open func signPsbt(slot: UInt8, psbt: String, cvc: String)async throws -> Strin try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satscard_sign_psbt( - self.uniffiCloneHandle(), - FfiConverterUInt8.lower(slot),FfiConverterString.lower(psbt),FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterUInt8.lower(slot),FfiConverterString.lower(psbt),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -784,14 +844,13 @@ open func signPsbt(slot: UInt8, psbt: String, cvc: String)async throws -> Strin errorHandler: FfiConverterTypeSignPsbtError_lift ) } - + open func status()async -> SatsCardStatus { return try! await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satscard_status( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -799,10 +858,10 @@ open func status()async -> SatsCardStatus { freeFunc: ffi_cktap_ffi_rust_future_free_rust_buffer, liftFunc: FfiConverterTypeSatsCardStatus_lift, errorHandler: nil - + ) } - + /** * Unseal currently active slot */ @@ -811,8 +870,7 @@ open func unseal(cvc: String)async throws -> SlotDetails { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satscard_unseal( - self.uniffiCloneHandle(), - FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -822,7 +880,7 @@ open func unseal(cvc: String)async throws -> SlotDetails { errorHandler: FfiConverterTypeUnsealError_lift ) } - + /** * Wait one second of auth delay and return the remaining delay, if any. */ @@ -831,8 +889,7 @@ open func wait()async throws -> UInt8? { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satscard_wait( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -842,9 +899,9 @@ open func wait()async throws -> UInt8? { errorHandler: FfiConverterTypeCkTapError_lift ) } - - + + } @@ -884,7 +941,7 @@ public func FfiConverterTypeSatsCard_lift(_ handle: UInt64) throws -> SatsCard { #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeSatsCard_lower(_ value: SatsCard) -> UInt64 { +@Sendable public func FfiConverterTypeSatsCard_lower(_ value: SatsCard) -> UInt64 { return FfiConverterTypeSatsCard.lower(value) } @@ -894,27 +951,27 @@ public func FfiConverterTypeSatsCard_lower(_ value: SatsCard) -> UInt64 { public protocol SatsChipProtocol: AnyObject, Sendable { - - func change(newCvc: String, cvc: String) async throws - - func checkCert() async throws - + + func change(newCvc: String, cvc: String) async throws + + func checkCert() async throws + func derive(path: [UInt32], cvc: String) async throws -> String - - func `init`(cvc: String) async throws - + + func `init`(cvc: String) async throws + func nfc() async throws -> String - + func read() async throws -> String - + func signPsbt(psbt: String, cvc: String) async throws -> String - + func status() async -> SatsChipStatus - + func wait() async throws -> UInt8? - + func xpub(master: Bool, cvc: String) async throws -> String - + } open class SatsChip: SatsChipProtocol, @unchecked Sendable { fileprivate let handle: UInt64 @@ -966,16 +1023,15 @@ open class SatsChip: SatsChipProtocol, @unchecked Sendable { try! rustCall { uniffi_cktap_ffi_fn_free_satschip(handle, $0) } } - - + + open func change(newCvc: String, cvc: String)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_change( - self.uniffiCloneHandle(), - FfiConverterString.lower(newCvc),FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterString.lower(newCvc),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_void, @@ -985,14 +1041,13 @@ open func change(newCvc: String, cvc: String)async throws { errorHandler: FfiConverterTypeChangeError_lift ) } - + open func checkCert()async throws { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_check_cert( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_void, @@ -1002,14 +1057,13 @@ open func checkCert()async throws { errorHandler: FfiConverterTypeCertsError_lift ) } - + open func derive(path: [UInt32], cvc: String)async throws -> String { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_derive( - self.uniffiCloneHandle(), - FfiConverterSequenceUInt32.lower(path),FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterSequenceUInt32.lower(path),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1019,14 +1073,13 @@ open func derive(path: [UInt32], cvc: String)async throws -> String { errorHandler: FfiConverterTypeDeriveError_lift ) } - + open func `init`(cvc: String)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_init( - self.uniffiCloneHandle(), - FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_void, @@ -1036,14 +1089,13 @@ open func `init`(cvc: String)async throws { errorHandler: FfiConverterTypeCkTapError_lift ) } - + open func nfc()async throws -> String { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_nfc( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1053,14 +1105,13 @@ open func nfc()async throws -> String { errorHandler: FfiConverterTypeCkTapError_lift ) } - + open func read()async throws -> String { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_read( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1070,14 +1121,13 @@ open func read()async throws -> String { errorHandler: FfiConverterTypeReadError_lift ) } - + open func signPsbt(psbt: String, cvc: String)async throws -> String { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_sign_psbt( - self.uniffiCloneHandle(), - FfiConverterString.lower(psbt),FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterString.lower(psbt),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1087,14 +1137,13 @@ open func signPsbt(psbt: String, cvc: String)async throws -> String { errorHandler: FfiConverterTypeSignPsbtError_lift ) } - + open func status()async -> SatsChipStatus { return try! await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_status( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1102,17 +1151,16 @@ open func status()async -> SatsChipStatus { freeFunc: ffi_cktap_ffi_rust_future_free_rust_buffer, liftFunc: FfiConverterTypeSatsChipStatus_lift, errorHandler: nil - + ) } - + open func wait()async throws -> UInt8? { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_wait( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1122,14 +1170,13 @@ open func wait()async throws -> UInt8? { errorHandler: FfiConverterTypeCkTapError_lift ) } - + open func xpub(master: Bool, cvc: String)async throws -> String { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_xpub( - self.uniffiCloneHandle(), - FfiConverterBool.lower(master),FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterBool.lower(master),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1139,9 +1186,9 @@ open func xpub(master: Bool, cvc: String)async throws -> String { errorHandler: FfiConverterTypeXpubError_lift ) } - - + + } @@ -1181,7 +1228,7 @@ public func FfiConverterTypeSatsChip_lift(_ handle: UInt64) throws -> SatsChip { #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeSatsChip_lower(_ value: SatsChip) -> UInt64 { +@Sendable public func FfiConverterTypeSatsChip_lower(_ value: SatsChip) -> UInt64 { return FfiConverterTypeSatsChip.lower(value) } @@ -1191,27 +1238,27 @@ public func FfiConverterTypeSatsChip_lower(_ value: SatsChip) -> UInt64 { public protocol TapSignerProtocol: AnyObject, Sendable { - - func change(newCvc: String, cvc: String) async throws - - func checkCert() async throws - + + func change(newCvc: String, cvc: String) async throws + + func checkCert() async throws + func derive(path: [UInt32], cvc: String) async throws -> String - - func `init`(cvc: String) async throws - + + func `init`(cvc: String) async throws + func nfc() async throws -> String - + func read(cvc: String) async throws -> String - + func signPsbt(psbt: String, cvc: String) async throws -> String - + func status() async -> TapSignerStatus - + func wait() async throws -> UInt8? - + func xpub(master: Bool, cvc: String) async throws -> String - + } open class TapSigner: TapSignerProtocol, @unchecked Sendable { fileprivate let handle: UInt64 @@ -1263,16 +1310,15 @@ open class TapSigner: TapSignerProtocol, @unchecked Sendable { try! rustCall { uniffi_cktap_ffi_fn_free_tapsigner(handle, $0) } } - - + + open func change(newCvc: String, cvc: String)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_change( - self.uniffiCloneHandle(), - FfiConverterString.lower(newCvc),FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterString.lower(newCvc),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_void, @@ -1282,14 +1328,13 @@ open func change(newCvc: String, cvc: String)async throws { errorHandler: FfiConverterTypeChangeError_lift ) } - + open func checkCert()async throws { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_check_cert( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_void, @@ -1299,14 +1344,13 @@ open func checkCert()async throws { errorHandler: FfiConverterTypeCertsError_lift ) } - + open func derive(path: [UInt32], cvc: String)async throws -> String { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_derive( - self.uniffiCloneHandle(), - FfiConverterSequenceUInt32.lower(path),FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterSequenceUInt32.lower(path),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1316,14 +1360,13 @@ open func derive(path: [UInt32], cvc: String)async throws -> String { errorHandler: FfiConverterTypeDeriveError_lift ) } - + open func `init`(cvc: String)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_init( - self.uniffiCloneHandle(), - FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_void, @@ -1333,14 +1376,13 @@ open func `init`(cvc: String)async throws { errorHandler: FfiConverterTypeCkTapError_lift ) } - + open func nfc()async throws -> String { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_nfc( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1350,14 +1392,13 @@ open func nfc()async throws -> String { errorHandler: FfiConverterTypeCkTapError_lift ) } - + open func read(cvc: String)async throws -> String { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_read( - self.uniffiCloneHandle(), - FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1367,14 +1408,13 @@ open func read(cvc: String)async throws -> String { errorHandler: FfiConverterTypeReadError_lift ) } - + open func signPsbt(psbt: String, cvc: String)async throws -> String { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_sign_psbt( - self.uniffiCloneHandle(), - FfiConverterString.lower(psbt),FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterString.lower(psbt),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1384,14 +1424,13 @@ open func signPsbt(psbt: String, cvc: String)async throws -> String { errorHandler: FfiConverterTypeSignPsbtError_lift ) } - + open func status()async -> TapSignerStatus { return try! await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_status( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1399,17 +1438,16 @@ open func status()async -> TapSignerStatus { freeFunc: ffi_cktap_ffi_rust_future_free_rust_buffer, liftFunc: FfiConverterTypeTapSignerStatus_lift, errorHandler: nil - + ) } - + open func wait()async throws -> UInt8? { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_wait( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1419,14 +1457,13 @@ open func wait()async throws -> UInt8? { errorHandler: FfiConverterTypeCkTapError_lift ) } - + open func xpub(master: Bool, cvc: String)async throws -> String { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_xpub( - self.uniffiCloneHandle(), - FfiConverterBool.lower(master),FfiConverterString.lower(cvc) + self.uniffiCloneHandle(),FfiConverterBool.lower(master),FfiConverterString.lower(cvc) ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1436,9 +1473,9 @@ open func xpub(master: Bool, cvc: String)async throws -> String { errorHandler: FfiConverterTypeXpubError_lift ) } - - + + } @@ -1478,7 +1515,7 @@ public func FfiConverterTypeTapSigner_lift(_ handle: UInt64) throws -> TapSigner #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeTapSigner_lower(_ value: TapSigner) -> UInt64 { +@Sendable public func FfiConverterTypeTapSigner_lower(_ value: TapSigner) -> UInt64 { return FfiConverterTypeTapSigner.lower(value) } @@ -1510,9 +1547,9 @@ public struct SatsCardStatus: Equatable, Hashable { self.authDelay = authDelay } - - + + } #if compiler(>=6) @@ -1526,14 +1563,14 @@ public struct FfiConverterTypeSatsCardStatus: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SatsCardStatus { return try SatsCardStatus( - proto: FfiConverterUInt32.read(from: &buf), - ver: FfiConverterString.read(from: &buf), - birth: FfiConverterUInt32.read(from: &buf), - activeSlot: FfiConverterUInt8.read(from: &buf), - numSlots: FfiConverterUInt8.read(from: &buf), - addr: FfiConverterOptionString.read(from: &buf), - pubkey: FfiConverterString.read(from: &buf), - cardIdent: FfiConverterString.read(from: &buf), + proto: FfiConverterUInt32.read(from: &buf), + ver: FfiConverterString.read(from: &buf), + birth: FfiConverterUInt32.read(from: &buf), + activeSlot: FfiConverterUInt8.read(from: &buf), + numSlots: FfiConverterUInt8.read(from: &buf), + addr: FfiConverterOptionString.read(from: &buf), + pubkey: FfiConverterString.read(from: &buf), + cardIdent: FfiConverterString.read(from: &buf), authDelay: FfiConverterOptionUInt8.read(from: &buf) ) } @@ -1562,7 +1599,7 @@ public func FfiConverterTypeSatsCardStatus_lift(_ buf: RustBuffer) throws -> Sat #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeSatsCardStatus_lower(_ value: SatsCardStatus) -> RustBuffer { +@Sendable public func FfiConverterTypeSatsCardStatus_lower(_ value: SatsCardStatus) -> RustBuffer { return FfiConverterTypeSatsCardStatus.lower(value) } @@ -1588,9 +1625,9 @@ public struct SatsChipStatus: Equatable, Hashable { self.authDelay = authDelay } - - + + } #if compiler(>=6) @@ -1604,12 +1641,12 @@ public struct FfiConverterTypeSatsChipStatus: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SatsChipStatus { return try SatsChipStatus( - proto: FfiConverterUInt32.read(from: &buf), - ver: FfiConverterString.read(from: &buf), - birth: FfiConverterUInt32.read(from: &buf), - path: FfiConverterOptionSequenceUInt32.read(from: &buf), - pubkey: FfiConverterString.read(from: &buf), - cardIdent: FfiConverterString.read(from: &buf), + proto: FfiConverterUInt32.read(from: &buf), + ver: FfiConverterString.read(from: &buf), + birth: FfiConverterUInt32.read(from: &buf), + path: FfiConverterOptionSequenceUInt32.read(from: &buf), + pubkey: FfiConverterString.read(from: &buf), + cardIdent: FfiConverterString.read(from: &buf), authDelay: FfiConverterOptionUInt8.read(from: &buf) ) } @@ -1636,7 +1673,7 @@ public func FfiConverterTypeSatsChipStatus_lift(_ buf: RustBuffer) throws -> Sat #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeSatsChipStatus_lower(_ value: SatsChipStatus) -> RustBuffer { +@Sendable public func FfiConverterTypeSatsChipStatus_lower(_ value: SatsChipStatus) -> RustBuffer { return FfiConverterTypeSatsChipStatus.lower(value) } @@ -1654,9 +1691,9 @@ public struct SlotDetails: Equatable, Hashable { self.pubkeyDescriptor = pubkeyDescriptor } - - + + } #if compiler(>=6) @@ -1670,8 +1707,8 @@ public struct FfiConverterTypeSlotDetails: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SlotDetails { return try SlotDetails( - privkey: FfiConverterOptionString.read(from: &buf), - pubkey: FfiConverterString.read(from: &buf), + privkey: FfiConverterOptionString.read(from: &buf), + pubkey: FfiConverterString.read(from: &buf), pubkeyDescriptor: FfiConverterString.read(from: &buf) ) } @@ -1694,7 +1731,7 @@ public func FfiConverterTypeSlotDetails_lift(_ buf: RustBuffer) throws -> SlotDe #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeSlotDetails_lower(_ value: SlotDetails) -> RustBuffer { +@Sendable public func FfiConverterTypeSlotDetails_lower(_ value: SlotDetails) -> RustBuffer { return FfiConverterTypeSlotDetails.lower(value) } @@ -1722,9 +1759,9 @@ public struct TapSignerStatus: Equatable, Hashable { self.authDelay = authDelay } - - + + } #if compiler(>=6) @@ -1738,13 +1775,13 @@ public struct FfiConverterTypeTapSignerStatus: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TapSignerStatus { return try TapSignerStatus( - proto: FfiConverterUInt32.read(from: &buf), - ver: FfiConverterString.read(from: &buf), - birth: FfiConverterUInt32.read(from: &buf), - path: FfiConverterOptionSequenceUInt32.read(from: &buf), - numBackups: FfiConverterUInt32.read(from: &buf), - pubkey: FfiConverterString.read(from: &buf), - cardIdent: FfiConverterString.read(from: &buf), + proto: FfiConverterUInt32.read(from: &buf), + ver: FfiConverterString.read(from: &buf), + birth: FfiConverterUInt32.read(from: &buf), + path: FfiConverterOptionSequenceUInt32.read(from: &buf), + numBackups: FfiConverterUInt32.read(from: &buf), + pubkey: FfiConverterString.read(from: &buf), + cardIdent: FfiConverterString.read(from: &buf), authDelay: FfiConverterOptionUInt8.read(from: &buf) ) } @@ -1772,7 +1809,7 @@ public func FfiConverterTypeTapSignerStatus_lift(_ buf: RustBuffer) throws -> Ta #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeTapSignerStatus_lower(_ value: TapSignerStatus) -> RustBuffer { +@Sendable public func FfiConverterTypeTapSignerStatus_lower(_ value: TapSignerStatus) -> RustBuffer { return FfiConverterTypeTapSignerStatus.lower(value) } @@ -1780,11 +1817,11 @@ public func FfiConverterTypeTapSignerStatus_lower(_ value: TapSignerStatus) -> R /** * Errors returned by the CkTap card. */ -public +public enum CardError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case UnluckyNumber case BadArguments case BadAuth @@ -1797,15 +1834,15 @@ enum CardError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { case BackupFirst case RateLimited - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -1822,9 +1859,9 @@ public struct FfiConverterTypeCardError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .UnluckyNumber case 2: return .BadArguments case 3: return .BadAuth @@ -1844,53 +1881,53 @@ public struct FfiConverterTypeCardError: FfiConverterRustBuffer { public static func write(_ value: CardError, into buf: inout [UInt8]) { switch value { - - - + + + case .UnluckyNumber: writeInt(&buf, Int32(1)) - - + + case .BadArguments: writeInt(&buf, Int32(2)) - - + + case .BadAuth: writeInt(&buf, Int32(3)) - - + + case .NeedsAuth: writeInt(&buf, Int32(4)) - - + + case .UnknownCommand: writeInt(&buf, Int32(5)) - - + + case .InvalidCommand: writeInt(&buf, Int32(6)) - - + + case .InvalidState: writeInt(&buf, Int32(7)) - - + + case .WeakNonce: writeInt(&buf, Int32(8)) - - + + case .BadCbor: writeInt(&buf, Int32(9)) - - + + case .BackupFirst: writeInt(&buf, Int32(10)) - - + + case .RateLimited: writeInt(&buf, Int32(11)) - + } } } @@ -1906,7 +1943,7 @@ public func FfiConverterTypeCardError_lift(_ buf: RustBuffer) throws -> CardErro #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeCardError_lower(_ value: CardError) -> RustBuffer { +@Sendable public func FfiConverterTypeCardError_lower(_ value: CardError) -> RustBuffer { return FfiConverterTypeCardError.lower(value) } @@ -1914,11 +1951,11 @@ public func FfiConverterTypeCardError_lower(_ value: CardError) -> RustBuffer { /** * Errors returned by the `certs` command. */ -public +public enum CertsError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Key(err: KeyError @@ -1926,15 +1963,15 @@ enum CertsError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { case InvalidRootCert(msg: String ) - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -1951,9 +1988,9 @@ public struct FfiConverterTypeCertsError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) @@ -1971,24 +2008,24 @@ public struct FfiConverterTypeCertsError: FfiConverterRustBuffer { public static func write(_ value: CertsError, into buf: inout [UInt8]) { switch value { - - - + + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - - + + case let .InvalidRootCert(msg): writeInt(&buf, Int32(3)) FfiConverterString.write(msg, into: &buf) - + } } } @@ -2004,7 +2041,7 @@ public func FfiConverterTypeCertsError_lift(_ buf: RustBuffer) throws -> CertsEr #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeCertsError_lower(_ value: CertsError) -> RustBuffer { +@Sendable public func FfiConverterTypeCertsError_lower(_ value: CertsError) -> RustBuffer { return FfiConverterTypeCertsError.lower(value) } @@ -2012,28 +2049,24 @@ public func FfiConverterTypeCertsError_lower(_ value: CertsError) -> RustBuffer /** * Errors returned by the `change` command. */ -public +public enum ChangeError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) - case TooShort(len: UInt32 - ) - case TooLong(len: UInt32 - ) case SameAsOld - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2050,19 +2083,13 @@ public struct FfiConverterTypeChangeError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) - case 2: return .TooShort( - len: try FfiConverterUInt32.read(from: &buf) - ) - case 3: return .TooLong( - len: try FfiConverterUInt32.read(from: &buf) - ) - case 4: return .SameAsOld + case 2: return .SameAsOld default: throw UniffiInternalError.unexpectedEnumCase } @@ -2071,28 +2098,18 @@ public struct FfiConverterTypeChangeError: FfiConverterRustBuffer { public static func write(_ value: ChangeError, into buf: inout [UInt8]) { switch value { - - - + + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - - case let .TooShort(len): - writeInt(&buf, Int32(2)) - FfiConverterUInt32.write(len, into: &buf) - - - case let .TooLong(len): - writeInt(&buf, Int32(3)) - FfiConverterUInt32.write(len, into: &buf) - - + + case .SameAsOld: - writeInt(&buf, Int32(4)) - + writeInt(&buf, Int32(2)) + } } } @@ -2108,14 +2125,14 @@ public func FfiConverterTypeChangeError_lift(_ buf: RustBuffer) throws -> Change #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeChangeError_lower(_ value: ChangeError) -> RustBuffer { +@Sendable public func FfiConverterTypeChangeError_lower(_ value: ChangeError) -> RustBuffer { return FfiConverterTypeChangeError.lower(value) } public enum CkTapCard { - + case satsCard(SatsCard ) case tapSigner(TapSigner @@ -2142,38 +2159,38 @@ public struct FfiConverterTypeCkTapCard: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> CkTapCard { let variant: Int32 = try readInt(&buf) switch variant { - + case 1: return .satsCard(try FfiConverterTypeSatsCard.read(from: &buf) ) - + case 2: return .tapSigner(try FfiConverterTypeTapSigner.read(from: &buf) ) - + case 3: return .satsChip(try FfiConverterTypeSatsChip.read(from: &buf) ) - + default: throw UniffiInternalError.unexpectedEnumCase } } public static func write(_ value: CkTapCard, into buf: inout [UInt8]) { switch value { - - + + case let .satsCard(v1): writeInt(&buf, Int32(1)) FfiConverterTypeSatsCard.write(v1, into: &buf) - - + + case let .tapSigner(v1): writeInt(&buf, Int32(2)) FfiConverterTypeTapSigner.write(v1, into: &buf) - - + + case let .satsChip(v1): writeInt(&buf, Int32(3)) FfiConverterTypeSatsChip.write(v1, into: &buf) - + } } } @@ -2189,7 +2206,7 @@ public func FfiConverterTypeCkTapCard_lift(_ buf: RustBuffer) throws -> CkTapCar #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeCkTapCard_lower(_ value: CkTapCard) -> RustBuffer { +@Sendable public func FfiConverterTypeCkTapCard_lower(_ value: CkTapCard) -> RustBuffer { return FfiConverterTypeCkTapCard.lower(value) } @@ -2198,11 +2215,11 @@ public func FfiConverterTypeCkTapCard_lower(_ value: CkTapCard) -> RustBuffer { /** * Errors returned by the card, CBOR deserialization or value encoding, or the APDU transport. */ -public +public enum CkTapError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case Card(err: CardError ) case CborDe(msg: String @@ -2211,17 +2228,19 @@ enum CkTapError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { ) case Transport(msg: String ) + case UnknownStatusWord(code: UInt16, message: String + ) case UnknownCardType - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2238,9 +2257,9 @@ public struct FfiConverterTypeCkTapError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .Card( err: try FfiConverterTypeCardError.read(from: &buf) ) @@ -2253,7 +2272,11 @@ public struct FfiConverterTypeCkTapError: FfiConverterRustBuffer { case 4: return .Transport( msg: try FfiConverterString.read(from: &buf) ) - case 5: return .UnknownCardType + case 5: return .UnknownStatusWord( + code: try FfiConverterUInt16.read(from: &buf), + message: try FfiConverterString.read(from: &buf) + ) + case 6: return .UnknownCardType default: throw UniffiInternalError.unexpectedEnumCase } @@ -2262,33 +2285,39 @@ public struct FfiConverterTypeCkTapError: FfiConverterRustBuffer { public static func write(_ value: CkTapError, into buf: inout [UInt8]) { switch value { - - - + + + case let .Card(err): writeInt(&buf, Int32(1)) FfiConverterTypeCardError.write(err, into: &buf) - - + + case let .CborDe(msg): writeInt(&buf, Int32(2)) FfiConverterString.write(msg, into: &buf) - - + + case let .CborValue(msg): writeInt(&buf, Int32(3)) FfiConverterString.write(msg, into: &buf) - - + + case let .Transport(msg): writeInt(&buf, Int32(4)) FfiConverterString.write(msg, into: &buf) - - - case .UnknownCardType: + + + case let .UnknownStatusWord(code,message): writeInt(&buf, Int32(5)) - + FfiConverterUInt16.write(code, into: &buf) + FfiConverterString.write(message, into: &buf) + + + case .UnknownCardType: + writeInt(&buf, Int32(6)) + } } } @@ -2304,7 +2333,7 @@ public func FfiConverterTypeCkTapError_lift(_ buf: RustBuffer) throws -> CkTapEr #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeCkTapError_lower(_ value: CkTapError) -> RustBuffer { +@Sendable public func FfiConverterTypeCkTapError_lower(_ value: CkTapError) -> RustBuffer { return FfiConverterTypeCkTapError.lower(value) } @@ -2312,11 +2341,11 @@ public func FfiConverterTypeCkTapError_lower(_ value: CkTapError) -> RustBuffer /** * Errors returned by the `derive` command. */ -public +public enum DeriveError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Key(err: KeyError @@ -2324,15 +2353,15 @@ enum DeriveError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { case InvalidChainCode(msg: String ) - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2349,9 +2378,9 @@ public struct FfiConverterTypeDeriveError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) @@ -2369,24 +2398,24 @@ public struct FfiConverterTypeDeriveError: FfiConverterRustBuffer { public static func write(_ value: DeriveError, into buf: inout [UInt8]) { switch value { - - - + + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - - + + case let .InvalidChainCode(msg): writeInt(&buf, Int32(3)) FfiConverterString.write(msg, into: &buf) - + } } } @@ -2402,7 +2431,7 @@ public func FfiConverterTypeDeriveError_lift(_ buf: RustBuffer) throws -> Derive #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeDeriveError_lower(_ value: DeriveError) -> RustBuffer { +@Sendable public func FfiConverterTypeDeriveError_lower(_ value: DeriveError) -> RustBuffer { return FfiConverterTypeDeriveError.lower(value) } @@ -2410,11 +2439,11 @@ public func FfiConverterTypeDeriveError_lower(_ value: DeriveError) -> RustBuffe /** * Errors returned by the `dump` command. */ -public +public enum DumpError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Key(err: KeyError @@ -2431,15 +2460,15 @@ enum DumpError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { case SlotTampered(slot: UInt8 ) - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2456,9 +2485,9 @@ public struct FfiConverterTypeDumpError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) @@ -2482,34 +2511,34 @@ public struct FfiConverterTypeDumpError: FfiConverterRustBuffer { public static func write(_ value: DumpError, into buf: inout [UInt8]) { switch value { - - - + + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - - + + case let .SlotSealed(slot): writeInt(&buf, Int32(3)) FfiConverterUInt8.write(slot, into: &buf) - - + + case let .SlotUnused(slot): writeInt(&buf, Int32(4)) FfiConverterUInt8.write(slot, into: &buf) - - + + case let .SlotTampered(slot): writeInt(&buf, Int32(5)) FfiConverterUInt8.write(slot, into: &buf) - + } } } @@ -2525,30 +2554,30 @@ public func FfiConverterTypeDumpError_lift(_ buf: RustBuffer) throws -> DumpErro #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeDumpError_lower(_ value: DumpError) -> RustBuffer { +@Sendable public func FfiConverterTypeDumpError_lower(_ value: DumpError) -> RustBuffer { return FfiConverterTypeDumpError.lower(value) } -public +public enum KeyError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case Secp256k1(msg: String ) case KeyFromSlice(msg: String ) - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2565,9 +2594,9 @@ public struct FfiConverterTypeKeyError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .Secp256k1( msg: try FfiConverterString.read(from: &buf) ) @@ -2582,19 +2611,19 @@ public struct FfiConverterTypeKeyError: FfiConverterRustBuffer { public static func write(_ value: KeyError, into buf: inout [UInt8]) { switch value { - - - + + + case let .Secp256k1(msg): writeInt(&buf, Int32(1)) FfiConverterString.write(msg, into: &buf) - - + + case let .KeyFromSlice(msg): writeInt(&buf, Int32(2)) FfiConverterString.write(msg, into: &buf) - + } } } @@ -2610,7 +2639,7 @@ public func FfiConverterTypeKeyError_lift(_ buf: RustBuffer) throws -> KeyError #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeKeyError_lower(_ value: KeyError) -> RustBuffer { +@Sendable public func FfiConverterTypeKeyError_lower(_ value: KeyError) -> RustBuffer { return FfiConverterTypeKeyError.lower(value) } @@ -2618,25 +2647,25 @@ public func FfiConverterTypeKeyError_lower(_ value: KeyError) -> RustBuffer { /** * Errors returned by the `read` command. */ -public +public enum ReadError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Key(err: KeyError ) - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2653,9 +2682,9 @@ public struct FfiConverterTypeReadError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) @@ -2670,19 +2699,19 @@ public struct FfiConverterTypeReadError: FfiConverterRustBuffer { public static func write(_ value: ReadError, into buf: inout [UInt8]) { switch value { - - - + + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - + } } } @@ -2698,16 +2727,16 @@ public func FfiConverterTypeReadError_lift(_ buf: RustBuffer) throws -> ReadErro #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeReadError_lower(_ value: ReadError) -> RustBuffer { +@Sendable public func FfiConverterTypeReadError_lower(_ value: ReadError) -> RustBuffer { return FfiConverterTypeReadError.lower(value) } -public +public enum SignPsbtError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case InvalidPath(index: UInt32 ) case InvalidScript(index: UInt32 @@ -2733,15 +2762,15 @@ enum SignPsbtError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError case Base64Encoding(msg: String ) - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2758,9 +2787,9 @@ public struct FfiConverterTypeSignPsbtError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .InvalidPath( index: try FfiConverterUInt32.read(from: &buf) ) @@ -2805,69 +2834,69 @@ public struct FfiConverterTypeSignPsbtError: FfiConverterRustBuffer { public static func write(_ value: SignPsbtError, into buf: inout [UInt8]) { switch value { - - - + + + case let .InvalidPath(index): writeInt(&buf, Int32(1)) FfiConverterUInt32.write(index, into: &buf) - - + + case let .InvalidScript(index): writeInt(&buf, Int32(2)) FfiConverterUInt32.write(index, into: &buf) - - + + case let .MissingPubkey(index): writeInt(&buf, Int32(3)) FfiConverterUInt32.write(index, into: &buf) - - + + case let .MissingUtxo(index): writeInt(&buf, Int32(4)) FfiConverterUInt32.write(index, into: &buf) - - + + case let .PubkeyMismatch(index): writeInt(&buf, Int32(5)) FfiConverterUInt32.write(index, into: &buf) - - + + case let .SighashError(msg): writeInt(&buf, Int32(6)) FfiConverterString.write(msg, into: &buf) - - + + case let .SignatureError(msg): writeInt(&buf, Int32(7)) FfiConverterString.write(msg, into: &buf) - - + + case let .SlotNotUnsealed(slot): writeInt(&buf, Int32(8)) FfiConverterUInt8.write(slot, into: &buf) - - + + case let .CkTap(err): writeInt(&buf, Int32(9)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .WitnessProgram(msg): writeInt(&buf, Int32(10)) FfiConverterString.write(msg, into: &buf) - - + + case let .PsbtEncoding(msg): writeInt(&buf, Int32(11)) FfiConverterString.write(msg, into: &buf) - - + + case let .Base64Encoding(msg): writeInt(&buf, Int32(12)) FfiConverterString.write(msg, into: &buf) - + } } } @@ -2883,7 +2912,7 @@ public func FfiConverterTypeSignPsbtError_lift(_ buf: RustBuffer) throws -> Sign #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeSignPsbtError_lower(_ value: SignPsbtError) -> RustBuffer { +@Sendable public func FfiConverterTypeSignPsbtError_lower(_ value: SignPsbtError) -> RustBuffer { return FfiConverterTypeSignPsbtError.lower(value) } @@ -2891,25 +2920,25 @@ public func FfiConverterTypeSignPsbtError_lower(_ value: SignPsbtError) -> RustB /** * Errors returned by the `status` command. */ -public +public enum StatusError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Key(err: KeyError ) - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2926,9 +2955,9 @@ public struct FfiConverterTypeStatusError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) @@ -2943,19 +2972,19 @@ public struct FfiConverterTypeStatusError: FfiConverterRustBuffer { public static func write(_ value: StatusError, into buf: inout [UInt8]) { switch value { - - - + + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - + } } } @@ -2971,7 +3000,7 @@ public func FfiConverterTypeStatusError_lift(_ buf: RustBuffer) throws -> Status #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeStatusError_lower(_ value: StatusError) -> RustBuffer { +@Sendable public func FfiConverterTypeStatusError_lower(_ value: StatusError) -> RustBuffer { return FfiConverterTypeStatusError.lower(value) } @@ -2979,25 +3008,25 @@ public func FfiConverterTypeStatusError_lower(_ value: StatusError) -> RustBuffe /** * Errors returned by the `unseal` command. */ -public +public enum UnsealError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Key(err: KeyError ) - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -3014,9 +3043,9 @@ public struct FfiConverterTypeUnsealError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) @@ -3031,19 +3060,19 @@ public struct FfiConverterTypeUnsealError: FfiConverterRustBuffer { public static func write(_ value: UnsealError, into buf: inout [UInt8]) { switch value { - - - + + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - + } } } @@ -3059,7 +3088,7 @@ public func FfiConverterTypeUnsealError_lift(_ buf: RustBuffer) throws -> Unseal #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeUnsealError_lower(_ value: UnsealError) -> RustBuffer { +@Sendable public func FfiConverterTypeUnsealError_lower(_ value: UnsealError) -> RustBuffer { return FfiConverterTypeUnsealError.lower(value) } @@ -3067,25 +3096,25 @@ public func FfiConverterTypeUnsealError_lower(_ value: UnsealError) -> RustBuffe /** * Errors returned by the `xpub` command. */ -public +public enum XpubError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Bip32(msg: String ) - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -3102,9 +3131,9 @@ public struct FfiConverterTypeXpubError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) @@ -3119,19 +3148,19 @@ public struct FfiConverterTypeXpubError: FfiConverterRustBuffer { public static func write(_ value: XpubError, into buf: inout [UInt8]) { switch value { - - - + + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Bip32(msg): writeInt(&buf, Int32(2)) FfiConverterString.write(msg, into: &buf) - + } } } @@ -3147,7 +3176,7 @@ public func FfiConverterTypeXpubError_lift(_ buf: RustBuffer) throws -> XpubErro #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeXpubError_lower(_ value: XpubError) -> RustBuffer { +@Sendable public func FfiConverterTypeXpubError_lower(_ value: XpubError) -> RustBuffer { return FfiConverterTypeXpubError.lower(value) } @@ -3155,9 +3184,9 @@ public func FfiConverterTypeXpubError_lower(_ value: XpubError) -> RustBuffer { public protocol CkTransport: AnyObject, Sendable { - + func transmitApdu(commandApdu: Data) async throws -> Data - + } @@ -3190,7 +3219,9 @@ fileprivate struct UniffiCallbackInterfaceCkTransport { uniffiCallbackData: UInt64, uniffiOutDroppedCallback: UnsafeMutablePointer ) in - let makeCall = { + nonisolated(unsafe) let commandApdu = commandApdu + + let makeCall: @Sendable () async throws -> Data = { () async throws -> Data in guard let uniffiObj = try? FfiConverterCallbackInterfaceCkTransport.handleMap.get(handle: uniffiHandle) else { throw UniffiInternalError.unexpectedStaleHandle @@ -3200,7 +3231,7 @@ fileprivate struct UniffiCallbackInterfaceCkTransport { ) } - let uniffiHandleSuccess = { (returnValue: Data) in + let uniffiHandleSuccess: @Sendable (Data) -> () = { (returnValue) in uniffiFutureCallback( uniffiCallbackData, UniffiForeignFutureResultRustBuffer( @@ -3209,7 +3240,7 @@ fileprivate struct UniffiCallbackInterfaceCkTransport { ) ) } - let uniffiHandleError = { (statusCode, errorBuf) in + let uniffiHandleError: @Sendable (Int8, RustBuffer) -> () = { (statusCode, errorBuf) in uniffiFutureCallback( uniffiCallbackData, UniffiForeignFutureResultRustBuffer( @@ -3230,7 +3261,11 @@ fileprivate struct UniffiCallbackInterfaceCkTransport { // Rust stores this pointer for future callback invocations, so it must live // for the process lifetime (not just for the init function call). - static let vtablePtr: UnsafePointer = { + // + // `nonisolated(unsafe)` is needed under Swift 6 strict concurrency. + // This is safe because the pointee is initialized once during static init + // and never mutated by either side of the FFI. Its fields are C function pointers. + nonisolated(unsafe) static let vtablePtr: UnsafePointer = { let ptr = UnsafeMutablePointer.allocate(capacity: 1) ptr.initialize(to: vtable) return UnsafePointer(ptr) @@ -3297,7 +3332,7 @@ public func FfiConverterCallbackInterfaceCkTransport_lift(_ handle: UInt64) thro #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterCallbackInterfaceCkTransport_lower(_ v: CkTransport) -> UInt64 { +@Sendable public func FfiConverterCallbackInterfaceCkTransport_lower(_ v: CkTransport) -> UInt64 { return FfiConverterCallbackInterfaceCkTransport.lower(v) } @@ -3446,9 +3481,9 @@ fileprivate func uniffiFutureContinuationCallback(handle: UInt64, pollResult: In } } private func uniffiTraitInterfaceCallAsync( - makeCall: @escaping () async throws -> T, - handleSuccess: @escaping (T) -> (), - handleError: @escaping (Int8, RustBuffer) -> (), + makeCall: @escaping @Sendable () async throws -> T, + handleSuccess: @escaping @Sendable (T) -> (), + handleError: @escaping @Sendable (Int8, RustBuffer) -> (), droppedCallback: UnsafeMutablePointer ) { let task = Task { @@ -3477,10 +3512,10 @@ private func uniffiTraitInterfaceCallAsync( } private func uniffiTraitInterfaceCallAsyncWithError( - makeCall: @escaping () async throws -> T, - handleSuccess: @escaping (T) -> (), - handleError: @escaping (Int8, RustBuffer) -> (), - lowerError: @escaping (E) -> RustBuffer, + makeCall: @escaping @Sendable () async throws -> T, + handleSuccess: @escaping @Sendable (T) -> (), + handleError: @escaping @Sendable (Int8, RustBuffer) -> (), + lowerError: @escaping @Sendable (E) -> RustBuffer, droppedCallback: UnsafeMutablePointer ) { let task = Task { @@ -3565,100 +3600,100 @@ private let initializationResult: InitializationResult = { if bindings_contract_version != scaffolding_contract_version { return InitializationResult.contractVersionMismatch } - if (uniffi_cktap_ffi_checksum_func_to_cktap() != 32899) { + if (uniffi_cktap_ffi_checksum_func_to_cktap() != 4207) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satscard_address() != 37827) { + if (uniffi_cktap_ffi_checksum_method_satscard_address() != 37022) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satscard_check_cert() != 25375) { + if (uniffi_cktap_ffi_checksum_method_satscard_check_cert() != 28398) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satscard_dump() != 20225) { + if (uniffi_cktap_ffi_checksum_method_satscard_dump() != 26387) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satscard_new_slot() != 360) { + if (uniffi_cktap_ffi_checksum_method_satscard_new_slot() != 38063) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satscard_nfc() != 5150) { + if (uniffi_cktap_ffi_checksum_method_satscard_nfc() != 48066) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satscard_read() != 18530) { + if (uniffi_cktap_ffi_checksum_method_satscard_read() != 14653) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satscard_sign_psbt() != 16908) { + if (uniffi_cktap_ffi_checksum_method_satscard_sign_psbt() != 6382) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satscard_status() != 2484) { + if (uniffi_cktap_ffi_checksum_method_satscard_status() != 59572) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satscard_unseal() != 18864) { + if (uniffi_cktap_ffi_checksum_method_satscard_unseal() != 60237) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satscard_wait() != 42374) { + if (uniffi_cktap_ffi_checksum_method_satscard_wait() != 34368) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satschip_change() != 64965) { + if (uniffi_cktap_ffi_checksum_method_satschip_change() != 5196) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satschip_check_cert() != 22418) { + if (uniffi_cktap_ffi_checksum_method_satschip_check_cert() != 59615) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satschip_derive() != 35847) { + if (uniffi_cktap_ffi_checksum_method_satschip_derive() != 50805) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satschip_init() != 43462) { + if (uniffi_cktap_ffi_checksum_method_satschip_init() != 498) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satschip_nfc() != 32869) { + if (uniffi_cktap_ffi_checksum_method_satschip_nfc() != 27858) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satschip_read() != 49709) { + if (uniffi_cktap_ffi_checksum_method_satschip_read() != 2044) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satschip_sign_psbt() != 141) { + if (uniffi_cktap_ffi_checksum_method_satschip_sign_psbt() != 29016) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satschip_status() != 7960) { + if (uniffi_cktap_ffi_checksum_method_satschip_status() != 34606) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satschip_wait() != 6345) { + if (uniffi_cktap_ffi_checksum_method_satschip_wait() != 10940) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satschip_xpub() != 63340) { + if (uniffi_cktap_ffi_checksum_method_satschip_xpub() != 45436) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_tapsigner_change() != 63099) { + if (uniffi_cktap_ffi_checksum_method_tapsigner_change() != 55091) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_tapsigner_check_cert() != 18657) { + if (uniffi_cktap_ffi_checksum_method_tapsigner_check_cert() != 27377) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_tapsigner_derive() != 36393) { + if (uniffi_cktap_ffi_checksum_method_tapsigner_derive() != 31084) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_tapsigner_init() != 19476) { + if (uniffi_cktap_ffi_checksum_method_tapsigner_init() != 29775) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_tapsigner_nfc() != 36157) { + if (uniffi_cktap_ffi_checksum_method_tapsigner_nfc() != 15163) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_tapsigner_read() != 700) { + if (uniffi_cktap_ffi_checksum_method_tapsigner_read() != 20560) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_tapsigner_sign_psbt() != 3541) { + if (uniffi_cktap_ffi_checksum_method_tapsigner_sign_psbt() != 16326) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_tapsigner_status() != 53193) { + if (uniffi_cktap_ffi_checksum_method_tapsigner_status() != 41548) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_tapsigner_wait() != 39921) { + if (uniffi_cktap_ffi_checksum_method_tapsigner_wait() != 25611) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_tapsigner_xpub() != 48830) { + if (uniffi_cktap_ffi_checksum_method_tapsigner_xpub() != 4761) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_cktransport_transmit_apdu() != 56609) { + if (uniffi_cktap_ffi_checksum_method_cktransport_transmit_apdu() != 57685) { return InitializationResult.apiChecksumMismatch } diff --git a/cktap-swift/build-xcframework.sh b/cktap-swift/build-xcframework.sh index 531202c..adec303 100755 --- a/cktap-swift/build-xcframework.sh +++ b/cktap-swift/build-xcframework.sh @@ -60,6 +60,14 @@ cargo run --package ${FFI_PKG_NAME} --bin cktap-uniffi-bindgen generate \ --out-dir ./Sources/CKTap \ --no-format +# uniffi 0.32 uses the configured module name for generated Swift artifacts +if [ -f "Sources/CKTap/CKTap.swift" ]; then + mv "Sources/CKTap/CKTap.swift" "Sources/CKTap/cktap_ffi.swift" + HEADER_BASENAME="CKTapFFI" + HEADER_FILENAME="${HEADER_BASENAME}.h" + GENERATED_MODULEMAP="CKTapFFI.modulemap" +fi + # Create universal library for simulator targets lipo ${TARGETDIR}/aarch64-apple-ios-sim/${RELDIR}/${STATIC_LIB_FILENAME} \ ${TARGETDIR}/x86_64-apple-ios/${RELDIR}/${STATIC_LIB_FILENAME} \ @@ -110,4 +118,3 @@ xcodebuild -create-xcframework \ -output "${OUTDIR}/${NAME}.xcframework" echo "Building Swift package completed." - From 15c2d3c6e472c66e05d154510ff9a4aa4f2c9ddc Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 18 Aug 2026 11:46:49 -0500 Subject: [PATCH 4/8] Preserve typed errors across the FFI Return local CVC validation details through each operation instead of reporting them as card BadArguments errors. Name unknown CBOR error codes as card error codes, and document that the numeric CVC model targets public firmware 1.0.1 and later. --- cktap-ffi/src/error.rs | 134 +++++++++++++++++++++++++----------- cktap-ffi/src/lib.rs | 78 +-------------------- cktap-ffi/src/sats_card.rs | 13 ++-- cktap-ffi/src/sats_chip.rs | 24 ++++--- cktap-ffi/src/tap_signer.rs | 24 ++++--- lib/src/apdu.rs | 8 +-- lib/src/cvc.rs | 12 ++-- lib/src/error.rs | 8 +-- 8 files changed, 145 insertions(+), 156 deletions(-) diff --git a/cktap-ffi/src/error.rs b/cktap-ffi/src/error.rs index 3098726..6896ed7 100644 --- a/cktap-ffi/src/error.rs +++ b/cktap-ffi/src/error.rs @@ -27,6 +27,36 @@ impl From for KeyError { } } +/// Errors returned when a CVC does not satisfy its local constraints +#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error)] +pub enum CvcError { + /// The CVC contains fewer than six bytes + #[error("CVC is too short: {length} bytes; minimum is 6")] + TooShort { length: u32 }, + /// The CVC contains more than 32 bytes + #[error("CVC is too long: {length} bytes; maximum is 32")] + TooLong { length: u32 }, + /// The CVC contains a byte that is not an ASCII digit + #[error("CVC contains a byte that is not an ASCII digit at byte index {index}")] + NonAsciiDigit { index: u32 }, +} + +impl From for CvcError { + fn from(value: rust_cktap::CvcError) -> Self { + match value { + rust_cktap::CvcError::TooShort { length } => Self::TooShort { + length: u32::try_from(length).unwrap_or(u32::MAX), + }, + rust_cktap::CvcError::TooLong { length } => Self::TooLong { + length: u32::try_from(length).unwrap_or(u32::MAX), + }, + rust_cktap::CvcError::NonAsciiDigit { index } => Self::NonAsciiDigit { + index: u32::try_from(index).unwrap_or(u32::MAX), + }, + } + } +} + /// Errors returned by the CkTap card. #[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error)] pub enum CardError { @@ -101,21 +131,12 @@ pub enum CkTapError { CborValue { msg: String }, #[error("APDU transport error: {msg}")] Transport { msg: String }, - #[error("Unknown APDU status word ({code}): {message}")] - UnknownStatusWord { code: u16, message: String }, + #[error("Unknown card error code ({code}): {message}")] + UnknownErrorCode { code: u16, message: String }, #[error("Unknown card type")] UnknownCardType, } -impl From for CkTapError { - fn from(_value: rust_cktap::CvcError) -> Self { - // cvc validation failures are local argument errors - Self::Card { - err: CardError::BadArguments, - } - } -} - impl From for CkTapError { fn from(value: rust_cktap::CkTapError) -> Self { match value { @@ -123,8 +144,8 @@ impl From for CkTapError { rust_cktap::CkTapError::CborDe(msg) => CkTapError::CborDe { msg }, rust_cktap::CkTapError::CborValue(msg) => CkTapError::CborValue { msg }, rust_cktap::CkTapError::Transport(msg) => CkTapError::Transport { msg }, - rust_cktap::CkTapError::UnknownStatusWord { code, message } => { - CkTapError::UnknownStatusWord { code, message } + rust_cktap::CkTapError::UnknownErrorCode { code, message } => { + CkTapError::UnknownErrorCode { code, message } } rust_cktap::CkTapError::UnknownCardType => CkTapError::UnknownCardType, } @@ -138,8 +159,8 @@ impl From for rust_cktap::CkTapError { CkTapError::CborDe { msg } => Self::CborDe(msg), CkTapError::CborValue { msg } => Self::CborValue(msg), CkTapError::Transport { msg } => Self::Transport(msg), - CkTapError::UnknownStatusWord { code, message } => { - Self::UnknownStatusWord { code, message } + CkTapError::UnknownErrorCode { code, message } => { + Self::UnknownErrorCode { code, message } } CkTapError::UnknownCardType => Self::UnknownCardType, } @@ -161,6 +182,23 @@ pub enum StatusError { }, } +/// Errors returned by the `init` command +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error)] +pub enum InitError { + /// The card or transport rejected the command + #[error(transparent)] + CkTap { + #[from] + err: CkTapError, + }, + /// The CVC failed local validation + #[error(transparent)] + Cvc { + #[from] + err: CvcError, + }, +} + impl From for StatusError { fn from(value: rust_cktap::StatusError) -> Self { match value { @@ -183,6 +221,12 @@ pub enum ReadError { #[from] err: KeyError, }, + /// The CVC failed local validation + #[error(transparent)] + Cvc { + #[from] + err: CvcError, + }, } impl From for ReadError { @@ -238,6 +282,12 @@ pub enum DeriveError { }, #[error("Invalid chain code: {msg}")] InvalidChainCode { msg: String }, + /// The CVC failed local validation + #[error(transparent)] + Cvc { + #[from] + err: CvcError, + }, } impl From for DeriveError { @@ -264,6 +314,12 @@ pub enum UnsealError { #[from] err: KeyError, }, + /// The CVC failed local validation + #[error(transparent)] + Cvc { + #[from] + err: CvcError, + }, } impl From for UnsealError { @@ -298,6 +354,12 @@ pub enum DumpError { /// successful `unseal` command. #[error("Slot was unsealed improperly: {slot}")] SlotTampered { slot: u8 }, + /// The CVC failed local validation + #[error(transparent)] + Cvc { + #[from] + err: CvcError, + }, } impl From for DumpError { @@ -342,6 +404,12 @@ pub enum SignPsbtError { PsbtEncoding { msg: String }, #[error("Error in PSBT Base64 encoding: {msg}")] Base64Encoding { msg: String }, + /// The CVC failed local validation + #[error(transparent)] + Cvc { + #[from] + err: CvcError, + }, } impl From for SignPsbtError { @@ -393,6 +461,12 @@ pub enum ChangeError { }, #[error("new cvc is the same as the old one")] SameAsOld, + /// The current CVC failed local validation + #[error("invalid current CVC: {err}")] + CurrentCvc { err: CvcError }, + /// The new CVC failed local validation + #[error("invalid new CVC: {err}")] + NewCvc { err: CvcError }, } impl From for ChangeError { @@ -414,6 +488,12 @@ pub enum XpubError { }, #[error("BIP32 error: {msg}")] Bip32 { msg: String }, + /// The CVC failed local validation + #[error(transparent)] + Cvc { + #[from] + err: CvcError, + }, } impl From for XpubError { @@ -426,27 +506,3 @@ impl From for XpubError { } } } - -#[cfg(test)] -mod tests { - use super::*; - use rust_cktap::CvcError; - - #[test] - fn cvc_validation_errors_map_to_bad_arguments() { - let errors = [ - CvcError::TooShort { length: 5 }, - CvcError::TooLong { length: 33 }, - CvcError::NonAsciiDigit { index: 5 }, - ]; - - for error in errors { - assert_eq!( - CkTapError::from(error), - CkTapError::Card { - err: CardError::BadArguments - } - ); - } - } -} diff --git a/cktap-ffi/src/lib.rs b/cktap-ffi/src/lib.rs index 9b7f1d6..264437f 100644 --- a/cktap-ffi/src/lib.rs +++ b/cktap-ffi/src/lib.rs @@ -8,7 +8,7 @@ mod tap_signer; uniffi::setup_scaffolding!(); -use crate::error::{CertsError, CkTapError, ReadError, StatusError}; +use crate::error::{CertsError, CkTapError, CvcError, ReadError, StatusError}; use crate::sats_card::SatsCard; use crate::sats_chip::SatsChip; use crate::tap_signer::TapSigner; @@ -39,77 +39,6 @@ impl rust_cktap::CkTransport for CkTransportWrapper { } } -#[cfg(test)] -mod transport_tests { - use super::*; - use crate::error::CardError; - use rust_cktap::CkTransport as _; - - struct ErrorTransport(CkTapError); - - #[async_trait::async_trait] - impl CkTransport for ErrorTransport { - async fn transmit_apdu(&self, _command_apdu: Vec) -> Result, CkTapError> { - Err(self.0.clone()) - } - } - - fn transmit_error(error: CkTapError) -> rust_cktap::CkTapError { - futures::executor::block_on( - CkTransportWrapper(Box::new(ErrorTransport(error))).transmit_apdu(Vec::new()), - ) - .expect_err("error transport must fail") - } - - #[test] - fn callback_errors_keep_their_typed_protocol_variants() { - let cases = [ - ( - CkTapError::Card { - err: CardError::BadAuth, - }, - rust_cktap::CkTapError::Card(rust_cktap::CardError::BadAuth), - ), - ( - CkTapError::CborDe { - msg: "invalid response".to_string(), - }, - rust_cktap::CkTapError::CborDe("invalid response".to_string()), - ), - ( - CkTapError::CborValue { - msg: "invalid value".to_string(), - }, - rust_cktap::CkTapError::CborValue("invalid value".to_string()), - ), - ( - CkTapError::Transport { - msg: "link lost".to_string(), - }, - rust_cktap::CkTapError::Transport("link lost".to_string()), - ), - ( - CkTapError::UnknownStatusWord { - code: 499, - message: "future status".to_string(), - }, - rust_cktap::CkTapError::UnknownStatusWord { - code: 499, - message: "future status".to_string(), - }, - ), - ( - CkTapError::UnknownCardType, - rust_cktap::CkTapError::UnknownCardType, - ), - ]; - - for (callback_error, expected) in cases { - assert_eq!(transmit_error(callback_error), expected); - } - } -} - #[derive(uniffi::Enum)] pub enum CkTapCard { SatsCard(Arc), @@ -141,10 +70,7 @@ async fn read( card: &mut (impl Read + Send + Sync), cvc: Option, ) -> Result { - let cvc = cvc - .map(Cvc::try_from) - .transpose() - .map_err(CkTapError::from)?; + let cvc = cvc.map(Cvc::try_from).transpose().map_err(CvcError::from)?; card.read(cvc) .await diff --git a/cktap-ffi/src/sats_card.rs b/cktap-ffi/src/sats_card.rs index 6ffb80a..d94fc18 100644 --- a/cktap-ffi/src/sats_card.rs +++ b/cktap-ffi/src/sats_card.rs @@ -3,7 +3,7 @@ use crate::check_cert; use crate::error::{ - CertsError, CkTapError, DeriveError, DumpError, ReadError, SignPsbtError, UnsealError, + CertsError, CkTapError, CvcError, DeriveError, DumpError, ReadError, SignPsbtError, UnsealError, }; use futures::lock::Mutex; use rust_cktap::descriptor::Wpkh; @@ -81,7 +81,7 @@ impl SatsCard { /// Open a new slot, it will be the current active but must be unused (no address) pub async fn new_slot(&self, cvc: String) -> Result { - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + let cvc = Cvc::try_from(cvc).map_err(CvcError::from)?; let mut card = self.0.lock().await; let (active_slot, _) = card.slots; let new_slot_chain_code = rand_chaincode(); @@ -100,7 +100,7 @@ impl SatsCard { /// Unseal currently active slot pub async fn unseal(&self, cvc: String) -> Result { - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + let cvc = Cvc::try_from(cvc).map_err(CvcError::from)?; let mut card = self.0.lock().await; let active_slot = card.slots.0; let (privkey, pubkey) = card.unseal(active_slot, &cvc).await?; @@ -114,10 +114,7 @@ impl SatsCard { /// This is only needed for debugging, use `sign_psbt` for signing /// If no CVC given only pubkey and pubkey descriptor returned. pub async fn dump(&self, slot: u8, cvc: Option) -> Result { - let cvc = cvc - .map(Cvc::try_from) - .transpose() - .map_err(CkTapError::from)?; + let cvc = cvc.map(Cvc::try_from).transpose().map_err(CvcError::from)?; let mut card = self.0.lock().await; let (privkey, pubkey) = card.dump(slot, cvc).await?; Ok(SlotDetails { @@ -134,7 +131,7 @@ impl SatsCard { psbt: String, cvc: String, ) -> Result { - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + let cvc = Cvc::try_from(cvc).map_err(CvcError::from)?; let mut card = self.0.lock().await; let psbt = Psbt::from_str(&psbt)?; let signed_psbt = card.sign_psbt(slot, psbt, &cvc).await?; diff --git a/cktap-ffi/src/sats_chip.rs b/cktap-ffi/src/sats_chip.rs index a217030..1a51316 100644 --- a/cktap-ffi/src/sats_chip.rs +++ b/cktap-ffi/src/sats_chip.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::error::{ - CertsError, ChangeError, CkTapError, DeriveError, ReadError, SignPsbtError, XpubError, + CertsError, ChangeError, CkTapError, CvcError, DeriveError, InitError, ReadError, + SignPsbtError, XpubError, }; use crate::tap_signer::{change, derive, init, sign_psbt}; use crate::{check_cert, read}; @@ -55,29 +56,34 @@ impl SatsChip { check_cert(&mut *card).await } - pub async fn init(&self, cvc: String) -> Result<(), CkTapError> { - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + pub async fn init(&self, cvc: String) -> Result<(), InitError> { + let cvc = Cvc::try_from(cvc).map_err(CvcError::from)?; let mut card = self.0.lock().await; - init(&mut *card, cvc).await + init(&mut *card, cvc).await?; + Ok(()) } pub async fn sign_psbt(&self, psbt: String, cvc: String) -> Result { - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + let cvc = Cvc::try_from(cvc).map_err(CvcError::from)?; let mut card = self.0.lock().await; let psbt = sign_psbt(&mut *card, psbt, cvc).await?; Ok(psbt) } pub async fn derive(&self, path: Vec, cvc: String) -> Result { - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + let cvc = Cvc::try_from(cvc).map_err(CvcError::from)?; let mut card = self.0.lock().await; let pubkey = derive(&mut *card, path, cvc).await?; Ok(pubkey) } pub async fn change(&self, new_cvc: String, cvc: String) -> Result<(), ChangeError> { - let new_cvc = Cvc::try_from(new_cvc).map_err(CkTapError::from)?; - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + let new_cvc = Cvc::try_from(new_cvc).map_err(|err| ChangeError::NewCvc { + err: CvcError::from(err), + })?; + let cvc = Cvc::try_from(cvc).map_err(|err| ChangeError::CurrentCvc { + err: CvcError::from(err), + })?; let mut card = self.0.lock().await; change(&mut *card, new_cvc, cvc).await?; Ok(()) @@ -90,7 +96,7 @@ impl SatsChip { } pub async fn xpub(&self, master: bool, cvc: String) -> Result { - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + let cvc = Cvc::try_from(cvc).map_err(CvcError::from)?; let mut card = self.0.lock().await; let xpub = card.xpub(master, &cvc).await?; Ok(xpub.to_string()) diff --git a/cktap-ffi/src/tap_signer.rs b/cktap-ffi/src/tap_signer.rs index 9d28800..501d0d2 100644 --- a/cktap-ffi/src/tap_signer.rs +++ b/cktap-ffi/src/tap_signer.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::error::{ - CertsError, ChangeError, CkTapError, DeriveError, ReadError, SignPsbtError, XpubError, + CertsError, ChangeError, CkTapError, CvcError, DeriveError, InitError, ReadError, + SignPsbtError, XpubError, }; use crate::{check_cert, read}; use futures::lock::Mutex; @@ -57,29 +58,34 @@ impl TapSigner { check_cert(&mut *card).await } - pub async fn init(&self, cvc: String) -> Result<(), CkTapError> { - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + pub async fn init(&self, cvc: String) -> Result<(), InitError> { + let cvc = Cvc::try_from(cvc).map_err(CvcError::from)?; let mut card = self.0.lock().await; - init(&mut *card, cvc).await + init(&mut *card, cvc).await?; + Ok(()) } pub async fn sign_psbt(&self, psbt: String, cvc: String) -> Result { - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + let cvc = Cvc::try_from(cvc).map_err(CvcError::from)?; let mut card = self.0.lock().await; let psbt = sign_psbt(&mut *card, psbt, cvc).await?; Ok(psbt) } pub async fn derive(&self, path: Vec, cvc: String) -> Result { - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + let cvc = Cvc::try_from(cvc).map_err(CvcError::from)?; let mut card = self.0.lock().await; let pubkey = derive(&mut *card, path, cvc).await?; Ok(pubkey) } pub async fn change(&self, new_cvc: String, cvc: String) -> Result<(), ChangeError> { - let new_cvc = Cvc::try_from(new_cvc).map_err(CkTapError::from)?; - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + let new_cvc = Cvc::try_from(new_cvc).map_err(|err| ChangeError::NewCvc { + err: CvcError::from(err), + })?; + let cvc = Cvc::try_from(cvc).map_err(|err| ChangeError::CurrentCvc { + err: CvcError::from(err), + })?; let mut card = self.0.lock().await; change(&mut *card, new_cvc, cvc).await?; Ok(()) @@ -92,7 +98,7 @@ impl TapSigner { } pub async fn xpub(&self, master: bool, cvc: String) -> Result { - let cvc = Cvc::try_from(cvc).map_err(CkTapError::from)?; + let cvc = Cvc::try_from(cvc).map_err(CvcError::from)?; let mut card = self.0.lock().await; let xpub = card.xpub(master, &cvc).await?; Ok(xpub.to_string()) diff --git a/lib/src/apdu.rs b/lib/src/apdu.rs index 614ee7f..a7f6b2f 100644 --- a/lib/src/apdu.rs +++ b/lib/src/apdu.rs @@ -45,7 +45,7 @@ pub trait ResponseApdu { let cbor_struct: Result = cbor_value.deserialized(); if let Ok(error_resp) = cbor_struct { - return Err(CkTapError::from_status_word( + return Err(CkTapError::from_error_response( error_resp.code, error_resp.error, )); @@ -899,7 +899,7 @@ mod tests { } #[test] - fn known_status_word_maps_to_card_error() { + fn known_error_code_maps_to_card_error() { let error = StatusResponse::from_cbor(encode_error_response(400, "bad arguments")) .expect_err("error response must fail to deserialize as status"); @@ -907,13 +907,13 @@ mod tests { } #[test] - fn unknown_status_word_preserves_code_and_message() { + fn unknown_error_code_preserves_code_and_message() { let error = StatusResponse::from_cbor(encode_error_response(499, "future protocol error")) .expect_err("error response must fail to deserialize as status"); assert_eq!( error, - CkTapError::UnknownStatusWord { + CkTapError::UnknownErrorCode { code: 499, message: "future protocol error".to_string(), } diff --git a/lib/src/cvc.rs b/lib/src/cvc.rs index de098ec..c090ed1 100644 --- a/lib/src/cvc.rs +++ b/lib/src/cvc.rs @@ -13,16 +13,14 @@ pub const MAX_CVC_LENGTH: usize = 32; /// A numeric secret used to authenticate CkTap commands /// -/// The protocol specification is internally inconsistent: its CVC content section permits -/// non-ASCII bytes, but its TAPSIGNER `change` command requires numeric digits +/// This library supports the numeric CVC behavior in public firmware 1.0.1 and later /// -/// This type follows factory-card behavior and the `change` command rule by accepting only ASCII -/// digits +/// Earlier firmware could retain a nonnumeric TAPSIGNER CVC, but that firmware did not reach +/// public cards, so current and replacement CVCs use one numeric-only type /// -/// See [CVC Length & Content] and [`change`] +/// See the [firmware 1.0.1 change log] /// -/// [CVC Length & Content]: https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#cvc-length--content -/// [`change`]: https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#change +/// [firmware 1.0.1 change log]: https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/change-log.md#101---early-july-2022 #[derive(Clone, PartialEq, Eq)] pub struct Cvc(String); diff --git a/lib/src/error.rs b/lib/src/error.rs index d240fdb..188754e 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -15,17 +15,17 @@ pub enum CkTapError { CborValue(String), #[error("APDU transport error: {0}")] Transport(String), - #[error("Unknown APDU status word ({code}): {message}")] - UnknownStatusWord { code: u16, message: String }, + #[error("Unknown card error code ({code}): {message}")] + UnknownErrorCode { code: u16, message: String }, #[error("Unknown card type")] UnknownCardType, } impl CkTapError { - pub(crate) fn from_status_word(code: u16, message: impl Into) -> Self { + pub(crate) fn from_error_response(code: u16, message: impl Into) -> Self { match CardError::error_from_code(code) { Some(error) => Self::Card(error), - None => Self::UnknownStatusWord { + None => Self::UnknownErrorCode { code, message: message.into(), }, From afc3b24d275e6e2e799e41196d3d47d65ea13f58 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 18 Aug 2026 11:47:12 -0500 Subject: [PATCH 5/8] Handle CLI CVC input errors Trim surrounding whitespace from prompted CVC values and preserve internal whitespace for validation. Return xpub input and command failures instead of panicking. --- cli/src/main.rs | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 456f520..349c78e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -6,7 +6,7 @@ use clap::{Parser, Subcommand}; use rpassword::read_password; #[cfg(feature = "emulator")] use rust_cktap::emulator; -use rust_cktap::error::{DumpError, StatusError, UnsealError}; +use rust_cktap::error::{DumpError, StatusError, UnsealError, XpubError}; #[cfg(not(feature = "emulator"))] use rust_cktap::pcsc; use rust_cktap::shared::{Authentication, Nfc, Read, Wait}; @@ -38,6 +38,8 @@ pub enum CliError { CkTap(#[from] CkTapError), #[error(transparent)] Cvc(#[from] CvcError), + #[error(transparent)] + Xpub(#[from] XpubError), #[error("unable to read CVC: {0}")] CvcInput(String), } @@ -262,7 +264,7 @@ async fn main() -> Result<(), CliError> { } TapSignerCommand::Wait => wait(ts).await, TapSignerCommand::Nfc => nfc(ts).await, - TapSignerCommand::Xpub { master } => xpub(ts, master).await, + TapSignerCommand::Xpub { master } => xpub(ts, master).await?, } } CkTapCard::SatsChip(sc) => { @@ -298,7 +300,7 @@ async fn main() -> Result<(), CliError> { } SatsChipCommand::Wait => wait(sc).await, SatsChipCommand::Nfc => nfc(sc).await, - SatsChipCommand::Xpub { master } => xpub(sc, master).await, + SatsChipCommand::Xpub { master } => xpub(sc, master).await?, } } } @@ -339,13 +341,19 @@ fn cvc() -> Result { print!("Enter cvc: "); io::stdout().flush().unwrap(); let cvc = read_password().map_err(|error| CliError::CvcInput(error.to_string()))?; - Ok(Cvc::try_from(cvc)?) + Ok(Cvc::try_from(cvc.trim())?) } fn optional_cvc() -> Result, CliError> { print!("Enter cvc (leave empty for none): "); io::stdout().flush().unwrap(); let cvc = read_password().map_err(|error| CliError::CvcInput(error.to_string()))?; + Ok(parse_optional_cvc_input(cvc)?) +} + +fn parse_optional_cvc_input(cvc: String) -> Result, CvcError> { + let cvc = cvc.trim(); + if cvc.is_empty() { Ok(None) } else { @@ -377,13 +385,25 @@ where println!("{nfc}"); } -async fn xpub(card: &mut C, master: bool) +async fn xpub(card: &mut C, master: bool) -> Result<(), CliError> where C: TapSignerShared + Send, { dbg!(master); - let cvc = cvc().expect("valid cvc"); - let xpub = card.xpub(master, &cvc).await.expect("xpub failed"); + let cvc = cvc()?; + let xpub = card.xpub(master, &cvc).await?; dbg!(&xpub); println!("{xpub}"); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn optional_prompt_treats_whitespace_as_no_cvc() { + let cvc = parse_optional_cvc_input(" \t\n".to_string()).expect("empty CVC is valid"); + assert_eq!(cvc, None); + } } From 6a74e1361ad15d04ceb4375bf64b77ad20ae8efc Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 18 Aug 2026 11:47:30 -0500 Subject: [PATCH 6/8] Regenerate Swift bindings for typed errors Expose the new CVC and card error variants to Swift consumers. UniFFI 0.32 emits nonisolated(unsafe), which requires Swift 5.10. Raise the package minimum instead of patching generated source so the checked-in bindings remain direct generator output. --- cktap-swift/Package.swift | 2 +- cktap-swift/Sources/CKTap/cktap_ffi.swift | 1008 +++++++++++++-------- 2 files changed, 638 insertions(+), 372 deletions(-) diff --git a/cktap-swift/Package.swift b/cktap-swift/Package.swift index f9b868f..5a1d026 100644 --- a/cktap-swift/Package.swift +++ b/cktap-swift/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:5.10 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/cktap-swift/Sources/CKTap/cktap_ffi.swift b/cktap-swift/Sources/CKTap/cktap_ffi.swift index f2ff1d1..103d8c1 100644 --- a/cktap-swift/Sources/CKTap/cktap_ffi.swift +++ b/cktap-swift/Sources/CKTap/cktap_ffi.swift @@ -398,7 +398,7 @@ private func uniffiTraitInterfaceCallWithError( callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) } } -// Initial value and increment amount for handles. +// Initial value and increment amount for handles. // These ensure that SWIFT handles always have the lowest bit set fileprivate let UNIFFI_HANDLEMAP_INITIAL: UInt64 = 1 fileprivate let UNIFFI_HANDLEMAP_DELTA: UInt64 = 2 @@ -608,55 +608,55 @@ fileprivate struct FfiConverterData: FfiConverterRustBuffer { public protocol SatsCardProtocol: AnyObject, Sendable { - + /** * Get the current active slot's receive address */ func address() async throws -> String - + /** * Verify the card has authentic Coinkite root certificate */ - func checkCert() async throws - + func checkCert() async throws + /** * This is only needed for debugging, use `sign_psbt` for signing * If no CVC given only pubkey and pubkey descriptor returned. */ func dump(slot: UInt8, cvc: String?) async throws -> SlotDetails - + /** * Open a new slot, it will be the current active but must be unused (no address) */ func newSlot(cvc: String) async throws -> UInt8 - + /** * Return the same URL as given with a NFC tap. */ func nfc() async throws -> String - + /** * Get the current active slot's wpkh public key descriptor */ func read() async throws -> String - + /** * Sign PSBT, base64 encoded */ func signPsbt(slot: UInt8, psbt: String, cvc: String) async throws -> String - + func status() async -> SatsCardStatus - + /** * Unseal currently active slot */ func unseal(cvc: String) async throws -> SlotDetails - + /** * Wait one second of auth delay and return the remaining delay, if any. */ func wait() async throws -> UInt8? - + } open class SatsCard: SatsCardProtocol, @unchecked Sendable { fileprivate let handle: UInt64 @@ -708,9 +708,9 @@ open class SatsCard: SatsCardProtocol, @unchecked Sendable { try! rustCall { uniffi_cktap_ffi_fn_free_satscard(handle, $0) } } + - - + /** * Get the current active slot's receive address */ @@ -729,7 +729,7 @@ open func address()async throws -> String { errorHandler: FfiConverterTypeReadError_lift ) } - + /** * Verify the card has authentic Coinkite root certificate */ @@ -748,7 +748,7 @@ open func checkCert()async throws { errorHandler: FfiConverterTypeCertsError_lift ) } - + /** * This is only needed for debugging, use `sign_psbt` for signing * If no CVC given only pubkey and pubkey descriptor returned. @@ -768,7 +768,7 @@ open func dump(slot: UInt8, cvc: String?)async throws -> SlotDetails { errorHandler: FfiConverterTypeDumpError_lift ) } - + /** * Open a new slot, it will be the current active but must be unused (no address) */ @@ -787,7 +787,7 @@ open func newSlot(cvc: String)async throws -> UInt8 { errorHandler: FfiConverterTypeDeriveError_lift ) } - + /** * Return the same URL as given with a NFC tap. */ @@ -806,7 +806,7 @@ open func nfc()async throws -> String { errorHandler: FfiConverterTypeCkTapError_lift ) } - + /** * Get the current active slot's wpkh public key descriptor */ @@ -825,7 +825,7 @@ open func read()async throws -> String { errorHandler: FfiConverterTypeReadError_lift ) } - + /** * Sign PSBT, base64 encoded */ @@ -844,7 +844,7 @@ open func signPsbt(slot: UInt8, psbt: String, cvc: String)async throws -> Strin errorHandler: FfiConverterTypeSignPsbtError_lift ) } - + open func status()async -> SatsCardStatus { return try! await uniffiRustCallAsync( @@ -858,10 +858,10 @@ open func status()async -> SatsCardStatus { freeFunc: ffi_cktap_ffi_rust_future_free_rust_buffer, liftFunc: FfiConverterTypeSatsCardStatus_lift, errorHandler: nil - + ) } - + /** * Unseal currently active slot */ @@ -880,7 +880,7 @@ open func unseal(cvc: String)async throws -> SlotDetails { errorHandler: FfiConverterTypeUnsealError_lift ) } - + /** * Wait one second of auth delay and return the remaining delay, if any. */ @@ -899,9 +899,9 @@ open func wait()async throws -> UInt8? { errorHandler: FfiConverterTypeCkTapError_lift ) } + - - + } @@ -951,27 +951,27 @@ public func FfiConverterTypeSatsCard_lift(_ handle: UInt64) throws -> SatsCard { public protocol SatsChipProtocol: AnyObject, Sendable { - - func change(newCvc: String, cvc: String) async throws - - func checkCert() async throws - + + func change(newCvc: String, cvc: String) async throws + + func checkCert() async throws + func derive(path: [UInt32], cvc: String) async throws -> String - - func `init`(cvc: String) async throws - + + func `init`(cvc: String) async throws + func nfc() async throws -> String - + func read() async throws -> String - + func signPsbt(psbt: String, cvc: String) async throws -> String - + func status() async -> SatsChipStatus - + func wait() async throws -> UInt8? - + func xpub(master: Bool, cvc: String) async throws -> String - + } open class SatsChip: SatsChipProtocol, @unchecked Sendable { fileprivate let handle: UInt64 @@ -1023,9 +1023,9 @@ open class SatsChip: SatsChipProtocol, @unchecked Sendable { try! rustCall { uniffi_cktap_ffi_fn_free_satschip(handle, $0) } } + - - + open func change(newCvc: String, cvc: String)async throws { return try await uniffiRustCallAsync( @@ -1041,7 +1041,7 @@ open func change(newCvc: String, cvc: String)async throws { errorHandler: FfiConverterTypeChangeError_lift ) } - + open func checkCert()async throws { return try await uniffiRustCallAsync( @@ -1057,7 +1057,7 @@ open func checkCert()async throws { errorHandler: FfiConverterTypeCertsError_lift ) } - + open func derive(path: [UInt32], cvc: String)async throws -> String { return try await uniffiRustCallAsync( @@ -1073,7 +1073,7 @@ open func derive(path: [UInt32], cvc: String)async throws -> String { errorHandler: FfiConverterTypeDeriveError_lift ) } - + open func `init`(cvc: String)async throws { return try await uniffiRustCallAsync( @@ -1086,10 +1086,10 @@ open func `init`(cvc: String)async throws { completeFunc: ffi_cktap_ffi_rust_future_complete_void, freeFunc: ffi_cktap_ffi_rust_future_free_void, liftFunc: { $0 }, - errorHandler: FfiConverterTypeCkTapError_lift + errorHandler: FfiConverterTypeInitError_lift ) } - + open func nfc()async throws -> String { return try await uniffiRustCallAsync( @@ -1105,7 +1105,7 @@ open func nfc()async throws -> String { errorHandler: FfiConverterTypeCkTapError_lift ) } - + open func read()async throws -> String { return try await uniffiRustCallAsync( @@ -1121,7 +1121,7 @@ open func read()async throws -> String { errorHandler: FfiConverterTypeReadError_lift ) } - + open func signPsbt(psbt: String, cvc: String)async throws -> String { return try await uniffiRustCallAsync( @@ -1137,7 +1137,7 @@ open func signPsbt(psbt: String, cvc: String)async throws -> String { errorHandler: FfiConverterTypeSignPsbtError_lift ) } - + open func status()async -> SatsChipStatus { return try! await uniffiRustCallAsync( @@ -1151,10 +1151,10 @@ open func status()async -> SatsChipStatus { freeFunc: ffi_cktap_ffi_rust_future_free_rust_buffer, liftFunc: FfiConverterTypeSatsChipStatus_lift, errorHandler: nil - + ) } - + open func wait()async throws -> UInt8? { return try await uniffiRustCallAsync( @@ -1170,7 +1170,7 @@ open func wait()async throws -> UInt8? { errorHandler: FfiConverterTypeCkTapError_lift ) } - + open func xpub(master: Bool, cvc: String)async throws -> String { return try await uniffiRustCallAsync( @@ -1186,9 +1186,9 @@ open func xpub(master: Bool, cvc: String)async throws -> String { errorHandler: FfiConverterTypeXpubError_lift ) } + - - + } @@ -1238,27 +1238,27 @@ public func FfiConverterTypeSatsChip_lift(_ handle: UInt64) throws -> SatsChip { public protocol TapSignerProtocol: AnyObject, Sendable { - - func change(newCvc: String, cvc: String) async throws - - func checkCert() async throws - + + func change(newCvc: String, cvc: String) async throws + + func checkCert() async throws + func derive(path: [UInt32], cvc: String) async throws -> String - - func `init`(cvc: String) async throws - + + func `init`(cvc: String) async throws + func nfc() async throws -> String - + func read(cvc: String) async throws -> String - + func signPsbt(psbt: String, cvc: String) async throws -> String - + func status() async -> TapSignerStatus - + func wait() async throws -> UInt8? - + func xpub(master: Bool, cvc: String) async throws -> String - + } open class TapSigner: TapSignerProtocol, @unchecked Sendable { fileprivate let handle: UInt64 @@ -1310,9 +1310,9 @@ open class TapSigner: TapSignerProtocol, @unchecked Sendable { try! rustCall { uniffi_cktap_ffi_fn_free_tapsigner(handle, $0) } } + - - + open func change(newCvc: String, cvc: String)async throws { return try await uniffiRustCallAsync( @@ -1328,7 +1328,7 @@ open func change(newCvc: String, cvc: String)async throws { errorHandler: FfiConverterTypeChangeError_lift ) } - + open func checkCert()async throws { return try await uniffiRustCallAsync( @@ -1344,7 +1344,7 @@ open func checkCert()async throws { errorHandler: FfiConverterTypeCertsError_lift ) } - + open func derive(path: [UInt32], cvc: String)async throws -> String { return try await uniffiRustCallAsync( @@ -1360,7 +1360,7 @@ open func derive(path: [UInt32], cvc: String)async throws -> String { errorHandler: FfiConverterTypeDeriveError_lift ) } - + open func `init`(cvc: String)async throws { return try await uniffiRustCallAsync( @@ -1373,10 +1373,10 @@ open func `init`(cvc: String)async throws { completeFunc: ffi_cktap_ffi_rust_future_complete_void, freeFunc: ffi_cktap_ffi_rust_future_free_void, liftFunc: { $0 }, - errorHandler: FfiConverterTypeCkTapError_lift + errorHandler: FfiConverterTypeInitError_lift ) } - + open func nfc()async throws -> String { return try await uniffiRustCallAsync( @@ -1392,7 +1392,7 @@ open func nfc()async throws -> String { errorHandler: FfiConverterTypeCkTapError_lift ) } - + open func read(cvc: String)async throws -> String { return try await uniffiRustCallAsync( @@ -1408,7 +1408,7 @@ open func read(cvc: String)async throws -> String { errorHandler: FfiConverterTypeReadError_lift ) } - + open func signPsbt(psbt: String, cvc: String)async throws -> String { return try await uniffiRustCallAsync( @@ -1424,7 +1424,7 @@ open func signPsbt(psbt: String, cvc: String)async throws -> String { errorHandler: FfiConverterTypeSignPsbtError_lift ) } - + open func status()async -> TapSignerStatus { return try! await uniffiRustCallAsync( @@ -1438,10 +1438,10 @@ open func status()async -> TapSignerStatus { freeFunc: ffi_cktap_ffi_rust_future_free_rust_buffer, liftFunc: FfiConverterTypeTapSignerStatus_lift, errorHandler: nil - + ) } - + open func wait()async throws -> UInt8? { return try await uniffiRustCallAsync( @@ -1457,7 +1457,7 @@ open func wait()async throws -> UInt8? { errorHandler: FfiConverterTypeCkTapError_lift ) } - + open func xpub(master: Bool, cvc: String)async throws -> String { return try await uniffiRustCallAsync( @@ -1473,9 +1473,9 @@ open func xpub(master: Bool, cvc: String)async throws -> String { errorHandler: FfiConverterTypeXpubError_lift ) } + - - + } @@ -1547,9 +1547,9 @@ public struct SatsCardStatus: Equatable, Hashable { self.authDelay = authDelay } + - - + } #if compiler(>=6) @@ -1563,14 +1563,14 @@ public struct FfiConverterTypeSatsCardStatus: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SatsCardStatus { return try SatsCardStatus( - proto: FfiConverterUInt32.read(from: &buf), - ver: FfiConverterString.read(from: &buf), - birth: FfiConverterUInt32.read(from: &buf), - activeSlot: FfiConverterUInt8.read(from: &buf), - numSlots: FfiConverterUInt8.read(from: &buf), - addr: FfiConverterOptionString.read(from: &buf), - pubkey: FfiConverterString.read(from: &buf), - cardIdent: FfiConverterString.read(from: &buf), + proto: FfiConverterUInt32.read(from: &buf), + ver: FfiConverterString.read(from: &buf), + birth: FfiConverterUInt32.read(from: &buf), + activeSlot: FfiConverterUInt8.read(from: &buf), + numSlots: FfiConverterUInt8.read(from: &buf), + addr: FfiConverterOptionString.read(from: &buf), + pubkey: FfiConverterString.read(from: &buf), + cardIdent: FfiConverterString.read(from: &buf), authDelay: FfiConverterOptionUInt8.read(from: &buf) ) } @@ -1625,9 +1625,9 @@ public struct SatsChipStatus: Equatable, Hashable { self.authDelay = authDelay } + - - + } #if compiler(>=6) @@ -1641,12 +1641,12 @@ public struct FfiConverterTypeSatsChipStatus: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SatsChipStatus { return try SatsChipStatus( - proto: FfiConverterUInt32.read(from: &buf), - ver: FfiConverterString.read(from: &buf), - birth: FfiConverterUInt32.read(from: &buf), - path: FfiConverterOptionSequenceUInt32.read(from: &buf), - pubkey: FfiConverterString.read(from: &buf), - cardIdent: FfiConverterString.read(from: &buf), + proto: FfiConverterUInt32.read(from: &buf), + ver: FfiConverterString.read(from: &buf), + birth: FfiConverterUInt32.read(from: &buf), + path: FfiConverterOptionSequenceUInt32.read(from: &buf), + pubkey: FfiConverterString.read(from: &buf), + cardIdent: FfiConverterString.read(from: &buf), authDelay: FfiConverterOptionUInt8.read(from: &buf) ) } @@ -1691,9 +1691,9 @@ public struct SlotDetails: Equatable, Hashable { self.pubkeyDescriptor = pubkeyDescriptor } + - - + } #if compiler(>=6) @@ -1707,8 +1707,8 @@ public struct FfiConverterTypeSlotDetails: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SlotDetails { return try SlotDetails( - privkey: FfiConverterOptionString.read(from: &buf), - pubkey: FfiConverterString.read(from: &buf), + privkey: FfiConverterOptionString.read(from: &buf), + pubkey: FfiConverterString.read(from: &buf), pubkeyDescriptor: FfiConverterString.read(from: &buf) ) } @@ -1759,9 +1759,9 @@ public struct TapSignerStatus: Equatable, Hashable { self.authDelay = authDelay } + - - + } #if compiler(>=6) @@ -1775,13 +1775,13 @@ public struct FfiConverterTypeTapSignerStatus: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TapSignerStatus { return try TapSignerStatus( - proto: FfiConverterUInt32.read(from: &buf), - ver: FfiConverterString.read(from: &buf), - birth: FfiConverterUInt32.read(from: &buf), - path: FfiConverterOptionSequenceUInt32.read(from: &buf), - numBackups: FfiConverterUInt32.read(from: &buf), - pubkey: FfiConverterString.read(from: &buf), - cardIdent: FfiConverterString.read(from: &buf), + proto: FfiConverterUInt32.read(from: &buf), + ver: FfiConverterString.read(from: &buf), + birth: FfiConverterUInt32.read(from: &buf), + path: FfiConverterOptionSequenceUInt32.read(from: &buf), + numBackups: FfiConverterUInt32.read(from: &buf), + pubkey: FfiConverterString.read(from: &buf), + cardIdent: FfiConverterString.read(from: &buf), authDelay: FfiConverterOptionUInt8.read(from: &buf) ) } @@ -1817,11 +1817,11 @@ public func FfiConverterTypeTapSignerStatus_lift(_ buf: RustBuffer) throws -> Ta /** * Errors returned by the CkTap card. */ -public +public enum CardError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case UnluckyNumber case BadArguments case BadAuth @@ -1834,15 +1834,15 @@ enum CardError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { case BackupFirst case RateLimited + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -1859,9 +1859,9 @@ public struct FfiConverterTypeCardError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .UnluckyNumber case 2: return .BadArguments case 3: return .BadAuth @@ -1881,53 +1881,53 @@ public struct FfiConverterTypeCardError: FfiConverterRustBuffer { public static func write(_ value: CardError, into buf: inout [UInt8]) { switch value { + - - - + + case .UnluckyNumber: writeInt(&buf, Int32(1)) - - + + case .BadArguments: writeInt(&buf, Int32(2)) - - + + case .BadAuth: writeInt(&buf, Int32(3)) - - + + case .NeedsAuth: writeInt(&buf, Int32(4)) - - + + case .UnknownCommand: writeInt(&buf, Int32(5)) - - + + case .InvalidCommand: writeInt(&buf, Int32(6)) - - + + case .InvalidState: writeInt(&buf, Int32(7)) - - + + case .WeakNonce: writeInt(&buf, Int32(8)) - - + + case .BadCbor: writeInt(&buf, Int32(9)) - - + + case .BackupFirst: writeInt(&buf, Int32(10)) - - + + case .RateLimited: writeInt(&buf, Int32(11)) - + } } } @@ -1951,11 +1951,11 @@ public func FfiConverterTypeCardError_lift(_ buf: RustBuffer) throws -> CardErro /** * Errors returned by the `certs` command. */ -public +public enum CertsError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Key(err: KeyError @@ -1963,15 +1963,15 @@ enum CertsError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { case InvalidRootCert(msg: String ) + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -1988,9 +1988,9 @@ public struct FfiConverterTypeCertsError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) @@ -2008,24 +2008,24 @@ public struct FfiConverterTypeCertsError: FfiConverterRustBuffer { public static func write(_ value: CertsError, into buf: inout [UInt8]) { switch value { + - - - + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - - + + case let .InvalidRootCert(msg): writeInt(&buf, Int32(3)) FfiConverterString.write(msg, into: &buf) - + } } } @@ -2049,24 +2049,28 @@ public func FfiConverterTypeCertsError_lift(_ buf: RustBuffer) throws -> CertsEr /** * Errors returned by the `change` command. */ -public +public enum ChangeError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case SameAsOld + case CurrentCvc(err: CvcError + ) + case NewCvc(err: CvcError + ) + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2083,13 +2087,19 @@ public struct FfiConverterTypeChangeError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) case 2: return .SameAsOld + case 3: return .CurrentCvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) + case 4: return .NewCvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -2098,18 +2108,28 @@ public struct FfiConverterTypeChangeError: FfiConverterRustBuffer { public static func write(_ value: ChangeError, into buf: inout [UInt8]) { switch value { + - - - + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case .SameAsOld: writeInt(&buf, Int32(2)) - + + + case let .CurrentCvc(err): + writeInt(&buf, Int32(3)) + FfiConverterTypeCvcError.write(err, into: &buf) + + + case let .NewCvc(err): + writeInt(&buf, Int32(4)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -2132,7 +2152,7 @@ public func FfiConverterTypeChangeError_lift(_ buf: RustBuffer) throws -> Change public enum CkTapCard { - + case satsCard(SatsCard ) case tapSigner(TapSigner @@ -2159,38 +2179,38 @@ public struct FfiConverterTypeCkTapCard: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> CkTapCard { let variant: Int32 = try readInt(&buf) switch variant { - + case 1: return .satsCard(try FfiConverterTypeSatsCard.read(from: &buf) ) - + case 2: return .tapSigner(try FfiConverterTypeTapSigner.read(from: &buf) ) - + case 3: return .satsChip(try FfiConverterTypeSatsChip.read(from: &buf) ) - + default: throw UniffiInternalError.unexpectedEnumCase } } public static func write(_ value: CkTapCard, into buf: inout [UInt8]) { switch value { - - + + case let .satsCard(v1): writeInt(&buf, Int32(1)) FfiConverterTypeSatsCard.write(v1, into: &buf) - - + + case let .tapSigner(v1): writeInt(&buf, Int32(2)) FfiConverterTypeTapSigner.write(v1, into: &buf) - - + + case let .satsChip(v1): writeInt(&buf, Int32(3)) FfiConverterTypeSatsChip.write(v1, into: &buf) - + } } } @@ -2215,11 +2235,11 @@ public func FfiConverterTypeCkTapCard_lift(_ buf: RustBuffer) throws -> CkTapCar /** * Errors returned by the card, CBOR deserialization or value encoding, or the APDU transport. */ -public +public enum CkTapError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case Card(err: CardError ) case CborDe(msg: String @@ -2228,19 +2248,19 @@ enum CkTapError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { ) case Transport(msg: String ) - case UnknownStatusWord(code: UInt16, message: String + case UnknownErrorCode(code: UInt16, message: String ) case UnknownCardType + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2257,9 +2277,9 @@ public struct FfiConverterTypeCkTapError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .Card( err: try FfiConverterTypeCardError.read(from: &buf) ) @@ -2272,8 +2292,8 @@ public struct FfiConverterTypeCkTapError: FfiConverterRustBuffer { case 4: return .Transport( msg: try FfiConverterString.read(from: &buf) ) - case 5: return .UnknownStatusWord( - code: try FfiConverterUInt16.read(from: &buf), + case 5: return .UnknownErrorCode( + code: try FfiConverterUInt16.read(from: &buf), message: try FfiConverterString.read(from: &buf) ) case 6: return .UnknownCardType @@ -2285,39 +2305,39 @@ public struct FfiConverterTypeCkTapError: FfiConverterRustBuffer { public static func write(_ value: CkTapError, into buf: inout [UInt8]) { switch value { + - - - + + case let .Card(err): writeInt(&buf, Int32(1)) FfiConverterTypeCardError.write(err, into: &buf) - - + + case let .CborDe(msg): writeInt(&buf, Int32(2)) FfiConverterString.write(msg, into: &buf) - - + + case let .CborValue(msg): writeInt(&buf, Int32(3)) FfiConverterString.write(msg, into: &buf) - - + + case let .Transport(msg): writeInt(&buf, Int32(4)) FfiConverterString.write(msg, into: &buf) - - - case let .UnknownStatusWord(code,message): + + + case let .UnknownErrorCode(code,message): writeInt(&buf, Int32(5)) FfiConverterUInt16.write(code, into: &buf) FfiConverterString.write(message, into: &buf) - - + + case .UnknownCardType: writeInt(&buf, Int32(6)) - + } } } @@ -2339,29 +2359,129 @@ public func FfiConverterTypeCkTapError_lift(_ buf: RustBuffer) throws -> CkTapEr /** - * Errors returned by the `derive` command. + * Errors returned when a CVC does not satisfy its local constraints */ -public -enum DeriveError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { +public +enum CvcError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { + + + + case TooShort(length: UInt32 + ) + case TooLong(length: UInt32 + ) + case NonAsciiDigit(index: UInt32 + ) + + + + + + + public var errorDescription: String? { + String(reflecting: self) + } + +} + +#if compiler(>=6) +extension CvcError: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeCvcError: FfiConverterRustBuffer { + typealias SwiftType = CvcError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> CvcError { + let variant: Int32 = try readInt(&buf) + switch variant { + + + + + case 1: return .TooShort( + length: try FfiConverterUInt32.read(from: &buf) + ) + case 2: return .TooLong( + length: try FfiConverterUInt32.read(from: &buf) + ) + case 3: return .NonAsciiDigit( + index: try FfiConverterUInt32.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: CvcError, into buf: inout [UInt8]) { + switch value { + + + + + + case let .TooShort(length): + writeInt(&buf, Int32(1)) + FfiConverterUInt32.write(length, into: &buf) + + + case let .TooLong(length): + writeInt(&buf, Int32(2)) + FfiConverterUInt32.write(length, into: &buf) + + + case let .NonAsciiDigit(index): + writeInt(&buf, Int32(3)) + FfiConverterUInt32.write(index, into: &buf) + + } + } +} +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeCvcError_lift(_ buf: RustBuffer) throws -> CvcError { + return try FfiConverterTypeCvcError.lift(buf) +} +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +@Sendable public func FfiConverterTypeCvcError_lower(_ value: CvcError) -> RustBuffer { + return FfiConverterTypeCvcError.lower(value) +} + + +/** + * Errors returned by the `derive` command. + */ +public +enum DeriveError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { + + + case CkTap(err: CkTapError ) case Key(err: KeyError ) case InvalidChainCode(msg: String ) + case Cvc(err: CvcError + ) + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2378,9 +2498,9 @@ public struct FfiConverterTypeDeriveError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) @@ -2390,6 +2510,9 @@ public struct FfiConverterTypeDeriveError: FfiConverterRustBuffer { case 3: return .InvalidChainCode( msg: try FfiConverterString.read(from: &buf) ) + case 4: return .Cvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -2398,24 +2521,29 @@ public struct FfiConverterTypeDeriveError: FfiConverterRustBuffer { public static func write(_ value: DeriveError, into buf: inout [UInt8]) { switch value { + - - - + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - - + + case let .InvalidChainCode(msg): writeInt(&buf, Int32(3)) FfiConverterString.write(msg, into: &buf) - + + + case let .Cvc(err): + writeInt(&buf, Int32(4)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -2439,11 +2567,11 @@ public func FfiConverterTypeDeriveError_lift(_ buf: RustBuffer) throws -> Derive /** * Errors returned by the `dump` command. */ -public +public enum DumpError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Key(err: KeyError @@ -2459,16 +2587,18 @@ enum DumpError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { */ case SlotTampered(slot: UInt8 ) + case Cvc(err: CvcError + ) + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2485,9 +2615,9 @@ public struct FfiConverterTypeDumpError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) @@ -2503,6 +2633,9 @@ public struct FfiConverterTypeDumpError: FfiConverterRustBuffer { case 5: return .SlotTampered( slot: try FfiConverterUInt8.read(from: &buf) ) + case 6: return .Cvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -2511,34 +2644,39 @@ public struct FfiConverterTypeDumpError: FfiConverterRustBuffer { public static func write(_ value: DumpError, into buf: inout [UInt8]) { switch value { + - - - + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - - + + case let .SlotSealed(slot): writeInt(&buf, Int32(3)) FfiConverterUInt8.write(slot, into: &buf) - - + + case let .SlotUnused(slot): writeInt(&buf, Int32(4)) FfiConverterUInt8.write(slot, into: &buf) - - + + case let .SlotTampered(slot): writeInt(&buf, Int32(5)) FfiConverterUInt8.write(slot, into: &buf) - + + + case let .Cvc(err): + writeInt(&buf, Int32(6)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -2559,25 +2697,113 @@ public func FfiConverterTypeDumpError_lift(_ buf: RustBuffer) throws -> DumpErro } -public -enum KeyError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { +/** + * Errors returned by the `init` command + */ +public +enum InitError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { + + + + case CkTap(err: CkTapError + ) + case Cvc(err: CvcError + ) + + + + public var errorDescription: String? { + String(reflecting: self) + } + +} + +#if compiler(>=6) +extension InitError: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeInitError: FfiConverterRustBuffer { + typealias SwiftType = InitError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> InitError { + let variant: Int32 = try readInt(&buf) + switch variant { + + + + + case 1: return .CkTap( + err: try FfiConverterTypeCkTapError.read(from: &buf) + ) + case 2: return .Cvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: InitError, into buf: inout [UInt8]) { + switch value { + + + + + + case let .CkTap(err): + writeInt(&buf, Int32(1)) + FfiConverterTypeCkTapError.write(err, into: &buf) + + + case let .Cvc(err): + writeInt(&buf, Int32(2)) + FfiConverterTypeCvcError.write(err, into: &buf) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeInitError_lift(_ buf: RustBuffer) throws -> InitError { + return try FfiConverterTypeInitError.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +@Sendable public func FfiConverterTypeInitError_lower(_ value: InitError) -> RustBuffer { + return FfiConverterTypeInitError.lower(value) +} + + +public +enum KeyError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { + + + case Secp256k1(msg: String ) case KeyFromSlice(msg: String ) + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2594,9 +2820,9 @@ public struct FfiConverterTypeKeyError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .Secp256k1( msg: try FfiConverterString.read(from: &buf) ) @@ -2611,19 +2837,19 @@ public struct FfiConverterTypeKeyError: FfiConverterRustBuffer { public static func write(_ value: KeyError, into buf: inout [UInt8]) { switch value { + - - - + + case let .Secp256k1(msg): writeInt(&buf, Int32(1)) FfiConverterString.write(msg, into: &buf) - - + + case let .KeyFromSlice(msg): writeInt(&buf, Int32(2)) FfiConverterString.write(msg, into: &buf) - + } } } @@ -2647,25 +2873,27 @@ public func FfiConverterTypeKeyError_lift(_ buf: RustBuffer) throws -> KeyError /** * Errors returned by the `read` command. */ -public +public enum ReadError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Key(err: KeyError ) + case Cvc(err: CvcError + ) + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2682,15 +2910,18 @@ public struct FfiConverterTypeReadError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) case 2: return .Key( err: try FfiConverterTypeKeyError.read(from: &buf) ) + case 3: return .Cvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -2699,19 +2930,24 @@ public struct FfiConverterTypeReadError: FfiConverterRustBuffer { public static func write(_ value: ReadError, into buf: inout [UInt8]) { switch value { + - - - + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - + + + case let .Cvc(err): + writeInt(&buf, Int32(3)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -2732,11 +2968,11 @@ public func FfiConverterTypeReadError_lift(_ buf: RustBuffer) throws -> ReadErro } -public +public enum SignPsbtError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case InvalidPath(index: UInt32 ) case InvalidScript(index: UInt32 @@ -2761,16 +2997,18 @@ enum SignPsbtError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError ) case Base64Encoding(msg: String ) + case Cvc(err: CvcError + ) + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2787,9 +3025,9 @@ public struct FfiConverterTypeSignPsbtError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .InvalidPath( index: try FfiConverterUInt32.read(from: &buf) ) @@ -2826,6 +3064,9 @@ public struct FfiConverterTypeSignPsbtError: FfiConverterRustBuffer { case 12: return .Base64Encoding( msg: try FfiConverterString.read(from: &buf) ) + case 13: return .Cvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -2834,69 +3075,74 @@ public struct FfiConverterTypeSignPsbtError: FfiConverterRustBuffer { public static func write(_ value: SignPsbtError, into buf: inout [UInt8]) { switch value { + - - - + + case let .InvalidPath(index): writeInt(&buf, Int32(1)) FfiConverterUInt32.write(index, into: &buf) - - + + case let .InvalidScript(index): writeInt(&buf, Int32(2)) FfiConverterUInt32.write(index, into: &buf) - - + + case let .MissingPubkey(index): writeInt(&buf, Int32(3)) FfiConverterUInt32.write(index, into: &buf) - - + + case let .MissingUtxo(index): writeInt(&buf, Int32(4)) FfiConverterUInt32.write(index, into: &buf) - - + + case let .PubkeyMismatch(index): writeInt(&buf, Int32(5)) FfiConverterUInt32.write(index, into: &buf) - - + + case let .SighashError(msg): writeInt(&buf, Int32(6)) FfiConverterString.write(msg, into: &buf) - - + + case let .SignatureError(msg): writeInt(&buf, Int32(7)) FfiConverterString.write(msg, into: &buf) - - + + case let .SlotNotUnsealed(slot): writeInt(&buf, Int32(8)) FfiConverterUInt8.write(slot, into: &buf) - - + + case let .CkTap(err): writeInt(&buf, Int32(9)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .WitnessProgram(msg): writeInt(&buf, Int32(10)) FfiConverterString.write(msg, into: &buf) - - + + case let .PsbtEncoding(msg): writeInt(&buf, Int32(11)) FfiConverterString.write(msg, into: &buf) - - + + case let .Base64Encoding(msg): writeInt(&buf, Int32(12)) FfiConverterString.write(msg, into: &buf) - + + + case let .Cvc(err): + writeInt(&buf, Int32(13)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -2920,25 +3166,25 @@ public func FfiConverterTypeSignPsbtError_lift(_ buf: RustBuffer) throws -> Sign /** * Errors returned by the `status` command. */ -public +public enum StatusError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Key(err: KeyError ) + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -2955,9 +3201,9 @@ public struct FfiConverterTypeStatusError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) @@ -2972,19 +3218,19 @@ public struct FfiConverterTypeStatusError: FfiConverterRustBuffer { public static func write(_ value: StatusError, into buf: inout [UInt8]) { switch value { + - - - + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - + } } } @@ -3008,25 +3254,27 @@ public func FfiConverterTypeStatusError_lift(_ buf: RustBuffer) throws -> Status /** * Errors returned by the `unseal` command. */ -public +public enum UnsealError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Key(err: KeyError ) + case Cvc(err: CvcError + ) + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -3043,15 +3291,18 @@ public struct FfiConverterTypeUnsealError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) case 2: return .Key( err: try FfiConverterTypeKeyError.read(from: &buf) ) + case 3: return .Cvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -3060,19 +3311,24 @@ public struct FfiConverterTypeUnsealError: FfiConverterRustBuffer { public static func write(_ value: UnsealError, into buf: inout [UInt8]) { switch value { + - - - + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Key(err): writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) - + + + case let .Cvc(err): + writeInt(&buf, Int32(3)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -3096,25 +3352,27 @@ public func FfiConverterTypeUnsealError_lift(_ buf: RustBuffer) throws -> Unseal /** * Errors returned by the `xpub` command. */ -public +public enum XpubError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case CkTap(err: CkTapError ) case Bip32(msg: String ) + case Cvc(err: CvcError + ) + + - - - + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -3131,15 +3389,18 @@ public struct FfiConverterTypeXpubError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { + - - + case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) case 2: return .Bip32( msg: try FfiConverterString.read(from: &buf) ) + case 3: return .Cvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -3148,19 +3409,24 @@ public struct FfiConverterTypeXpubError: FfiConverterRustBuffer { public static func write(_ value: XpubError, into buf: inout [UInt8]) { switch value { + - - - + + case let .CkTap(err): writeInt(&buf, Int32(1)) FfiConverterTypeCkTapError.write(err, into: &buf) - - + + case let .Bip32(msg): writeInt(&buf, Int32(2)) FfiConverterString.write(msg, into: &buf) - + + + case let .Cvc(err): + writeInt(&buf, Int32(3)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -3184,9 +3450,9 @@ public func FfiConverterTypeXpubError_lift(_ buf: RustBuffer) throws -> XpubErro public protocol CkTransport: AnyObject, Sendable { - + func transmitApdu(commandApdu: Data) async throws -> Data - + } @@ -3642,7 +3908,7 @@ private let initializationResult: InitializationResult = { if (uniffi_cktap_ffi_checksum_method_satschip_derive() != 50805) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_satschip_init() != 498) { + if (uniffi_cktap_ffi_checksum_method_satschip_init() != 42524) { return InitializationResult.apiChecksumMismatch } if (uniffi_cktap_ffi_checksum_method_satschip_nfc() != 27858) { @@ -3672,7 +3938,7 @@ private let initializationResult: InitializationResult = { if (uniffi_cktap_ffi_checksum_method_tapsigner_derive() != 31084) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cktap_ffi_checksum_method_tapsigner_init() != 29775) { + if (uniffi_cktap_ffi_checksum_method_tapsigner_init() != 26344) { return InitializationResult.apiChecksumMismatch } if (uniffi_cktap_ffi_checksum_method_tapsigner_nfc() != 15163) { From 4f5a1415d5ff591811d8515d036ef5582a5aa0d6 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 18 Aug 2026 13:57:56 -0500 Subject: [PATCH 7/8] Use UniFFI Swift artifact names UniFFI 0.32 derives Swift artifact names from the configured CKTap module. Keep those names in the package and build script so generation no longer needs a version-dependent rename branch. --- cktap-swift/README.md | 2 +- .../Sources/CKTap/{cktap_ffi.swift => CKTap.swift} | 2 +- cktap-swift/build-xcframework.sh | 12 ++---------- 3 files changed, 4 insertions(+), 12 deletions(-) rename cktap-swift/Sources/CKTap/{cktap_ffi.swift => CKTap.swift} (99%) diff --git a/cktap-swift/README.md b/cktap-swift/README.md index ef72f38..d286077 100644 --- a/cktap-swift/README.md +++ b/cktap-swift/README.md @@ -6,7 +6,7 @@ manifest used during development. ## Local development - Run `just build` (or `./build-xcframework.sh`) to regenerate - `Sources/CKTap/cktap_ffi.swift` and `cktapFFI.xcframework`. + `Sources/CKTap/CKTap.swift` and `cktapFFI.xcframework`. - Run `just test` to execute Swift tests. ## Publishing the Swift package diff --git a/cktap-swift/Sources/CKTap/cktap_ffi.swift b/cktap-swift/Sources/CKTap/CKTap.swift similarity index 99% rename from cktap-swift/Sources/CKTap/cktap_ffi.swift rename to cktap-swift/Sources/CKTap/CKTap.swift index 103d8c1..c6d369a 100644 --- a/cktap-swift/Sources/CKTap/cktap_ffi.swift +++ b/cktap-swift/Sources/CKTap/CKTap.swift @@ -3980,4 +3980,4 @@ public func uniffiEnsureCktapFfiInitialized() { } } -// swiftlint:enable all \ No newline at end of file +// swiftlint:enable all diff --git a/cktap-swift/build-xcframework.sh b/cktap-swift/build-xcframework.sh index adec303..e7a7320 100755 --- a/cktap-swift/build-xcframework.sh +++ b/cktap-swift/build-xcframework.sh @@ -15,10 +15,10 @@ FFI_LIB_NAME="cktap_ffi" FFI_PKG_NAME="cktap-ffi" DYLIB_FILENAME="lib${FFI_LIB_NAME}.dylib" -HEADER_BASENAME="${FFI_LIB_NAME}FFI" +HEADER_BASENAME="CKTapFFI" HEADER_FILENAME="${HEADER_BASENAME}.h" MODULEMAP_FILENAME="module.modulemap" -GENERATED_MODULEMAP="${FFI_LIB_NAME}FFI.modulemap" +GENERATED_MODULEMAP="${HEADER_BASENAME}.modulemap" NAME="cktapFFI" STATIC_LIB_FILENAME="lib${FFI_LIB_NAME}.a" @@ -60,14 +60,6 @@ cargo run --package ${FFI_PKG_NAME} --bin cktap-uniffi-bindgen generate \ --out-dir ./Sources/CKTap \ --no-format -# uniffi 0.32 uses the configured module name for generated Swift artifacts -if [ -f "Sources/CKTap/CKTap.swift" ]; then - mv "Sources/CKTap/CKTap.swift" "Sources/CKTap/cktap_ffi.swift" - HEADER_BASENAME="CKTapFFI" - HEADER_FILENAME="${HEADER_BASENAME}.h" - GENERATED_MODULEMAP="CKTapFFI.modulemap" -fi - # Create universal library for simulator targets lipo ${TARGETDIR}/aarch64-apple-ios-sim/${RELDIR}/${STATIC_LIB_FILENAME} \ ${TARGETDIR}/x86_64-apple-ios/${RELDIR}/${STATIC_LIB_FILENAME} \ From 3d74272692278f31e2e230b0963a12ebe9b8745b Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 18 Aug 2026 14:03:28 -0500 Subject: [PATCH 8/8] Share FFI helpers for CVC validation Deduplicate optional CVC parsing and ChangeError wrapping so card entry points map validation failures consistently. --- cktap-ffi/src/error.rs | 12 ++++++++++++ cktap-ffi/src/lib.rs | 6 +++++- cktap-ffi/src/sats_card.rs | 3 ++- cktap-ffi/src/sats_chip.rs | 8 ++------ cktap-ffi/src/tap_signer.rs | 8 ++------ 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/cktap-ffi/src/error.rs b/cktap-ffi/src/error.rs index 6896ed7..66b109b 100644 --- a/cktap-ffi/src/error.rs +++ b/cktap-ffi/src/error.rs @@ -469,6 +469,18 @@ pub enum ChangeError { NewCvc { err: CvcError }, } +impl ChangeError { + /// Wrap a CVC validation failure for the current CVC + pub(crate) fn current_cvc(err: impl Into) -> Self { + Self::CurrentCvc { err: err.into() } + } + + /// Wrap a CVC validation failure for the new CVC + pub(crate) fn new_cvc(err: impl Into) -> Self { + Self::NewCvc { err: err.into() } + } +} + impl From for ChangeError { fn from(value: rust_cktap::ChangeError) -> Self { match value { diff --git a/cktap-ffi/src/lib.rs b/cktap-ffi/src/lib.rs index 264437f..332e0a0 100644 --- a/cktap-ffi/src/lib.rs +++ b/cktap-ffi/src/lib.rs @@ -66,11 +66,15 @@ pub async fn to_cktap(transport: Box) -> Result) -> Result, CvcError> { + cvc.map(Cvc::try_from).transpose().map_err(CvcError::from) +} + async fn read( card: &mut (impl Read + Send + Sync), cvc: Option, ) -> Result { - let cvc = cvc.map(Cvc::try_from).transpose().map_err(CvcError::from)?; + let cvc = parse_optional_cvc(cvc)?; card.read(cvc) .await diff --git a/cktap-ffi/src/sats_card.rs b/cktap-ffi/src/sats_card.rs index d94fc18..ea8d5e4 100644 --- a/cktap-ffi/src/sats_card.rs +++ b/cktap-ffi/src/sats_card.rs @@ -5,6 +5,7 @@ use crate::check_cert; use crate::error::{ CertsError, CkTapError, CvcError, DeriveError, DumpError, ReadError, SignPsbtError, UnsealError, }; +use crate::parse_optional_cvc; use futures::lock::Mutex; use rust_cktap::descriptor::Wpkh; use rust_cktap::shared::{Authentication, Nfc, Read, Wait}; @@ -114,7 +115,7 @@ impl SatsCard { /// This is only needed for debugging, use `sign_psbt` for signing /// If no CVC given only pubkey and pubkey descriptor returned. pub async fn dump(&self, slot: u8, cvc: Option) -> Result { - let cvc = cvc.map(Cvc::try_from).transpose().map_err(CvcError::from)?; + let cvc = parse_optional_cvc(cvc)?; let mut card = self.0.lock().await; let (privkey, pubkey) = card.dump(slot, cvc).await?; Ok(SlotDetails { diff --git a/cktap-ffi/src/sats_chip.rs b/cktap-ffi/src/sats_chip.rs index 1a51316..cb6ddc3 100644 --- a/cktap-ffi/src/sats_chip.rs +++ b/cktap-ffi/src/sats_chip.rs @@ -78,12 +78,8 @@ impl SatsChip { } pub async fn change(&self, new_cvc: String, cvc: String) -> Result<(), ChangeError> { - let new_cvc = Cvc::try_from(new_cvc).map_err(|err| ChangeError::NewCvc { - err: CvcError::from(err), - })?; - let cvc = Cvc::try_from(cvc).map_err(|err| ChangeError::CurrentCvc { - err: CvcError::from(err), - })?; + let new_cvc = Cvc::try_from(new_cvc).map_err(ChangeError::new_cvc)?; + let cvc = Cvc::try_from(cvc).map_err(ChangeError::current_cvc)?; let mut card = self.0.lock().await; change(&mut *card, new_cvc, cvc).await?; Ok(()) diff --git a/cktap-ffi/src/tap_signer.rs b/cktap-ffi/src/tap_signer.rs index 501d0d2..cf6cb5b 100644 --- a/cktap-ffi/src/tap_signer.rs +++ b/cktap-ffi/src/tap_signer.rs @@ -80,12 +80,8 @@ impl TapSigner { } pub async fn change(&self, new_cvc: String, cvc: String) -> Result<(), ChangeError> { - let new_cvc = Cvc::try_from(new_cvc).map_err(|err| ChangeError::NewCvc { - err: CvcError::from(err), - })?; - let cvc = Cvc::try_from(cvc).map_err(|err| ChangeError::CurrentCvc { - err: CvcError::from(err), - })?; + let new_cvc = Cvc::try_from(new_cvc).map_err(ChangeError::new_cvc)?; + let cvc = Cvc::try_from(cvc).map_err(ChangeError::current_cvc)?; let mut card = self.0.lock().await; change(&mut *card, new_cvc, cvc).await?; Ok(())