v0.2.5-pre.007

This commit is contained in:
2026-08-19 18:08:01 +02:00
parent 25edcc231e
commit 46afe10423
17 changed files with 1431 additions and 59 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/constants.rs
// version: 3
// version: 4
//! Wallet-owned constants.
@@ -27,6 +27,8 @@ pub const KSPWALLET_V1_NOTE_ID_BYTES: usize = 16;
pub const KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES: usize = 32;
/// Byte length of an Ed25519 detached state signature.
pub const KSPWALLET_V1_ED25519_SIGNATURE_BYTES: usize = 64;
/// Byte length of every Solana Ed25519 message signature returned by Wallet OWNER.
pub const KSPWALLET_SOLANA_SIGNATURE_BYTES: usize = 64;
/// Byte length of an XChaCha20-Poly1305 nonce.
pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
/// Byte length of the Poly1305 authentication tag appended to each ciphertext.
@@ -67,7 +69,6 @@ pub const KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_M
pub const KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES: usize = 4096;
/// Initial protected payload version used independently by control, metadata and secret compartments.
pub const KSPWALLET_V1_INITIAL_PAYLOAD_VERSION: u32 = 1;
/// Domain separator for the OWNER state-signature transcript.
pub const KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V1-STATE";
/// Domain separator for OWNER key-slot wrapping AAD.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/error.rs
// version: 3
// version: 4
/// Error code used when a native Wallet structure is invalid.
pub const ERROR_CODE_FORMAT_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "format_invalid");
@@ -31,3 +31,7 @@ pub const ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED: ksp_core_lib::ErrorCode = ksp_
pub const ERROR_CODE_KEY_MATERIAL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "key_material_invalid");
/// Error code used when a Wallet signing operation fails.
pub const ERROR_CODE_SIGNATURE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "signature_failed");
/// Error code used when a protected Wallet note identifier is not present.
pub const ERROR_CODE_NOTE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "note_not_found");
/// Error code used when an administrative replacement targets a different or stale Wallet state.
pub const ERROR_CODE_STATE_CONFLICT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "state_conflict");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/lib.rs
// version: 6
// version: 7
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -10,9 +10,9 @@
//! error contract. `0.2.5-pre.003` freezes the strict V1 JSON envelope, canonical Base64url decoding, structural limits and deterministic
//! state-transcript/AEAD-AAD byte codecs. `0.2.5-pre.004` adds the in-memory Argon2id/XChaCha20-Poly1305/CSPRNG primitives and deterministic crypto
//! vectors. `0.2.5-pre.005` adds exact protected payloads, OWNER Ed25519 state authentication and async in-memory create/open flows for VIEW and OWNER.
//! `0.2.5-pre.006` adds bounded async-first filesystem reads plus same-directory synchronized no-clobber publication for new native files. Solana
//! transaction signing and OWNER/VIEW administration remain outside this tranche. Public
//! keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral observability uses only
//! `0.2.5-pre.006` adds bounded async-first filesystem reads plus same-directory synchronized no-clobber publication for new native files. `0.2.5-pre.007`
//! adds Solana message signing, protected metadata administration, password rotation and strong VIEW disable/recreate with capability-bound atomic
//! replacement. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral observability uses only
//! `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
mod capability;
@@ -37,6 +37,8 @@ pub use self::constants::KSPWALLET_FORMAT_VERSION_V1;
pub use self::constants::KSPWALLET_MAGIC;
/// Maximum accepted `.kspwallet` document size before parsing.
pub use self::constants::KSPWALLET_MAX_FILE_BYTES;
/// Byte length of every Solana Ed25519 signature returned by OWNER.
pub use self::constants::KSPWALLET_SOLANA_SIGNATURE_BYTES;
/// Byte length of the AEAD authentication tag appended to V1 ciphertexts.
pub use self::constants::KSPWALLET_V1_AEAD_TAG_BYTES;
/// Argon2 version serialized by `.kspwallet` V1 key slots.
@@ -127,12 +129,16 @@ pub use self::error::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED;
pub use self::error::ERROR_CODE_IO_FAILED;
/// Error code used when imported or decoded key material is invalid.
pub use self::error::ERROR_CODE_KEY_MATERIAL_INVALID;
/// Error code used when a protected Wallet note identifier is absent.
pub use self::error::ERROR_CODE_NOTE_NOT_FOUND;
/// Error code used when an OWNER unlock attempt fails without exposing a finer cryptographic oracle.
pub use self::error::ERROR_CODE_OWNER_UNLOCK_FAILED;
/// Error code used when the operating-system cryptographic random source cannot provide bytes.
pub use self::error::ERROR_CODE_RANDOMNESS_FAILED;
/// Error code used when a Wallet signing operation fails.
pub use self::error::ERROR_CODE_SIGNATURE_FAILED;
/// Error code used when an administrative replacement sees a different or stale Wallet state.
pub use self::error::ERROR_CODE_STATE_CONFLICT;
/// Error code used when an import/export transfer format is unsupported.
pub use self::error::ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED;
/// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle.

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-wallet-lib/src/owner.rs
// version: 2
// version: 3
/// Authorized OWNER capability handle.
///
/// OWNER exposes protected metadata and retains the authenticated in-memory state required by later signing and administration tranches without
/// exposing a general-purpose secret-key getter.
/// OWNER exposes protected metadata, Solana message signing and the authenticated administration operations defined by native Wallet V1. Secret key
/// material remains encapsulated and is never exposed through a general-purpose getter.
pub struct WalletOwner {
info: crate::WalletInfo,
state: crate::wallet::OwnerStateV1,
@@ -51,10 +51,212 @@ impl WalletOwner {
return &self.info;
}
/// Signs one caller-provided message with the Wallet's immutable Solana keypair.
///
/// The returned bytes are the 64-byte Ed25519 signature. The Solana secret key is never returned or logged.
pub fn sign(&self, message: &[u8]) -> ksp_core_lib::Result<[u8; crate::KSPWALLET_SOLANA_SIGNATURE_BYTES]> {
let signature = self.state.sign_message(message);
if signature.is_ok() {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_sign",
capability = "owner",
message_bytes = message.len(),
"wallet message signed"
);
}
return signature;
}
/// Updates or clears the protected internal alias and atomically replaces the existing wallet file.
pub async fn update_alias(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
alias: std::option::Option<std::string::String>,
) -> ksp_core_lib::Result<()> {
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
let mutation_result = payload.set_alias(alias);
if let std::result::Result::Err(error) = mutation_result {
return std::result::Result::Err(error);
}
return self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_update_alias").await;
}
/// Adds one protected note and returns its generated stable note identifier after successful persistence.
pub async fn add_note(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
text: std::string::String,
) -> ksp_core_lib::Result<std::string::String> {
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
let note_id = match payload.add_note(text) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_add_note").await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(note_id);
}
/// Updates one protected note selected by its stable identifier.
pub async fn update_note(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
note_id: &str,
text: std::string::String,
) -> ksp_core_lib::Result<()> {
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
let mutation_result = payload.update_note(note_id, text);
if let std::result::Result::Err(error) = mutation_result {
return std::result::Result::Err(error);
}
return self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_update_note").await;
}
/// Deletes one protected note selected by its stable identifier.
pub async fn delete_note(&mut self, destination: impl std::convert::AsRef<std::path::Path>, note_id: &str) -> ksp_core_lib::Result<()> {
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
let mutation_result = payload.delete_note(note_id);
if let std::result::Result::Err(error) = mutation_result {
return std::result::Result::Err(error);
}
return self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_delete_note").await;
}
/// Rotates the OWNER password while preserving the Wallet identity, metadata content key and Solana keypair.
pub async fn rotate_owner_password(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
new_password: crate::OwnerPassword,
) -> ksp_core_lib::Result<()> {
let envelope = match self.state.stage_owner_password_rotation(new_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_rotate_owner_password",
capability = "owner",
"wallet OWNER password rotated"
);
return std::result::Result::Ok(());
}
/// Rotates the existing VIEW password without knowing the previous VIEW password.
///
/// This is a credential rotation only: it rewraps the same metadata content key in the same OWNER-signed VIEW slot descriptor.
pub async fn rotate_view_password(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
new_password: crate::ViewPassword,
) -> ksp_core_lib::Result<()> {
let envelope = match self.state.stage_view_password_rotation(new_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_rotate_view_password",
capability = "owner",
"wallet VIEW password rotated by OWNER"
);
return std::result::Result::Ok(());
}
/// Strongly disables VIEW by generating a new metadata content key and removing the VIEW slot/descriptor.
///
/// Existing historical copies remain outside the revocation guarantee, but the old VIEW metadata key cannot decrypt future metadata states.
pub async fn disable_view(&mut self, destination: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<()> {
let (envelope, metadata_key) = match self.state.stage_disable_view() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_strong_view_state(envelope, metadata_key);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_disable_view",
capability = "owner",
"wallet VIEW capability strongly disabled"
);
return std::result::Result::Ok(());
}
/// Strongly recreates VIEW with a fresh metadata content key, fresh slot identifier and new VIEW password.
pub async fn recreate_view(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
new_password: crate::ViewPassword,
) -> ksp_core_lib::Result<()> {
let (envelope, metadata_key) = match self.state.stage_recreate_view(new_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_strong_view_state(envelope, metadata_key);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_recreate_view",
capability = "owner",
"wallet VIEW capability strongly recreated"
);
return std::result::Result::Ok(());
}
/// Serializes the complete locked `.kspwallet` V1 document without exposing any unlocked secret material.
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.envelope().to_json_bytes();
}
async fn persist_metadata_payload(
&mut self,
destination: std::path::PathBuf,
payload: crate::payload::MetadataPayloadV1,
operation: &'static str,
) -> ksp_core_lib::Result<()> {
let (envelope, info) = match self.state.stage_metadata_payload(payload) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination, self.state.envelope(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
self.info = info;
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, operation = operation, capability = "owner", "wallet protected metadata updated");
return std::result::Result::Ok(());
}
}
async fn persist_staged(
destination: std::path::PathBuf,
expected_current: &crate::KspWalletEnvelopeV1,
envelope: &crate::KspWalletEnvelopeV1,
) -> ksp_core_lib::Result<()> {
let serialized = match envelope.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::persistence::replace_wallet_file_v1(destination, expected_current.clone(), serialized).await;
}
impl std::fmt::Debug for WalletOwner {
@@ -62,3 +264,7 @@ impl std::fmt::Debug for WalletOwner {
return formatter.debug_struct("WalletOwner").field("info", &self.info).field("unlocked_state", &"<redacted>").finish();
}
}
#[cfg(test)]
#[path = "../unit_tests/administration.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/payload.rs
// version: 2
// version: 3
//! Plaintext payload codecs protected inside native `.kspwallet` V1 compartments.
@@ -17,6 +17,99 @@ impl MetadataPayloadV1 {
return &self.pubkey;
}
pub(crate) fn from_info(info: &crate::WalletInfo) -> Self {
let alias = info.alias().map(std::string::String::from);
let notes = info.notes().to_vec();
return Self { pubkey: *info.pubkey(), alias, notes };
}
pub(crate) fn set_alias(&mut self, alias: std::option::Option<std::string::String>) -> ksp_core_lib::Result<()> {
let validation_result = validate_alias(alias.as_deref());
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
self.alias = alias;
return std::result::Result::Ok(());
}
pub(crate) fn add_note(&mut self, text: std::string::String) -> ksp_core_lib::Result<std::string::String> {
if self.notes.len() >= crate::KSPWALLET_V1_MAX_NOTES {
return std::result::Result::Err(metadata_error("Wallet note count exceeds the V1 limit", "metadata.notes"));
}
let validation_result = validate_note_text(text.as_str());
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let note_id_bytes = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_NOTE_ID_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let note_id = encode_base64url(note_id_bytes.as_slice());
for note in &self.notes {
if note.id() == note_id.as_str() {
return std::result::Result::Err(random_note_identifier_error());
}
}
self.notes.push(crate::WalletNote::new(note_id.clone(), text));
return std::result::Result::Ok(note_id);
}
pub(crate) fn update_note(&mut self, note_id: &str, text: std::string::String) -> ksp_core_lib::Result<()> {
let validation_result = validate_note_text(text.as_str());
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let mut index = 0_usize;
while index < self.notes.len() {
if self.notes[index].id() == note_id {
self.notes[index] = crate::WalletNote::new(std::string::String::from(note_id), text);
return std::result::Result::Ok(());
}
index += 1;
}
return std::result::Result::Err(note_not_found_error());
}
pub(crate) fn delete_note(&mut self, note_id: &str) -> ksp_core_lib::Result<()> {
let mut index = 0_usize;
while index < self.notes.len() {
if self.notes[index].id() == note_id {
self.notes.remove(index);
return std::result::Result::Ok(());
}
index += 1;
}
return std::result::Result::Err(note_not_found_error());
}
pub(crate) fn encode(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let validation_result = validate_alias(self.alias.as_deref());
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
if self.notes.len() > crate::KSPWALLET_V1_MAX_NOTES {
return std::result::Result::Err(metadata_error("Wallet note count exceeds the V1 limit", "metadata.notes"));
}
let mut raw_notes = std::vec::Vec::with_capacity(self.notes.len());
for note in &self.notes {
let validation_result = validate_note_text(note.text());
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
raw_notes.push(RawMetadataNoteV1 { id: std::string::String::from(note.id()), text: std::string::String::from(note.text()) });
}
let raw = RawMetadataPayloadV1 { pubkey: self.pubkey.to_string(), alias: self.alias.clone(), notes: raw_notes };
let serialized_result = serde_json::to_vec(&raw);
let serialized = match serialized_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata payload cannot be serialized", "metadata")),
};
if serialized.len() > crate::KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES {
return std::result::Result::Err(metadata_error("Wallet metadata payload exceeds the V1 plaintext limit", "metadata"));
}
return std::result::Result::Ok(serialized);
}
pub(crate) fn into_info(self, capability: crate::WalletCapability) -> crate::WalletInfo {
return crate::WalletInfo::new(capability, self.pubkey, self.alias, self.notes);
}
@@ -64,21 +157,20 @@ pub(crate) fn encode_initial_metadata_payload(
metadata: crate::WalletCreateMetadataV1,
) -> ksp_core_lib::Result<(std::vec::Vec<u8>, MetadataPayloadV1)> {
let (alias, note_texts) = metadata.into_parts();
if let std::option::Option::Some(alias_value) = alias.as_ref()
&& alias_value.len() > crate::KSPWALLET_V1_MAX_ALIAS_BYTES
{
return std::result::Result::Err(metadata_error("Wallet alias exceeds the V1 UTF-8 byte limit", "metadata.alias"));
let validation_result = validate_alias(alias.as_deref());
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
if note_texts.len() > crate::KSPWALLET_V1_MAX_NOTES {
return std::result::Result::Err(metadata_error("Wallet note count exceeds the V1 limit", "metadata.notes"));
}
let mut raw_notes = std::vec::Vec::with_capacity(note_texts.len());
let mut notes = std::vec::Vec::with_capacity(note_texts.len());
let mut note_ids = std::collections::BTreeSet::new();
for text in note_texts {
if text.len() > crate::KSPWALLET_V1_MAX_NOTE_TEXT_BYTES {
return std::result::Result::Err(metadata_error("Wallet note text exceeds the V1 UTF-8 byte limit", "metadata.notes.text"));
let validation_result = validate_note_text(text.as_str());
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let note_id_bytes = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_NOTE_ID_BYTES }>() {
std::result::Result::Ok(value) => value,
@@ -88,20 +180,15 @@ pub(crate) fn encode_initial_metadata_payload(
if !note_ids.insert(note_id.clone()) {
return std::result::Result::Err(random_note_identifier_error());
}
raw_notes.push(RawMetadataNoteV1 { id: note_id.clone(), text: text.clone() });
notes.push(crate::WalletNote::new(note_id, text));
}
let raw = RawMetadataPayloadV1 { pubkey: pubkey.to_string(), alias: alias.clone(), notes: raw_notes };
let serialized_result = serde_json::to_vec(&raw);
let serialized = match serialized_result {
let payload = MetadataPayloadV1 { pubkey, alias, notes };
let serialized = match payload.encode() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata payload cannot be serialized", "metadata")),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if serialized.len() > crate::KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES {
return std::result::Result::Err(metadata_error("Wallet metadata payload exceeds the V1 plaintext limit", "metadata"));
}
return std::result::Result::Ok((serialized, MetadataPayloadV1 { pubkey, alias, notes }));
return std::result::Result::Ok((serialized, payload));
}
pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<MetadataPayloadV1> {
@@ -113,10 +200,9 @@ pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<Met
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata payload is invalid", "metadata")),
};
if let std::option::Option::Some(alias) = raw.alias.as_ref()
&& alias.len() > crate::KSPWALLET_V1_MAX_ALIAS_BYTES
{
return std::result::Result::Err(metadata_error("Wallet alias exceeds the V1 UTF-8 byte limit", "metadata.alias"));
let validation_result = validate_alias(raw.alias.as_deref());
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
if raw.notes.len() > crate::KSPWALLET_V1_MAX_NOTES {
return std::result::Result::Err(metadata_error("Wallet note count exceeds the V1 limit", "metadata.notes"));
@@ -134,8 +220,9 @@ pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<Met
let mut notes = std::vec::Vec::with_capacity(raw.notes.len());
let mut note_ids = std::collections::BTreeSet::new();
for raw_note in raw.notes {
if raw_note.text.len() > crate::KSPWALLET_V1_MAX_NOTE_TEXT_BYTES {
return std::result::Result::Err(metadata_error("Wallet note text exceeds the V1 UTF-8 byte limit", "metadata.notes.text"));
let validation_result = validate_note_text(raw_note.text.as_str());
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let id_bytes = match decode_base64url(raw_note.id.as_str()) {
std::result::Result::Ok(value) => value,
@@ -181,6 +268,22 @@ pub(crate) fn decode_owner_control_payload(source: &[u8]) -> ksp_core_lib::Resul
});
}
fn validate_alias(alias: std::option::Option<&str>) -> ksp_core_lib::Result<()> {
if let std::option::Option::Some(alias_value) = alias
&& alias_value.len() > crate::KSPWALLET_V1_MAX_ALIAS_BYTES
{
return std::result::Result::Err(metadata_error("Wallet alias exceeds the V1 UTF-8 byte limit", "metadata.alias"));
}
return std::result::Result::Ok(());
}
fn validate_note_text(text: &str) -> ksp_core_lib::Result<()> {
if text.len() > crate::KSPWALLET_V1_MAX_NOTE_TEXT_BYTES {
return std::result::Result::Err(metadata_error("Wallet note text exceeds the V1 UTF-8 byte limit", "metadata.notes.text"));
}
return std::result::Result::Ok(());
}
fn encode_base64url(bytes: &[u8]) -> std::string::String {
return base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
}
@@ -198,6 +301,10 @@ fn random_note_identifier_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_RANDOMNESS_FAILED, "Wallet generated a duplicate protected note identifier");
}
fn note_not_found_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_NOTE_NOT_FOUND, "Wallet note identifier was not found");
}
fn metadata_error(message: &'static str, field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message).with_context("field", field);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/persistence.rs
// version: 1
// version: 2
//! Async-first native Wallet V1 filesystem persistence.
@@ -98,6 +98,20 @@ async fn persist_new_wallet_async(destination: std::path::PathBuf, content: std:
};
}
pub(crate) async fn replace_wallet_file_v1(
destination: std::path::PathBuf,
expected_current: crate::KspWalletEnvelopeV1,
content: std::vec::Vec<u8>,
) -> ksp_core_lib::Result<()> {
let task = tokio::task::spawn_blocking(move || {
return replace_wallet_file_checked_blocking(destination.as_path(), &expected_current, content.as_slice());
});
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(error) => std::result::Result::Err(blocking_atomic_error("replace_task", error)),
};
}
fn read_wallet_file_blocking(source: &std::path::Path) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let opened = std::fs::File::open(source);
let file = match opened {
@@ -130,6 +144,95 @@ fn persist_new_wallet_blocking(destination: &std::path::Path, content: &[u8]) ->
return persist_new_wallet_with_hook(destination, content, || return std::result::Result::Ok(()));
}
fn replace_wallet_file_checked_blocking(
destination: &std::path::Path,
expected_current: &crate::KspWalletEnvelopeV1,
content: &[u8],
) -> ksp_core_lib::Result<()> {
let current_check = verify_expected_wallet_state(destination, expected_current);
if let std::result::Result::Err(error) = current_check {
return std::result::Result::Err(error);
}
return replace_wallet_file_with_hook(destination, content, || return verify_expected_wallet_state(destination, expected_current));
}
fn replace_wallet_file_blocking(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return replace_wallet_file_with_hook(destination, content, || return std::result::Result::Ok(()));
}
fn verify_expected_wallet_state(destination: &std::path::Path, expected_current: &crate::KspWalletEnvelopeV1) -> ksp_core_lib::Result<()> {
let current_bytes = match read_wallet_file_blocking(destination) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let current = match crate::KspWalletEnvelopeV1::parse_json(current_bytes.as_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_result = crate::wallet::verify_state_signature(&current);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
if &current != expected_current {
return std::result::Result::Err(state_conflict_error());
}
return std::result::Result::Ok(());
}
fn replace_wallet_file_with_hook<F>(destination: &std::path::Path, content: &[u8], before_publish: F) -> ksp_core_lib::Result<()>
where
F: std::ops::FnOnce() -> ksp_core_lib::Result<()>,
{
if content.len() > crate::KSPWALLET_MAX_FILE_BYTES {
return std::result::Result::Err(oversized_document_error());
}
let parent = match destination_parent(destination) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if destination.file_name().is_none() {
return std::result::Result::Err(atomic_error("destination", "Wallet destination has no file name"));
}
let existing_metadata = std::fs::metadata(destination);
let existing_metadata = match existing_metadata {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(io_error("replace_metadata", error)),
};
if !existing_metadata.is_file() {
return std::result::Result::Err(atomic_error("replace_destination", "Wallet replacement destination is not a regular file"));
}
let temporary_result = tempfile::Builder::new().prefix(".kspwallet-replace-").suffix(".tmp").tempfile_in(parent);
let mut temporary = match temporary_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(atomic_io_error("replacement_temporary_create", error)),
};
let write_result = temporary.write_all(content);
if let std::result::Result::Err(error) = write_result {
return std::result::Result::Err(atomic_io_error("replacement_temporary_write", error));
}
let sync_result = temporary.as_file().sync_all();
if let std::result::Result::Err(error) = sync_result {
return std::result::Result::Err(atomic_io_error("replacement_temporary_sync", error));
}
let hook_result = before_publish();
if let std::result::Result::Err(error) = hook_result {
return std::result::Result::Err(error);
}
let persist_result = temporary.persist(destination);
let persisted = match persist_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(atomic_io_error("replacement_publish", error.error)),
};
let final_sync_result = persisted.sync_all();
if let std::result::Result::Err(error) = final_sync_result {
return std::result::Result::Err(atomic_io_error("replacement_file_sync", error));
}
sync_parent_directory_best_effort(parent);
return std::result::Result::Ok(());
}
fn persist_new_wallet_with_hook<F>(destination: &std::path::Path, content: &[u8], before_publish: F) -> ksp_core_lib::Result<()>
where
F: std::ops::FnOnce() -> ksp_core_lib::Result<()>,
@@ -230,6 +333,10 @@ fn destination_exists_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_DESTINATION_EXISTS, "Wallet destination already exists");
}
fn state_conflict_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_STATE_CONFLICT, "Wallet replacement source state does not match the authenticated handle");
}
fn io_error(operation: &'static str, source: std::io::Error) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_IO_FAILED, "Wallet filesystem I/O failed")
.with_context("operation", operation)
@@ -270,6 +377,18 @@ pub(crate) fn persist_new_wallet_for_test(destination: &std::path::Path, content
return persist_new_wallet_blocking(destination, content);
}
#[cfg(test)]
pub(crate) fn replace_wallet_fault_before_publish(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return replace_wallet_file_with_hook(destination, content, || {
return std::result::Result::Err(atomic_error("replace_fault_injection", "Injected Wallet replacement failure before publication"));
});
}
#[cfg(test)]
pub(crate) fn replace_wallet_for_test(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return replace_wallet_file_blocking(destination, content);
}
#[cfg(test)]
#[path = "../unit_tests/persistence.rs"]
mod tests;

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-wallet-lib/src/view.rs
// version: 2
// version: 3
/// Authorized VIEW capability handle.
///
/// VIEW exposes protected metadata and retains only the authenticated metadata capability needed for its future self-service password rotation. It never
/// owns the Solana secret or OWNER administration material.
/// VIEW exposes protected metadata and retains only the authenticated metadata capability required for self-service password rotation. It never owns the
/// Solana secret, OWNER administration material or metadata-write authority.
pub struct WalletView {
info: crate::WalletInfo,
state: crate::wallet::ViewStateV1,
@@ -51,6 +51,37 @@ impl WalletView {
return &self.info;
}
/// Rotates only this VIEW credential and atomically replaces the existing wallet file.
///
/// The same metadata content key and OWNER-signed VIEW slot identifier are retained. Alias, notes, Pubkey, OWNER material, activation state and the
/// Solana secret remain unchanged.
pub async fn rotate_view_password(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
new_password: crate::ViewPassword,
) -> ksp_core_lib::Result<()> {
let envelope = match self.state.stage_view_password_rotation(new_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let serialized = match envelope.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = crate::persistence::replace_wallet_file_v1(destination.as_ref().to_path_buf(), self.state.envelope().clone(), serialized).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_rotate_view_password",
capability = "view",
"wallet VIEW password self-rotated"
);
return std::result::Result::Ok(());
}
/// Serializes the unchanged locked `.kspwallet` V1 document without exposing the metadata content key.
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.envelope().to_json_bytes();

View File

@@ -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");
}