// file: crates/ksp-app-wallet-desk/src/wallet_import.rs // version: 3 //! Native-picker transfer import staging with bounded secret material kept only in Rust. use tokio::io::AsyncReadExt; // rust-rules: trait-import use ts_rs::TS; // rust-rules: trait-import /// Request that chooses only the transfer format; the source path comes from the native Rust picker. #[derive(serde::Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_import/WalletImportSourceRequestDto.ts")] pub(crate) struct WalletImportSourceRequestDto { /// Transfer encoding expected for the selected external file. pub(crate) format: crate::WalletTransferFormatDto, } /// Safe projection returned after validating one native-picker import source. #[derive(Clone, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_import/WalletTransferInspectionDto.ts")] pub(crate) struct WalletTransferInspectionDto { /// Transfer format used to validate the secret source. pub(crate) format: crate::WalletTransferFormatDto, /// Public identity derived from the imported keypair. pub(crate) pubkey: String, /// Basename only; no parent path is ever projected. pub(crate) source_name: String, } /// Import request whose new native Wallet credentials move frontend -> Rust only. #[derive(serde::Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_import/WalletImportRequestDto.ts")] pub(crate) struct WalletImportRequestDto { /// Optional protected internal alias stored in the new native Wallet. pub(crate) alias: std::option::Option, /// Root-scoped destination filename under the effective Wallet directory. pub(crate) filename: String, /// Optional first protected note stored in the imported Wallet. pub(crate) initial_note: std::option::Option, /// New OWNER password transported only frontend -> Rust. pub(crate) owner_password: String, /// Optional new VIEW password; `None` keeps VIEW disabled. pub(crate) view_password: std::option::Option, } /// Rust-only staged transfer source. Secret bytes are bounded and zeroized on drop. pub(crate) struct PendingWalletImport { bytes: zeroize::Zeroizing>, format: crate::WalletTransferFormatDto, inspection: WalletTransferInspectionDto, } impl PendingWalletImport { /// Returns the safe inspection projection without exposing source bytes or path. #[must_use] pub(crate) fn safe_projection(&self) -> WalletTransferInspectionDto { return self.inspection.clone(); } /// Returns the bounded secret transfer bytes for one immediate Wallet import operation. #[must_use] pub(crate) fn source_bytes(&self) -> &[u8] { return self.bytes.as_slice(); } /// Returns the stable Wallet transfer format corresponding to the staged source. #[must_use] pub(crate) const fn wallet_format(&self) -> ksp_wallet_lib::WalletTransferFormat { return self.format.wallet_format(); } } /// Reads, bounds and validates one native-picker transfer source while retaining no external path. pub(crate) async fn stage_wallet_import_source( source: std::path::PathBuf, format: crate::WalletTransferFormatDto, ) -> ksp_core_lib::Result { let metadata = tokio::fs::symlink_metadata(source.as_path()).await; let metadata = match metadata { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Cannot inspect the selected Wallet transfer source") .with_source(error), ); }, }; if metadata.file_type().is_symlink() || !metadata.is_file() { return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Selected Wallet transfer source must be one regular non-symlink file", )); } let source_name = source.file_name().and_then(std::ffi::OsStr::to_str); let source_name = match source_name { std::option::Option::Some(value) if !value.is_empty() => value.to_owned(), _ => "selected-transfer".to_owned(), }; let file = tokio::fs::File::open(source.as_path()).await; let file = match file { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Cannot open the selected Wallet transfer source").with_source(error), ); }, }; let maximum_source_bytes = format.maximum_source_bytes(); let read_limit = maximum_source_bytes.saturating_add(1); let read_limit = u64::try_from(read_limit); let read_limit = match read_limit { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Wallet transfer source bound exceeds platform range").with_source(error), ); }, }; let mut reader = file.take(read_limit); let mut bytes = zeroize::Zeroizing::new(std::vec::Vec::with_capacity(maximum_source_bytes.saturating_add(1))); let read_result = reader.read_to_end(&mut bytes).await; if let std::result::Result::Err(error) = read_result { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Cannot read the selected Wallet transfer source").with_source(error), ); } if bytes.len() > maximum_source_bytes { return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Selected Wallet transfer source exceeds the supported format bound", )); } let inspection = ksp_wallet_lib::inspect_wallet_transfer(bytes.as_slice(), format.wallet_format()); let inspection = match inspection { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let dto = WalletTransferInspectionDto { format, pubkey: inspection.pubkey().to_string(), source_name }; return std::result::Result::Ok(PendingWalletImport { bytes, format, inspection: dto }); } #[cfg(test)] #[path = "../unit_tests/wallet_import.rs"] mod tests;