⎇ 3fc6dcfa72e4783f03da32b3ab5900b06f483bbd
crates/init/ente-zero/src/keypair_store.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
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}