724 lines
25 KiB
Rust
724 lines
25 KiB
Rust
use wasm_bindgen::prelude::*;
|
|
|
|
use mtp_codec::{
|
|
DataValue, DecodeLimits, EncodeLimits, ProtectedError, ProtectedLimits,
|
|
ProtectedMessageBuilder, ProtectedOpenOptions, ProtectionError, ProtectionPolicy,
|
|
ProtectionPurpose, VerifiedProtectedMessage,
|
|
};
|
|
|
|
use crate::crypto::{
|
|
keyrings_from_js, protection_policy_from_suite, public_key_bundles_from_js,
|
|
relay_signer_from_keyring,
|
|
};
|
|
use crate::relay::{decode_error, decode_frame_with_limits, structured_error};
|
|
|
|
const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
|
|
|
|
fn optional_u64(value: &JsValue, name: &str) -> Result<Option<u64>, JsValue> {
|
|
if value.is_null() || value.is_undefined() {
|
|
return Ok(None);
|
|
}
|
|
if let Some(number) = value.as_f64() {
|
|
if !number.is_finite()
|
|
|| number.fract() != 0.0
|
|
|| !(0.0..=MAX_SAFE_INTEGER).contains(&number)
|
|
{
|
|
return Err(structured_error(
|
|
"invalid-option",
|
|
format!("{name} must be an exact non-negative integer"),
|
|
));
|
|
}
|
|
return Ok(Some(number as u64));
|
|
}
|
|
if value.js_typeof().as_string().as_deref() == Some("bigint") {
|
|
let bigint = value.clone().unchecked_into::<js_sys::BigInt>();
|
|
let text = bigint.to_string(10)?.as_string().ok_or_else(|| {
|
|
structured_error("invalid-option", format!("failed to stringify {name}"))
|
|
})?;
|
|
return text
|
|
.parse::<u64>()
|
|
.map(Some)
|
|
.map_err(|_| structured_error("invalid-option", format!("{name} is out of range")));
|
|
}
|
|
Err(structured_error(
|
|
"invalid-option",
|
|
format!("{name} must be a number or bigint"),
|
|
))
|
|
}
|
|
|
|
fn limit_usize(options: &JsValue, key: &str, default: usize) -> Result<usize, JsValue> {
|
|
if options.is_null() || options.is_undefined() {
|
|
return Ok(default);
|
|
}
|
|
let value = js_sys::Reflect::get(options, &JsValue::from_str(key))?;
|
|
if value.is_null() || value.is_undefined() {
|
|
return Ok(default);
|
|
}
|
|
let Some(number) = value.as_f64() else {
|
|
return Err(structured_error(
|
|
"invalid-limit",
|
|
format!("{key} must be a number"),
|
|
));
|
|
};
|
|
if !number.is_finite() || number.fract() != 0.0 || number < 0.0 {
|
|
return Err(structured_error(
|
|
"invalid-limit",
|
|
format!("{key} must be a non-negative integer"),
|
|
));
|
|
}
|
|
usize::try_from(number as u64)
|
|
.map_err(|_| structured_error("invalid-limit", format!("{key} is out of range")))
|
|
}
|
|
|
|
fn protected_open_options(
|
|
expected_receiver_id: Option<u64>,
|
|
signature_purpose: u8,
|
|
encryption_purpose: u8,
|
|
policy: mtp_codec::ProtectionPolicy,
|
|
limits: &JsValue,
|
|
) -> Result<ProtectedOpenOptions, JsValue> {
|
|
let defaults = DecodeLimits::default();
|
|
let encode_defaults = EncodeLimits::default();
|
|
let protected_defaults = ProtectedLimits::default();
|
|
let decode_limits = DecodeLimits {
|
|
max_depth: limit_usize(limits, "maxDepth", defaults.max_depth)?,
|
|
max_values: limit_usize(limits, "maxValues", defaults.max_values)?,
|
|
max_blob_size: limit_usize(limits, "maxBlobSize", defaults.max_blob_size)?,
|
|
max_recipients: limit_usize(limits, "maxRecipients", defaults.max_recipients)?,
|
|
max_allocated_bytes: limit_usize(
|
|
limits,
|
|
"maxAllocatedBytes",
|
|
defaults.max_allocated_bytes,
|
|
)?,
|
|
};
|
|
let encode_limits = EncodeLimits {
|
|
max_depth: limit_usize(limits, "maxDepth", encode_defaults.max_depth)?,
|
|
max_values: limit_usize(limits, "maxValues", encode_defaults.max_values)?,
|
|
max_output_size: limit_usize(limits, "maxOutputSize", encode_defaults.max_output_size)?,
|
|
};
|
|
let protected_limits = ProtectedLimits {
|
|
max_message_id_bytes: limit_usize(
|
|
limits,
|
|
"maxMessageIdBytes",
|
|
protected_defaults.max_message_id_bytes,
|
|
)?,
|
|
max_metadata_encoded_bytes: limit_usize(
|
|
limits,
|
|
"maxMetadataEncodedBytes",
|
|
protected_defaults.max_metadata_encoded_bytes,
|
|
)?,
|
|
max_signer_key_history: limit_usize(
|
|
limits,
|
|
"maxSignerKeyHistory",
|
|
protected_defaults.max_signer_key_history,
|
|
)?,
|
|
max_decryption_key_history: limit_usize(
|
|
limits,
|
|
"maxDecryptionKeyHistory",
|
|
protected_defaults.max_decryption_key_history,
|
|
)?,
|
|
};
|
|
Ok(ProtectedOpenOptions::new(
|
|
expected_receiver_id,
|
|
ProtectionPurpose::from(signature_purpose),
|
|
ProtectionPurpose::from(encryption_purpose),
|
|
policy,
|
|
)
|
|
.with_limits(decode_limits, protected_limits)
|
|
.with_encode_limits(encode_limits))
|
|
}
|
|
|
|
pub(crate) fn protected_error(error: ProtectedError) -> JsValue {
|
|
let code = protected_error_code(&error);
|
|
let value = structured_error(code, format!("protected opening failed: {error}"));
|
|
if let ProtectedError::UnsupportedProtectedVersion(version) = &error {
|
|
let _ = js_sys::Reflect::set(
|
|
&value,
|
|
&JsValue::from_str("protectedVersion"),
|
|
&JsValue::bigint_from_str(&version.to_string()),
|
|
);
|
|
}
|
|
if let ProtectedError::ReservedApplicationType(application_type) = &error {
|
|
let _ = js_sys::Reflect::set(
|
|
&value,
|
|
&JsValue::from_str("applicationType"),
|
|
&JsValue::from_str(application_type),
|
|
);
|
|
}
|
|
value
|
|
}
|
|
|
|
fn protected_error_code(error: &ProtectedError) -> &'static str {
|
|
match error {
|
|
ProtectedError::NotApplicationFrame => "not-application-frame",
|
|
ProtectedError::MissingReceiver => "missing-receiver",
|
|
ProtectedError::PayloadNotEncrypted => "payload-not-encrypted",
|
|
ProtectedError::PayloadNotSigned => "payload-not-signed",
|
|
ProtectedError::MissingEnvelope => "missing-envelope",
|
|
ProtectedError::InvalidLayout(_) => "invalid-layout",
|
|
ProtectedError::MissingProtectedVersion => "missing-protected-version",
|
|
ProtectedError::UnsupportedProtectedVersion(_) => "unsupported-protected-version",
|
|
ProtectedError::MessageTypeMismatch => "message-type-mismatch",
|
|
ProtectedError::FinalRecipientMismatch => "final-recipient-mismatch",
|
|
ProtectedError::SenderMismatch => "sender-id-mismatch",
|
|
ProtectedError::ExpectedReceiverMismatch => "receiver-id-mismatch",
|
|
ProtectedError::ReservedApplicationType(_) => "reserved-application-type",
|
|
ProtectedError::Replay => "replay",
|
|
ProtectedError::ResourceLimit(_) => "resource-limit",
|
|
ProtectedError::ReplayGuard(_) => "replay-guard-error",
|
|
ProtectedError::Protection(error) => match error {
|
|
ProtectionError::NoMatchingRecipient => "no-matching-recipient",
|
|
ProtectionError::InvalidSignature => "invalid-signature",
|
|
ProtectionError::SignaturePolicyMismatch { .. } => "signature-policy-mismatch",
|
|
ProtectionError::PurposeMismatch { .. } => "purpose-mismatch",
|
|
ProtectionError::SignerIdMismatch { .. } => "signer-id-mismatch",
|
|
ProtectionError::SignerKeyNotFound(_) => "signer-key-not-found",
|
|
ProtectionError::ResourceLimit(_) => "resource-limit",
|
|
ProtectionError::Crypto(mtp_crypto::CryptoError::InvalidSignature)
|
|
| ProtectionError::Crypto(mtp_crypto::CryptoError::VerificationFailed) => {
|
|
"invalid-signature"
|
|
}
|
|
_ => "protection-error",
|
|
},
|
|
}
|
|
}
|
|
|
|
fn serialize_data_value(value: &DataValue) -> Result<Vec<u8>, JsValue> {
|
|
value.to_bytes().map_err(|error| {
|
|
structured_error(
|
|
"invalid-data-value",
|
|
format!("protected value encoding failed: {error}"),
|
|
)
|
|
})
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub struct WasmVerifiedProtectedMessage {
|
|
inner: VerifiedProtectedMessage,
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
impl WasmVerifiedProtectedMessage {
|
|
pub fn protected_version(&self) -> u64 {
|
|
self.inner.protected_version
|
|
}
|
|
|
|
pub fn signer_id(&self) -> u64 {
|
|
self.inner.signer_id
|
|
}
|
|
|
|
pub fn final_recipient_id(&self) -> u64 {
|
|
self.inner.final_recipient_id
|
|
}
|
|
|
|
pub fn message_id(&self) -> String {
|
|
self.inner.message_id.clone()
|
|
}
|
|
|
|
pub fn created_at(&self) -> u64 {
|
|
self.inner.created_at
|
|
}
|
|
|
|
pub fn message_type(&self) -> String {
|
|
self.inner.message_type.clone()
|
|
}
|
|
|
|
pub fn content(&self) -> Result<Vec<u8>, JsValue> {
|
|
serialize_data_value(&self.inner.content)
|
|
}
|
|
|
|
pub fn matched_signer_key_index(&self) -> usize {
|
|
self.inner.matched_signer_key_index
|
|
}
|
|
}
|
|
|
|
/// Build a complete encrypted direct protected frame in the native codec.
|
|
/// The native builder owns both the protected envelope and the clear outer
|
|
/// routing fields, including the optional sender exposure and frame ID.
|
|
#[wasm_bindgen]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn build_protected_frame_with_keyring(
|
|
message_type: &str,
|
|
encoded_content: &[u8],
|
|
signer_id: u64,
|
|
final_recipient_id: u64,
|
|
message_id: &str,
|
|
created_at: u64,
|
|
signature_purpose: u8,
|
|
encryption_purpose: u8,
|
|
keyring_bytes: &[u8],
|
|
signature_suite: u8,
|
|
frame_id: Option<u32>,
|
|
expose_sender: bool,
|
|
recipient_public_key_bundles: JsValue,
|
|
) -> Result<Vec<u8>, JsValue> {
|
|
build_protected_frame_with_keyring_impl(
|
|
message_type,
|
|
encoded_content,
|
|
signer_id,
|
|
final_recipient_id,
|
|
message_id,
|
|
created_at,
|
|
signature_purpose,
|
|
encryption_purpose,
|
|
keyring_bytes,
|
|
signature_suite,
|
|
frame_id,
|
|
expose_sender,
|
|
recipient_public_key_bundles,
|
|
JsValue::UNDEFINED,
|
|
)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn build_protected_frame_with_keyring_impl(
|
|
message_type: &str,
|
|
encoded_content: &[u8],
|
|
signer_id: u64,
|
|
final_recipient_id: u64,
|
|
message_id: &str,
|
|
created_at: u64,
|
|
signature_purpose: u8,
|
|
encryption_purpose: u8,
|
|
keyring_bytes: &[u8],
|
|
signature_suite: u8,
|
|
frame_id: Option<u32>,
|
|
expose_sender: bool,
|
|
recipient_public_key_bundles: JsValue,
|
|
limits: JsValue,
|
|
) -> Result<Vec<u8>, JsValue> {
|
|
let encode_limits = if limits.is_null() || limits.is_undefined() {
|
|
EncodeLimits::default()
|
|
} else {
|
|
crate::client::encode_limits_from_js(&limits)?
|
|
};
|
|
let open_options = protected_open_options(
|
|
None,
|
|
signature_purpose,
|
|
encryption_purpose,
|
|
ProtectionPolicy::any_supported(),
|
|
&limits,
|
|
)?;
|
|
let content = DataValue::try_from_bytes_with_limits(
|
|
encoded_content,
|
|
DecodeLimits::for_transport_message_size(encode_limits.max_output_size as u64),
|
|
)
|
|
.map_err(|error| decode_error(error, "DataValue decoding failed"))?;
|
|
let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes).map_err(|error| {
|
|
structured_error(
|
|
"invalid-keyring",
|
|
format!("keyring initialization failed: {error}"),
|
|
)
|
|
})?;
|
|
let signer = relay_signer_from_keyring(&keyring, signature_suite)?;
|
|
let recipients = public_key_bundles_from_js(&recipient_public_key_bundles)?;
|
|
let mut builder = ProtectedMessageBuilder::new(
|
|
message_type,
|
|
content,
|
|
signer_id,
|
|
final_recipient_id,
|
|
&signer,
|
|
ProtectionPurpose::from(signature_purpose),
|
|
ProtectionPurpose::from(encryption_purpose),
|
|
)
|
|
.message_id(message_id)
|
|
.created_at(created_at)
|
|
.recipients(recipients)
|
|
.encode_limits(encode_limits)
|
|
.protected_limits(open_options.protected_limits)
|
|
.expose_sender(expose_sender);
|
|
if let Some(frame_id) = frame_id {
|
|
builder = builder.frame_id(frame_id);
|
|
}
|
|
let frame = builder.build().map_err(protected_error)?;
|
|
frame.to_bytes_with_limits(encode_limits).map_err(|error| {
|
|
structured_error(
|
|
"invalid-frame",
|
|
format!("protected frame encoding failed: {error}"),
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Build a complete encrypted protected frame with explicit encoder and
|
|
/// semantic protected-field limits.
|
|
#[wasm_bindgen]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn build_protected_frame_with_keyring_with_limits(
|
|
message_type: &str,
|
|
encoded_content: &[u8],
|
|
signer_id: u64,
|
|
final_recipient_id: u64,
|
|
message_id: &str,
|
|
created_at: u64,
|
|
signature_purpose: u8,
|
|
encryption_purpose: u8,
|
|
keyring_bytes: &[u8],
|
|
signature_suite: u8,
|
|
frame_id: Option<u32>,
|
|
expose_sender: bool,
|
|
recipient_public_key_bundles: JsValue,
|
|
limits: JsValue,
|
|
) -> Result<Vec<u8>, JsValue> {
|
|
build_protected_frame_with_keyring_impl(
|
|
message_type,
|
|
encoded_content,
|
|
signer_id,
|
|
final_recipient_id,
|
|
message_id,
|
|
created_at,
|
|
signature_purpose,
|
|
encryption_purpose,
|
|
keyring_bytes,
|
|
signature_suite,
|
|
frame_id,
|
|
expose_sender,
|
|
recipient_public_key_bundles,
|
|
limits,
|
|
)
|
|
}
|
|
|
|
/// Read the claimed, unverified signer ID after decrypting the protected
|
|
/// payload. The result may only select trusted keys for the same signer ID.
|
|
#[wasm_bindgen]
|
|
#[deprecated(note = "use protected_claimed_signer_id_with_limits")]
|
|
pub fn protected_claimed_signer_id(
|
|
frame: &[u8],
|
|
keyrings: JsValue,
|
|
encryption_purpose: u8,
|
|
) -> Result<u64, JsValue> {
|
|
protected_claimed_signer_id_impl(frame, keyrings, encryption_purpose, JsValue::UNDEFINED)
|
|
}
|
|
|
|
fn protected_claimed_signer_id_impl(
|
|
frame: &[u8],
|
|
keyrings: JsValue,
|
|
encryption_purpose: u8,
|
|
limits: JsValue,
|
|
) -> Result<u64, JsValue> {
|
|
let options = protected_open_options(
|
|
None,
|
|
0,
|
|
encryption_purpose,
|
|
ProtectionPolicy::any_supported(),
|
|
&limits,
|
|
)?;
|
|
let frame = decode_frame_with_limits(frame, options.decode_limits)?;
|
|
let keyrings = keyrings_from_js(&keyrings).map_err(|error| {
|
|
structured_error(
|
|
"invalid-recipient-keyrings",
|
|
error.as_string().unwrap_or_default(),
|
|
)
|
|
})?;
|
|
if keyrings.len() > options.protected_limits.max_decryption_key_history {
|
|
return Err(protected_error(ProtectedError::ResourceLimit(
|
|
"decryption key history",
|
|
)));
|
|
}
|
|
let references: Vec<&mtp_crypto::Keyring> = keyrings.iter().collect();
|
|
mtp_codec::protected_claimed_signer_id_with_options(
|
|
&frame,
|
|
&references,
|
|
ProtectionPurpose::from(encryption_purpose),
|
|
options.decode_limits,
|
|
options.protected_limits,
|
|
)
|
|
.map_err(protected_error)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn protected_claimed_signer_id_with_limits(
|
|
frame: &[u8],
|
|
keyrings: JsValue,
|
|
encryption_purpose: u8,
|
|
limits: JsValue,
|
|
) -> Result<u64, JsValue> {
|
|
let options = protected_open_options(
|
|
None,
|
|
0,
|
|
encryption_purpose,
|
|
ProtectionPolicy::any_supported(),
|
|
&limits,
|
|
)?;
|
|
let frame = decode_frame_with_limits(frame, options.decode_limits)?;
|
|
let keyrings = keyrings_from_js(&keyrings).map_err(|error| {
|
|
structured_error(
|
|
"invalid-recipient-keyrings",
|
|
error.as_string().unwrap_or_default(),
|
|
)
|
|
})?;
|
|
if keyrings.len() > options.protected_limits.max_decryption_key_history {
|
|
return Err(protected_error(ProtectedError::ResourceLimit(
|
|
"decryption key history",
|
|
)));
|
|
}
|
|
let references: Vec<&mtp_crypto::Keyring> = keyrings.iter().collect();
|
|
mtp_codec::protected_claimed_signer_id_with_options(
|
|
&frame,
|
|
&references,
|
|
ProtectionPurpose::from(encryption_purpose),
|
|
options.decode_limits,
|
|
options.protected_limits,
|
|
)
|
|
.map_err(protected_error)
|
|
}
|
|
|
|
/// Open a protected value without replay protection. This raw entry point is
|
|
/// intended for stored/forensic messages; message-processing callers should
|
|
/// apply their replay guard in the SDK or use a checked native API.
|
|
#[wasm_bindgen]
|
|
pub fn open_protected_with_keyrings_without_replay(
|
|
frame: &[u8],
|
|
keyrings: JsValue,
|
|
expected_signer_id: JsValue,
|
|
signer_public_key_bundles: JsValue,
|
|
expected_receiver_id: JsValue,
|
|
signature_purpose: u8,
|
|
encryption_purpose: u8,
|
|
signature_suite: u8,
|
|
) -> Result<WasmVerifiedProtectedMessage, JsValue> {
|
|
open_protected_with_keyrings_impl(
|
|
frame,
|
|
keyrings,
|
|
expected_signer_id,
|
|
signer_public_key_bundles,
|
|
expected_receiver_id,
|
|
signature_purpose,
|
|
encryption_purpose,
|
|
signature_suite,
|
|
JsValue::UNDEFINED,
|
|
)
|
|
}
|
|
|
|
fn open_protected_with_keyrings_impl(
|
|
frame: &[u8],
|
|
keyrings: JsValue,
|
|
expected_signer_id: JsValue,
|
|
signer_public_key_bundles: JsValue,
|
|
expected_receiver_id: JsValue,
|
|
signature_purpose: u8,
|
|
encryption_purpose: u8,
|
|
signature_suite: u8,
|
|
limits: JsValue,
|
|
) -> Result<WasmVerifiedProtectedMessage, JsValue> {
|
|
let keyrings = keyrings_from_js(&keyrings).map_err(|error| {
|
|
structured_error(
|
|
"invalid-recipient-keyrings",
|
|
error.as_string().unwrap_or_default(),
|
|
)
|
|
})?;
|
|
let references: Vec<&mtp_crypto::Keyring> = keyrings.iter().collect();
|
|
let signer_public_keys =
|
|
public_key_bundles_from_js(&signer_public_key_bundles).map_err(|error| {
|
|
structured_error("invalid-signer-keys", error.as_string().unwrap_or_default())
|
|
})?;
|
|
let expected_signer_id = optional_u64(&expected_signer_id, "expectedSignerId")?
|
|
.ok_or_else(|| structured_error("invalid-option", "expectedSignerId is required"))?;
|
|
let expected_receiver_id = optional_u64(&expected_receiver_id, "expectedReceiverId")?;
|
|
let policy = protection_policy_from_suite(signature_suite).map_err(|error| {
|
|
structured_error(
|
|
"unsupported-signature-suite",
|
|
error.as_string().unwrap_or_default(),
|
|
)
|
|
})?;
|
|
let options = protected_open_options(
|
|
expected_receiver_id,
|
|
signature_purpose,
|
|
encryption_purpose,
|
|
policy,
|
|
&limits,
|
|
)?;
|
|
let frame = decode_frame_with_limits(frame, options.decode_limits)?;
|
|
let message = mtp_codec::open_protected_with_keys_without_replay(
|
|
&frame,
|
|
&references,
|
|
expected_signer_id,
|
|
&signer_public_keys,
|
|
options,
|
|
)
|
|
.map_err(protected_error)?;
|
|
Ok(WasmVerifiedProtectedMessage { inner: message })
|
|
}
|
|
|
|
/// Open a bounded protected value without replay protection. The raw WASM
|
|
/// boundary cannot accept a native replay-guard trait, so message-processing
|
|
/// callers must use the SDK guard or a native checked API.
|
|
#[wasm_bindgen]
|
|
pub fn open_protected_with_keyrings_with_limits_without_replay(
|
|
frame: &[u8],
|
|
keyrings: JsValue,
|
|
expected_signer_id: JsValue,
|
|
signer_public_key_bundles: JsValue,
|
|
expected_receiver_id: JsValue,
|
|
signature_purpose: u8,
|
|
encryption_purpose: u8,
|
|
signature_suite: u8,
|
|
limits: JsValue,
|
|
) -> Result<WasmVerifiedProtectedMessage, JsValue> {
|
|
open_protected_with_keyrings_impl(
|
|
frame,
|
|
keyrings,
|
|
expected_signer_id,
|
|
signer_public_key_bundles,
|
|
expected_receiver_id,
|
|
signature_purpose,
|
|
encryption_purpose,
|
|
signature_suite,
|
|
limits,
|
|
)
|
|
}
|
|
|
|
#[cfg(all(test, target_arch = "wasm32"))]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::crypto::relay_signer_from_keyring;
|
|
use mtp_codec::{CommunicationType, CommunicationValue, DataType, TypeMap};
|
|
use wasm_bindgen::JsCast;
|
|
use wasm_bindgen_test::*;
|
|
|
|
const SIGNATURE_PURPOSE: u8 = 0x40;
|
|
const ENCRYPTION_PURPOSE: u8 = 0x41;
|
|
|
|
fn structured_error_code(error: JsValue) -> String {
|
|
js_sys::Reflect::get(&error, &JsValue::from_str("code"))
|
|
.expect("structured error code")
|
|
.as_string()
|
|
.expect("structured error code string")
|
|
}
|
|
|
|
fn protected_frame_with_version(
|
|
sender: &mtp_crypto::Keyring,
|
|
recipient: &mtp_crypto::Keyring,
|
|
version: Option<u128>,
|
|
) -> Vec<u8> {
|
|
let type_map = TypeMap::latest();
|
|
let field = |data_type: DataType| data_type.try_to_id(&type_map).expect("field mapping");
|
|
let mut fields = Vec::new();
|
|
if let Some(version) = version {
|
|
fields.push((
|
|
field(DataType::ProtectedVersion),
|
|
DataValue::UnsignedNumber(version),
|
|
));
|
|
}
|
|
fields.extend([
|
|
(
|
|
field(DataType::MessageType),
|
|
DataValue::Str("ProtectedMessage".into()),
|
|
),
|
|
(
|
|
field(DataType::FinalRecipientId),
|
|
DataValue::UnsignedNumber(42),
|
|
),
|
|
(
|
|
field(DataType::MessageId),
|
|
DataValue::Str("wasm-structured-error".into()),
|
|
),
|
|
(field(DataType::CreatedAt), DataValue::UnsignedNumber(123)),
|
|
(field(DataType::Content), DataValue::Str("hello".into())),
|
|
]);
|
|
let signer = relay_signer_from_keyring(sender, 1).expect("Ed25519 signer");
|
|
let signed = DataValue::Container(fields)
|
|
.sign(7, ProtectionPurpose::from(SIGNATURE_PURPOSE), &signer)
|
|
.expect("sign protected envelope");
|
|
let encrypted = signed
|
|
.encrypt_for(
|
|
&[recipient.public_key_bundle()],
|
|
ProtectionPurpose::from(ENCRYPTION_PURPOSE),
|
|
)
|
|
.expect("encrypt protected envelope");
|
|
CommunicationValue::new_with_type_map(
|
|
CommunicationType::from_name("ProtectedMessage").expect("application type"),
|
|
&type_map,
|
|
)
|
|
.with_receiver(42)
|
|
.with_payload(encrypted)
|
|
.to_bytes()
|
|
.expect("encode protected frame")
|
|
}
|
|
|
|
fn open_for_error(
|
|
frame: &[u8],
|
|
sender: &mtp_crypto::Keyring,
|
|
recipient: &mtp_crypto::Keyring,
|
|
) -> JsValue {
|
|
let recipient_bytes = recipient.try_to_bytes().expect("recipient serialization");
|
|
let signer_bundle_bytes = sender
|
|
.public_key_bundle()
|
|
.try_as_bytes()
|
|
.expect("signer bundle serialization");
|
|
match open_protected_with_keyrings_without_replay(
|
|
frame,
|
|
js_sys::Uint8Array::from(&recipient_bytes[..]).into(),
|
|
JsValue::bigint_from_str("7"),
|
|
js_sys::Uint8Array::from(&signer_bundle_bytes[..]).into(),
|
|
JsValue::bigint_from_str("42"),
|
|
SIGNATURE_PURPOSE,
|
|
ENCRYPTION_PURPOSE,
|
|
1,
|
|
) {
|
|
Ok(_) => panic!("protected opening should fail"),
|
|
Err(error) => error,
|
|
}
|
|
}
|
|
|
|
#[wasm_bindgen_test]
|
|
fn protected_builder_returns_the_complete_frame() {
|
|
let sender = mtp_crypto::Keyring::generate();
|
|
let recipient = mtp_crypto::Keyring::generate();
|
|
let sender_bytes = sender.try_to_bytes().expect("sender serialization");
|
|
let recipient_bundle_bytes = recipient
|
|
.public_key_bundle()
|
|
.try_as_bytes()
|
|
.expect("recipient bundle serialization");
|
|
let content = DataValue::Str("complete-frame".into())
|
|
.to_bytes()
|
|
.expect("encode content");
|
|
let frame = build_protected_frame_with_keyring(
|
|
"ProtectedMessage",
|
|
&content,
|
|
7,
|
|
42,
|
|
"wasm-complete-frame",
|
|
123,
|
|
SIGNATURE_PURPOSE,
|
|
ENCRYPTION_PURPOSE,
|
|
&sender_bytes,
|
|
1,
|
|
Some(19),
|
|
true,
|
|
js_sys::Uint8Array::from(&recipient_bundle_bytes[..]).into(),
|
|
)
|
|
.expect("build complete protected frame");
|
|
let decoded = CommunicationValue::from_bytes(&frame).expect("decode complete frame");
|
|
assert_eq!(decoded.id(), Some(19));
|
|
assert_eq!(decoded.sender(), Some(7));
|
|
assert_eq!(decoded.receiver(), Some(42));
|
|
assert!(decoded.payload().as_encrypted().is_some());
|
|
}
|
|
|
|
#[wasm_bindgen_test]
|
|
fn protected_opening_maps_missing_and_unsupported_versions() {
|
|
let sender = mtp_crypto::Keyring::generate();
|
|
let recipient = mtp_crypto::Keyring::generate();
|
|
let missing = protected_frame_with_version(&sender, &recipient, None);
|
|
assert_eq!(
|
|
structured_error_code(open_for_error(&missing, &sender, &recipient)),
|
|
"missing-protected-version"
|
|
);
|
|
|
|
let unsupported = protected_frame_with_version(&sender, &recipient, Some(2));
|
|
let error = open_for_error(&unsupported, &sender, &recipient);
|
|
assert_eq!(
|
|
structured_error_code(error.clone()),
|
|
"unsupported-protected-version"
|
|
);
|
|
let version = js_sys::Reflect::get(&error, &JsValue::from_str("protectedVersion"))
|
|
.expect("protected version");
|
|
let version = version
|
|
.unchecked_into::<js_sys::BigInt>()
|
|
.to_string(10)
|
|
.expect("protected version string")
|
|
.as_string()
|
|
.expect("protected version text");
|
|
assert_eq!(version, "2");
|
|
}
|
|
}
|