ping-openmls-sdk-core 0.0.1

Platform-agnostic OpenMLS-based messaging engine
//! Device model — every install of the SDK is one device. Devices are first-class MLS members.
//!
//! See `docs/MULTIDEVICE.md` for the full design.

use ed25519_dalek::{SigningKey, VerifyingKey};
use rand_core::{OsRng, RngCore};
use serde::{Deserialize, Serialize};

use crate::{codec, identity::UserId};

/// 32-byte device identifier — SHA-256 of the device's signing public key.
#[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)
    }
}

/// Public-facing device record exposed across the FFI.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceInfo {
    pub device_id: DeviceId,
    pub user_id: UserId,
    pub label: String, // human-readable "iPhone 15", set by the host
    pub created_at_ms: u64,
    pub last_seen_ms: u64,
    pub revoked: bool,
}

/// Local device — owns its signing keypair. Never leaves the device that created it.
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,
        }
    }
}

/// Encoded payload presented to a new device during the linking flow.
///
/// Carries the existing device's user-identity public key + a signed device-binding for the new
/// device + the Welcome that admits the new device to the user's DeviceGroup. The whole thing
/// is HPKE-sealed against the new device's ephemeral pubkey before transmission.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkingTicket {
    pub v: u8,
    pub user_id: UserId,
    #[serde(with = "serde_bytes")]
    pub user_pubkey: Vec<u8>, // VerifyingKey bytes (32)
    pub new_device_id: DeviceId,
    #[serde(with = "serde_bytes")]
    pub device_binding_sig: Vec<u8>, // identity signature over (user_id || device_id)
    #[serde(with = "serde_bytes")]
    pub device_group_welcome: Vec<u8>, // serialized OpenMLS Welcome
    #[serde(with = "serde_bytes")]
    pub catchup_snapshot: Vec<u8>, // optional encrypted UX snapshot, see MULTIDEVICE.md
}