v0.5.2-pre.004

This commit is contained in:
2026-08-10 16:43:19 +02:00
parent ef630e3123
commit b8e6751747
15 changed files with 1489 additions and 89 deletions

View File

@@ -1,8 +1,11 @@
// file: ks-wallet/src/native.rs
// version: 2
// version: 4
//! Strict reader and validator for native `.kswallet` containers.
//! Native `.kswallet` codec, password protection and atomic file publication.
use chacha20poly1305::aead::Aead; // rust-rules: trait-import
use chacha20poly1305::aead::KeyInit; // rust-rules: trait-import
use solana_signer::Signer; // rust-rules: trait-import
use tokio::io::AsyncReadExt; // rust-rules: trait-import
use zeroize::Zeroize; // rust-rules: trait-import
@@ -23,21 +26,38 @@ const OFFSET_CIPHERTEXT_LENGTH: usize = 36;
const OFFSET_PUBLIC_KEY: usize = 40;
const RESERVED_LENGTH: usize = 4;
const PUBLIC_KEY_LENGTH: usize = 32;
const TEMP_CREATE_ATTEMPTS: u64 = 16;
static NATIVE_TEMP_FILE_COUNTER: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
#[derive(Clone, Copy)]
struct NativeWalletKdfParameters {
memory_kib: u32,
iterations: u32,
parallelism: u32,
}
/// Parsed non-secret identity from one structurally valid native wallet container.
impl NativeWalletKdfParameters {
fn version_one_default() -> Self {
return Self {
memory_kib: crate::KSWALLET_ARGON2_DEFAULT_MEMORY_KIB,
iterations: crate::KSWALLET_ARGON2_DEFAULT_ITERATIONS,
parallelism: crate::KSWALLET_ARGON2_DEFAULT_PARALLELISM,
};
}
}
/// Parsed native wallet container kept strictly inside the wallet boundary.
///
/// The full file buffer, including salt, nonce and ciphertext, is zeroized after
/// structural validation. This type therefore retains only the fields required
/// by discovery before authenticated unlock.
/// The container is intentionally neither public nor `Debug`. Ciphertext is
/// retained only so an inspected file can later be authenticated and unlocked.
pub(crate) struct NativeWalletContainer {
alias: crate::WalletAlias,
public_key: solana_pubkey::Pubkey,
kdf_parameters: NativeWalletKdfParameters,
salt: [u8; crate::KSWALLET_SALT_LENGTH],
nonce: [u8; crate::KSWALLET_NONCE_LENGTH],
ciphertext: zeroize::Zeroizing<std::vec::Vec<u8>>,
}
impl crate::NativeWalletContainer {
@@ -50,6 +70,25 @@ impl crate::NativeWalletContainer {
pub(crate) fn public_key(&self) -> &solana_pubkey::Pubkey {
return &self.public_key;
}
fn authenticated_prefix(&self) -> ks_core::Result<zeroize::Zeroizing<std::vec::Vec<u8>>> {
return encode_authenticated_prefix(
&self.alias,
&self.public_key,
self.kdf_parameters,
&self.salt,
&self.nonce,
);
}
fn encode(&self) -> ks_core::Result<zeroize::Zeroizing<std::vec::Vec<u8>>> {
let mut bytes = match self.authenticated_prefix() {
std::result::Result::Ok(bytes) => bytes,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
bytes.extend_from_slice(self.ciphertext.as_slice());
return std::result::Result::Ok(bytes);
}
}
/// Reads and strictly validates one native wallet file with a pre-allocation size bound.
@@ -122,6 +161,628 @@ pub(crate) async fn read_native_wallet_container(
return decoded;
}
/// Protects one exact Solana keypair payload with a caller-owned password.
pub(crate) async fn protect_native_wallet_keypair(
alias: crate::WalletAlias,
public_key: solana_pubkey::Pubkey,
keypair_bytes: zeroize::Zeroizing<[u8; crate::SOLANA_KEYPAIR_LENGTH]>,
password: crate::WalletPassword,
) -> ks_core::Result<crate::NativeWalletContainer> {
let task = tokio::task::spawn_blocking(move || {
return protect_native_wallet_keypair_blocking(alias, public_key, keypair_bytes, password);
});
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
"wallet_native_crypto_task_failed",
"native wallet cryptographic task failed",
)),
};
}
/// Authenticates and decrypts one native wallet into a signing keypair.
pub(crate) async fn unlock_native_wallet_keypair(
container: crate::NativeWalletContainer,
password: crate::WalletPassword,
) -> ks_core::Result<solana_keypair::Keypair> {
let task = tokio::task::spawn_blocking(move || {
return unlock_native_wallet_keypair_blocking(container, password);
});
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
"wallet_native_crypto_task_failed",
"native wallet cryptographic task failed",
)),
};
}
/// Publishes one new native wallet atomically without replacing an existing destination.
pub(crate) async fn write_native_wallet_file_atomic(
path: std::path::PathBuf,
container: &crate::NativeWalletContainer,
) -> ks_core::Result<()> {
let bytes = match container.encode() {
std::result::Result::Ok(bytes) => bytes,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let alias = container.alias().clone();
let task = tokio::task::spawn_blocking(move || {
return write_native_wallet_file_atomic_blocking(path, alias, bytes);
});
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
"wallet_native_write_task_failed",
"native wallet file publication task failed",
)),
};
}
/// Atomically replaces one authenticated native wallet during password rotation.
pub(crate) async fn replace_native_wallet_file_atomic(
path: std::path::PathBuf,
container: &crate::NativeWalletContainer,
) -> ks_core::Result<()> {
let bytes = match container.encode() {
std::result::Result::Ok(bytes) => bytes,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let alias = container.alias().clone();
let task = tokio::task::spawn_blocking(move || {
return replace_native_wallet_file_atomic_blocking(path, alias, bytes);
});
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
"wallet_native_write_task_failed",
"native wallet file replacement task failed",
)),
};
}
fn protect_native_wallet_keypair_blocking(
alias: crate::WalletAlias,
public_key: solana_pubkey::Pubkey,
keypair_bytes: zeroize::Zeroizing<[u8; crate::SOLANA_KEYPAIR_LENGTH]>,
password: crate::WalletPassword,
) -> ks_core::Result<crate::NativeWalletContainer> {
let kdf_parameters = NativeWalletKdfParameters::version_one_default();
let mut salt = [0_u8; crate::KSWALLET_SALT_LENGTH];
let mut nonce = [0_u8; crate::KSWALLET_NONCE_LENGTH];
if getrandom::fill(&mut salt).is_err() {
salt.zeroize();
nonce.zeroize();
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_random_failed",
"secure random generation failed",
));
}
if getrandom::fill(&mut nonce).is_err() {
salt.zeroize();
nonce.zeroize();
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_random_failed",
"secure random generation failed",
));
}
let authenticated_prefix =
match encode_authenticated_prefix(&alias, &public_key, kdf_parameters, &salt, &nonce) {
std::result::Result::Ok(prefix) => prefix,
std::result::Result::Err(error) => {
salt.zeroize();
nonce.zeroize();
return std::result::Result::Err(error);
},
};
let key = match derive_native_wallet_key(password.as_bytes(), &salt, kdf_parameters) {
std::result::Result::Ok(key) => key,
std::result::Result::Err(error) => {
salt.zeroize();
nonce.zeroize();
return std::result::Result::Err(error);
},
};
let cipher = match chacha20poly1305::XChaCha20Poly1305::new_from_slice(&*key) {
std::result::Result::Ok(cipher) => cipher,
std::result::Result::Err(_) => {
salt.zeroize();
nonce.zeroize();
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_aead_key_invalid",
"native wallet AEAD key length is invalid",
));
},
};
let xnonce = chacha20poly1305::XNonce::from(nonce);
let ciphertext = match cipher.encrypt(
&xnonce,
chacha20poly1305::aead::Payload {
msg: &*keypair_bytes,
aad: authenticated_prefix.as_slice(),
},
) {
std::result::Result::Ok(ciphertext) => ciphertext,
std::result::Result::Err(_) => {
salt.zeroize();
nonce.zeroize();
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_encrypt_failed",
"native wallet encryption failed",
));
},
};
if ciphertext.len() != crate::KSWALLET_CIPHERTEXT_LENGTH {
salt.zeroize();
nonce.zeroize();
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_ciphertext_length_invalid",
"native wallet encryption produced an invalid ciphertext length",
));
}
return std::result::Result::Ok(crate::NativeWalletContainer {
alias,
public_key,
kdf_parameters,
salt,
nonce,
ciphertext: zeroize::Zeroizing::new(ciphertext),
});
}
fn unlock_native_wallet_keypair_blocking(
container: crate::NativeWalletContainer,
password: crate::WalletPassword,
) -> ks_core::Result<solana_keypair::Keypair> {
let authenticated_prefix = match container.authenticated_prefix() {
std::result::Result::Ok(prefix) => prefix,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let key = match derive_native_wallet_key(
password.as_bytes(),
&container.salt,
container.kdf_parameters,
) {
std::result::Result::Ok(key) => key,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let cipher = match chacha20poly1305::XChaCha20Poly1305::new_from_slice(&*key) {
std::result::Result::Ok(cipher) => cipher,
std::result::Result::Err(_) => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_aead_key_invalid",
"native wallet AEAD key length is invalid",
));
},
};
let xnonce = chacha20poly1305::XNonce::from(container.nonce);
let plaintext = match cipher.decrypt(
&xnonce,
chacha20poly1305::aead::Payload {
msg: container.ciphertext.as_slice(),
aad: authenticated_prefix.as_slice(),
},
) {
std::result::Result::Ok(plaintext) => zeroize::Zeroizing::new(plaintext),
std::result::Result::Err(_) => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_authentication_failed",
"native wallet password or authenticated data is invalid",
));
},
};
if plaintext.len() != crate::SOLANA_KEYPAIR_LENGTH {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_keypair_length_invalid",
"decrypted native wallet keypair length is invalid",
));
}
let keypair = match solana_keypair::Keypair::try_from(plaintext.as_slice()) {
std::result::Result::Ok(keypair) => keypair,
std::result::Result::Err(_) => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_keypair_invalid",
"decrypted native wallet keypair is invalid",
));
},
};
if keypair.pubkey() != container.public_key {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_public_key_mismatch",
"decrypted native wallet public key does not match the authenticated header",
));
}
return std::result::Result::Ok(keypair);
}
fn derive_native_wallet_key(
password: &[u8],
salt: &[u8; crate::KSWALLET_SALT_LENGTH],
parameters: NativeWalletKdfParameters,
) -> ks_core::Result<zeroize::Zeroizing<[u8; crate::KSWALLET_AEAD_KEY_LENGTH]>> {
match validate_kdf_parameters(&parameters) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let params = match argon2::Params::new(
parameters.memory_kib,
parameters.iterations,
parameters.parallelism,
std::option::Option::Some(crate::KSWALLET_AEAD_KEY_LENGTH),
) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(_) => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_kdf_parameters_invalid",
"native wallet Argon2id parameters are invalid",
));
},
};
let argon2 = argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut key = zeroize::Zeroizing::new([0_u8; crate::KSWALLET_AEAD_KEY_LENGTH]);
if argon2.hash_password_into(password, salt, &mut *key).is_err() {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_kdf_failed",
"native wallet Argon2id derivation failed",
));
}
return std::result::Result::Ok(key);
}
fn encode_authenticated_prefix(
alias: &crate::WalletAlias,
public_key: &solana_pubkey::Pubkey,
kdf_parameters: NativeWalletKdfParameters,
salt: &[u8; crate::KSWALLET_SALT_LENGTH],
nonce: &[u8; crate::KSWALLET_NONCE_LENGTH],
) -> ks_core::Result<zeroize::Zeroizing<std::vec::Vec<u8>>> {
match validate_kdf_parameters(&kdf_parameters) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let alias_bytes = alias.as_str().as_bytes();
let header_length = match crate::KSWALLET_FIXED_HEADER_LENGTH.checked_add(alias_bytes.len()) {
std::option::Option::Some(length) => length,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_header_length_invalid",
"native wallet header length overflowed the platform size",
));
},
};
let header_length_u16 = match u16::try_from(header_length) {
std::result::Result::Ok(length) => length,
std::result::Result::Err(_) => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_header_length_invalid",
"native wallet header length cannot be encoded",
));
},
};
let alias_length = match u8::try_from(alias_bytes.len()) {
std::result::Result::Ok(length) => length,
std::result::Result::Err(_) => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_alias_length_invalid",
"native wallet alias length cannot be encoded",
));
},
};
let prefix_length = header_length + crate::KSWALLET_SALT_LENGTH + crate::KSWALLET_NONCE_LENGTH;
let mut bytes = zeroize::Zeroizing::new(vec![0_u8; prefix_length]);
bytes[0..crate::KSWALLET_MAGIC.len()].copy_from_slice(crate::KSWALLET_MAGIC);
bytes[OFFSET_FORMAT_VERSION..OFFSET_FORMAT_VERSION + 2]
.copy_from_slice(&crate::KSWALLET_FORMAT_VERSION.to_le_bytes());
bytes[OFFSET_HEADER_LENGTH..OFFSET_HEADER_LENGTH + 2]
.copy_from_slice(&header_length_u16.to_le_bytes());
bytes[OFFSET_FLAGS..OFFSET_FLAGS + 2]
.copy_from_slice(&crate::KSWALLET_FLAGS_NONE.to_le_bytes());
bytes[OFFSET_KDF_ID] = crate::KSWALLET_KDF_ARGON2ID;
bytes[OFFSET_KDF_VERSION] = crate::KSWALLET_ARGON2_VERSION;
bytes[OFFSET_AEAD_ID] = crate::KSWALLET_AEAD_XCHACHA20_POLY1305;
bytes[OFFSET_SALT_LENGTH] = crate::KSWALLET_SALT_LENGTH as u8;
bytes[OFFSET_NONCE_LENGTH] = crate::KSWALLET_NONCE_LENGTH as u8;
bytes[OFFSET_ALIAS_LENGTH] = alias_length;
bytes[OFFSET_MEMORY_KIB..OFFSET_MEMORY_KIB + 4]
.copy_from_slice(&kdf_parameters.memory_kib.to_le_bytes());
bytes[OFFSET_ITERATIONS..OFFSET_ITERATIONS + 4]
.copy_from_slice(&kdf_parameters.iterations.to_le_bytes());
bytes[OFFSET_PARALLELISM..OFFSET_PARALLELISM + 4]
.copy_from_slice(&kdf_parameters.parallelism.to_le_bytes());
bytes[OFFSET_CIPHERTEXT_LENGTH..OFFSET_CIPHERTEXT_LENGTH + 4]
.copy_from_slice(&(crate::KSWALLET_CIPHERTEXT_LENGTH as u32).to_le_bytes());
bytes[OFFSET_PUBLIC_KEY..OFFSET_PUBLIC_KEY + PUBLIC_KEY_LENGTH]
.copy_from_slice(&public_key.to_bytes());
bytes[crate::KSWALLET_FIXED_HEADER_LENGTH..header_length].copy_from_slice(alias_bytes);
let salt_offset = header_length;
let nonce_offset = salt_offset + crate::KSWALLET_SALT_LENGTH;
bytes[salt_offset..nonce_offset].copy_from_slice(salt);
bytes[nonce_offset..].copy_from_slice(nonce);
return std::result::Result::Ok(bytes);
}
fn write_native_wallet_file_atomic_blocking(
path: std::path::PathBuf,
alias: crate::WalletAlias,
bytes: zeroize::Zeroizing<std::vec::Vec<u8>>,
) -> ks_core::Result<()> {
match validate_native_destination(&path, &alias) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let directory = match path.parent() {
std::option::Option::Some(directory) => directory,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_parent_missing",
"native wallet destination must have a parent directory",
));
},
};
match prepare_native_wallet_directory(directory) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if path.exists() {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_already_exists",
"native wallet already exists for this alias",
));
}
let (temporary_path, mut file) = match create_private_temp_file(directory, &path) {
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(io_error("wallet_native_temp_write_failed", error));
}
if let std::result::Result::Err(error) = file.sync_all() {
let _ = std::fs::remove_file(&temporary_path);
return std::result::Result::Err(io_error("wallet_native_temp_sync_failed", error));
}
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_native_already_exists",
"native wallet already exists for this alias",
));
}
return std::result::Result::Err(io_error("wallet_native_publish_failed", error));
}
if let std::result::Result::Err(error) = sync_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(io_error("wallet_native_temp_remove_failed", error));
}
return sync_directory(directory);
}
fn replace_native_wallet_file_atomic_blocking(
path: std::path::PathBuf,
alias: crate::WalletAlias,
bytes: zeroize::Zeroizing<std::vec::Vec<u8>>,
) -> ks_core::Result<()> {
match validate_native_destination(&path, &alias) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let directory = match path.parent() {
std::option::Option::Some(directory) => directory,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_parent_missing",
"native wallet destination must have a parent directory",
));
},
};
match prepare_native_wallet_directory(directory) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
match validate_native_wallet_file_metadata_blocking(&path) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let (temporary_path, mut file) = match create_private_temp_file(directory, &path) {
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(io_error("wallet_native_temp_write_failed", error));
}
if let std::result::Result::Err(error) = file.sync_all() {
let _ = std::fs::remove_file(&temporary_path);
return std::result::Result::Err(io_error("wallet_native_temp_sync_failed", error));
}
std::mem::drop(file);
if let std::result::Result::Err(error) = std::fs::rename(&temporary_path, &path) {
let _ = std::fs::remove_file(&temporary_path);
return std::result::Result::Err(io_error("wallet_native_replace_failed", error));
}
return sync_directory(directory);
}
fn create_private_temp_file(
directory: &std::path::Path,
destination: &std::path::Path,
) -> ks_core::Result<(std::path::PathBuf, std::fs::File)> {
let file_name = match destination.file_name().and_then(std::ffi::OsStr::to_str) {
std::option::Option::Some(file_name) => file_name,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_filename_invalid",
"native wallet destination filename is invalid",
));
},
};
for _ in 0..TEMP_CREATE_ATTEMPTS {
let sequence = NATIVE_TEMP_FILE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let temporary_path =
directory.join(format!(".{file_name}.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(io_error(
"wallet_native_temp_create_failed",
error,
));
},
}
}
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_temp_create_failed",
"native wallet temporary file name could not be reserved",
));
}
fn prepare_native_wallet_directory(directory: &std::path::Path) -> ks_core::Result<()> {
if directory.exists() {
let metadata = match std::fs::symlink_metadata(directory) {
std::result::Result::Ok(metadata) => metadata,
std::result::Result::Err(error) => {
return std::result::Result::Err(io_error(
"wallet_directory_metadata_failed",
error,
));
},
};
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return std::result::Result::Err(ks_core::Error::new(
"wallet_directory_type_invalid",
"wallet directory must be a directory and not a symlink",
));
}
return validate_private_directory_permissions(&metadata);
}
let mut builder = std::fs::DirBuilder::new();
builder.recursive(true);
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt; // rust-rules: trait-import
builder.mode(0o700);
}
if let std::result::Result::Err(error) = builder.create(directory) {
return std::result::Result::Err(io_error("wallet_directory_create_failed", error));
}
let metadata = match std::fs::symlink_metadata(directory) {
std::result::Result::Ok(metadata) => metadata,
std::result::Result::Err(error) => {
return std::result::Result::Err(io_error("wallet_directory_metadata_failed", error));
},
};
return validate_private_directory_permissions(&metadata);
}
fn validate_private_directory_permissions(metadata: &std::fs::Metadata) -> ks_core::Result<()> {
#[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_directory_permissions_too_open",
format!("wallet directory has mode {mode:o}; expected no group or other access"),
));
}
}
return std::result::Result::Ok(());
}
fn validate_native_destination(
path: &std::path::Path,
alias: &crate::WalletAlias,
) -> ks_core::Result<()> {
if path.extension()
!= std::option::Option::Some(std::ffi::OsStr::new(crate::KSWALLET_FILE_EXTENSION))
{
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_extension_invalid",
"native wallet file must use the .kswallet extension",
));
}
let stem = match path.file_stem().and_then(std::ffi::OsStr::to_str) {
std::option::Option::Some(stem) => stem,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_alias_encoding_invalid",
"native wallet filename must contain a UTF-8 wallet alias",
));
},
};
if stem != alias.as_str() {
return std::result::Result::Err(ks_core::Error::new(
"wallet_native_alias_mismatch",
"native wallet filename alias does not match the container alias",
));
}
return std::result::Result::Ok(());
}
fn validate_native_wallet_file_metadata_blocking(path: &std::path::Path) -> ks_core::Result<()> {
let metadata = match std::fs::symlink_metadata(path) {
std::result::Result::Ok(metadata) => metadata,
std::result::Result::Err(error) => {
return std::result::Result::Err(io_error("wallet_file_metadata_failed", error));
},
};
if metadata.file_type().is_symlink() || !metadata.is_file() {
return std::result::Result::Err(ks_core::Error::new(
"wallet_file_type_invalid",
"wallet file 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_file_permissions_too_open",
format!("wallet file has mode {mode:o}; expected no group or other access"),
));
}
}
return std::result::Result::Ok(());
}
fn sync_directory(directory: &std::path::Path) -> ks_core::Result<()> {
#[cfg(unix)]
{
let file = match std::fs::File::open(directory) {
std::result::Result::Ok(file) => file,
std::result::Result::Err(error) => {
return std::result::Result::Err(io_error(
"wallet_directory_sync_open_failed",
error,
));
},
};
if let std::result::Result::Err(error) = file.sync_all() {
return std::result::Result::Err(io_error("wallet_directory_sync_failed", error));
}
}
return std::result::Result::Ok(());
}
fn decode_native_wallet_container(bytes: &[u8]) -> ks_core::Result<crate::NativeWalletContainer> {
if bytes.len() < crate::KSWALLET_MIN_FILE_LENGTH
|| bytes.len() > crate::KSWALLET_MAX_FILE_LENGTH
@@ -268,9 +929,21 @@ fn decode_native_wallet_container(bytes: &[u8]) -> ks_core::Result<crate::Native
let mut public_key_bytes = [0_u8; PUBLIC_KEY_LENGTH];
public_key_bytes
.copy_from_slice(&bytes[OFFSET_PUBLIC_KEY..OFFSET_PUBLIC_KEY + PUBLIC_KEY_LENGTH]);
let salt_offset = header_length;
let nonce_offset = salt_offset + crate::KSWALLET_SALT_LENGTH;
let ciphertext_offset = nonce_offset + crate::KSWALLET_NONCE_LENGTH;
let mut salt = [0_u8; crate::KSWALLET_SALT_LENGTH];
salt.copy_from_slice(&bytes[salt_offset..nonce_offset]);
let mut nonce = [0_u8; crate::KSWALLET_NONCE_LENGTH];
nonce.copy_from_slice(&bytes[nonce_offset..ciphertext_offset]);
let ciphertext = zeroize::Zeroizing::new(bytes[ciphertext_offset..].to_vec());
return std::result::Result::Ok(crate::NativeWalletContainer {
alias,
public_key: solana_pubkey::Pubkey::new_from_array(public_key_bytes),
kdf_parameters,
salt,
nonce,
ciphertext,
});
}