315 lines
14 KiB
Rust
315 lines
14 KiB
Rust
// file: crates/ksp-wallet-lib/src/payload.rs
|
|
// version: 3
|
|
|
|
//! Plaintext payload codecs protected inside native `.kspwallet` V1 compartments.
|
|
|
|
use base64::Engine as _;
|
|
use std::str::FromStr as _;
|
|
|
|
pub(crate) struct MetadataPayloadV1 {
|
|
pubkey: ksp_core_lib::Pubkey,
|
|
alias: std::option::Option<std::string::String>,
|
|
notes: std::vec::Vec<crate::WalletNote>,
|
|
}
|
|
|
|
impl MetadataPayloadV1 {
|
|
pub(crate) const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
|
|
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);
|
|
}
|
|
}
|
|
|
|
pub(crate) struct OwnerControlMaterialV1 {
|
|
admin_signing_secret: [u8; crate::crypto::SECRET_KEY_BYTES],
|
|
metadata_key: crate::crypto::SecretKeyV1,
|
|
secret_key: crate::crypto::SecretKeyV1,
|
|
}
|
|
|
|
impl OwnerControlMaterialV1 {
|
|
pub(crate) fn into_parts(mut self) -> ([u8; crate::crypto::SECRET_KEY_BYTES], crate::crypto::SecretKeyV1, crate::crypto::SecretKeyV1) {
|
|
let mut admin_signing_secret = [0_u8; crate::crypto::SECRET_KEY_BYTES];
|
|
std::mem::swap(&mut admin_signing_secret, &mut self.admin_signing_secret);
|
|
let metadata_key = std::mem::replace(&mut self.metadata_key, crate::crypto::SecretKeyV1::from_bytes([0_u8; crate::crypto::SECRET_KEY_BYTES]));
|
|
let secret_key = std::mem::replace(&mut self.secret_key, crate::crypto::SecretKeyV1::from_bytes([0_u8; crate::crypto::SECRET_KEY_BYTES]));
|
|
return (admin_signing_secret, metadata_key, secret_key);
|
|
}
|
|
}
|
|
|
|
impl std::ops::Drop for OwnerControlMaterialV1 {
|
|
fn drop(&mut self) {
|
|
zeroize::Zeroize::zeroize(&mut self.admin_signing_secret);
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Deserialize, serde::Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct RawMetadataPayloadV1 {
|
|
pubkey: std::string::String,
|
|
alias: std::option::Option<std::string::String>,
|
|
notes: std::vec::Vec<RawMetadataNoteV1>,
|
|
}
|
|
|
|
#[derive(serde::Deserialize, serde::Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct RawMetadataNoteV1 {
|
|
id: std::string::String,
|
|
text: std::string::String,
|
|
}
|
|
|
|
pub(crate) fn encode_initial_metadata_payload(
|
|
pubkey: ksp_core_lib::Pubkey,
|
|
metadata: crate::WalletCreateMetadataV1,
|
|
) -> ksp_core_lib::Result<(std::vec::Vec<u8>, MetadataPayloadV1)> {
|
|
let (alias, note_texts) = metadata.into_parts();
|
|
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 notes = std::vec::Vec::with_capacity(note_texts.len());
|
|
let mut note_ids = std::collections::BTreeSet::new();
|
|
for text in note_texts {
|
|
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());
|
|
if !note_ids.insert(note_id.clone()) {
|
|
return std::result::Result::Err(random_note_identifier_error());
|
|
}
|
|
notes.push(crate::WalletNote::new(note_id, text));
|
|
}
|
|
|
|
let payload = MetadataPayloadV1 { pubkey, alias, notes };
|
|
let serialized = match payload.encode() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok((serialized, payload));
|
|
}
|
|
|
|
pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<MetadataPayloadV1> {
|
|
if source.len() > crate::KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES {
|
|
return std::result::Result::Err(metadata_error("Wallet metadata payload exceeds the V1 plaintext limit", "metadata"));
|
|
}
|
|
let raw_result = serde_json::from_slice::<RawMetadataPayloadV1>(source);
|
|
let raw = match raw_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata payload is invalid", "metadata")),
|
|
};
|
|
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"));
|
|
}
|
|
|
|
let pubkey_result = ksp_core_lib::Pubkey::from_str(raw.pubkey.as_str());
|
|
let pubkey = match pubkey_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata Pubkey is invalid", "metadata.pubkey")),
|
|
};
|
|
if pubkey.to_string() != raw.pubkey {
|
|
return std::result::Result::Err(metadata_error("Wallet metadata Pubkey is not canonical Base58", "metadata.pubkey"));
|
|
}
|
|
|
|
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 {
|
|
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,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if id_bytes.len() != crate::KSPWALLET_V1_NOTE_ID_BYTES || encode_base64url(id_bytes.as_slice()) != raw_note.id {
|
|
return std::result::Result::Err(metadata_error("Wallet note identifier is not canonical V1 Base64url", "metadata.notes.id"));
|
|
}
|
|
if !note_ids.insert(raw_note.id.clone()) {
|
|
return std::result::Result::Err(metadata_error("Wallet note identifiers must be unique", "metadata.notes.id"));
|
|
}
|
|
notes.push(crate::WalletNote::new(raw_note.id, raw_note.text));
|
|
}
|
|
return std::result::Result::Ok(MetadataPayloadV1 { pubkey, alias: raw.alias, notes });
|
|
}
|
|
|
|
pub(crate) fn encode_owner_control_payload(
|
|
admin_signing_secret: &[u8; crate::crypto::SECRET_KEY_BYTES],
|
|
metadata_key: &crate::crypto::SecretKeyV1,
|
|
secret_key: &crate::crypto::SecretKeyV1,
|
|
) -> [u8; crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES] {
|
|
let mut output = [0_u8; crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES];
|
|
output[0..32].copy_from_slice(admin_signing_secret);
|
|
output[32..64].copy_from_slice(metadata_key.as_bytes());
|
|
output[64..96].copy_from_slice(secret_key.as_bytes());
|
|
return output;
|
|
}
|
|
|
|
pub(crate) fn decode_owner_control_payload(source: &[u8]) -> ksp_core_lib::Result<OwnerControlMaterialV1> {
|
|
if source.len() != crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES {
|
|
return std::result::Result::Err(metadata_error("Wallet OWNER-control payload length is invalid", "owner_control"));
|
|
}
|
|
let mut admin_signing_secret = [0_u8; crate::crypto::SECRET_KEY_BYTES];
|
|
admin_signing_secret.copy_from_slice(&source[0..32]);
|
|
let mut metadata_key = [0_u8; crate::crypto::SECRET_KEY_BYTES];
|
|
metadata_key.copy_from_slice(&source[32..64]);
|
|
let mut secret_key = [0_u8; crate::crypto::SECRET_KEY_BYTES];
|
|
secret_key.copy_from_slice(&source[64..96]);
|
|
return std::result::Result::Ok(OwnerControlMaterialV1 {
|
|
admin_signing_secret,
|
|
metadata_key: crate::crypto::SecretKeyV1::from_bytes(metadata_key),
|
|
secret_key: crate::crypto::SecretKeyV1::from_bytes(secret_key),
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
fn decode_base64url(encoded: &str) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
|
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded.as_bytes());
|
|
let value = match decoded {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata Base64url field is invalid", "metadata.notes.id")),
|
|
};
|
|
return std::result::Result::Ok(value);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/payload.rs"]
|
|
mod tests;
|