Skip to main content

rift_core/
identity.rs

1//! Identity management: keypair generation, peer-id derivation, and persistence.
2//!
3//! This module is used by both native and WASM clients. Persistence is gated
4//! by `cfg(target_arch = "wasm32")` because browsers cannot access the host
5//! filesystem directly. For WASM targets, identity is ephemeral unless a higher
6//! level chooses to store it (e.g., IndexedDB).
7
8#[cfg(not(target_arch = "wasm32"))]
9use std::fs;
10use std::path::{Path, PathBuf};
11
12use blake3::Hasher;
13use ed25519_dalek::Keypair;
14use rand::rngs::OsRng;
15
16use crate::{CoreError, PeerId};
17
18#[derive(Debug)]
19pub struct Identity {
20    /// Long-term Ed25519 identity keypair.
21    pub keypair: Keypair,
22    /// Stable peer identifier derived from the public key.
23    pub peer_id: PeerId,
24}
25
26impl Identity {
27    /// Generate a new identity using OS randomness.
28    pub fn generate() -> Self {
29        let mut rng = OsRng;
30        let keypair = Keypair::generate(&mut rng);
31        Self::generate_from_keypair(keypair)
32    }
33
34    /// Wrap an existing keypair and derive its peer id.
35    pub fn generate_from_keypair(keypair: Keypair) -> Self {
36        let peer_id = peer_id_from_keypair(&keypair);
37        Self { keypair, peer_id }
38    }
39
40    #[cfg(not(target_arch = "wasm32"))]
41    /// Default on-disk identity path on native targets.
42    pub fn default_path() -> Result<PathBuf, CoreError> {
43        let base = dirs::config_dir().ok_or(CoreError::ConfigDirMissing)?;
44        Ok(base.join("rift").join("identity.key"))
45    }
46
47    #[cfg(target_arch = "wasm32")]
48    /// WASM targets cannot resolve a filesystem-backed config directory.
49    pub fn default_path() -> Result<PathBuf, CoreError> {
50        Err(CoreError::ConfigDirMissing)
51    }
52
53    #[cfg(not(target_arch = "wasm32"))]
54    /// Load an identity from disk or return a detailed error.
55    pub fn load(path: Option<&Path>) -> Result<Self, CoreError> {
56        let path = match path {
57            Some(path) => path.to_path_buf(),
58            None => Self::default_path()?,
59        };
60        let bytes = fs::read(&path)
61            .map_err(|err| if err.kind() == std::io::ErrorKind::NotFound {
62                CoreError::IdentityMissing(path.display().to_string())
63            } else {
64                CoreError::Io(err)
65            })?;
66
67        if bytes.len() != 64 {
68            return Err(CoreError::InvalidKeyLength);
69        }
70        let keypair = Keypair::from_bytes(&bytes).map_err(|_| CoreError::InvalidKeyLength)?;
71        let peer_id = peer_id_from_keypair(&keypair);
72        Ok(Self { keypair, peer_id })
73    }
74
75    #[cfg(target_arch = "wasm32")]
76    /// WASM targets cannot load from disk; callers should provide their own storage.
77    pub fn load(path: Option<&Path>) -> Result<Self, CoreError> {
78        let _ = path;
79        Err(CoreError::IdentityMissing("identity storage unsupported on wasm".to_string()))
80    }
81
82    #[cfg(not(target_arch = "wasm32"))]
83    /// Load from disk or generate and persist a new identity if missing.
84    pub fn load_or_generate(path: Option<&Path>) -> Result<(Self, bool), CoreError> {
85        let path = match path {
86            Some(path) => path.to_path_buf(),
87            None => Self::default_path()?,
88        };
89        match Self::load(Some(&path)) {
90            Ok(identity) => Ok((identity, false)),
91            Err(CoreError::IdentityMissing(_)) => {
92                let identity = Self::generate();
93                identity.save(&path)?;
94                Ok((identity, true))
95            }
96            Err(err) => Err(err),
97        }
98    }
99
100    #[cfg(target_arch = "wasm32")]
101    /// On WASM targets, always generate a new ephemeral identity.
102    pub fn load_or_generate(_path: Option<&Path>) -> Result<(Self, bool), CoreError> {
103        Ok((Self::generate(), true))
104    }
105
106    #[cfg(not(target_arch = "wasm32"))]
107    /// Persist identity to disk.
108    pub fn save(&self, path: &Path) -> Result<(), CoreError> {
109        if let Some(parent) = path.parent() {
110            fs::create_dir_all(parent)?;
111        }
112        let bytes = self.keypair.to_bytes();
113        fs::write(path, bytes)?;
114        Ok(())
115    }
116
117    #[cfg(target_arch = "wasm32")]
118    /// WASM targets cannot save to disk; surface an explicit error.
119    pub fn save(&self, _path: &Path) -> Result<(), CoreError> {
120        Err(CoreError::Io(std::io::Error::new(
121            std::io::ErrorKind::Unsupported,
122            "identity storage unsupported on wasm",
123        )))
124    }
125}
126
127/// Hash the public key to derive a stable peer id.
128fn peer_id_from_keypair(keypair: &Keypair) -> PeerId {
129    let mut hasher = Hasher::new();
130    hasher.update(keypair.public.as_bytes());
131    let hash = hasher.finalize();
132    PeerId(*hash.as_bytes())
133}
134
135/// Convert raw public key bytes to a peer id, with length validation.
136pub fn peer_id_from_public_key_bytes(bytes: &[u8]) -> Result<PeerId, CoreError> {
137    if bytes.len() != 32 {
138        return Err(CoreError::InvalidKeyLength);
139    }
140    let mut hasher = Hasher::new();
141    hasher.update(bytes);
142    let hash = hasher.finalize();
143    Ok(PeerId(*hash.as_bytes()))
144}