ping-openmls-sdk-core 0.5.2

Platform-agnostic OpenMLS-based messaging engine
Documentation
//! User identity. A user has one long-term Ed25519 key and many devices.
//!
//! `Identity` is the in-memory handle. Persistent storage of the secret half is the host's
//! responsibility (Keychain on iOS, Keystore on Android, IndexedDB+passphrase on Web). The
//! [`Identity::export`] / [`Identity::import`] pair are the only paths private material crosses
//! the FFI.

use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use rand_core::{OsRng, RngCore};
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;

use crate::{codec, Error, Result};

/// Stable user identifier — 32 bytes, derived from the identity public key.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct UserId(#[serde(with = "serde_bytes")] pub Vec<u8>);

impl UserId {
    pub fn from_pubkey(pk: &VerifyingKey) -> Self {
        UserId(codec::sha256(pk.as_bytes()).to_vec())
    }
    pub fn as_hex(&self) -> String {
        hex::encode(&self.0)
    }
}

/// Long-term identity. Holds the signing key only on the originating device.
pub struct Identity {
    user_id: UserId,
    signing: SigningKey,
}

impl std::fmt::Debug for Identity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Identity")
            .field("user_id", &self.user_id.as_hex())
            .finish()
    }
}

impl Identity {
    pub fn generate() -> Self {
        let mut seed = [0u8; 32];
        OsRng.fill_bytes(&mut seed);
        let signing = SigningKey::from_bytes(&seed);
        let user_id = UserId::from_pubkey(&signing.verifying_key());
        Identity { user_id, signing }
    }

    pub fn user_id(&self) -> &UserId {
        &self.user_id
    }
    pub fn public_key(&self) -> VerifyingKey {
        self.signing.verifying_key()
    }

    /// Sign a (user_id || device_id) binding to issue a device credential.
    pub fn sign_device_binding(&self, device_id: &[u8]) -> Vec<u8> {
        let mut buf = Vec::with_capacity(self.user_id.0.len() + device_id.len());
        buf.extend_from_slice(&self.user_id.0);
        buf.extend_from_slice(device_id);
        self.signing.sign(&buf).to_bytes().to_vec()
    }

    pub fn verify_device_binding(
        user_pk: &VerifyingKey,
        user_id: &UserId,
        device_id: &[u8],
        sig: &[u8],
    ) -> Result<()> {
        let sig: [u8; 64] = sig
            .try_into()
            .map_err(|_| Error::Identity("bad signature length".into()))?;
        let signature = Signature::from_bytes(&sig);
        let mut buf = Vec::with_capacity(user_id.0.len() + device_id.len());
        buf.extend_from_slice(&user_id.0);
        buf.extend_from_slice(device_id);
        user_pk
            .verify(&buf, &signature)
            .map_err(|e| Error::Identity(format!("signature verify failed: {e}")))
    }

    /// Export the identity for backup. The returned bytes contain the secret seed and must be
    /// treated as such by the caller.
    pub fn export(&self) -> Zeroizing<Vec<u8>> {
        #[derive(Serialize)]
        struct Export<'a> {
            v: u8,
            #[serde(with = "serde_bytes")]
            seed: &'a [u8],
        }
        let bytes = codec::encode(&Export {
            v: 1,
            seed: self.signing.as_bytes(),
        })
        .expect("identity export cannot fail");
        Zeroizing::new(bytes)
    }

    pub fn import(bytes: &[u8]) -> Result<Self> {
        #[derive(Deserialize)]
        struct Export {
            v: u8,
            #[serde(with = "serde_bytes")]
            seed: Vec<u8>,
        }
        let imported: Export = codec::decode(bytes)?;
        if imported.v != 1 {
            return Err(Error::Identity(format!(
                "unknown export version {}",
                imported.v
            )));
        }
        let seed: [u8; 32] = imported
            .seed
            .as_slice()
            .try_into()
            .map_err(|_| Error::Identity("bad seed length".into()))?;
        let signing = SigningKey::from_bytes(&seed);
        let user_id = UserId::from_pubkey(&signing.verifying_key());
        Ok(Identity { user_id, signing })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn export_roundtrip() {
        let id = Identity::generate();
        let exported = id.export();
        let restored = Identity::import(&exported).unwrap();
        assert_eq!(id.user_id(), restored.user_id());
        assert_eq!(id.public_key().as_bytes(), restored.public_key().as_bytes());
    }

    #[test]
    fn device_binding_verifies() {
        let id = Identity::generate();
        let device_id = b"device-1";
        let sig = id.sign_device_binding(device_id);
        Identity::verify_device_binding(&id.public_key(), id.user_id(), device_id, &sig).unwrap();
    }
}