use ed25519_dalek::{SigningKey, VerifyingKey};
use rand_core::{OsRng, RngCore};
use serde::{Deserialize, Serialize};
use crate::{
codec,
conversation::{ConversationId, ConversationMeta},
error::{Error, Result},
identity::UserId,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct DeviceId(#[serde(with = "serde_bytes")] pub Vec<u8>);
impl DeviceId {
pub fn from_pubkey(pk: &VerifyingKey) -> Self {
DeviceId(codec::sha256(pk.as_bytes()).to_vec())
}
pub fn as_hex(&self) -> String {
hex::encode(&self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceInfo {
pub device_id: DeviceId,
pub user_id: UserId,
pub label: String, pub created_at_ms: u64,
pub last_seen_ms: u64,
pub revoked: bool,
}
pub struct LocalDevice {
pub device_id: DeviceId,
pub user_id: UserId,
pub label: String,
pub(crate) signing: SigningKey,
pub created_at_ms: u64,
}
impl std::fmt::Debug for LocalDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LocalDevice")
.field("device_id", &self.device_id.as_hex())
.field("user_id", &self.user_id.as_hex())
.field("label", &self.label)
.finish()
}
}
impl LocalDevice {
pub fn generate(user_id: UserId, label: String, now_ms: u64) -> Self {
let mut seed = [0u8; 32];
OsRng.fill_bytes(&mut seed);
let signing = SigningKey::from_bytes(&seed);
let device_id = DeviceId::from_pubkey(&signing.verifying_key());
LocalDevice {
device_id,
user_id,
label,
signing,
created_at_ms: now_ms,
}
}
pub fn public_key(&self) -> VerifyingKey {
self.signing.verifying_key()
}
pub fn info(&self, last_seen_ms: u64) -> DeviceInfo {
DeviceInfo {
device_id: self.device_id.clone(),
user_id: self.user_id.clone(),
label: self.label.clone(),
created_at_ms: self.created_at_ms,
last_seen_ms,
revoked: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkingTicket {
pub v: u8,
pub user_id: UserId,
#[serde(with = "serde_bytes")]
pub user_pubkey: Vec<u8>, pub new_device_id: DeviceId,
#[serde(with = "serde_bytes")]
pub device_binding_sig: Vec<u8>, #[serde(with = "serde_bytes")]
pub device_group_welcome: Vec<u8>, #[serde(with = "serde_bytes")]
pub catchup_snapshot: Vec<u8>, }
pub const GROUP_SNAPSHOT_SOFT_CAP: usize = 128 * 1024;
pub const GROUP_SNAPSHOT_HARD_CAP: usize = 256 * 1024;
pub const GROUP_SNAPSHOT_VERSION: u8 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupStateSnapshot {
pub v: u8,
pub group_id: ConversationId,
pub openmls_storage_version: u16,
pub snapshot_created_at_ms: u64,
pub entries: Vec<GroupSnapshotEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupSnapshotEntry {
#[serde(with = "serde_bytes")]
pub key: Vec<u8>,
#[serde(with = "serde_bytes")]
pub value: Vec<u8>,
}
impl GroupStateSnapshot {
pub fn encode(&self) -> Result<Vec<u8>> {
let bytes = codec::encode(self)?;
if bytes.len() > GROUP_SNAPSHOT_HARD_CAP {
return Err(Error::Invalid(format!(
"group snapshot {} bytes exceeds hard cap {}",
bytes.len(),
GROUP_SNAPSHOT_HARD_CAP
)));
}
if bytes.len() > GROUP_SNAPSHOT_SOFT_CAP {
tracing::warn!(
target: "ping_core::device",
size = bytes.len(),
soft_cap = GROUP_SNAPSHOT_SOFT_CAP,
"group snapshot exceeds soft cap"
);
}
Ok(bytes)
}
pub fn decode(bytes: &[u8]) -> Result<Self> {
if bytes.len() > GROUP_SNAPSHOT_HARD_CAP {
return Err(Error::Invalid(format!(
"group snapshot input {} bytes exceeds hard cap {}",
bytes.len(),
GROUP_SNAPSHOT_HARD_CAP
)));
}
let snap: Self = codec::decode(bytes)?;
if snap.v != GROUP_SNAPSHOT_VERSION {
return Err(Error::Invalid(format!(
"group snapshot v={} not supported (this SDK supports v={})",
snap.v, GROUP_SNAPSHOT_VERSION
)));
}
Ok(snap)
}
}
pub const CATCHUP_SNAPSHOT_SOFT_CAP: usize = 64 * 1024;
pub const CATCHUP_SNAPSHOT_HARD_CAP: usize = 256 * 1024;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CatchupSnapshot {
pub v: u8,
pub conversation_metas: Vec<CatchupConversationEntry>,
pub last_app_events_per_conv: Vec<CatchupAppEventEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CatchupConversationEntry {
pub conversation_id: ConversationId,
pub meta: ConversationMeta,
#[serde(with = "serde_bytes")]
pub group_state_bytes: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CatchupAppEventEntry {
pub conversation_id: ConversationId,
#[serde(with = "serde_bytes")]
pub app_event_bytes: Vec<u8>,
}
pub const CATCHUP_SNAPSHOT_VERSION: u8 = 1;
impl CatchupSnapshot {
pub fn empty() -> Self {
Self {
v: CATCHUP_SNAPSHOT_VERSION,
conversation_metas: Vec::new(),
last_app_events_per_conv: Vec::new(),
}
}
pub fn encode(&self) -> Result<Vec<u8>> {
let bytes = codec::encode(self)?;
if bytes.len() > CATCHUP_SNAPSHOT_HARD_CAP {
return Err(Error::Invalid(format!(
"catchup snapshot {} bytes exceeds hard cap {} — trim last_app_events_per_conv",
bytes.len(),
CATCHUP_SNAPSHOT_HARD_CAP
)));
}
if bytes.len() > CATCHUP_SNAPSHOT_SOFT_CAP {
tracing::warn!(
target: "ping_core::device",
size = bytes.len(),
soft_cap = CATCHUP_SNAPSHOT_SOFT_CAP,
"catchup snapshot exceeds soft cap; new device boot may be sluggish"
);
}
Ok(bytes)
}
pub fn decode(bytes: &[u8]) -> Result<Self> {
if bytes.len() > CATCHUP_SNAPSHOT_HARD_CAP {
return Err(Error::Invalid(format!(
"catchup snapshot input {} bytes exceeds hard cap {}",
bytes.len(),
CATCHUP_SNAPSHOT_HARD_CAP
)));
}
let snap: Self = codec::decode(bytes)?;
if snap.v != CATCHUP_SNAPSHOT_VERSION {
return Err(Error::Invalid(format!(
"catchup snapshot v={} not supported (this SDK supports v={})",
snap.v, CATCHUP_SNAPSHOT_VERSION
)));
}
Ok(snap)
}
}