1
use std::fs;
2
use std::io::Write as _;
3
use std::path::Path;
4
use std::process::Command;
5

            
6
use anyhow::Context as _;
7
use clap::Args;
8
use mk_lib::file::DisplayPath as _;
9

            
10
use crate::secrets::context::Context;
11

            
12
use super::KEY_LOCATION_HELP;
13

            
14
#[derive(Debug, Args)]
15
pub struct ImportKey {
16
  /// The GPG key ID or fingerprint to import from the local keyring
17
  #[arg(
18
    long,
19
    help = "GPG key ID or fingerprint from your local keyring (e.g. a YubiKey-backed key)"
20
  )]
21
  gpg: String,
22

            
23
  /// The location where the key reference will be stored
24
  #[arg(short, long, help = KEY_LOCATION_HELP)]
25
  location: Option<String>,
26

            
27
  /// The name assigned to this key reference (default: "default")
28
  #[arg(short, long, help = "The key name")]
29
  name: Option<String>,
30
}
31

            
32
impl ImportKey {
33
  pub fn execute(&self, context: &Context) -> anyhow::Result<()> {
34
    let location: &str = &self.location.clone().unwrap_or_else(|| context.keys_location());
35
    let name: &str = &self.name.clone().unwrap_or("default".to_string());
36
    let gpg_key_id: &str = &self.gpg;
37

            
38
    println!("Importing GPG key '{gpg_key_id}' as '{name}' into {location}");
39

            
40
    // Verify the key exists in the local GPG keyring
41
    let check = Command::new("gpg")
42
      .args(["--batch", "--list-keys", gpg_key_id])
43
      .output()
44
      .context("Failed to run gpg — is it installed and available in PATH?")?;
45
    if !check.status.success() {
46
      let stderr = String::from_utf8_lossy(&check.stderr);
47
      anyhow::bail!("GPG key '{}' not found in keyring: {}", gpg_key_id, stderr.trim());
48
    }
49

            
50
    // Create the keys directory if it does not exist
51
    let location_path = Path::new(location);
52
    if !location_path.exists() {
53
      fs::create_dir_all(location_path)?;
54
    }
55

            
56
    // Write a .gpg metadata file containing just the key ID/fingerprint.
57
    // The vault uses this to know which GPG key to call when gpg_key_id is set.
58
    let meta_path = location_path.join(format!("{name}.gpg"));
59
    let mut meta_file = fs::File::create(&meta_path)?;
60
    writeln!(meta_file, "{gpg_key_id}")?;
61
    meta_file.flush()?;
62
    println!("Key reference saved to {}", meta_path.display_lossy());
63

            
64
    // Export and store the public key (ASCII-armored) for auditing / re-encryption
65
    let pub_path = location_path.join(format!("{name}.pub"));
66
    let export = Command::new("gpg")
67
      .args(["--batch", "--export", "--armor", gpg_key_id])
68
      .output()
69
      .context("Failed to export GPG public key")?;
70
    if !export.status.success() {
71
      let stderr = String::from_utf8_lossy(&export.stderr);
72
      anyhow::bail!("Failed to export GPG public key: {}", stderr.trim());
73
    }
74
    let mut pub_file = fs::File::create(&pub_path)?;
75
    pub_file.write_all(&export.stdout)?;
76
    pub_file.flush()?;
77
    println!("Public key exported to {}", pub_path.display_lossy());
78

            
79
    println!();
80
    println!("To use this key, add the following to your tasks.yaml:");
81
    println!("  gpg_key_id: {gpg_key_id}");
82

            
83
    Ok(())
84
  }
85
}