Skip to main content

rift_core/
keystore.rs

1//! On-disk identity storage and rotation.
2//!
3//! The keystore is a native-only utility. WASM targets return explicit
4//! "unsupported" errors and should provide their own storage abstractions
5//! (e.g., IndexedDB or in-memory).
6
7#[cfg(not(target_arch = "wasm32"))]
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use ed25519_dalek::{Keypair, PublicKey};
13use rand::rngs::OsRng;
14use thiserror::Error;
15
16use crate::{CoreError, Identity};
17
18#[derive(Debug, Error)]
19pub enum KeyStoreError {
20    /// I/O error reading or writing identity material.
21    #[error("io error: {0}")]
22    Io(#[from] std::io::Error),
23    /// Corrupted identity file or incorrect key length.
24    #[error("invalid key length")]
25    InvalidKeyLength,
26    /// Missing identity file.
27    #[error("identity not found at {0}")]
28    IdentityMissing(String),
29    /// Wrapped error from core helpers.
30    #[error("core error: {0}")]
31    Core(#[from] CoreError),
32}
33
34pub struct KeyStore {
35    /// Path to the current identity file.
36    path: PathBuf,
37    /// Cached identity material.
38    identity: Identity,
39}
40
41impl KeyStore {
42    #[cfg(not(target_arch = "wasm32"))]
43    /// Load an identity or create it if it doesn't exist.
44    pub fn load_or_generate(path: &Path) -> Result<Identity, KeyStoreError> {
45        match Identity::load(Some(path)) {
46            Ok(identity) => Ok(identity),
47            Err(CoreError::IdentityMissing(_)) => {
48                let mut rng = OsRng;
49                let keypair = Keypair::generate(&mut rng);
50                let identity = Identity::generate_from_keypair(keypair);
51                identity.save(path)?;
52                Ok(identity)
53            }
54            Err(err) => Err(KeyStoreError::Core(err)),
55        }
56    }
57
58    #[cfg(target_arch = "wasm32")]
59    /// WASM targets are ephemeral: always generate a fresh identity.
60    pub fn load_or_generate(_path: &Path) -> Result<Identity, KeyStoreError> {
61        Ok(Identity::generate())
62    }
63
64    #[cfg(not(target_arch = "wasm32"))]
65    /// Open an existing keystore at a specific path.
66    pub fn open(path: &Path) -> Result<Self, KeyStoreError> {
67        let identity = Identity::load(Some(path)).map_err(KeyStoreError::Core)?;
68        Ok(Self {
69            path: path.to_path_buf(),
70            identity,
71        })
72    }
73
74    #[cfg(target_arch = "wasm32")]
75    /// WASM targets cannot open a file-backed keystore.
76    pub fn open(_path: &Path) -> Result<Self, KeyStoreError> {
77        Err(KeyStoreError::Io(std::io::Error::new(
78            std::io::ErrorKind::Unsupported,
79            "keystore unsupported on wasm",
80        )))
81    }
82
83    #[cfg(not(target_arch = "wasm32"))]
84    /// Rotate the identity by archiving the current key and generating a new one.
85    pub fn rotate(&mut self) -> Result<(), KeyStoreError> {
86        let old_dir = self.old_dir();
87        fs::create_dir_all(&old_dir)?;
88        let ts = SystemTime::now()
89            .duration_since(UNIX_EPOCH)
90            .unwrap_or_default()
91            .as_secs();
92        let archived = old_dir.join(format!("identity-{ts}.key"));
93        fs::rename(&self.path, archived)?;
94
95        let mut rng = OsRng;
96        let keypair = Keypair::generate(&mut rng);
97        let identity = Identity::generate_from_keypair(keypair);
98        identity.save(&self.path)?;
99        self.identity = identity;
100        Ok(())
101    }
102
103    #[cfg(target_arch = "wasm32")]
104    /// WASM targets cannot rotate a file-backed keystore.
105    pub fn rotate(&mut self) -> Result<(), KeyStoreError> {
106        Err(KeyStoreError::Io(std::io::Error::new(
107            std::io::ErrorKind::Unsupported,
108            "keystore unsupported on wasm",
109        )))
110    }
111
112    #[cfg(not(target_arch = "wasm32"))]
113    /// Return all known public keys, including archived ones.
114    pub fn list_public_keys(&self) -> Vec<PublicKey> {
115        let mut keys = Vec::new();
116        if let Ok(keypair) = read_keypair(&self.path) {
117            keys.push(keypair.public);
118        }
119        if let Ok(entries) = fs::read_dir(self.old_dir()) {
120            for entry in entries.flatten() {
121                if let Ok(keypair) = read_keypair(&entry.path()) {
122                    keys.push(keypair.public);
123                }
124            }
125        }
126        keys
127    }
128
129    #[cfg(target_arch = "wasm32")]
130    /// WASM targets only have the in-memory identity.
131    pub fn list_public_keys(&self) -> Vec<PublicKey> {
132        vec![self.identity.keypair.public]
133    }
134
135    /// Borrow the current identity.
136    pub fn identity(&self) -> &Identity {
137        &self.identity
138    }
139
140    #[cfg(not(target_arch = "wasm32"))]
141    /// Directory for archived keys.
142    fn old_dir(&self) -> PathBuf {
143        self.path
144            .parent()
145            .unwrap_or_else(|| Path::new("."))
146            .join(".old")
147    }
148
149    #[cfg(target_arch = "wasm32")]
150    /// No archive directory in WASM.
151    fn old_dir(&self) -> PathBuf {
152        PathBuf::new()
153    }
154}
155
156#[cfg(not(target_arch = "wasm32"))]
157/// Read a keypair from disk with length validation.
158fn read_keypair(path: &Path) -> Result<Keypair, KeyStoreError> {
159    let bytes = fs::read(path)?;
160    if bytes.len() != 64 {
161        return Err(KeyStoreError::InvalidKeyLength);
162    }
163    Keypair::from_bytes(&bytes).map_err(|_| KeyStoreError::InvalidKeyLength)
164}