diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 0495954..2669307 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -1252,7 +1252,77 @@ fn cmd_sync() -> Result<()> { eprintln!("Sync complete."); Ok(()) } -fn cmd_device(_a: DeviceAction) -> Result<()> { bail!("not yet implemented"); } +fn cmd_device(action: DeviceAction) -> Result<()> { + use std::fs; + use ed25519_dalek::SigningKey; + use rand::rngs::OsRng; + + let root = crate::helpers::vault_dir()?; + let devices_path = root.join(".relicario").join("devices.json"); + + #[derive(serde::Serialize, serde::Deserialize)] + struct DeviceEntry { name: String, public_key: String } + + match action { + DeviceAction::Add { name } => { + let mut existing: Vec = + serde_json::from_slice(&fs::read(&devices_path)?).unwrap_or_default(); + if existing.iter().any(|d| d.name == name) { + anyhow::bail!("device `{name}` already exists"); + } + let signing = SigningKey::generate(&mut OsRng); + let verifying = signing.verifying_key(); + let pubkey_hex = hex::encode(verifying.to_bytes()); + + existing.push(DeviceEntry { name: name.clone(), public_key: pubkey_hex.clone() }); + fs::write(&devices_path, serde_json::to_string_pretty(&existing)?)?; + + let cfg_dir = dirs::config_dir() + .ok_or_else(|| anyhow::anyhow!("no config dir"))? + .join("relicario").join("devices"); + fs::create_dir_all(&cfg_dir)?; + let key_path = cfg_dir.join(format!("{name}.key")); + fs::write(&key_path, signing.to_bytes())?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600))?; + } + + let status = crate::helpers::git_command(&root, + &["add", ".relicario/devices.json"]).status()?; + if !status.success() { anyhow::bail!("git add failed"); } + let status = crate::helpers::git_command(&root, + &["commit", "-m", &format!("device: add {name}")]).status()?; + if !status.success() { anyhow::bail!("git commit failed"); } + eprintln!("Added device `{name}` (pubkey: {pubkey_hex})"); + } + DeviceAction::List => { + let existing: Vec = + serde_json::from_slice(&fs::read(&devices_path)?).unwrap_or_default(); + if existing.is_empty() { eprintln!("(no devices)"); return Ok(()); } + for d in existing { + println!("{:<20} {}", d.name, d.public_key); + } + } + DeviceAction::Revoke { name } => { + let mut existing: Vec = + serde_json::from_slice(&fs::read(&devices_path)?).unwrap_or_default(); + let before = existing.len(); + existing.retain(|d| d.name != name); + if existing.len() == before { anyhow::bail!("device `{name}` not found"); } + fs::write(&devices_path, serde_json::to_string_pretty(&existing)?)?; + let status = crate::helpers::git_command(&root, + &["add", ".relicario/devices.json"]).status()?; + if !status.success() { anyhow::bail!("git add failed"); } + let status = crate::helpers::git_command(&root, + &["commit", "-m", &format!("device: revoke {name}")]).status()?; + if !status.success() { anyhow::bail!("git commit failed"); } + eprintln!("Revoked device `{name}`"); + } + } + Ok(()) +} #[derive(serde::Serialize)] struct ParamsFile {