666 lines
27 KiB
Rust
666 lines
27 KiB
Rust
// file: ks-wallet/src/transfer.rs
|
|
// version: 1
|
|
|
|
//! Explicit migration and secret import/export adapters.
|
|
|
|
use solana_signer::Signer; // rust-rules: trait-import
|
|
use tokio::io::AsyncReadExt; // rust-rules: trait-import
|
|
use zeroize::Zeroize; // rust-rules: trait-import
|
|
|
|
const MAX_SOLANA_CLI_JSON_LENGTH: u64 = 1_024;
|
|
const MAX_SOLANA_PRIVATE_KEY_BASE58_LENGTH: u64 = 128;
|
|
const SECRET_EXPORT_TEMP_ATTEMPTS: u64 = 16;
|
|
static SECRET_EXPORT_TEMP_FILE_COUNTER: std::sync::atomic::AtomicU64 =
|
|
std::sync::atomic::AtomicU64::new(1);
|
|
|
|
/// Explicit secret transfer formats supported by `ks-wallet`.
|
|
#[non_exhaustive]
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum WalletTransferFormat {
|
|
/// Standard Solana CLI keypair JSON array containing exactly 64 bytes.
|
|
SolanaCliJson,
|
|
/// Base58 encoding of the complete 64-byte Solana keypair used by wallet private-key imports.
|
|
SolanaPrivateKeyBase58,
|
|
}
|
|
|
|
impl crate::WalletManager {
|
|
/// Imports one secret file into a new password-protected native wallet.
|
|
///
|
|
/// The source file is never modified. The destination alias and public key
|
|
/// must both be unique among native wallets already present in the configured
|
|
/// store. The supplied password protects only the newly created `.kswallet`.
|
|
pub async fn import_file(
|
|
&self,
|
|
alias: crate::WalletAlias,
|
|
password: crate::WalletPassword,
|
|
source_path: impl std::convert::AsRef<std::path::Path>,
|
|
format: crate::WalletTransferFormat,
|
|
) -> ks_core::Result<crate::UnlockedWallet> {
|
|
let source_path = source_path.as_ref().to_path_buf();
|
|
let destination = native_path(self, &alias);
|
|
match ensure_native_alias_available(&destination).await {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
let keypair = match read_transfer_keypair(&source_path, format).await {
|
|
std::result::Result::Ok(keypair) => keypair,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
match ensure_public_key_available(self, &alias, &keypair.pubkey().to_string()).await {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
return persist_imported_keypair(self, alias, password, keypair, format, destination).await;
|
|
}
|
|
|
|
/// Migrates the configured legacy `<alias>.json` file into a new `.kswallet`.
|
|
///
|
|
/// Migration is non-destructive: the legacy source remains byte-for-byte
|
|
/// untouched so it can be used for rollback until the operator explicitly
|
|
/// removes it after validation.
|
|
pub async fn migrate_legacy(
|
|
&self,
|
|
alias: crate::WalletAlias,
|
|
password: crate::WalletPassword,
|
|
) -> ks_core::Result<crate::UnlockedWallet> {
|
|
let source_path = self.directory().join(format!("{}.json", alias.as_str()));
|
|
let destination = native_path(self, &alias);
|
|
match ensure_native_alias_available(&destination).await {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
let keypair =
|
|
match read_transfer_keypair(&source_path, crate::WalletTransferFormat::SolanaCliJson)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(keypair) => keypair,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
match ensure_public_key_available(self, &alias, &keypair.pubkey().to_string()).await {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
let public_key = keypair.pubkey();
|
|
let wallet = match persist_imported_keypair(
|
|
self,
|
|
alias.clone(),
|
|
password,
|
|
keypair,
|
|
crate::WalletTransferFormat::SolanaCliJson,
|
|
destination,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(wallet) => wallet,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
tracing::info!(
|
|
target: crate::TRACING_TARGET,
|
|
action = "migrate_legacy_wallet",
|
|
wallet_alias = alias.as_str(),
|
|
public_key = %public_key,
|
|
"migrated legacy Solana JSON wallet without modifying the source"
|
|
);
|
|
return std::result::Result::Ok(wallet);
|
|
}
|
|
|
|
/// Exports one native wallet secret to an explicit external file format.
|
|
///
|
|
/// A valid native-wallet password is mandatory before any secret bytes are
|
|
/// encoded or any destination file is created. Export refuses to overwrite an
|
|
/// existing destination and creates a private file (`0600` on Unix).
|
|
pub async fn export_file(
|
|
&self,
|
|
alias: &crate::WalletAlias,
|
|
password: crate::WalletPassword,
|
|
destination_path: impl std::convert::AsRef<std::path::Path>,
|
|
format: crate::WalletTransferFormat,
|
|
) -> ks_core::Result<()> {
|
|
let destination_path = destination_path.as_ref().to_path_buf();
|
|
let wallet = match self.unlock(alias, password).await {
|
|
std::result::Result::Ok(wallet) => wallet,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let public_key = wallet.public_key();
|
|
let keypair_bytes = wallet.keypair_bytes();
|
|
let encoded = match encode_transfer_keypair(keypair_bytes, format) {
|
|
std::result::Result::Ok(encoded) => encoded,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
match write_secret_export_file_atomic(destination_path, encoded).await {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
tracing::info!(
|
|
target: crate::TRACING_TARGET,
|
|
action = "export_wallet_secret",
|
|
wallet_alias = alias.as_str(),
|
|
public_key = public_key.as_str(),
|
|
transfer_format = transfer_format_code(format),
|
|
"exported wallet secret after password authentication"
|
|
);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
}
|
|
|
|
async fn persist_imported_keypair(
|
|
manager: &crate::WalletManager,
|
|
alias: crate::WalletAlias,
|
|
password: crate::WalletPassword,
|
|
keypair: solana_keypair::Keypair,
|
|
format: crate::WalletTransferFormat,
|
|
destination: std::path::PathBuf,
|
|
) -> ks_core::Result<crate::UnlockedWallet> {
|
|
let public_key = keypair.pubkey();
|
|
let keypair_bytes = zeroize::Zeroizing::new(keypair.to_bytes());
|
|
let container = match crate::protect_native_wallet_keypair(
|
|
alias.clone(),
|
|
public_key,
|
|
keypair_bytes,
|
|
password,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(container) => container,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
match crate::write_native_wallet_file_atomic(destination.clone(), &container).await {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
let handle = match manager.inspect_file(&destination).await {
|
|
std::result::Result::Ok(handle) => handle,
|
|
std::result::Result::Err(error) => {
|
|
return rollback_failed_import(destination, error).await;
|
|
},
|
|
};
|
|
if handle.alias() != &alias || handle.public_key() != public_key.to_string() {
|
|
return rollback_failed_import(
|
|
destination,
|
|
ks_core::Error::new(
|
|
"wallet_import_verification_failed",
|
|
"imported native wallet identity does not match the source keypair",
|
|
),
|
|
)
|
|
.await;
|
|
}
|
|
tracing::info!(
|
|
target: crate::TRACING_TARGET,
|
|
action = "import_wallet_secret",
|
|
wallet_alias = alias.as_str(),
|
|
public_key = %public_key,
|
|
transfer_format = transfer_format_code(format),
|
|
"imported wallet secret into native protected storage"
|
|
);
|
|
return std::result::Result::Ok(crate::UnlockedWallet::new(alias, keypair));
|
|
}
|
|
|
|
async fn rollback_failed_import<T>(
|
|
destination: std::path::PathBuf,
|
|
original_error: ks_core::Error,
|
|
) -> ks_core::Result<T> {
|
|
return match tokio::fs::remove_file(destination).await {
|
|
std::result::Result::Ok(()) => std::result::Result::Err(original_error),
|
|
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_rollback_failed",
|
|
"import verification failed and the new native destination could not be removed",
|
|
)),
|
|
};
|
|
}
|
|
|
|
async fn ensure_native_alias_available(path: &std::path::Path) -> ks_core::Result<()> {
|
|
return match tokio::fs::try_exists(path).await {
|
|
std::result::Result::Ok(false) => std::result::Result::Ok(()),
|
|
std::result::Result::Ok(true) => std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_native_already_exists",
|
|
"native wallet already exists for this alias",
|
|
)),
|
|
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_file_exists_check_failed",
|
|
error.to_string(),
|
|
)),
|
|
};
|
|
}
|
|
|
|
async fn ensure_public_key_available(
|
|
manager: &crate::WalletManager,
|
|
alias: &crate::WalletAlias,
|
|
public_key: &str,
|
|
) -> ks_core::Result<()> {
|
|
let wallets = match manager.scan().await {
|
|
std::result::Result::Ok(wallets) => wallets,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
for wallet in wallets {
|
|
if wallet.public_key() == public_key && wallet.alias() != alias {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_public_key_already_exists",
|
|
"native wallet store already contains this public key under another alias",
|
|
));
|
|
}
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
async fn read_transfer_keypair(
|
|
path: &std::path::Path,
|
|
format: crate::WalletTransferFormat,
|
|
) -> ks_core::Result<solana_keypair::Keypair> {
|
|
let maximum_length = match format {
|
|
crate::WalletTransferFormat::SolanaCliJson => MAX_SOLANA_CLI_JSON_LENGTH,
|
|
crate::WalletTransferFormat::SolanaPrivateKeyBase58 => MAX_SOLANA_PRIVATE_KEY_BASE58_LENGTH,
|
|
};
|
|
let mut encoded = match read_private_source_file(path, maximum_length).await {
|
|
std::result::Result::Ok(encoded) => encoded,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let result = match format {
|
|
crate::WalletTransferFormat::SolanaCliJson => decode_solana_cli_json(encoded.as_slice()),
|
|
crate::WalletTransferFormat::SolanaPrivateKeyBase58 => {
|
|
decode_solana_private_key_base58(encoded.as_slice())
|
|
},
|
|
};
|
|
encoded.zeroize();
|
|
return result;
|
|
}
|
|
|
|
async fn read_private_source_file(
|
|
path: &std::path::Path,
|
|
maximum_length: u64,
|
|
) -> ks_core::Result<std::vec::Vec<u8>> {
|
|
let metadata = match tokio::fs::symlink_metadata(path).await {
|
|
std::result::Result::Ok(metadata) => metadata,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_metadata_failed",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
};
|
|
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_type_invalid",
|
|
"wallet import source must be a regular file and not a symlink",
|
|
));
|
|
}
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
|
|
let mode = metadata.permissions().mode() & 0o777;
|
|
if mode & 0o077 != 0 {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_permissions_too_open",
|
|
format!(
|
|
"wallet import source has mode {mode:o}; expected no group or other access"
|
|
),
|
|
));
|
|
}
|
|
}
|
|
let file_length = metadata.len();
|
|
if file_length == 0 || file_length > maximum_length {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_length_invalid",
|
|
"wallet import source length is outside the accepted bound",
|
|
));
|
|
}
|
|
let allocation_length = match usize::try_from(file_length) {
|
|
std::result::Result::Ok(length) => length,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_length_invalid",
|
|
"wallet import source length exceeds the platform address space",
|
|
));
|
|
},
|
|
};
|
|
let mut file = match tokio::fs::File::open(path).await {
|
|
std::result::Result::Ok(file) => file,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_open_failed",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
};
|
|
let opened_metadata = match file.metadata().await {
|
|
std::result::Result::Ok(opened_metadata) => opened_metadata,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_metadata_failed",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
};
|
|
if !opened_metadata.is_file() || opened_metadata.len() != file_length {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_changed",
|
|
"wallet import source changed before it could be read",
|
|
));
|
|
}
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::MetadataExt; // rust-rules: trait-import
|
|
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
|
|
if opened_metadata.dev() != metadata.dev() || opened_metadata.ino() != metadata.ino() {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_changed",
|
|
"wallet import source changed before it could be read",
|
|
));
|
|
}
|
|
let opened_mode = opened_metadata.permissions().mode() & 0o777;
|
|
if opened_mode & 0o077 != 0 {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_permissions_too_open",
|
|
format!(
|
|
"wallet import source has mode {opened_mode:o}; expected no group or other access"
|
|
),
|
|
));
|
|
}
|
|
}
|
|
let mut bytes = vec![0_u8; allocation_length];
|
|
if let std::result::Result::Err(error) = file.read_exact(bytes.as_mut_slice()).await {
|
|
bytes.zeroize();
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_read_failed",
|
|
error.to_string(),
|
|
));
|
|
}
|
|
let mut trailing = [0_u8; 1];
|
|
match file.read(trailing.as_mut_slice()).await {
|
|
std::result::Result::Ok(0) => {},
|
|
std::result::Result::Ok(_) => {
|
|
bytes.zeroize();
|
|
trailing.zeroize();
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_length_changed",
|
|
"wallet import source changed while it was being read",
|
|
));
|
|
},
|
|
std::result::Result::Err(error) => {
|
|
bytes.zeroize();
|
|
trailing.zeroize();
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_source_read_failed",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
}
|
|
trailing.zeroize();
|
|
return std::result::Result::Ok(bytes);
|
|
}
|
|
|
|
fn decode_solana_cli_json(bytes: &[u8]) -> ks_core::Result<solana_keypair::Keypair> {
|
|
let mut parsed = match serde_json::from_slice::<std::vec::Vec<u8>>(bytes) {
|
|
std::result::Result::Ok(parsed) => parsed,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_solana_json_invalid",
|
|
"Solana CLI keypair JSON is invalid",
|
|
));
|
|
},
|
|
};
|
|
if parsed.len() != crate::SOLANA_KEYPAIR_LENGTH {
|
|
parsed.zeroize();
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_keypair_length_invalid",
|
|
"wallet import must contain exactly 64 keypair bytes",
|
|
));
|
|
}
|
|
let keypair = solana_keypair::Keypair::try_from(parsed.as_slice());
|
|
parsed.zeroize();
|
|
return match keypair {
|
|
std::result::Result::Ok(keypair) => std::result::Result::Ok(keypair),
|
|
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_keypair_invalid",
|
|
"wallet import does not contain a valid Solana keypair",
|
|
)),
|
|
};
|
|
}
|
|
|
|
fn decode_solana_private_key_base58(bytes: &[u8]) -> ks_core::Result<solana_keypair::Keypair> {
|
|
let text = match std::str::from_utf8(bytes) {
|
|
std::result::Result::Ok(text) => text.trim(),
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_base58_encoding_invalid",
|
|
"Solana private key Base58 source must be UTF-8 text",
|
|
));
|
|
},
|
|
};
|
|
if text.is_empty() {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_base58_empty",
|
|
"Solana private key Base58 source must not be empty",
|
|
));
|
|
}
|
|
let mut keypair_bytes = match bs58::decode(text).into_vec() {
|
|
std::result::Result::Ok(keypair_bytes) => keypair_bytes,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_base58_invalid",
|
|
"Solana private key Base58 source is invalid",
|
|
));
|
|
},
|
|
};
|
|
if keypair_bytes.len() != crate::SOLANA_KEYPAIR_LENGTH {
|
|
keypair_bytes.zeroize();
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_keypair_length_invalid",
|
|
"wallet import must decode to exactly 64 keypair bytes",
|
|
));
|
|
}
|
|
let keypair = solana_keypair::Keypair::try_from(keypair_bytes.as_slice());
|
|
keypair_bytes.zeroize();
|
|
return match keypair {
|
|
std::result::Result::Ok(keypair) => std::result::Result::Ok(keypair),
|
|
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_import_keypair_invalid",
|
|
"wallet import does not contain a valid Solana keypair",
|
|
)),
|
|
};
|
|
}
|
|
|
|
fn encode_transfer_keypair(
|
|
keypair_bytes: zeroize::Zeroizing<[u8; crate::SOLANA_KEYPAIR_LENGTH]>,
|
|
format: crate::WalletTransferFormat,
|
|
) -> ks_core::Result<zeroize::Zeroizing<std::vec::Vec<u8>>> {
|
|
return match format {
|
|
crate::WalletTransferFormat::SolanaCliJson => {
|
|
match serde_json::to_vec(keypair_bytes.as_slice()) {
|
|
std::result::Result::Ok(encoded) => {
|
|
std::result::Result::Ok(zeroize::Zeroizing::new(encoded))
|
|
},
|
|
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_solana_json_failed",
|
|
"Solana CLI keypair JSON could not be encoded",
|
|
)),
|
|
}
|
|
},
|
|
crate::WalletTransferFormat::SolanaPrivateKeyBase58 => {
|
|
let encoded = bs58::encode(keypair_bytes.as_slice()).into_string();
|
|
std::result::Result::Ok(zeroize::Zeroizing::new(encoded.into_bytes()))
|
|
},
|
|
};
|
|
}
|
|
|
|
async fn write_secret_export_file_atomic(
|
|
path: std::path::PathBuf,
|
|
bytes: zeroize::Zeroizing<std::vec::Vec<u8>>,
|
|
) -> ks_core::Result<()> {
|
|
let task = tokio::task::spawn_blocking(move || {
|
|
return write_secret_export_file_atomic_blocking(path, bytes);
|
|
});
|
|
return match task.await {
|
|
std::result::Result::Ok(result) => result,
|
|
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_task_failed",
|
|
"wallet secret export task failed",
|
|
)),
|
|
};
|
|
}
|
|
|
|
fn write_secret_export_file_atomic_blocking(
|
|
path: std::path::PathBuf,
|
|
bytes: zeroize::Zeroizing<std::vec::Vec<u8>>,
|
|
) -> ks_core::Result<()> {
|
|
let directory = match path.parent() {
|
|
std::option::Option::Some(directory) if !directory.as_os_str().is_empty() => directory,
|
|
std::option::Option::Some(_) => std::path::Path::new("."),
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_parent_missing",
|
|
"wallet export destination must have a parent directory",
|
|
));
|
|
},
|
|
};
|
|
let metadata = match std::fs::symlink_metadata(directory) {
|
|
std::result::Result::Ok(metadata) => metadata,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_directory_metadata_failed",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
};
|
|
if metadata.file_type().is_symlink() || !metadata.is_dir() {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_directory_type_invalid",
|
|
"wallet export parent must be a directory and not a symlink",
|
|
));
|
|
}
|
|
if path.exists() {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_already_exists",
|
|
"wallet export destination already exists",
|
|
));
|
|
}
|
|
let file_name = match path.file_name().and_then(std::ffi::OsStr::to_str) {
|
|
std::option::Option::Some(file_name) if !file_name.is_empty() => file_name,
|
|
_ => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_filename_invalid",
|
|
"wallet export destination filename is invalid",
|
|
));
|
|
},
|
|
};
|
|
let (temporary_path, mut file) = match create_secret_export_temp_file(directory, file_name) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
use std::io::Write; // rust-rules: trait-import
|
|
if let std::result::Result::Err(error) = file.write_all(bytes.as_slice()) {
|
|
let _ = std::fs::remove_file(&temporary_path);
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_write_failed",
|
|
error.to_string(),
|
|
));
|
|
}
|
|
if let std::result::Result::Err(error) = file.sync_all() {
|
|
let _ = std::fs::remove_file(&temporary_path);
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_sync_failed",
|
|
error.to_string(),
|
|
));
|
|
}
|
|
std::mem::drop(file);
|
|
if let std::result::Result::Err(error) = std::fs::hard_link(&temporary_path, &path) {
|
|
let _ = std::fs::remove_file(&temporary_path);
|
|
if error.kind() == std::io::ErrorKind::AlreadyExists {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_already_exists",
|
|
"wallet export destination already exists",
|
|
));
|
|
}
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_publish_failed",
|
|
error.to_string(),
|
|
));
|
|
}
|
|
if let std::result::Result::Err(error) = sync_export_directory(directory) {
|
|
let _ = std::fs::remove_file(&temporary_path);
|
|
return std::result::Result::Err(error);
|
|
}
|
|
if let std::result::Result::Err(error) = std::fs::remove_file(&temporary_path) {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_temp_remove_failed",
|
|
error.to_string(),
|
|
));
|
|
}
|
|
return sync_export_directory(directory);
|
|
}
|
|
|
|
fn create_secret_export_temp_file(
|
|
directory: &std::path::Path,
|
|
file_name: &str,
|
|
) -> ks_core::Result<(std::path::PathBuf, std::fs::File)> {
|
|
for _ in 0..SECRET_EXPORT_TEMP_ATTEMPTS {
|
|
let sequence =
|
|
SECRET_EXPORT_TEMP_FILE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
let temporary_path = directory
|
|
.join(format!(".{file_name}.kswallet-export-tmp-{}-{sequence}", std::process::id()));
|
|
let mut options = std::fs::OpenOptions::new();
|
|
options.write(true).create_new(true);
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::OpenOptionsExt; // rust-rules: trait-import
|
|
options.mode(0o600);
|
|
}
|
|
match options.open(&temporary_path) {
|
|
std::result::Result::Ok(file) => {
|
|
return std::result::Result::Ok((temporary_path, file));
|
|
},
|
|
std::result::Result::Err(error)
|
|
if error.kind() == std::io::ErrorKind::AlreadyExists => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_temp_create_failed",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
}
|
|
}
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_temp_create_failed",
|
|
"wallet export temporary filename could not be reserved",
|
|
));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn sync_export_directory(directory: &std::path::Path) -> ks_core::Result<()> {
|
|
let file = match std::fs::File::open(directory) {
|
|
std::result::Result::Ok(file) => file,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_directory_sync_failed",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
};
|
|
return match file.sync_all() {
|
|
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
|
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
|
"wallet_export_directory_sync_failed",
|
|
error.to_string(),
|
|
)),
|
|
};
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn sync_export_directory(_directory: &std::path::Path) -> ks_core::Result<()> {
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn native_path(manager: &crate::WalletManager, alias: &crate::WalletAlias) -> std::path::PathBuf {
|
|
return manager.directory().join(format!(
|
|
"{}.{}",
|
|
alias.as_str(),
|
|
crate::KSWALLET_FILE_EXTENSION
|
|
));
|
|
}
|
|
|
|
fn transfer_format_code(format: crate::WalletTransferFormat) -> &'static str {
|
|
return match format {
|
|
crate::WalletTransferFormat::SolanaCliJson => "solana_cli_json",
|
|
crate::WalletTransferFormat::SolanaPrivateKeyBase58 => "solana_private_key_base58",
|
|
};
|
|
}
|