// file: crates/ksp-wallet-lib/src/wallet.rs // version: 10 //! In-memory native Wallet V1 create/open orchestration. use ed25519_dalek::Signer; // rust-rules: trait-import use zeroize::Zeroize; // rust-rules: trait-import /// Crate-internal `OwnerStateV1` state shared across the owning crate. pub(crate) struct OwnerStateV1 { envelope: crate::KspWalletEnvelopeV1, owner_root: std::option::Option, metadata_key: std::option::Option, secret_key: std::option::Option, admin_signing_key: std::option::Option, solana_keypair: std::option::Option, } impl OwnerStateV1 { /// Creates a new `OwnerStateV1` value. pub(crate) fn new( envelope: crate::KspWalletEnvelopeV1, owner_root: crate::SecretKeyV1, metadata_key: crate::SecretKeyV1, secret_key: crate::SecretKeyV1, admin_signing_key: ed25519_dalek::SigningKey, solana_keypair: solana_keypair::Keypair, ) -> Self { return Self { envelope, owner_root: std::option::Option::Some(owner_root), metadata_key: std::option::Option::Some(metadata_key), secret_key: std::option::Option::Some(secret_key), admin_signing_key: std::option::Option::Some(admin_signing_key), solana_keypair: std::option::Option::Some(solana_keypair), }; } /// Returns the current envelope. pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 { return &self.envelope; } /// Applies envelope. pub(crate) fn apply_envelope(&mut self, envelope: crate::KspWalletEnvelopeV1) { self.envelope = envelope; return; } /// Applies strong view state. pub(crate) fn apply_strong_view_state(&mut self, envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::SecretKeyV1) { self.envelope = envelope; let previous = self.metadata_key.replace(metadata_key); drop(previous); return; } /// Reconstructs one short-lived Solana keypair copy for authenticated V1 -> V2 migration. pub(crate) fn clone_solana_keypair_for_migration(&self) -> ksp_core_lib::Result { let keypair = match self.solana_keypair.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(key_material_error()), }; let mut bytes = keypair.to_bytes(); let cloned = solana_keypair::Keypair::try_from(bytes.as_slice()); bytes.zeroize(); return match cloned { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(_) => std::result::Result::Err(key_material_error()), }; } /// Executes the crate-internal export transfer operation for `OwnerStateV1`. pub(crate) fn export_transfer(&self, format: crate::WalletTransferFormat) -> ksp_core_lib::Result> { let keypair = match self.solana_keypair.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(key_material_error()), }; return match format { crate::WalletTransferFormat::SolanaCliJson => { let mut keypair_bytes = keypair.to_bytes(); let encoded = serde_json::to_vec(keypair_bytes.as_slice()); keypair_bytes.zeroize(); match encoded { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(_) => std::result::Result::Err(key_material_error()), } }, crate::WalletTransferFormat::SolanaKeypairBase58 => { let mut encoded = keypair.to_base58_string(); let output = encoded.as_bytes().to_vec(); encoded.zeroize(); std::result::Result::Ok(output) }, }; } /// Executes the crate-internal sign message operation for `OwnerStateV1`. pub(crate) fn sign_message(&self, message: &[u8]) -> ksp_core_lib::Result<[u8; crate::KSPWALLET_SOLANA_SIGNATURE_BYTES]> { let keypair = match self.solana_keypair.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(signing_error()), }; let mut keypair_bytes = keypair.to_bytes(); let mut seed = [0_u8; crate::SECRET_KEY_BYTES]; seed.copy_from_slice(&keypair_bytes[0..crate::SECRET_KEY_BYTES]); let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed); let signature = signing_key.sign(message).to_bytes(); seed.zeroize(); keypair_bytes.zeroize(); return std::result::Result::Ok(signature); } /// Executes the crate-internal stage metadata payload operation for `OwnerStateV1`. pub(crate) fn stage_metadata_payload(&self, payload: crate::MetadataPayloadV1) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::WalletInfo)> { let metadata_key = match self.metadata_key.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), }; let admin_signing_key = match self.admin_signing_key.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), }; let mut plaintext = match payload.encode() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let nonce = match crate::random_nonce() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { plaintext.zeroize(); return std::result::Result::Err(error); }, }; let encrypted = crate::encrypt_bytes( metadata_key, &nonce, self.envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(), plaintext.as_slice(), ); plaintext.zeroize(); let ciphertext = match encrypted { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let metadata = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Metadata, nonce, ciphertext); let envelope = resign_envelope( &self.envelope, *self.envelope.view_descriptor(), self.envelope.owner_slot().clone(), self.envelope.view_slot().cloned(), self.envelope.owner_control().clone(), metadata, self.envelope.secret().clone(), admin_signing_key, ); let verify_result = verify_state_signature(&envelope); if let std::result::Result::Err(error) = verify_result { return std::result::Result::Err(error); } return std::result::Result::Ok((envelope, payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner))); } /// Executes the crate-internal stage owner password rotation operation for `OwnerStateV1`. pub(crate) async fn stage_owner_password_rotation(&self, new_password: crate::OwnerPassword) -> ksp_core_lib::Result { let owner_root = match self.owner_root.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), }; let admin_signing_key = match self.admin_signing_key.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), }; let owner_slot = match rewrap_slot( &self.envelope, *self.envelope.owner_slot().slot_id(), crate::WalletKeySlotRoleV1::Owner, owner_root, PasswordRotationV1::Owner(new_password), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let envelope = resign_envelope( &self.envelope, *self.envelope.view_descriptor(), owner_slot, self.envelope.view_slot().cloned(), self.envelope.owner_control().clone(), self.envelope.metadata().clone(), self.envelope.secret().clone(), admin_signing_key, ); return verify_and_return(envelope); } /// Executes the crate-internal stage view password rotation operation for `OwnerStateV1`. pub(crate) async fn stage_view_password_rotation(&self, new_password: crate::ViewPassword) -> ksp_core_lib::Result { let metadata_key = match self.metadata_key.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), }; let view_slot = match self.envelope.view_slot() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")), }; let new_slot = match rewrap_slot(&self.envelope, *view_slot.slot_id(), crate::WalletKeySlotRoleV1::View, metadata_key, PasswordRotationV1::View(new_password)) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let envelope = crate::KspWalletEnvelopeV1::new_internal( *self.envelope.owner_auth_public_key(), *self.envelope.view_descriptor(), self.envelope.owner_slot().clone(), std::option::Option::Some(new_slot), self.envelope.owner_control().clone(), self.envelope.metadata().clone(), self.envelope.secret().clone(), self.envelope.state_signature().clone(), ); return verify_and_return(envelope); } /// Executes the crate-internal stage disable view operation for `OwnerStateV1`. pub(crate) fn stage_disable_view(&self) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> { if !self.envelope.view_descriptor().enabled() { return std::result::Result::Err(capability_error("Wallet VIEW capability is already disabled")); } return self.stage_strong_view_change(std::option::Option::None); } /// Executes the crate-internal stage recreate view operation for `OwnerStateV1`. pub(crate) async fn stage_recreate_view( &self, new_password: crate::ViewPassword, ) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> { let slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let new_metadata_key = match crate::SecretKeyV1::random() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let view_slot = match rewrap_slot(&self.envelope, slot_id, crate::WalletKeySlotRoleV1::View, &new_metadata_key, PasswordRotationV1::View(new_password)).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return self.stage_strong_view_change_with_key(std::option::Option::Some(view_slot), new_metadata_key); } fn stage_strong_view_change( &self, view_slot: std::option::Option, ) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> { let new_metadata_key = match crate::SecretKeyV1::random() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return self.stage_strong_view_change_with_key(view_slot, new_metadata_key); } fn stage_strong_view_change_with_key( &self, view_slot: std::option::Option, new_metadata_key: crate::SecretKeyV1, ) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> { let current_metadata_key = match self.metadata_key.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), }; let owner_root = match self.owner_root.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), }; let secret_key = match self.secret_key.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), }; let admin_signing_key = match self.admin_signing_key.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), }; let current_plaintext_result = crate::decrypt_bytes( current_metadata_key, self.envelope.metadata().nonce(), self.envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(), self.envelope.metadata().ciphertext(), ); let mut metadata_plaintext = match current_plaintext_result { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()), }; let metadata_nonce = match crate::random_nonce() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { metadata_plaintext.zeroize(); return std::result::Result::Err(error); }, }; let metadata_ciphertext_result = crate::encrypt_bytes( &new_metadata_key, &metadata_nonce, self.envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(), metadata_plaintext.as_slice(), ); metadata_plaintext.zeroize(); let metadata_ciphertext = match metadata_ciphertext_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let metadata = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Metadata, metadata_nonce, metadata_ciphertext); let mut admin_secret = admin_signing_key.to_bytes(); let mut owner_control_plaintext = crate::encode_owner_control_payload(&admin_secret, &new_metadata_key, secret_key); admin_secret.zeroize(); let owner_control_nonce = match crate::random_nonce() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { owner_control_plaintext.zeroize(); return std::result::Result::Err(error); }, }; let owner_control_ciphertext_result = crate::encrypt_bytes( owner_root, &owner_control_nonce, self.envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(), owner_control_plaintext.as_slice(), ); owner_control_plaintext.zeroize(); let owner_control_ciphertext = match owner_control_ciphertext_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let owner_control = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::OwnerControl, owner_control_nonce, owner_control_ciphertext); let view_descriptor = match view_slot.as_ref() { std::option::Option::Some(slot) => crate::WalletViewDescriptorV1::enabled_for_slot(*slot.slot_id()), std::option::Option::None => crate::WalletViewDescriptorV1::disabled(), }; let envelope = resign_envelope( &self.envelope, view_descriptor, self.envelope.owner_slot().clone(), view_slot, owner_control, metadata, self.envelope.secret().clone(), admin_signing_key, ); let envelope = match verify_and_return(envelope) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok((envelope, new_metadata_key)); } } impl std::ops::Drop for OwnerStateV1 { fn drop(&mut self) { let owner_root = self.owner_root.take(); let metadata_key = self.metadata_key.take(); let secret_key = self.secret_key.take(); let admin_signing_key = self.admin_signing_key.take(); let solana_keypair = self.solana_keypair.take(); drop(owner_root); drop(metadata_key); drop(secret_key); drop(admin_signing_key); drop(solana_keypair); } } /// Crate-internal `ViewStateV1` state shared across the owning crate. pub(crate) struct ViewStateV1 { envelope: crate::KspWalletEnvelopeV1, metadata_key: std::option::Option, } impl ViewStateV1 { /// Creates a new `ViewStateV1` value. pub(crate) fn new(envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::SecretKeyV1) -> Self { return Self { envelope, metadata_key: std::option::Option::Some(metadata_key) }; } /// Returns the current envelope. pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 { return &self.envelope; } /// Applies envelope. pub(crate) fn apply_envelope(&mut self, envelope: crate::KspWalletEnvelopeV1) { self.envelope = envelope; return; } /// Executes the crate-internal stage view password rotation operation for `ViewStateV1`. pub(crate) async fn stage_view_password_rotation(&self, new_password: crate::ViewPassword) -> ksp_core_lib::Result { let metadata_key = match self.metadata_key.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), }; let view_slot = match self.envelope.view_slot() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")), }; let new_slot = match rewrap_slot(&self.envelope, *view_slot.slot_id(), crate::WalletKeySlotRoleV1::View, metadata_key, PasswordRotationV1::View(new_password)) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let envelope = crate::KspWalletEnvelopeV1::new_internal( *self.envelope.owner_auth_public_key(), *self.envelope.view_descriptor(), self.envelope.owner_slot().clone(), std::option::Option::Some(new_slot), self.envelope.owner_control().clone(), self.envelope.metadata().clone(), self.envelope.secret().clone(), self.envelope.state_signature().clone(), ); return verify_and_return(envelope); } } impl std::ops::Drop for ViewStateV1 { fn drop(&mut self) { let metadata_key = self.metadata_key.take(); drop(metadata_key); } } enum PasswordRotationV1 { Owner(crate::OwnerPassword), View(crate::ViewPassword), } struct ViewCreationMaterialV1 { slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES], kdf: crate::WalletKdfParametersV1, wrap_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], password: crate::ViewPassword, } /// Creates a new in-memory native `.kspwallet` V1 with a fresh Solana keypair. /// /// The optional VIEW password creates an independent VIEW slot. This function performs no filesystem I/O; use [`crate::create_wallet_file_v1`] when /// the caller also wants no-clobber native filesystem publication. pub async fn create_wallet_v1( owner_password: crate::OwnerPassword, view_password: std::option::Option, metadata: crate::WalletCreateMetadataV1, ) -> ksp_core_lib::Result { let mut solana_secret = match crate::random_bytes::<32>() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let solana_keypair = solana_keypair::Keypair::new_from_array(solana_secret); solana_secret.zeroize(); return crate::create_wallet_v1_from_keypair(owner_password, view_password, metadata, solana_keypair).await; } /// Opens the VIEW capability from a native `.kspwallet` V1 JSON document. pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result { let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let verify_result = verify_state_signature(&envelope); if let std::result::Result::Err(error) = verify_result { return std::result::Result::Err(error); } let view_slot = match envelope.view_slot() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")), }; let derived_result = derive_view_password_key_async(password, view_slot.kdf().clone()).await; let derived = match derived_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let aad = envelope.view_slot_aad(); let view_aad = match aad { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")), }; let metadata_key_result = crate::unwrap_key(&derived, view_slot.wrap().nonce(), view_aad.as_slice(), view_slot.wrap().ciphertext()); let metadata_key = match metadata_key_result { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()), }; let metadata_plaintext_result = crate::decrypt_bytes( &metadata_key, envelope.metadata().nonce(), envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(), envelope.metadata().ciphertext(), ); let mut metadata_plaintext = match metadata_plaintext_result { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()), }; let metadata_payload_result = crate::decode_metadata_payload(metadata_plaintext.as_slice()); metadata_plaintext.zeroize(); let metadata_payload = match metadata_payload_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::View); let state = ViewStateV1::new(envelope, metadata_key); ksp_logging_lib::debug!( target: crate::TRACING_TARGET, operation = "wallet_open_view", format_version = crate::KSPWALLET_FORMAT_VERSION_V1, capability = "view", "native wallet VIEW capability opened" ); return std::result::Result::Ok(crate::WalletView::from_unlocked(info, crate::ViewState::V1(state))); } /// Opens the OWNER capability from a native `.kspwallet` V1 JSON document. pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result { let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let verify_result = verify_state_signature(&envelope); if let std::result::Result::Err(error) = verify_result { return std::result::Result::Err(error); } let owner_slot = envelope.owner_slot(); let derived_result = derive_owner_password_key_async(password, owner_slot.kdf().clone()).await; let derived = match derived_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let owner_root_result = crate::unwrap_key(&derived, owner_slot.wrap().nonce(), envelope.owner_slot_aad().as_slice(), owner_slot.wrap().ciphertext()); let owner_root = match owner_root_result { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()), }; let owner_control_plaintext_result = crate::decrypt_bytes( &owner_root, envelope.owner_control().nonce(), envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(), envelope.owner_control().ciphertext(), ); let mut owner_control_plaintext = match owner_control_plaintext_result { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()), }; let control_result = crate::decode_owner_control_payload(owner_control_plaintext.as_slice()); owner_control_plaintext.zeroize(); let control = match control_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let (mut admin_secret, metadata_key, secret_key) = control.into_parts(); let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret); admin_secret.zeroize(); if admin_signing_key.verifying_key().to_bytes() != *envelope.owner_auth_public_key() { return std::result::Result::Err(authentication_error()); } let metadata_plaintext_result = crate::decrypt_bytes( &metadata_key, envelope.metadata().nonce(), envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(), envelope.metadata().ciphertext(), ); let mut metadata_plaintext = match metadata_plaintext_result { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()), }; let metadata_payload_result = crate::decode_metadata_payload(metadata_plaintext.as_slice()); metadata_plaintext.zeroize(); let metadata_payload = match metadata_payload_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let secret_plaintext_result = crate::decrypt_bytes( &secret_key, envelope.secret().nonce(), envelope.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(), envelope.secret().ciphertext(), ); let mut secret_plaintext = match secret_plaintext_result { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()), }; if secret_plaintext.len() != crate::KSPWALLET_V1_SECRET_PLAINTEXT_BYTES { secret_plaintext.zeroize(); return std::result::Result::Err(key_material_error()); } let keypair_result = solana_keypair::Keypair::try_from(secret_plaintext.as_slice()); let solana_keypair = match keypair_result { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { secret_plaintext.zeroize(); return std::result::Result::Err(key_material_error()); }, }; let secret_pubkey_result = pubkey_from_keypair_bytes(secret_plaintext.as_slice()); let secret_pubkey = match secret_pubkey_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { secret_plaintext.zeroize(); return std::result::Result::Err(error); }, }; secret_plaintext.zeroize(); if &secret_pubkey != metadata_payload.pubkey() { return std::result::Result::Err(key_material_error()); } let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner); let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair); ksp_logging_lib::debug!( target: crate::TRACING_TARGET, operation = "wallet_open_owner", format_version = crate::KSPWALLET_FORMAT_VERSION_V1, capability = "owner", "native wallet OWNER capability opened" ); return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, crate::OwnerState::V1(state))); } /// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material. pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result { let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let verify_result = verify_state_signature(&envelope); if let std::result::Result::Err(error) = verify_result { return std::result::Result::Err(error); } return std::result::Result::Ok(crate::LockedWalletInfo::new(crate::KSPWALLET_FORMAT_VERSION_V1, envelope.view_descriptor().enabled())); } /// Executes the crate-internal create wallet v1 from keypair operation for the owning module. pub(crate) async fn create_wallet_v1_from_keypair( owner_password: crate::OwnerPassword, view_password: std::option::Option, metadata: crate::WalletCreateMetadataV1, solana_keypair: solana_keypair::Keypair, ) -> ksp_core_lib::Result { let owner_root = match crate::SecretKeyV1::random() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let metadata_key = match crate::SecretKeyV1::random() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let secret_key = match crate::SecretKeyV1::random() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut admin_secret = match crate::random_bytes::<{ crate::SECRET_KEY_BYTES }>() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret); let owner_auth_public_key = admin_signing_key.verifying_key().to_bytes(); let mut solana_keypair_bytes = solana_keypair.to_bytes(); let pubkey_result = pubkey_from_keypair_bytes(&solana_keypair_bytes); let pubkey = match pubkey_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { admin_secret.zeroize(); solana_keypair_bytes.zeroize(); return std::result::Result::Err(error); }, }; let metadata_encoded_result = crate::encode_initial_metadata_payload(pubkey, metadata); let (mut metadata_plaintext, metadata_payload) = match metadata_encoded_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { admin_secret.zeroize(); solana_keypair_bytes.zeroize(); return std::result::Result::Err(error); }, }; let mut owner_control_plaintext = crate::encode_owner_control_payload(&admin_secret, &metadata_key, &secret_key); admin_secret.zeroize(); let owner_slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; let owner_salt = match crate::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; let owner_kdf = crate::WalletKdfParametersV1::new_creation(owner_salt.to_vec()); let owner_wrap_nonce = match crate::random_nonce() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; let view_material_result = prepare_view_creation(view_password); let view_material = match view_material_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; let owner_control_nonce = match crate::random_nonce() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; let metadata_nonce = match crate::random_nonce() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; let secret_nonce = match crate::random_nonce() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; let provisional = provisional_envelope( owner_auth_public_key, owner_slot_id, owner_kdf.clone(), owner_wrap_nonce, view_material.as_ref(), owner_control_nonce, metadata_nonce, secret_nonce, ); let owner_derived_result = derive_owner_password_key_async(owner_password, owner_kdf.clone()).await; let owner_derived = match owner_derived_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; let owner_wrapped_result = crate::wrap_key(&owner_derived, &owner_root, &owner_wrap_nonce, provisional.owner_slot_aad().as_slice()); let owner_wrapped = match owner_wrapped_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; let view_slot_result = seal_view_slot(&provisional, view_material, &metadata_key).await; let view_slot = match view_slot_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; let owner_control_ciphertext_result = crate::encrypt_bytes( &owner_root, &owner_control_nonce, provisional.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(), owner_control_plaintext.as_slice(), ); let owner_control_ciphertext = match owner_control_ciphertext_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; owner_control_plaintext.zeroize(); let metadata_ciphertext_result = crate::encrypt_bytes( &metadata_key, &metadata_nonce, provisional.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(), metadata_plaintext.as_slice(), ); let metadata_ciphertext = match metadata_ciphertext_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; metadata_plaintext.zeroize(); let secret_ciphertext_result = crate::encrypt_bytes( &secret_key, &secret_nonce, provisional.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(), solana_keypair_bytes.as_slice(), ); let secret_ciphertext = match secret_ciphertext_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); }, }; solana_keypair_bytes.zeroize(); let owner_slot = crate::WalletKeySlotV1::new(owner_slot_id, crate::WalletKeySlotRoleV1::Owner, owner_kdf, crate::WalletKeyWrapV1::new(owner_wrap_nonce, owner_wrapped)); let view_descriptor = match view_slot.as_ref() { std::option::Option::Some(slot) => crate::WalletViewDescriptorV1::enabled_for_slot(*slot.slot_id()), std::option::Option::None => crate::WalletViewDescriptorV1::disabled(), }; let owner_control = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::OwnerControl, owner_control_nonce, owner_control_ciphertext); let metadata_compartment = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Metadata, metadata_nonce, metadata_ciphertext); let secret_compartment = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Secret, secret_nonce, secret_ciphertext); let unsigned = crate::KspWalletEnvelopeV1::new_internal( owner_auth_public_key, view_descriptor, owner_slot.clone(), view_slot.clone(), owner_control.clone(), metadata_compartment.clone(), secret_compartment.clone(), crate::WalletStateSignatureV1::new([0_u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]), ); let signature = admin_signing_key.sign(unsigned.state_transcript().as_slice()).to_bytes(); let envelope = crate::KspWalletEnvelopeV1::new_internal( owner_auth_public_key, view_descriptor, owner_slot, view_slot, owner_control, metadata_compartment, secret_compartment, crate::WalletStateSignatureV1::new(signature), ); let verify_result = verify_state_signature(&envelope); if let std::result::Result::Err(error) = verify_result { return std::result::Result::Err(error); } let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner); let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair); ksp_logging_lib::debug!( target: crate::TRACING_TARGET, operation = "wallet_create", format_version = crate::KSPWALLET_FORMAT_VERSION_V1, view_enabled = state.envelope().view_descriptor().enabled(), "native wallet created in memory" ); return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, crate::OwnerState::V1(state))); } /// Verifies state signature. pub(crate) fn verify_state_signature(envelope: &crate::KspWalletEnvelopeV1) -> ksp_core_lib::Result<()> { let verifying_key_result = ed25519_dalek::VerifyingKey::from_bytes(envelope.owner_auth_public_key()); let verifying_key = match verifying_key_result { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()), }; let signature = ed25519_dalek::Signature::from_bytes(envelope.state_signature().signature()); let verify_result = verifying_key.verify_strict(envelope.state_transcript().as_slice(), &signature); return match verify_result { std::result::Result::Ok(()) => std::result::Result::Ok(()), std::result::Result::Err(_) => std::result::Result::Err(authentication_error()), }; } async fn rewrap_slot( envelope: &crate::KspWalletEnvelopeV1, slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES], role: crate::WalletKeySlotRoleV1, capability_key: &crate::SecretKeyV1, password: PasswordRotationV1, ) -> ksp_core_lib::Result { let salt = match crate::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let kdf = crate::WalletKdfParametersV1::new_creation(salt.to_vec()); let nonce = match crate::random_nonce() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let derived_result = match password { PasswordRotationV1::Owner(value) => derive_owner_password_key_async(value, kdf.clone()).await, PasswordRotationV1::View(value) => derive_view_password_key_async(value, kdf.clone()).await, }; let derived = match derived_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let provisional = crate::WalletKeySlotV1::new( slot_id, role, kdf.clone(), crate::WalletKeyWrapV1::new(nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]), ); let aad = crate::slot_aad(envelope, &provisional); let wrapped = match crate::wrap_key(&derived, capability_key, &nonce, aad.as_slice()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(crate::WalletKeySlotV1::new(slot_id, role, kdf, crate::WalletKeyWrapV1::new(nonce, wrapped))); } fn resign_envelope( base: &crate::KspWalletEnvelopeV1, view_descriptor: crate::WalletViewDescriptorV1, owner_slot: crate::WalletKeySlotV1, view_slot: std::option::Option, owner_control: crate::WalletEncryptedCompartmentV1, metadata: crate::WalletEncryptedCompartmentV1, secret: crate::WalletEncryptedCompartmentV1, admin_signing_key: &ed25519_dalek::SigningKey, ) -> crate::KspWalletEnvelopeV1 { let unsigned = crate::KspWalletEnvelopeV1::new_internal( *base.owner_auth_public_key(), view_descriptor, owner_slot.clone(), view_slot.clone(), owner_control.clone(), metadata.clone(), secret.clone(), crate::WalletStateSignatureV1::new([0_u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]), ); let signature = admin_signing_key.sign(unsigned.state_transcript().as_slice()).to_bytes(); return crate::KspWalletEnvelopeV1::new_internal( *base.owner_auth_public_key(), view_descriptor, owner_slot, view_slot, owner_control, metadata, secret, crate::WalletStateSignatureV1::new(signature), ); } fn verify_and_return(envelope: crate::KspWalletEnvelopeV1) -> ksp_core_lib::Result { let verify_result = verify_state_signature(&envelope); if let std::result::Result::Err(error) = verify_result { return std::result::Result::Err(error); } return std::result::Result::Ok(envelope); } fn prepare_view_creation(password: std::option::Option) -> ksp_core_lib::Result> { let password = match password { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Ok(std::option::Option::None), }; let slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let salt = match crate::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let wrap_nonce = match crate::random_nonce() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(std::option::Option::Some(ViewCreationMaterialV1 { slot_id, kdf: crate::WalletKdfParametersV1::new_creation(salt.to_vec()), wrap_nonce, password, })); } async fn seal_view_slot( envelope: &crate::KspWalletEnvelopeV1, material: std::option::Option, metadata_key: &crate::SecretKeyV1, ) -> ksp_core_lib::Result> { let material = match material { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Ok(std::option::Option::None), }; let derived_result = derive_view_password_key_async(material.password, material.kdf.clone()).await; let derived = match derived_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let provisional_slot = crate::WalletKeySlotV1::new( material.slot_id, crate::WalletKeySlotRoleV1::View, material.kdf.clone(), crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]), ); let aad = crate::slot_aad(envelope, &provisional_slot); let wrapped_result = crate::wrap_key(&derived, metadata_key, &material.wrap_nonce, aad.as_slice()); let wrapped = match wrapped_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(std::option::Option::Some(crate::WalletKeySlotV1::new( material.slot_id, crate::WalletKeySlotRoleV1::View, material.kdf, crate::WalletKeyWrapV1::new(material.wrap_nonce, wrapped), ))); } fn provisional_envelope( owner_auth_public_key: [u8; crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES], owner_slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES], owner_kdf: crate::WalletKdfParametersV1, owner_wrap_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], view_material: std::option::Option<&ViewCreationMaterialV1>, owner_control_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], metadata_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], secret_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ) -> crate::KspWalletEnvelopeV1 { let owner_slot = crate::WalletKeySlotV1::new( owner_slot_id, crate::WalletKeySlotRoleV1::Owner, owner_kdf, crate::WalletKeyWrapV1::new(owner_wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]), ); let (view_descriptor, view_slot) = match view_material { std::option::Option::Some(material) => ( crate::WalletViewDescriptorV1::enabled_for_slot(material.slot_id), std::option::Option::Some(crate::WalletKeySlotV1::new( material.slot_id, crate::WalletKeySlotRoleV1::View, material.kdf.clone(), crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]), )), ), std::option::Option::None => (crate::WalletViewDescriptorV1::disabled(), std::option::Option::None), }; return crate::KspWalletEnvelopeV1::new_internal( owner_auth_public_key, view_descriptor, owner_slot, view_slot, crate::WalletEncryptedCompartmentV1::new( crate::WalletCompartmentKindV1::OwnerControl, owner_control_nonce, std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES], ), crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Metadata, metadata_nonce, std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES]), crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Secret, secret_nonce, std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES]), crate::WalletStateSignatureV1::new([0_u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]), ); } async fn derive_owner_password_key_async(password: crate::OwnerPassword, kdf: crate::WalletKdfParametersV1) -> ksp_core_lib::Result { let task = tokio::task::spawn_blocking(move || return crate::derive_password_key(password.as_bytes(), &kdf)); return match task.await { std::result::Result::Ok(result) => result, std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()), }; } async fn derive_view_password_key_async(password: crate::ViewPassword, kdf: crate::WalletKdfParametersV1) -> ksp_core_lib::Result { let task = tokio::task::spawn_blocking(move || return crate::derive_password_key(password.as_bytes(), &kdf)); return match task.await { std::result::Result::Ok(result) => result, std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()), }; } fn pubkey_from_keypair_bytes(bytes: &[u8]) -> ksp_core_lib::Result { let public_slice = match bytes.get(32..64) { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(key_material_error()), }; let public_array = match <[u8; 32]>::try_from(public_slice) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(key_material_error()), }; return std::result::Result::Ok(ksp_core_lib::Pubkey::new_from_array(public_array)); } fn cleanup_create_error( error: ksp_core_lib::Error, owner_control_plaintext: &mut [u8], metadata_plaintext: &mut [u8], solana_keypair_bytes: &mut [u8], ) -> ksp_core_lib::Result { owner_control_plaintext.zeroize(); metadata_plaintext.zeroize(); solana_keypair_bytes.zeroize(); return std::result::Result::Err(error); } fn authentication_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_AUTHENTICATION_FAILED, "Wallet OWNER-authenticated state verification failed"); } fn crypto_operation_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_CRYPTO_OPERATION_FAILED, "Wallet blocking cryptographic operation could not complete"); } fn view_unlock_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_VIEW_UNLOCK_FAILED, "Wallet VIEW capability could not be unlocked"); } fn owner_unlock_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_OWNER_UNLOCK_FAILED, "Wallet OWNER capability could not be unlocked"); } fn capability_error(message: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_CAPABILITY_INSUFFICIENT, message); } fn signing_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_SIGNATURE_FAILED, "Wallet Solana message signing failed"); } fn key_material_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_KEY_MATERIAL_INVALID, "Wallet Solana key material is invalid or inconsistent"); } #[cfg(test)] #[path = "../unit_tests/wallet.rs"] mod tests;