1
use clap::Args;
2
use mk_lib::file::DisplayPath as _;
3
use mk_lib::secrets::{
4
  encrypt_with_gpg,
5
  verify_vault,
6
  SecretBackend,
7
};
8
use pgp::composed::{
9
  ArmorOptions,
10
  Deserializable,
11
  SignedSecretKey,
12
};
13
use pgp::crypto::sym::SymmetricKeyAlgorithm;
14
use rand::thread_rng;
15
use std::fs::{
16
  self,
17
  File,
18
};
19
use std::io::{
20
  self,
21
  IsTerminal,
22
  Read as _,
23
  Write as _,
24
};
25
use std::path::Path;
26

            
27
use crate::secrets::context::Context;
28
use crate::secrets::vault::verify_key;
29

            
30
#[derive(Debug, Args)]
31
pub struct StoreSecret {
32
  #[arg(help = "The secret identifier")]
33
  path: String,
34

            
35
  #[arg(help = "The secret value")]
36
  value: Option<String>,
37

            
38
  #[arg(short, long, help = "The path to the secret vault")]
39
  vault_location: Option<String>,
40

            
41
  #[arg(long, help = "The key location")]
42
  keys_location: Option<String>,
43

            
44
  #[arg(short, long, help = "The key name", conflicts_with = "gpg_key_id")]
45
  key_name: Option<String>,
46

            
47
  #[arg(
48
    long,
49
    conflicts_with = "key_name",
50
    help = "GPG key ID or fingerprint for hardware/passphrase-protected keys. Cannot be combined with --key-name."
51
  )]
52
  gpg_key_id: Option<String>,
53

            
54
  /// If the secret already exists, it will be overwritten
55
  #[arg(short, long, help = "Force overwrite the secret")]
56
  force: bool,
57
}
58

            
59
impl StoreSecret {
60
  pub fn execute(&self, context: &Context) -> anyhow::Result<()> {
61
    let path = self.path.as_str();
62
    let value: String = match &self.value {
63
      Some(value) => value.clone(),
64
      None => {
65
        let stdin = io::stdin();
66
        if stdin.is_terminal() {
67
          return Err(anyhow::anyhow!(
68
            "No secret value provided. Pass a value as the second argument or pipe it via stdin."
69
          ));
70
        }
71

            
72
        let mut buffer = String::new();
73
        let mut handle = stdin.lock();
74
        match handle.read_to_string(&mut buffer) {
75
          Ok(0) => {
76
            return Err(anyhow::anyhow!(
77
              "No secret value provided. Pass a value as the second argument or pipe it via stdin."
78
            ))
79
          },
80
          Ok(_) => buffer.trim().to_string(),
81
          Err(e) => return Err(anyhow::anyhow!("Failed to read from stdin: {}", e)),
82
        }
83
      },
84
    };
85

            
86
    let mut cli_overrides = context.settings().clone();
87
    if let Some(vault_location) = &self.vault_location {
88
      cli_overrides.vault_location = Some(vault_location.clone());
89
    }
90
    if let Some(keys_location) = &self.keys_location {
91
      cli_overrides.keys_location = Some(keys_location.clone());
92
    }
93
    if let Some(key_name) = &self.key_name {
94
      cli_overrides.key_name = Some(key_name.clone());
95
    }
96
    if let Some(gpg_key_id) = &self.gpg_key_id {
97
      cli_overrides.gpg_key_id = Some(gpg_key_id.clone());
98
    }
99
    let secret_config = context.resolve_with_settings(&cli_overrides);
100
    let vault_location = secret_config.vault_location.to_string_lossy().to_string();
101
    let keys_location = secret_config.keys_location.to_string_lossy().to_string();
102
    let key_name = secret_config.key_name.clone();
103
    let gpg_key_id = secret_config.gpg_key_id.clone();
104

            
105
    assert!(!path.is_empty(), "Path must be provided");
106
    assert!(!value.is_empty(), "Value must be provided");
107
    assert!(!vault_location.is_empty(), "Store location must be provided");
108
    assert!(!keys_location.is_empty(), "Keys location must be provided");
109
    assert!(!key_name.is_empty(), "Key name must be provided");
110

            
111
    verify_vault(Path::new(&vault_location))?;
112
    let backend = secret_config.backend.clone();
113
    if matches!(backend, SecretBackend::BuiltInPgp) {
114
      verify_key(&keys_location, &key_name)?;
115
    }
116

            
117
    let secret_path = Path::new(&vault_location).join(path);
118
    let data_path = secret_path.join("data.asc");
119
    if secret_path.exists()
120
      && secret_path.is_dir()
121
      && data_path.exists()
122
      && data_path.is_file()
123
      && !self.force
124
    {
125
      println!(
126
        "Secret already exists at path {path} in {}",
127
        secret_path.display_lossy()
128
      );
129
    } else {
130
      fs::create_dir_all(&secret_path)?;
131

            
132
      if matches!(backend, SecretBackend::Gpg) {
133
        let gpg_id = gpg_key_id
134
          .as_deref()
135
          .ok_or_else(|| anyhow::anyhow!("GPG backend selected but no gpg_key_id is configured"))?;
136
        // GPG path: encrypt via system gpg binary (supports YubiKey and passphrase-protected keys)
137
        let encrypted = encrypt_with_gpg(gpg_id, value.as_bytes())?;
138
        let mut writer = File::create(data_path)?;
139
        writer.write_all(&encrypted)?;
140
        writer.flush()?;
141
      } else {
142
        // Built-in pgp path: key file must exist in keys_location
143
        let key_name = format!("{}.key", key_name);
144
        let key_path = Path::new(&keys_location).join(key_name);
145
        let mut secret_key_string = File::open(key_path)?;
146
        let (signed_secret_key, _) = SignedSecretKey::from_armor_single(&mut secret_key_string)?;
147
        signed_secret_key.verify_bindings()?;
148

            
149
        // Get the public key (signed form implements PublicKeyTrait)
150
        let pubkey = signed_secret_key.to_public_key();
151

            
152
        // Encrypt the value using MessageBuilder and write armored output
153
        let mut rng = thread_rng();
154
        let builder = pgp::composed::MessageBuilder::from_bytes("", value.into_bytes())
155
          .seipd_v1(&mut rng, SymmetricKeyAlgorithm::AES128);
156
        // Add recipient public key(s)
157
        let mut builder = builder;
158
        builder.encrypt_to_key(&mut rng, &pubkey)?;
159
        let armored = builder.to_armored_string(&mut rng, ArmorOptions::default())?;
160

            
161
        // Save the armored encrypted message to a file
162
        let mut writer = File::create(data_path)?;
163
        write!(writer, "{}", armored)?;
164
        writer.flush()?;
165
      }
166

            
167
      println!("Secret stored at {}", secret_path.display_lossy());
168
    }
169
    Ok(())
170
  }
171
}