Skip to main content

mneme/crypto/
keys.rs

1use std::path::PathBuf;
2
3/// Clave pública para encriptación.
4#[derive(Debug, Clone)]
5pub enum RecipientKey {
6    /// SSH key (ed25519 o RSA).
7    Ssh(String),
8    /// age native key (age1...).
9    Age(String),
10}
11
12impl RecipientKey {
13    /// Carga desde archivo de clave pública SSH.
14    pub fn from_ssh_file(path: &PathBuf) -> crate::error::Result<Self> {
15        let content = std::fs::read_to_string(path).map_err(crate::error::MnemeError::Io)?;
16        Ok(RecipientKey::Ssh(content.trim().to_string()))
17    }
18
19    /// Carga desde string (detecta tipo automáticamente).
20    pub fn from_string(s: &str) -> crate::error::Result<Self> {
21        let s = s.trim();
22        if s.starts_with("age1") {
23            Ok(RecipientKey::Age(s.to_string()))
24        } else {
25            Ok(RecipientKey::Ssh(s.to_string()))
26        }
27    }
28
29    /// Retorna el tipo como string para almacenar en DB.
30    pub fn key_type(&self) -> &str {
31        match self {
32            RecipientKey::Ssh(s) => {
33                if s.contains("ssh-ed25519") {
34                    "ssh-ed25519"
35                } else if s.contains("ssh-rsa") {
36                    "ssh-rsa"
37                } else {
38                    "ssh"
39                }
40            }
41            RecipientKey::Age(_) => "age",
42        }
43    }
44
45    /// Retorna la representación en string para almacenar en DB.
46    pub fn public_key_string(&self) -> String {
47        match self {
48            RecipientKey::Ssh(s) | RecipientKey::Age(s) => s.clone(),
49        }
50    }
51}
52
53/// Identidad para desencriptación (clave privada).
54#[derive(Debug)]
55pub enum IdentityKey {
56    /// SSH private key.
57    Ssh(PathBuf),
58    /// age native identity file.
59    Age(PathBuf),
60}
61
62impl IdentityKey {
63    /// Detecta y carga la identidad disponible en el sistema.
64    pub fn detect() -> crate::error::Result<Self> {
65        // 1. MNEME_IDENTITY env var
66        if let Ok(val) = std::env::var("MNEME_IDENTITY") {
67            return Self::from_path(&PathBuf::from(val));
68        }
69        // 2. ~/.ssh/id_ed25519
70        if let Some(mut home) = dirs::home_dir() {
71            home.push(".ssh");
72            let ed25519 = home.join("id_ed25519");
73            if ed25519.exists() {
74                return Ok(IdentityKey::Ssh(ed25519));
75            }
76            // 3. ~/.ssh/id_rsa
77            let rsa = home.join("id_rsa");
78            if rsa.exists() {
79                return Ok(IdentityKey::Ssh(rsa));
80            }
81        }
82        // 4. ~/.age/key.txt
83        if let Some(mut home) = dirs::home_dir() {
84            home.push(".age");
85            let key = home.join("key.txt");
86            if key.exists() {
87                return Ok(IdentityKey::Age(key));
88            }
89        }
90        Err(crate::error::MnemeError::IdentityNotLoaded)
91    }
92
93    /// Carga desde path explícito.
94    pub fn from_path(path: &PathBuf) -> crate::error::Result<Self> {
95        if !path.exists() {
96            return Err(crate::error::MnemeError::Io(std::io::Error::new(
97                std::io::ErrorKind::NotFound,
98                format!("identity file not found: {}", path.display()),
99            )));
100        }
101        // Detectar tipo por extensión o contenido
102        if path.extension().and_then(|e| e.to_str()) == Some("txt") {
103            return Ok(IdentityKey::Age(path.clone()));
104        }
105        let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
106        if filename.starts_with("id_") {
107            Ok(IdentityKey::Ssh(path.clone()))
108        } else {
109            // Intentar leer primeras líneas para detectar
110            let content = std::fs::read_to_string(path)?;
111            if content.contains("AGE-SECRET-KEY") {
112                Ok(IdentityKey::Age(path.clone()))
113            } else {
114                Ok(IdentityKey::Ssh(path.clone()))
115            }
116        }
117    }
118
119    /// Retorna el path de la identidad.
120    pub fn path(&self) -> &PathBuf {
121        match self {
122            IdentityKey::Ssh(p) | IdentityKey::Age(p) => p,
123        }
124    }
125}