Skip to main content

ping_core/
identity.rs

1//! User identity. A user has one long-term Ed25519 key and many devices.
2//!
3//! `Identity` is the in-memory handle. Persistent storage of the secret half is the host's
4//! responsibility (Keychain on iOS, Keystore on Android, IndexedDB+passphrase on Web). The
5//! [`Identity::export`] / [`Identity::import`] pair are the only paths private material crosses
6//! the FFI.
7
8use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
9use rand_core::{OsRng, RngCore};
10use serde::{Deserialize, Serialize};
11use zeroize::Zeroizing;
12
13use crate::{codec, Error, Result};
14
15/// Stable user identifier — 32 bytes, derived from the identity public key.
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct UserId(#[serde(with = "serde_bytes")] pub Vec<u8>);
18
19impl UserId {
20    pub fn from_pubkey(pk: &VerifyingKey) -> Self {
21        UserId(codec::sha256(pk.as_bytes()).to_vec())
22    }
23    pub fn as_hex(&self) -> String {
24        hex::encode(&self.0)
25    }
26}
27
28/// Long-term identity. Holds the signing key only on the originating device.
29pub struct Identity {
30    user_id: UserId,
31    signing: SigningKey,
32}
33
34impl std::fmt::Debug for Identity {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.debug_struct("Identity")
37            .field("user_id", &self.user_id.as_hex())
38            .finish()
39    }
40}
41
42impl Identity {
43    pub fn generate() -> Self {
44        let mut seed = [0u8; 32];
45        OsRng.fill_bytes(&mut seed);
46        let signing = SigningKey::from_bytes(&seed);
47        let user_id = UserId::from_pubkey(&signing.verifying_key());
48        Identity { user_id, signing }
49    }
50
51    pub fn user_id(&self) -> &UserId {
52        &self.user_id
53    }
54    pub fn public_key(&self) -> VerifyingKey {
55        self.signing.verifying_key()
56    }
57
58    /// Sign a (user_id || device_id) binding to issue a device credential.
59    pub fn sign_device_binding(&self, device_id: &[u8]) -> Vec<u8> {
60        let mut buf = Vec::with_capacity(self.user_id.0.len() + device_id.len());
61        buf.extend_from_slice(&self.user_id.0);
62        buf.extend_from_slice(device_id);
63        self.signing.sign(&buf).to_bytes().to_vec()
64    }
65
66    pub fn verify_device_binding(
67        user_pk: &VerifyingKey,
68        user_id: &UserId,
69        device_id: &[u8],
70        sig: &[u8],
71    ) -> Result<()> {
72        let sig: [u8; 64] = sig
73            .try_into()
74            .map_err(|_| Error::Identity("bad signature length".into()))?;
75        let signature = Signature::from_bytes(&sig);
76        let mut buf = Vec::with_capacity(user_id.0.len() + device_id.len());
77        buf.extend_from_slice(&user_id.0);
78        buf.extend_from_slice(device_id);
79        user_pk
80            .verify(&buf, &signature)
81            .map_err(|e| Error::Identity(format!("signature verify failed: {e}")))
82    }
83
84    /// Export the identity for backup. The returned bytes contain the secret seed and must be
85    /// treated as such by the caller.
86    pub fn export(&self) -> Zeroizing<Vec<u8>> {
87        #[derive(Serialize)]
88        struct Export<'a> {
89            v: u8,
90            #[serde(with = "serde_bytes")]
91            seed: &'a [u8],
92        }
93        let bytes = codec::encode(&Export {
94            v: 1,
95            seed: self.signing.as_bytes(),
96        })
97        .expect("identity export cannot fail");
98        Zeroizing::new(bytes)
99    }
100
101    pub fn import(bytes: &[u8]) -> Result<Self> {
102        #[derive(Deserialize)]
103        struct Export {
104            v: u8,
105            #[serde(with = "serde_bytes")]
106            seed: Vec<u8>,
107        }
108        let imported: Export = codec::decode(bytes)?;
109        if imported.v != 1 {
110            return Err(Error::Identity(format!(
111                "unknown export version {}",
112                imported.v
113            )));
114        }
115        let seed: [u8; 32] = imported
116            .seed
117            .as_slice()
118            .try_into()
119            .map_err(|_| Error::Identity("bad seed length".into()))?;
120        let signing = SigningKey::from_bytes(&seed);
121        let user_id = UserId::from_pubkey(&signing.verifying_key());
122        Ok(Identity { user_id, signing })
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn export_roundtrip() {
132        let id = Identity::generate();
133        let exported = id.export();
134        let restored = Identity::import(&exported).unwrap();
135        assert_eq!(id.user_id(), restored.user_id());
136        assert_eq!(id.public_key().as_bytes(), restored.public_key().as_bytes());
137    }
138
139    #[test]
140    fn device_binding_verifies() {
141        let id = Identity::generate();
142        let device_id = b"device-1";
143        let sig = id.sign_device_binding(device_id);
144        Identity::verify_device_binding(&id.public_key(), id.user_id(), device_id, &sig).unwrap();
145    }
146}