v0.2.5-pre.005
This commit is contained in:
207
crates/ksp-wallet-lib/src/payload.rs
Normal file
207
crates/ksp-wallet-lib/src/payload.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
// file: crates/ksp-wallet-lib/src/payload.rs
|
||||
// version: 2
|
||||
|
||||
//! 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 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();
|
||||
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"));
|
||||
}
|
||||
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 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());
|
||||
}
|
||||
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 {
|
||||
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, MetadataPayloadV1 { pubkey, alias, notes }));
|
||||
}
|
||||
|
||||
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")),
|
||||
};
|
||||
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"));
|
||||
}
|
||||
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 {
|
||||
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 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 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 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;
|
||||
Reference in New Issue
Block a user