ssh_cli/vps/
secrets_cmd.rs1#![forbid(unsafe_code)]
4use super::config_io::{load, resolve_config_path, save};
7use crate::cli::{OutputFormat, SecretsAction};
8use crate::errors::SshCliError;
9use anyhow::Result;
10use std::path::{Path, PathBuf};
11
12pub async fn run_secrets_command(
18 action: SecretsAction,
19 config_override: Option<PathBuf>,
20 format: OutputFormat,
21) -> Result<()> {
22 crate::secrets::set_config_dir(config_override.clone());
24 match action {
25 SecretsAction::Status { json } => {
26 let seg = crate::secrets::secrets_status()?;
27 let use_json = json || format == OutputFormat::Json;
28 if use_json {
29 let v = serde_json::json!({
30 "encryption_active": seg.encryption_active,
31 "key_source": seg.source.as_str(),
32 "key_file": seg.key_file_path.display().to_string(),
33 "plaintext_opt_out": seg.plaintext_opt_out,
34 "at_rest": if seg.encryption_active { "encrypted" } else { "plaintext" },
35 });
36 crate::output::print_json_value(&v)?;
37 } else {
38 let at_rest = if seg.encryption_active {
39 "encrypted"
40 } else {
41 "plaintext"
42 };
43 crate::output::print_success_fmt(format_args!(
44 "at-rest: {at_rest} | source: {} | key_file: {} | plaintext_opt_out: {}",
45 seg.source.as_str(),
46 seg.key_file_path.display(),
47 seg.plaintext_opt_out
48 ));
49 }
50 Ok(())
51 }
52 SecretsAction::Init {
53 keyring,
54 force,
55 json,
56 } => {
57 let path = resolve_config_path(config_override.as_deref())?;
61 let hosts_to_reencrypt = if force && path.is_file() {
62 Some(load(&path)?)
63 } else {
64 None
65 };
66
67 if crate::cli::dry_run_stop(
72 "secrets-init",
73 &[
74 ("force", serde_json::json!(force)),
75 ("keyring", serde_json::json!(keyring)),
76 (
77 "hosts_to_reencrypt",
78 serde_json::json!(hosts_to_reencrypt.as_ref().map_or(0, |f| f.hosts.len())),
79 ),
80 ("config_path", serde_json::json!(path.display().to_string())),
81 ],
82 )? {
83 return Ok(());
84 }
85
86 let seg = crate::secrets::init_primary_key(keyring, force)?;
87 let mut reencrypted_hosts = 0usize;
88
89 if let Some(file) = hosts_to_reencrypt {
90 reencrypted_hosts = file.hosts.len();
91 save(&path, &file).map_err(|e| {
92 SshCliError::Config(format!(
93 "primary key was rotated but re-encrypting config failed: {e}; \
94 restore secrets.key.bak if present and re-run `secrets reencrypt`"
95 ))
96 })?;
97 }
98
99 let use_json = json || format == OutputFormat::Json;
100 crate::output::emit_success(
101 "secrets-init",
102 serde_json::json!({
103 "key_source": seg.source.as_str(),
104 "key_file": seg.key_file_path.display().to_string(),
105 "reencrypted_hosts": reencrypted_hosts,
106 "force": force,
107 }),
108 &crate::i18n::t(crate::i18n::Message::PrimaryKeyReady {
109 source: seg.source.as_str().to_string(),
110 key_file: seg.key_file_path.display().to_string(),
111 }),
112 use_json,
113 )?;
114 Ok(())
115 }
116 SecretsAction::Reencrypt { json } => {
117 let path = resolve_config_path(config_override.as_deref())?;
118 if crate::cli::dry_run_stop(
120 "secrets-reencrypt",
121 &[
122 ("config_path", serde_json::json!(path.display().to_string())),
123 (
124 "hosts",
125 serde_json::json!(if path.is_file() {
126 load(&path)?.hosts.len()
127 } else {
128 0
129 }),
130 ),
131 ],
132 )? {
133 return Ok(());
134 }
135 run_reencrypt(&path, json || format == OutputFormat::Json)?;
136 Ok(())
137 }
138 }
139}
140
141fn run_reencrypt(path: &Path, json: bool) -> Result<()> {
147 let (key, _source) = crate::secrets::ensure_key_for_write()?;
148 if key.is_none() {
149 return Err(SshCliError::InvalidArgument(
150 "no primary-key; run `ssh-cli secrets init` or pass --allow-plaintext-secrets"
151 .to_string(),
152 )
153 .into());
154 }
155 if let Some(mut k) = key {
156 use zeroize::Zeroize;
157 k.zeroize();
158 }
159 let file = load(path)?;
160 let hosts = file.hosts.len();
161 save(path, &file)?;
162 crate::output::emit_success(
163 "secrets-reencrypt",
164 serde_json::json!({ "hosts": hosts }),
165 &crate::i18n::t(crate::i18n::Message::ReencryptCompleted { hosts }),
166 json,
167 )?;
168 Ok(())
169}
170
171#[derive(Debug, Clone)]
176pub(crate) struct AutoKeyMeta {
177 pub key_file: String,
179 pub key_source: &'static str,
181}
182
183#[must_use]
185pub(crate) fn take_auto_key_meta() -> Option<AutoKeyMeta> {
186 if crate::secrets::take_auto_key_created() {
187 let path = crate::secrets::secrets_key_path().unwrap_or_default();
188 Some(AutoKeyMeta {
189 key_file: path.display().to_string(),
190 key_source: "xdg_file",
191 })
192 } else {
193 None
194 }
195}