diff --git a/cktap-ffi/src/error.rs b/cktap-ffi/src/error.rs index 25df813..66b109b 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 { @@ -72,6 +102,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 +131,8 @@ pub enum CkTapError { CborValue { msg: String }, #[error("APDU transport error: {msg}")] Transport { msg: String }, + #[error("Unknown card error code ({code}): {message}")] + UnknownErrorCode { code: u16, message: String }, #[error("Unknown card type")] UnknownCardType, } @@ -94,11 +144,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::UnknownErrorCode { code, message } => { + CkTapError::UnknownErrorCode { 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::UnknownErrorCode { code, message } => { + Self::UnknownErrorCode { code, message } + } + CkTapError::UnknownCardType => Self::UnknownCardType, + } + } +} + /// Errors returned by the `status` command. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error)] pub enum StatusError { @@ -114,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 { @@ -136,6 +221,12 @@ pub enum ReadError { #[from] err: KeyError, }, + /// The CVC failed local validation + #[error(transparent)] + Cvc { + #[from] + err: CvcError, + }, } impl From for ReadError { @@ -191,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 { @@ -217,6 +314,12 @@ pub enum UnsealError { #[from] err: KeyError, }, + /// The CVC failed local validation + #[error(transparent)] + Cvc { + #[from] + err: CvcError, + }, } impl From for UnsealError { @@ -251,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 { @@ -295,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 { @@ -344,20 +459,32 @@ 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, + /// 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 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 { 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, } } @@ -373,6 +500,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 { diff --git a/cktap-ffi/src/lib.rs b/cktap-ffi/src/lib.rs index 4f284b7..332e0a0 100644 --- a/cktap-ffi/src/lib.rs +++ b/cktap-ffi/src/lib.rs @@ -8,11 +8,12 @@ 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; use futures::lock::Mutex; +use rust_cktap::Cvc; use rust_cktap::shared::{Certificate, Read}; use std::fmt::Debug; use std::sync::Arc; @@ -34,7 +35,7 @@ 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) } } @@ -65,10 +66,16 @@ 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 = parse_optional_cvc(cvc)?; + 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..ea8d5e4 100644 --- a/cktap-ffi/src/sats_card.rs +++ b/cktap-ffi/src/sats_card.rs @@ -3,12 +3,13 @@ use crate::check_cert; use crate::error::{ - CertsError, CkTapError, DeriveError, DumpError, ReadError, SignPsbtError, UnsealError, + 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}; -use rust_cktap::{Psbt, rand_chaincode}; +use rust_cktap::{Cvc, Psbt, rand_chaincode}; use std::str::FromStr; #[derive(uniffi::Object)] @@ -81,6 +82,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(CvcError::from)?; let mut card = self.0.lock().await; let (active_slot, _) = card.slots; let new_slot_chain_code = rand_chaincode(); @@ -99,6 +101,7 @@ impl SatsCard { /// Unseal currently active slot pub async fn unseal(&self, cvc: String) -> Result { + 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?; @@ -112,6 +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 = parse_optional_cvc(cvc)?; let mut card = self.0.lock().await; let (privkey, pubkey) = card.dump(slot, cvc).await?; Ok(SlotDetails { @@ -128,6 +132,7 @@ impl SatsCard { psbt: String, cvc: String, ) -> Result { + 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 67f5b59..cb6ddc3 100644 --- a/cktap-ffi/src/sats_chip.rs +++ b/cktap-ffi/src/sats_chip.rs @@ -2,11 +2,13 @@ // 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}; use futures::lock::Mutex; +use rust_cktap::Cvc; use rust_cktap::shared::{Authentication, Nfc, Wait}; use rust_cktap::tap_signer::TapSignerShared; @@ -54,24 +56,30 @@ impl SatsChip { check_cert(&mut *card).await } - pub async fn init(&self, cvc: String) -> Result<(), CkTapError> { + 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(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(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(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(()) @@ -84,6 +92,7 @@ impl SatsChip { } pub async fn xpub(&self, master: bool, cvc: String) -> Result { + 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 901cdc4..cf6cb5b 100644 --- a/cktap-ffi/src/tap_signer.rs +++ b/cktap-ffi/src/tap_signer.rs @@ -2,13 +2,14 @@ // 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; 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)] @@ -57,24 +58,30 @@ impl TapSigner { check_cert(&mut *card).await } - pub async fn init(&self, cvc: String) -> Result<(), CkTapError> { + 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(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(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(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(()) @@ -87,6 +94,7 @@ impl TapSigner { } pub async fn xpub(&self, master: bool, cvc: String) -> Result { + 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()) @@ -96,7 +104,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 +116,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 +127,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 +135,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/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/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 85% rename from cktap-swift/Sources/CKTap/cktap_ffi.swift rename to cktap-swift/Sources/CKTap/CKTap.swift index f117c34..c6d369a 100644 --- a/cktap-swift/Sources/CKTap/cktap_ffi.swift +++ b/cktap-swift/Sources/CKTap/CKTap.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 @@ -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]) { @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -790,8 +850,7 @@ open func status()async -> SatsCardStatus { try! await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satscard_status( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -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, @@ -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, @@ -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) } @@ -974,8 +1031,7 @@ open func change(newCvc: String, cvc: String)async throws { 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, @@ -991,8 +1047,7 @@ open func checkCert()async throws { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_check_cert( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_void, @@ -1008,8 +1063,7 @@ open func derive(path: [UInt32], cvc: String)async throws -> String { 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, @@ -1025,15 +1079,14 @@ open func `init`(cvc: String)async throws { 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, completeFunc: ffi_cktap_ffi_rust_future_complete_void, freeFunc: ffi_cktap_ffi_rust_future_free_void, liftFunc: { $0 }, - errorHandler: FfiConverterTypeCkTapError_lift + errorHandler: FfiConverterTypeInitError_lift ) } @@ -1042,8 +1095,7 @@ open func nfc()async throws -> String { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_nfc( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1059,8 +1111,7 @@ open func read()async throws -> String { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_read( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1076,8 +1127,7 @@ open func signPsbt(psbt: String, cvc: String)async throws -> String { 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, @@ -1093,8 +1143,7 @@ open func status()async -> SatsChipStatus { try! await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_status( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1111,8 +1160,7 @@ open func wait()async throws -> UInt8? { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_satschip_wait( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1128,8 +1176,7 @@ open func xpub(master: Bool, cvc: String)async throws -> String { 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, @@ -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) } @@ -1271,8 +1318,7 @@ open func change(newCvc: String, cvc: String)async throws { 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, @@ -1288,8 +1334,7 @@ open func checkCert()async throws { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_check_cert( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_void, @@ -1305,8 +1350,7 @@ open func derive(path: [UInt32], cvc: String)async throws -> String { 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, @@ -1322,15 +1366,14 @@ open func `init`(cvc: String)async throws { 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, completeFunc: ffi_cktap_ffi_rust_future_complete_void, freeFunc: ffi_cktap_ffi_rust_future_free_void, liftFunc: { $0 }, - errorHandler: FfiConverterTypeCkTapError_lift + errorHandler: FfiConverterTypeInitError_lift ) } @@ -1339,8 +1382,7 @@ open func nfc()async throws -> String { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_nfc( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1356,8 +1398,7 @@ open func read(cvc: String)async throws -> String { 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, @@ -1373,8 +1414,7 @@ open func signPsbt(psbt: String, cvc: String)async throws -> String { 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, @@ -1390,8 +1430,7 @@ open func status()async -> TapSignerStatus { try! await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_status( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1408,8 +1447,7 @@ open func wait()async throws -> UInt8? { try await uniffiRustCallAsync( rustFutureFunc: { uniffi_cktap_ffi_fn_method_tapsigner_wait( - self.uniffiCloneHandle() - + self.uniffiCloneHandle() ) }, pollFunc: ffi_cktap_ffi_rust_future_poll_rust_buffer, @@ -1425,8 +1463,7 @@ open func xpub(master: Bool, cvc: String)async throws -> String { 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, @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -2019,11 +2056,11 @@ enum ChangeError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { case CkTap(err: CkTapError ) - case TooShort(len: UInt32 + case SameAsOld + case CurrentCvc(err: CvcError ) - case TooLong(len: UInt32 + case NewCvc(err: CvcError ) - case SameAsOld @@ -2056,13 +2093,13 @@ public struct FfiConverterTypeChangeError: FfiConverterRustBuffer { case 1: return .CkTap( err: try FfiConverterTypeCkTapError.read(from: &buf) ) - case 2: return .TooShort( - len: try FfiConverterUInt32.read(from: &buf) + case 2: return .SameAsOld + case 3: return .CurrentCvc( + err: try FfiConverterTypeCvcError.read(from: &buf) ) - case 3: return .TooLong( - len: try FfiConverterUInt32.read(from: &buf) + case 4: return .NewCvc( + err: try FfiConverterTypeCvcError.read(from: &buf) ) - case 4: return .SameAsOld default: throw UniffiInternalError.unexpectedEnumCase } @@ -2080,19 +2117,19 @@ public struct FfiConverterTypeChangeError: FfiConverterRustBuffer { FfiConverterTypeCkTapError.write(err, into: &buf) - case let .TooShort(len): + case .SameAsOld: writeInt(&buf, Int32(2)) - FfiConverterUInt32.write(len, into: &buf) - - case let .TooLong(len): + + case let .CurrentCvc(err): writeInt(&buf, Int32(3)) - FfiConverterUInt32.write(len, into: &buf) + FfiConverterTypeCvcError.write(err, into: &buf) - case .SameAsOld: + case let .NewCvc(err): writeInt(&buf, Int32(4)) - + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -2108,7 +2145,7 @@ 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) } @@ -2189,7 +2226,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) } @@ -2211,6 +2248,8 @@ enum CkTapError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { ) case Transport(msg: String ) + case UnknownErrorCode(code: UInt16, message: String + ) case UnknownCardType @@ -2253,7 +2292,11 @@ public struct FfiConverterTypeCkTapError: FfiConverterRustBuffer { case 4: return .Transport( msg: try FfiConverterString.read(from: &buf) ) - case 5: return .UnknownCardType + case 5: return .UnknownErrorCode( + code: try FfiConverterUInt16.read(from: &buf), + message: try FfiConverterString.read(from: &buf) + ) + case 6: return .UnknownCardType default: throw UniffiInternalError.unexpectedEnumCase } @@ -2286,8 +2329,14 @@ public struct FfiConverterTypeCkTapError: FfiConverterRustBuffer { FfiConverterString.write(msg, into: &buf) - case .UnknownCardType: + 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)) } } @@ -2304,11 +2353,109 @@ 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) } +/** + * Errors returned when a CVC does not satisfy its local constraints + */ +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. */ @@ -2323,6 +2470,8 @@ enum DeriveError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { ) case InvalidChainCode(msg: String ) + case Cvc(err: CvcError + ) @@ -2361,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 } @@ -2387,6 +2539,11 @@ public struct FfiConverterTypeDeriveError: FfiConverterRustBuffer { writeInt(&buf, Int32(3)) FfiConverterString.write(msg, into: &buf) + + case let .Cvc(err): + writeInt(&buf, Int32(4)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -2402,7 +2559,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) } @@ -2430,6 +2587,8 @@ enum DumpError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { */ case SlotTampered(slot: UInt8 ) + case Cvc(err: CvcError + ) @@ -2474,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 } @@ -2510,6 +2672,11 @@ public struct FfiConverterTypeDumpError: FfiConverterRustBuffer { writeInt(&buf, Int32(5)) FfiConverterUInt8.write(slot, into: &buf) + + case let .Cvc(err): + writeInt(&buf, Int32(6)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -2525,11 +2692,99 @@ 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) } +/** + * 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 { @@ -2610,7 +2865,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) } @@ -2627,6 +2882,8 @@ enum ReadError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { ) case Key(err: KeyError ) + case Cvc(err: CvcError + ) @@ -2662,6 +2919,9 @@ public struct FfiConverterTypeReadError: FfiConverterRustBuffer { case 2: return .Key( err: try FfiConverterTypeKeyError.read(from: &buf) ) + case 3: return .Cvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -2683,6 +2943,11 @@ public struct FfiConverterTypeReadError: FfiConverterRustBuffer { writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) + + case let .Cvc(err): + writeInt(&buf, Int32(3)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -2698,7 +2963,7 @@ 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) } @@ -2732,6 +2997,8 @@ enum SignPsbtError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError ) case Base64Encoding(msg: String ) + case Cvc(err: CvcError + ) @@ -2797,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 } @@ -2868,6 +3138,11 @@ public struct FfiConverterTypeSignPsbtError: FfiConverterRustBuffer { writeInt(&buf, Int32(12)) FfiConverterString.write(msg, into: &buf) + + case let .Cvc(err): + writeInt(&buf, Int32(13)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -2883,7 +3158,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) } @@ -2971,7 +3246,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) } @@ -2988,6 +3263,8 @@ enum UnsealError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { ) case Key(err: KeyError ) + case Cvc(err: CvcError + ) @@ -3023,6 +3300,9 @@ public struct FfiConverterTypeUnsealError: FfiConverterRustBuffer { case 2: return .Key( err: try FfiConverterTypeKeyError.read(from: &buf) ) + case 3: return .Cvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -3044,6 +3324,11 @@ public struct FfiConverterTypeUnsealError: FfiConverterRustBuffer { writeInt(&buf, Int32(2)) FfiConverterTypeKeyError.write(err, into: &buf) + + case let .Cvc(err): + writeInt(&buf, Int32(3)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -3059,7 +3344,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) } @@ -3076,6 +3361,8 @@ enum XpubError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { ) case Bip32(msg: String ) + case Cvc(err: CvcError + ) @@ -3111,6 +3398,9 @@ public struct FfiConverterTypeXpubError: FfiConverterRustBuffer { case 2: return .Bip32( msg: try FfiConverterString.read(from: &buf) ) + case 3: return .Cvc( + err: try FfiConverterTypeCvcError.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -3132,6 +3422,11 @@ public struct FfiConverterTypeXpubError: FfiConverterRustBuffer { writeInt(&buf, Int32(2)) FfiConverterString.write(msg, into: &buf) + + case let .Cvc(err): + writeInt(&buf, Int32(3)) + FfiConverterTypeCvcError.write(err, into: &buf) + } } } @@ -3147,7 +3442,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) } @@ -3190,7 +3485,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 +3497,7 @@ fileprivate struct UniffiCallbackInterfaceCkTransport { ) } - let uniffiHandleSuccess = { (returnValue: Data) in + let uniffiHandleSuccess: @Sendable (Data) -> () = { (returnValue) in uniffiFutureCallback( uniffiCallbackData, UniffiForeignFutureResultRustBuffer( @@ -3209,7 +3506,7 @@ fileprivate struct UniffiCallbackInterfaceCkTransport { ) ) } - let uniffiHandleError = { (statusCode, errorBuf) in + let uniffiHandleError: @Sendable (Int8, RustBuffer) -> () = { (statusCode, errorBuf) in uniffiFutureCallback( uniffiCallbackData, UniffiForeignFutureResultRustBuffer( @@ -3230,7 +3527,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 +3598,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 +3747,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 +3778,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 +3866,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() != 42524) { 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() != 26344) { 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 } @@ -3679,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 531202c..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" @@ -110,4 +110,3 @@ xcodebuild -create-xcframework \ -output "${OUTDIR}/${NAME}.xcframework" echo "Building Swift package completed." - diff --git a/cli/src/main.rs b/cli/src/main.rs index 9df3199..349c78e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -6,13 +6,14 @@ 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}; 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,12 @@ pub enum CliError { Dump(#[from] DumpError), #[error(transparent)] CkTap(#[from] CkTapError), + #[error(transparent)] + Cvc(#[from] CvcError), + #[error(transparent)] + Xpub(#[from] XpubError), + #[error("unable to read CVC: {0}")] + CvcInput(String), } /// SatsCard CLI @@ -187,26 +194,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,35 +230,41 @@ 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, TapSignerCommand::Nfc => nfc(ts).await, - TapSignerCommand::Xpub { master } => xpub(ts, master).await, + TapSignerCommand::Xpub { master } => xpub(ts, master).await?, } } CkTapCard::SatsChip(sc) => { @@ -259,28 +274,33 @@ 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, SatsChipCommand::Nfc => nfc(sc).await, - SatsChipCommand::Xpub { master } => xpub(sc, master).await, + SatsChipCommand::Xpub { master } => xpub(sc, master).await?, } } } @@ -304,7 +324,7 @@ where } } -async fn read(card: &mut C, cvc: Option) +async fn read(card: &mut C, cvc: Option) where C: Read + Send, { @@ -317,11 +337,28 @@ 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.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 { + Ok(Some(Cvc::try_from(cvc)?)) + } } async fn wait(card: &mut C) @@ -348,12 +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 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); + } } 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..a7f6b2f 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_error_response( + error_resp.code, + error_resp.error, + )); } let cbor_struct: Self = cbor_value.deserialized()?; @@ -625,7 +627,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, @@ -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_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"); + + assert_eq!(error, CkTapError::Card(CardError::BadArguments)); + } + + #[test] + 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::UnknownErrorCode { + code: 499, + message: "future protocol error".to_string(), + } + ); + } +} diff --git a/lib/src/cvc.rs b/lib/src/cvc.rs new file mode 100644 index 0000000..c090ed1 --- /dev/null +++ b/lib/src/cvc.rs @@ -0,0 +1,159 @@ +// 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 +/// +/// This library supports the numeric CVC behavior in public firmware 1.0.1 and later +/// +/// 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 the [firmware 1.0.1 change log] +/// +/// [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); + +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..188754e 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 card error code ({code}): {message}")] + UnknownErrorCode { code: u16, message: String }, #[error("Unknown card type")] UnknownCardType, } +impl CkTapError { + 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::UnknownErrorCode { + code, + message: message.into(), + }, + } + } +} + /// Errors returned by the CkTap card. #[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] pub enum CardError { @@ -130,10 +144,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);