tawasuyu

tawasuyu/brahman

Rust 332TOML 127JSON 17Markdown 154710 Sutichaykuna

⎇ 3fc6dcfa72e4783f03da32b3ab5900b06f483bbd crates/init/ente-zero/src/keypair_store.rs Rust · 184 siq'ikuna · 6.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG 550c98f27 · sergio ·

1//! Persistencia de la keypair Ed25519 de identidad libp2p de Arje.
2//!
3//! El `peer_id` que Arje presenta en la malla `brahman-net` deriva de
4//! esta keypair. Si se regenera en cada arranque, el peer_id cambia
5//! y los nodos remotos pierden la referencia. Persistir el secret a
6//! disco (32 bytes raw, permisos 0o600) garantiza identidad estable.
7//!
8//! ## Path
9//!
10//! Por orden de precedencia:
11//! 1. `BRAHMAN_KEYPAIR_PATH` env var (override explícito).
12//! 2. Si PID 1 / root: `/var/lib/brahman/init-keypair.bin`.
13//! 3. Si dev mode: `$XDG_DATA_HOME/brahman/init-keypair.bin`, fallback
14//!    a `$HOME/.local/share/brahman/init-keypair.bin`, último recurso
15//!    `/tmp/brahman-init-keypair.bin` (sin persistencia útil pero al
16//!    menos no rompe en CI minimalista).
17//!
18//! ## Formato
19//!
20//! 32 bytes raw del secret Ed25519. Sin header, sin metadata. La
21//! public key se deriva determinísticamente al cargar. Esto evita
22//! depender de un schema de serialización (postcard, json) que
23//! pudiera bumpear y romper compat de identidad.
24
25use std::path::{Path, PathBuf};
26
27use anyhow::{bail, Context, Result};
28use brahman_net::Keypair;
29
30/// Tamaño exacto del secret Ed25519.
31const SECRET_LEN: usize = 32;
32
33/// Carga la keypair desde `path` si existe, o genera una nueva,
34/// la persiste y la devuelve. Devuelve también si fue cargada (true)
35/// o generada (false), para logging.
36pub fn load_or_generate(path: &Path) -> Result<(Keypair, bool)> {
37    if path.exists() {
38        let bytes = std::fs::read(path)
39            .with_context(|| format!("leer keypair de {}", path.display()))?;
40        if bytes.len() != SECRET_LEN {
41            bail!(
42                "keypair en {} tiene {} bytes, esperaba {}",
43                path.display(),
44                bytes.len(),
45                SECRET_LEN
46            );
47        }
48        let mut secret = [0u8; SECRET_LEN];
49        secret.copy_from_slice(&bytes);
50        let kp = Keypair::ed25519_from_bytes(secret)
51            .with_context(|| format!("decodificar keypair en {}", path.display()))?;
52        Ok((kp, true))
53    } else {
54        let kp = Keypair::generate_ed25519();
55        save(path, &kp).context("persistir keypair recién generada")?;
56        Ok((kp, false))
57    }
58}
59
60/// Persiste el secret de `keypair` a `path`. Crea directorios padres,
61/// escribe atómico (vía rename), y aplica permisos 0o600 (sólo dueño).
62fn save(path: &Path, keypair: &Keypair) -> Result<()> {
63    let secret = extract_secret_bytes(keypair)?;
64    if let Some(parent) = path.parent() {
65        std::fs::create_dir_all(parent)
66            .with_context(|| format!("crear dir {}", parent.display()))?;
67    }
68    let tmp = path.with_extension("tmp");
69    std::fs::write(&tmp, secret).with_context(|| format!("write tmp {}", tmp.display()))?;
70    apply_owner_only_perms(&tmp).context("permisos 0o600 en tmp")?;
71    std::fs::rename(&tmp, path)
72        .with_context(|| format!("rename {} → {}", tmp.display(), path.display()))?;
73    Ok(())
74}
75
76fn extract_secret_bytes(keypair: &Keypair) -> Result<[u8; SECRET_LEN]> {
77    // libp2p::Keypair no expone secret() directo; pasamos
78    // por la variante ed25519. Solo Ed25519 soportado en brahman-net,
79    // así que el unwrap es seguro tras with_keypair.
80    let ed = keypair
81        .clone()
82        .try_into_ed25519()
83        .map_err(|_| anyhow::anyhow!("la keypair no es Ed25519 (no debería pasar)"))?;
84    let bytes = ed.secret();
85    let raw: &[u8] = bytes.as_ref();
86    if raw.len() != SECRET_LEN {
87        bail!("ed25519 secret no es {} bytes", SECRET_LEN);
88    }
89    let mut out = [0u8; SECRET_LEN];
90    out.copy_from_slice(raw);
91    Ok(out)
92}
93
94#[cfg(unix)]
95fn apply_owner_only_perms(path: &Path) -> std::io::Result<()> {
96    use std::os::unix::fs::PermissionsExt;
97    let perms = std::fs::Permissions::from_mode(0o600);
98    std::fs::set_permissions(path, perms)
99}
100
101#[cfg(not(unix))]
102fn apply_owner_only_perms(_path: &Path) -> std::io::Result<()> {
103    Ok(())
104}
105
106/// Resuelve el path del keystore según convención (env > root path >
107/// XDG > HOME > tmp).
108pub fn default_path(dev_mode: bool) -> PathBuf {
109    if let Ok(p) = std::env::var("BRAHMAN_KEYPAIR_PATH") {
110        return PathBuf::from(p);
111    }
112
113    if !dev_mode {
114        // PID 1: paths del sistema. /var/lib es el lugar canónico
115        // para state persistente de servicios root.
116        return PathBuf::from("/var/lib/brahman/init-keypair.bin");
117    }
118
119    // Dev mode: paths de usuario.
120    if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
121        return PathBuf::from(xdg).join("brahman").join("init-keypair.bin");
122    }
123    if let Ok(home) = std::env::var("HOME") {
124        return PathBuf::from(home)
125            .join(".local")
126            .join("share")
127            .join("brahman")
128            .join("init-keypair.bin");
129    }
130    PathBuf::from("/tmp/brahman-init-keypair.bin")
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use tempfile::TempDir;
137
138    #[test]
139    fn generate_persist_and_reload_yields_same_peer_id() {
140        let tmp = TempDir::new().unwrap();
141        let path = tmp.path().join("identity.bin");
142        let (kp1, loaded) = load_or_generate(&path).unwrap();
143        assert!(!loaded, "primera vez debe generar");
144        let peer1 = kp1.public().to_peer_id();
145
146        let (kp2, loaded) = load_or_generate(&path).unwrap();
147        assert!(loaded, "segunda vez debe cargar");
148        let peer2 = kp2.public().to_peer_id();
149
150        assert_eq!(peer1, peer2, "peer_id estable across reloads");
151    }
152
153    #[test]
154    fn rejects_corrupted_file() {
155        let tmp = TempDir::new().unwrap();
156        let path = tmp.path().join("bad.bin");
157        std::fs::write(&path, b"too short").unwrap();
158        assert!(load_or_generate(&path).is_err());
159    }
160
161    #[test]
162    #[cfg(unix)]
163    fn persisted_file_is_owner_only() {
164        use std::os::unix::fs::PermissionsExt;
165        let tmp = TempDir::new().unwrap();
166        let path = tmp.path().join("perm.bin");
167        let _ = load_or_generate(&path).unwrap();
168        let mode = std::fs::metadata(&path).unwrap().permissions().mode();
169        assert_eq!(
170            mode & 0o777,
171            0o600,
172            "permisos del keypair file deben ser 0o600 (solo dueño), got {:o}",
173            mode & 0o777
174        );
175    }
176
177    #[test]
178    fn default_path_honors_env() {
179        std::env::set_var("BRAHMAN_KEYPAIR_PATH", "/custom/path.bin");
180        assert_eq!(default_path(false), PathBuf::from("/custom/path.bin"));
181        assert_eq!(default_path(true), PathBuf::from("/custom/path.bin"));
182        std::env::remove_var("BRAHMAN_KEYPAIR_PATH");
183    }
184}