v0.2.5-pre.007
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/wallet.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! In-memory native Wallet V1 create/open orchestration.
|
||||
|
||||
@@ -37,6 +37,283 @@ impl OwnerStateV1 {
|
||||
pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 {
|
||||
return &self.envelope;
|
||||
}
|
||||
|
||||
pub(crate) fn apply_envelope(&mut self, envelope: crate::KspWalletEnvelopeV1) {
|
||||
self.envelope = envelope;
|
||||
return;
|
||||
}
|
||||
|
||||
pub(crate) fn apply_strong_view_state(&mut self, envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::crypto::SecretKeyV1) {
|
||||
self.envelope = envelope;
|
||||
let previous = self.metadata_key.replace(metadata_key);
|
||||
drop(previous);
|
||||
return;
|
||||
}
|
||||
|
||||
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::crypto::SECRET_KEY_BYTES];
|
||||
seed.copy_from_slice(&keypair_bytes[0..crate::crypto::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);
|
||||
}
|
||||
|
||||
pub(crate) fn stage_metadata_payload(
|
||||
&self,
|
||||
payload: crate::payload::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::crypto::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
plaintext.zeroize();
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
let encrypted = crate::crypto::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::WalletCapability::Owner)));
|
||||
}
|
||||
|
||||
pub(crate) async fn stage_owner_password_rotation(&self, new_password: crate::OwnerPassword) -> ksp_core_lib::Result<crate::KspWalletEnvelopeV1> {
|
||||
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);
|
||||
}
|
||||
|
||||
pub(crate) async fn stage_view_password_rotation(&self, new_password: crate::ViewPassword) -> ksp_core_lib::Result<crate::KspWalletEnvelopeV1> {
|
||||
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);
|
||||
}
|
||||
|
||||
pub(crate) fn stage_disable_view(&self) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::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);
|
||||
}
|
||||
|
||||
pub(crate) async fn stage_recreate_view(
|
||||
&self,
|
||||
new_password: crate::ViewPassword,
|
||||
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::SecretKeyV1)> {
|
||||
let slot_id = match crate::crypto::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::crypto::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<crate::WalletKeySlotV1>,
|
||||
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::SecretKeyV1)> {
|
||||
let new_metadata_key = match crate::crypto::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<crate::WalletKeySlotV1>,
|
||||
new_metadata_key: crate::crypto::SecretKeyV1,
|
||||
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::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::crypto::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::crypto::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::crypto::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::payload::encode_owner_control_payload(&admin_secret, &new_metadata_key, secret_key);
|
||||
admin_secret.zeroize();
|
||||
let owner_control_nonce = match crate::crypto::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::crypto::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 {
|
||||
@@ -67,6 +344,40 @@ impl ViewStateV1 {
|
||||
pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 {
|
||||
return &self.envelope;
|
||||
}
|
||||
|
||||
pub(crate) fn apply_envelope(&mut self, envelope: crate::KspWalletEnvelopeV1) {
|
||||
self.envelope = envelope;
|
||||
return;
|
||||
}
|
||||
|
||||
pub(crate) async fn stage_view_password_rotation(&self, new_password: crate::ViewPassword) -> ksp_core_lib::Result<crate::KspWalletEnvelopeV1> {
|
||||
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 {
|
||||
@@ -78,8 +389,8 @@ impl std::ops::Drop for ViewStateV1 {
|
||||
|
||||
/// 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 the OWNER handle's
|
||||
/// `to_json_bytes()` projection for the complete locked document until `pre.006` adds atomic persistence.
|
||||
/// 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<crate::ViewPassword>,
|
||||
@@ -500,6 +811,90 @@ pub(crate) fn verify_state_signature(envelope: &crate::KspWalletEnvelopeV1) -> k
|
||||
};
|
||||
}
|
||||
|
||||
enum PasswordRotationV1 {
|
||||
Owner(crate::OwnerPassword),
|
||||
View(crate::ViewPassword),
|
||||
}
|
||||
|
||||
async fn rewrap_slot(
|
||||
envelope: &crate::KspWalletEnvelopeV1,
|
||||
slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
|
||||
role: crate::WalletKeySlotRoleV1,
|
||||
capability_key: &crate::crypto::SecretKeyV1,
|
||||
password: PasswordRotationV1,
|
||||
) -> ksp_core_lib::Result<crate::WalletKeySlotV1> {
|
||||
let salt = match crate::crypto::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::crypto::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::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
);
|
||||
let aad = crate::slot_aad(envelope, &provisional);
|
||||
let wrapped = match crate::crypto::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<crate::WalletKeySlotV1>,
|
||||
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<crate::KspWalletEnvelopeV1> {
|
||||
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);
|
||||
}
|
||||
|
||||
struct ViewCreationMaterialV1 {
|
||||
slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
|
||||
kdf: crate::WalletKdfParametersV1,
|
||||
@@ -673,6 +1068,10 @@ 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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user