use alloc::{
format,
string::{String, ToString},
vec,
vec::Vec,
};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use mls_rs::client_builder::MlsConfig;
use mls_rs::group::ReceivedMessage;
use mls_rs::{Client, Group, MlsMessage};
use offline_protocol_core::{Address, AppId, Message, MessagePriority};
use offline_protocol_sealed::{
prefixes, EncryptedMessage, GroupId, KeyPackagePayload, MlsMessageType, WelcomeMessage,
MLS_ENVELOPE_COMPACT_V1,
};
use crate::adapters::{KeyPackageAdapter, PeerRecord, PRIOR_EPOCH_RETENTION};
use crate::error::{LeafError, Result};
use crate::frames;
use crate::identity::{build_client, Identity};
use crate::keypkg;
use crate::shared::{Arc, SharedStore};
use crate::store::{
LeafStore, KEY_TYPE_GROUP_EPOCH, KEY_TYPE_GROUP_STATE, KEY_TYPE_KEY_PACKAGE, KEY_TYPE_PEER,
KEY_TYPE_PEER_INDEX,
};
const MAX_PEERS: usize = 16;
const FORGET_EPOCH_SLACK: u64 = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LeafEvent {
PeerAdvertised {
peer: String,
},
SessionEstablished {
peer: String,
},
SessionReset {
peer: String,
},
MessageReceived {
peer: String,
text: String,
},
CommitApplied {
peer: String,
epoch: u64,
},
Ignored {
reason: String,
},
}
#[derive(Debug, Default)]
pub struct Handled {
pub outbound: Vec<Message>,
pub events: Vec<LeafEvent>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Sender {
Proven,
Claimed,
}
pub struct LeafDevice {
store: Arc<dyn LeafStore>,
identity: Identity,
app_id: AppId,
}
impl LeafDevice {
pub fn provision(store: SharedStore, app_id: &str) -> Result<Self> {
let identity = Identity::provision(&store)?;
Ok(Self {
store,
identity,
app_id: parse_app_id(app_id)?,
})
}
pub fn resume(store: SharedStore, app_id: &str) -> Result<Self> {
let identity = Identity::resume(&store)?;
Ok(Self {
store,
identity,
app_id: parse_app_id(app_id)?,
})
}
pub fn open(store: SharedStore, app_id: &str) -> Result<Self> {
match Self::resume(Arc::clone(&store), app_id) {
Err(LeafError::NotProvisioned) => Self::provision(store, app_id),
other => other,
}
}
pub fn address(&self) -> &Address {
&self.identity.address
}
pub fn key_package_frame(&mut self, peer: &str, now_unix_secs: u64) -> Result<Message> {
self.key_package_frame_inner(peer, now_unix_secs, false)
}
fn key_package_frame_inner(
&mut self,
peer: &str,
now_unix_secs: u64,
session_reset: bool,
) -> Result<Message> {
let client = self.client()?;
let minted = keypkg::mint(&client, now_unix_secs)?;
let payload = keypkg::payload(
&self.identity.address.to_string(),
minted.data,
session_reset,
);
let body = serde_json::to_string(&payload)
.map_err(|e| LeafError::MalformedFrame(format!("cannot encode key package: {e}")))?;
let mut message = frames::build(
&self.store,
&self.identity,
&self.app_id,
peer,
format!("{}{}", prefixes::KEY_PACKAGE, body),
now_unix_secs,
MessagePriority::High,
)?;
frames::sign_control_frame(&self.identity, &mut message)?;
let mut record = self.peer_record(peer)?;
record.key_package_sent = true;
record.key_package_ref = Some(minted.reference);
record
.save(&self.store, peer)
.map_err(|e| LeafError::Storage(e.to_string()))?;
self.index_peer(peer)?;
Ok(message)
}
pub fn seal(&mut self, peer: &str, plaintext: &str, now_unix_secs: u64) -> Result<Message> {
self.seal_content(peer, plaintext.as_bytes(), now_unix_secs)
}
fn seal_content(
&mut self,
peer: &str,
plaintext: &[u8],
now_unix_secs: u64,
) -> Result<Message> {
let client = self.client()?;
let group_id = self.group_id(peer)?;
let mut group = self.load_group(&client, peer, &group_id)?;
self.require_still_a_pair(&group, peer)?;
let sealed = group
.encrypt_application_message(plaintext, Vec::new())
.map_err(|e| LeafError::Mls(format!("cannot seal: {e:?}")))?
.to_bytes()
.map_err(|e| LeafError::Mls(format!("cannot encode sealed message: {e:?}")))?;
let epoch = group.current_epoch();
group
.write_to_storage()
.map_err(|e| LeafError::Storage(format!("cannot persist group state: {e:?}")))?;
let envelope = EncryptedMessage {
group_id,
message_type: MlsMessageType::Application,
epoch,
ciphertext: sealed,
sender_id: self.identity.address.to_string(),
timestamp_ms: (now_unix_secs.saturating_mul(1000)),
};
let body = self.encode_envelope(peer, &envelope)?;
frames::build(
&self.store,
&self.identity,
&self.app_id,
peer,
format!("{}{}", prefixes::ENCRYPTED, body),
now_unix_secs,
MessagePriority::Medium,
)
}
fn encode_envelope(&self, peer: &str, envelope: &EncryptedMessage) -> Result<String> {
let record = self.peer_record(peer)?;
if record.env_versions.contains(&MLS_ENVELOPE_COMPACT_V1) {
Ok(BASE64.encode(envelope.to_bytes()))
} else {
serde_json::to_string(envelope)
.map_err(|e| LeafError::MalformedFrame(format!("cannot encode envelope: {e}")))
}
}
pub fn handle(&mut self, message: &Message, now_unix_secs: u64) -> Result<Handled> {
if !self.is_addressed_to_me(message) {
return Ok(ignored("frame is addressed to another node"));
}
if let Some(repeat) = self.repeat_acknowledgement(message, now_unix_secs)? {
return Ok(repeat);
}
let (mut handled, sender) = self.dispatch(message, now_unix_secs)?;
if sender == Sender::Proven {
match self.acknowledge(message, now_unix_secs) {
Ok(Some(ack)) => handled.outbound.push(ack),
Ok(None) => {}
Err(e) => handled.events.push(LeafEvent::Ignored {
reason: format!("the answer this frame is owed could not be stored: {e}"),
}),
}
}
Ok(handled)
}
fn dispatch(&mut self, message: &Message, now_unix_secs: u64) -> Result<(Handled, Sender)> {
let content = &message.content;
if let Some(body) = frames::strip_prefix(content, prefixes::KEY_PACKAGE) {
Ok((
self.on_key_package(message, body, now_unix_secs)?,
Sender::Proven,
))
} else if let Some(body) = frames::strip_prefix(content, prefixes::WELCOME) {
Ok((
self.on_welcome(message, body, now_unix_secs)?,
Sender::Proven,
))
} else if let Some(body) = frames::strip_prefix(content, prefixes::ENCRYPTED) {
Ok((
self.on_encrypted(message, body, now_unix_secs)?,
Sender::Proven,
))
} else if frames::strip_prefix(content, prefixes::SESSION_CONFIRM_PROBE).is_some() {
Ok((self.on_probe(message, now_unix_secs)?, Sender::Proven))
} else if frames::strip_prefix(content, prefixes::SESSION_CONFIRM_ACK).is_some() {
Ok((
ignored("an acknowledgement arrived for a probe this device never sends"),
Sender::Claimed,
))
} else {
Ok((
ignored("frame carries no prefix this device answers"),
Sender::Claimed,
))
}
}
fn on_key_package(
&mut self,
message: &Message,
body: &str,
now_unix_secs: u64,
) -> Result<Handled> {
let signed_under =
frames::verify_control_frame(message, now_unix_secs, frames::UndatedPayload::Admitted)?;
let payload: KeyPackagePayload = serde_json::from_str(body)
.map_err(|e| LeafError::MalformedFrame(format!("key package body: {e}")))?;
let sender = message.sender.as_str();
if payload.user_id != sender {
return Err(LeafError::IdentityBinding(format!(
"key package claims to be '{}' but the frame is signed by '{}'",
payload.user_id, sender
)));
}
self.admit_peer(sender)?;
let mut events = Vec::new();
let mut record = self.peer_record(sender)?;
if payload.session_reset && signed_under != frames::ControlPayload::Fresh {
events.push(LeafEvent::Ignored {
reason: String::from(
"a session reset arrived on a frame that states no time, so its age \
cannot be judged and its teardown is refused",
),
});
} else if payload.session_reset {
let stamp = message.timestamp.as_millis();
if !record.reset_is_unspent(stamp) {
events.push(LeafEvent::Ignored {
reason: String::from("a session reset arrived that this device has spent"),
});
} else {
record.remember_reset(stamp);
record.key_package_sent = false;
record
.save(&self.store, sender)
.map_err(|e| LeafError::Storage(e.to_string()))?;
self.forget_session(sender)?;
events.push(LeafEvent::SessionReset {
peer: sender.to_string(),
});
}
}
record.env_versions = payload.env_versions.clone();
record.wire_versions = payload.wire_versions.clone();
record
.save(&self.store, sender)
.map_err(|e| LeafError::Storage(e.to_string()))?;
events.push(LeafEvent::PeerAdvertised {
peer: sender.to_string(),
});
let outbound = if record.key_package_sent {
Vec::new()
} else {
vec![self.key_package_frame_inner(sender, now_unix_secs, false)?]
};
Ok(Handled { outbound, events })
}
fn on_welcome(&mut self, message: &Message, body: &str, now_unix_secs: u64) -> Result<Handled> {
frames::verify_control_frame(message, now_unix_secs, frames::UndatedPayload::Refused)?;
let welcome: WelcomeMessage = serde_json::from_str(body)
.map_err(|e| LeafError::MalformedFrame(format!("welcome body: {e}")))?;
let sender = message.sender.as_str();
if welcome.inviter_id != sender {
return Err(LeafError::IdentityBinding(format!(
"welcome names inviter '{}' but the frame is signed by '{}'",
welcome.inviter_id, sender
)));
}
let expected = self.group_id(sender)?;
if welcome.group_id != expected {
return Err(LeafError::IdentityBinding(format!(
"welcome is for group '{}', not this pair's '{}'",
welcome.group_id, expected
)));
}
let client = self.client()?;
let welcome_message = MlsMessage::from_bytes(&welcome.welcome_data)
.map_err(|e| LeafError::Mls(format!("welcome does not decode: {e:?}")))?;
self.require_own_key_package(sender, &welcome_message)?;
let (mut group, _info) = client
.join_group(None, &welcome_message, None)
.map_err(|e| LeafError::Mls(format!("cannot join from the welcome: {e:?}")))?;
if group.group_id() != expected.as_str().as_bytes() {
return Err(LeafError::IdentityBinding(format!(
"welcome claimed group '{expected}' but joined a different one"
)));
}
group
.write_to_storage()
.map_err(|e| LeafError::Storage(format!("cannot persist group state: {e:?}")))?;
self.index_peer(sender)?;
let confirm = self.seal_content(
sender,
prefixes::SESSION_CONFIRM_ENCRYPTED.as_bytes(),
now_unix_secs,
)?;
Ok(Handled {
outbound: vec![confirm],
events: vec![LeafEvent::SessionEstablished {
peer: sender.to_string(),
}],
})
}
fn on_encrypted(&mut self, message: &Message, body: &str, _now: u64) -> Result<Handled> {
let envelope = parse_envelope(body)?;
let sender = message.sender.as_str();
if envelope.sender_id != sender {
return Err(LeafError::IdentityBinding(format!(
"envelope claims sender '{}' but the frame came from '{}'",
envelope.sender_id, sender
)));
}
let client = self.client()?;
let group_id = self.group_id(sender)?;
if envelope.group_id != group_id {
return Err(LeafError::IdentityBinding(format!(
"envelope is for group '{}', not this pair's '{}'",
envelope.group_id, group_id
)));
}
let mut group = self.load_group(&client, sender, &group_id)?;
self.require_still_a_pair(&group, sender)?;
let inbound = MlsMessage::from_bytes(&envelope.ciphertext)
.map_err(|e| LeafError::Mls(format!("sealed payload does not decode: {e:?}")))?;
let received = group
.process_incoming_message(inbound)
.map_err(|e| LeafError::Mls(format!("cannot open: {e:?}")))?;
let epoch = group.current_epoch();
group
.write_to_storage()
.map_err(|e| LeafError::Storage(format!("cannot persist group state: {e:?}")))?;
let events = match received {
ReceivedMessage::ApplicationMessage(app) => {
self.bind_sender_credential(&group, app.sender_index, sender)?;
let text = String::from_utf8_lossy(app.data()).to_string();
if text == prefixes::SESSION_CONFIRM_ENCRYPTED {
vec![LeafEvent::SessionEstablished {
peer: sender.to_string(),
}]
} else {
vec![LeafEvent::MessageReceived {
peer: sender.to_string(),
text,
}]
}
}
ReceivedMessage::Commit(commit) => {
self.require_still_a_pair(&group, sender)?;
self.bind_sender_credential(&group, commit.committer, sender)?;
vec![LeafEvent::CommitApplied {
peer: sender.to_string(),
epoch,
}]
}
_ => vec![LeafEvent::Ignored {
reason: String::from("sealed frame carried nothing this device acts on"),
}],
};
Ok(Handled {
outbound: Vec::new(),
events,
})
}
fn on_probe(&mut self, message: &Message, now_unix_secs: u64) -> Result<Handled> {
frames::verify_control_frame(message, now_unix_secs, frames::UndatedPayload::Refused)?;
let sender = message.sender.as_str();
let client = self.client()?;
let group_id = self.group_id(sender)?;
let group = match self.load_group(&client, sender, &group_id) {
Ok(group) => group,
Err(LeafError::NoSession(_)) => {
return Ok(ignored(
"a confirmation probe arrived for a peer this device has no session with",
))
}
Err(e) => return Err(e),
};
self.require_still_a_pair(&group, sender)?;
let mut ack = frames::build(
&self.store,
&self.identity,
&self.app_id,
sender,
String::from(prefixes::SESSION_CONFIRM_ACK),
now_unix_secs,
MessagePriority::High,
)?;
frames::sign_control_frame(&self.identity, &mut ack)?;
Ok(Handled {
outbound: vec![ack],
events: Vec::new(),
})
}
fn forget_session(&mut self, peer: &str) -> Result<()> {
let group_id = self.group_id(peer)?;
let key = crate::adapters::hex(group_id.as_str().as_bytes());
let anchor = match crate::adapters::state_marker(&self.store, &key) {
Some(highest) => Some(highest),
None => self.max_epoch(&key)?,
};
let highest = anchor
.or_else(|| self.current_epoch(&group_id))
.unwrap_or(0);
let floor = highest.saturating_sub(PRIOR_EPOCH_RETENTION + FORGET_EPOCH_SLACK);
let ceiling = highest.saturating_add(FORGET_EPOCH_SLACK);
for epoch in floor..=ceiling {
self.store
.delete(KEY_TYPE_GROUP_EPOCH, &format!("{key}:{epoch}"))
.map_err(|e| LeafError::Storage(e.to_string()))?;
}
self.store
.delete(KEY_TYPE_GROUP_STATE, &key)
.map_err(|e| LeafError::Storage(e.to_string()))?;
self.store
.delete(KEY_TYPE_GROUP_EPOCH, &format!("{key}:max"))
.map_err(|e| LeafError::Storage(e.to_string()))?;
Ok(())
}
fn max_epoch(&self, group_key: &str) -> Result<Option<u64>> {
let raw = self
.store
.load(KEY_TYPE_GROUP_EPOCH, &format!("{group_key}:max"))
.map_err(|e| LeafError::Storage(e.to_string()))?;
Ok(raw
.and_then(|bytes| <[u8; 8]>::try_from(bytes.as_slice()).ok())
.map(u64::from_be_bytes))
}
fn forget_peer(&mut self, peer: &str) -> Result<()> {
let minted = self
.peer_record(peer)
.ok()
.and_then(|record| record.key_package_ref);
self.forget_session(peer)?;
if let Some(reference) = minted {
KeyPackageAdapter::new(Arc::clone(&self.store))
.erase(&reference)
.map_err(|e| LeafError::Storage(e.to_string()))?;
}
self.store
.delete(KEY_TYPE_PEER, peer)
.map_err(|e| LeafError::Storage(e.to_string()))?;
Ok(())
}
fn peer_index(&self) -> Result<Vec<String>> {
let Some(raw) = self
.store
.load(KEY_TYPE_PEER_INDEX, "peers")
.map_err(|e| LeafError::Storage(e.to_string()))?
else {
return Ok(Vec::new());
};
Ok(serde_json::from_slice(&raw).unwrap_or_default())
}
fn save_peer_index(&self, index: &[String]) -> Result<()> {
let encoded = serde_json::to_vec(index)
.map_err(|e| LeafError::MalformedFrame(format!("cannot encode peer index: {e}")))?;
self.store
.store(KEY_TYPE_PEER_INDEX, "peers", &encoded)
.map_err(|e| LeafError::Storage(e.to_string()))
}
fn index_peer(&mut self, peer: &str) -> Result<()> {
let mut index = self.peer_index()?;
if index.iter().any(|held| held == peer) {
return Ok(());
}
index.push(peer.to_string());
self.save_peer_index(&index)
}
fn admit_peer(&mut self, peer: &str) -> Result<()> {
let mut index = self.peer_index()?;
if index.iter().any(|held| held == peer) {
return Ok(());
}
if index.len() >= MAX_PEERS {
let victim = index
.iter()
.find(|held| !self.has_session(held).unwrap_or(true))
.cloned();
let Some(victim) = victim else {
return Err(LeafError::TooManyPeers);
};
self.forget_peer(&victim)?;
index.retain(|held| held != &victim);
}
index.push(peer.to_string());
self.save_peer_index(&index)
}
fn require_still_a_pair(&self, group: &Group<impl MlsConfig>, peer: &str) -> Result<()> {
let roster = group.roster();
let members = roster.members();
if members.len() != 2 {
return Err(LeafError::IdentityBinding(format!(
"a commit left this group holding {} members, and a leaf node's group is a pair",
members.len()
)));
}
let mine = self.identity.address.to_string();
for member in &members {
let credential = member
.signing_identity
.credential
.as_basic()
.ok_or_else(|| {
LeafError::IdentityBinding(String::from(
"a group member presents no basic credential, so it names no address",
))
})?;
let address = crate::adapters::credential_address(credential)?;
if address != mine && address != peer {
return Err(LeafError::IdentityBinding(format!(
"a commit left '{address}' in this group, which is neither this device nor '{peer}'"
)));
}
frames::verify_sender_derivation(
address,
member.signing_identity.signature_key.as_bytes(),
)?;
}
Ok(())
}
fn require_own_key_package(&self, peer: &str, welcome: &MlsMessage) -> Result<()> {
let Some(minted) = self.peer_record(peer)?.key_package_ref else {
return Err(LeafError::UnsolicitedWelcome(format!(
"no key package was ever minted for '{peer}', so nothing it sends can spend one"
)));
};
let spends_ours = welcome
.welcome_key_package_references()
.into_iter()
.any(|reference| crate::adapters::hex(reference) == minted);
if !spends_ours {
return Err(LeafError::UnsolicitedWelcome(format!(
"welcome from '{peer}' spends a key package this device did not mint for it"
)));
}
if self
.store
.load(KEY_TYPE_KEY_PACKAGE, &minted)
.map_err(|e| LeafError::Storage(e.to_string()))?
.is_none()
{
return Err(LeafError::StaleKeyPackage(format!(
"the key package '{peer}' spends was minted for it and is no longer held"
)));
}
Ok(())
}
fn bind_sender_credential(
&self,
group: &Group<impl MlsConfig>,
index: u32,
claimed: &str,
) -> Result<()> {
let member = group.member_at_index(index).ok_or_else(|| {
LeafError::IdentityBinding(format!("no group member at index {index}"))
})?;
let credential = member
.signing_identity
.credential
.as_basic()
.ok_or_else(|| {
LeafError::IdentityBinding(String::from(
"group member presents no basic credential, so it names no address",
))
})?;
let credential_address = crate::adapters::credential_address(credential)?;
if credential_address != claimed {
return Err(LeafError::IdentityBinding(format!(
"authored by group member '{credential_address}', but the frame claims '{claimed}'"
)));
}
frames::verify_sender_derivation(
credential_address,
member.signing_identity.signature_key.as_bytes(),
)
}
fn is_addressed_to_me(&self, message: &Message) -> bool {
message
.recipient
.as_str()
.parse::<Address>()
.is_ok_and(|recipient| recipient == self.identity.address)
}
fn client(&self) -> Result<Client<impl MlsConfig>> {
build_client(&self.identity, &self.store)
}
fn load_group<C: MlsConfig>(
&self,
client: &Client<C>,
peer: &str,
group_id: &GroupId,
) -> Result<Group<C>> {
match client.load_group(group_id.as_str().as_bytes()) {
Ok(group) => Ok(group),
Err(e) if self.state_present(group_id)? => Err(LeafError::Storage(format!(
"group state for {peer} is on flash but does not load: {e:?}"
))),
Err(_) => Err(LeafError::NoSession(peer.to_string())),
}
}
fn current_epoch(&self, group_id: &GroupId) -> Option<u64> {
let client = self.client().ok()?;
client
.load_group(group_id.as_str().as_bytes())
.ok()
.map(|group| group.current_epoch())
}
fn group_id(&self, peer: &str) -> Result<GroupId> {
Ok(GroupId::for_session(
&self.identity.address.to_string(),
peer,
)?)
}
fn repeat_acknowledgement(
&mut self,
message: &Message,
now_unix_secs: u64,
) -> Result<Option<Handled>> {
if !message.requires_ack {
return Ok(None);
}
let Some(record) = self.known_peer(message.sender.as_str())? else {
return Ok(None);
};
if !record.was_acknowledged(message.id.to_string().as_str()) {
return Ok(None);
}
Ok(Some(Handled {
outbound: alloc::vec![frames::acknowledge(
&self.store,
&self.identity,
&self.app_id,
message,
now_unix_secs,
)?],
events: alloc::vec![LeafEvent::Ignored {
reason: String::from("a frame this device already answered arrived again"),
}],
}))
}
fn acknowledge(&mut self, message: &Message, now_unix_secs: u64) -> Result<Option<Message>> {
if !message.requires_ack {
return Ok(None);
}
let peer = message.sender.as_str();
let Some(mut record) = self.known_peer(peer)? else {
return Ok(None);
};
let ack = frames::acknowledge(
&self.store,
&self.identity,
&self.app_id,
message,
now_unix_secs,
)?;
record.remember_acknowledged(message.id.to_string().as_str());
record
.save(&self.store, peer)
.map_err(|e| LeafError::Storage(e.to_string()))?;
Ok(Some(ack))
}
fn known_peer(&self, peer: &str) -> Result<Option<PeerRecord>> {
PeerRecord::load(&self.store, peer).map_err(|e| LeafError::Storage(e.to_string()))
}
fn peer_record(&self, peer: &str) -> Result<PeerRecord> {
Ok(PeerRecord::load(&self.store, peer)
.map_err(|e| LeafError::Storage(e.to_string()))?
.unwrap_or_default())
}
pub fn has_session(&self, peer: &str) -> Result<bool> {
let group_id = self.group_id(peer)?;
self.state_present(&group_id)
}
fn state_present(&self, group_id: &GroupId) -> Result<bool> {
Ok(self
.store
.load(
KEY_TYPE_GROUP_STATE,
&crate::adapters::hex(group_id.as_str().as_bytes()),
)
.map_err(|e| LeafError::Storage(e.to_string()))?
.is_some())
}
pub fn peers(&self) -> Result<Vec<String>> {
self.peer_index()
}
pub fn peer_env_versions(&self, peer: &str) -> Result<Vec<u8>> {
Ok(self.peer_record(peer)?.env_versions)
}
pub fn unpair(&mut self, peer: &str) -> Result<()> {
self.forget_peer(peer)?;
let mut index = self.peer_index()?;
if index.iter().any(|held| held == peer) {
index.retain(|held| held != peer);
self.save_peer_index(&index)?;
}
Ok(())
}
}
impl core::fmt::Debug for LeafDevice {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LeafDevice")
.field("address", &self.identity.address.to_string())
.finish_non_exhaustive()
}
}
fn ignored(reason: &str) -> Handled {
Handled {
outbound: Vec::new(),
events: vec![LeafEvent::Ignored {
reason: String::from(reason),
}],
}
}
fn parse_app_id(app_id: &str) -> Result<AppId> {
AppId::new(app_id).map_err(|e| LeafError::MalformedFrame(format!("app id: {e}")))
}
fn parse_envelope(body: &str) -> Result<EncryptedMessage> {
if body.starts_with('{') {
return serde_json::from_str(body)
.map_err(|e| LeafError::MalformedFrame(format!("json envelope: {e}")));
}
let bytes = BASE64
.decode(body)
.map_err(|e| LeafError::MalformedFrame(format!("envelope is not base64: {e}")))?;
if let Ok(envelope) = EncryptedMessage::from_bytes(&bytes) {
return Ok(envelope);
}
serde_json::from_slice(&bytes)
.map_err(|e| LeafError::MalformedFrame(format!("base64 envelope: {e}")))
}