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/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);
}