1use anyhow::Context;
2use core_types::{EventKind, SecurityLevel, SensitiveBytes, TrustProfileName};
3use owo_colors::OwoColorize;
4use zeroize::Zeroize;
5
6use crate::helpers::{format_denial_reason, validate_profile_in_config, validate_secret_key};
7use crate::ipc::{connect, rpc};
8
9pub(crate) async fn cmd_secret_set(profile: &str, key: &str) -> anyhow::Result<()> {
10 validate_secret_key(key)?;
11 validate_profile_in_config(profile)?;
12 let client = connect().await?;
13 let profile = TrustProfileName::try_from(profile).map_err(|e| anyhow::anyhow!("{e}"))?;
14
15 let mut value = if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
16 dialoguer::Password::new()
17 .with_prompt(format!("Value for '{key}'"))
18 .interact()
19 .context("failed to read secret value")?
20 } else {
21 let mut buf = String::new();
22 std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)
23 .context("failed to read secret value from stdin")?;
24 if buf.ends_with('\n') {
26 buf.pop();
27 if buf.ends_with('\r') {
28 buf.pop();
29 }
30 }
31 buf
32 };
33
34 let event = EventKind::SecretSet {
35 profile: profile.clone(),
36 key: key.to_owned(),
37 value: SensitiveBytes::from_slice(value.as_bytes()),
38 };
39 value.zeroize();
40
41 match rpc(&client, event, SecurityLevel::SecretsOnly).await? {
42 EventKind::SecretSetResponse { success: true, .. } => {
43 println!("Secret '{key}' stored in profile '{profile}'.");
44 }
45 EventKind::SecretSetResponse {
46 success: false,
47 denial,
48 } => {
49 if let Some(reason) = denial {
50 anyhow::bail!("{}", format_denial_reason(&reason, key, &profile));
51 }
52 anyhow::bail!("failed to store secret");
53 }
54 other => anyhow::bail!("unexpected response: {other:?}"),
55 }
56
57 Ok(())
58}
59
60pub(crate) async fn cmd_secret_get(profile: &str, key: &str) -> anyhow::Result<()> {
61 validate_secret_key(key)?;
62 validate_profile_in_config(profile)?;
63 let client = connect().await?;
64 let profile = TrustProfileName::try_from(profile).map_err(|e| anyhow::anyhow!("{e}"))?;
65
66 let event = EventKind::SecretGet {
67 profile: profile.clone(),
68 key: key.to_owned(),
69 };
70
71 match rpc(&client, event, SecurityLevel::SecretsOnly).await? {
72 EventKind::SecretGetResponse {
73 key: k,
74 value,
75 denial,
76 } => {
77 if let Some(reason) = denial {
78 anyhow::bail!("{}", format_denial_reason(&reason, &k, &profile));
79 }
80 if value.is_empty() {
81 anyhow::bail!("secret '{k}' not found in profile '{profile}'");
82 }
83 match String::from_utf8(value.as_bytes().to_vec()) {
87 Ok(mut s) => {
88 println!("{s}");
89 s.zeroize();
90 }
91 Err(_) => {
92 let mut hex: String = value
93 .as_bytes()
94 .iter()
95 .map(|b| format!("{b:02x}"))
96 .collect();
97 println!("{hex}");
98 hex.zeroize();
99 }
100 }
101 }
102 other => anyhow::bail!("unexpected response: {other:?}"),
103 }
104
105 Ok(())
106}
107
108pub(crate) async fn cmd_secret_delete(
109 profile: &str,
110 key: &str,
111 skip_confirm: bool,
112) -> anyhow::Result<()> {
113 validate_secret_key(key)?;
114 validate_profile_in_config(profile)?;
115 let client = connect().await?;
116 let profile = TrustProfileName::try_from(profile).map_err(|e| anyhow::anyhow!("{e}"))?;
117
118 if !skip_confirm {
120 let confirmed = if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
121 dialoguer::Confirm::new()
122 .with_prompt(format!("Delete secret '{key}' from profile '{profile}'?"))
123 .default(false)
124 .interact()
125 .context("failed to read confirmation")?
126 } else {
127 eprintln!("Delete secret '{key}' from profile '{profile}'? [y/N]");
129 let mut buf = String::new();
130 std::io::BufRead::read_line(&mut std::io::BufReader::new(std::io::stdin()), &mut buf)
131 .context("failed to read confirmation from stdin")?;
132 let answer = buf.trim().to_lowercase();
133 answer == "y" || answer == "yes"
134 };
135
136 if !confirmed {
137 println!("Cancelled.");
138 return Ok(());
139 }
140 }
141
142 let event = EventKind::SecretDelete {
143 profile: profile.clone(),
144 key: key.to_owned(),
145 };
146
147 match rpc(&client, event, SecurityLevel::SecretsOnly).await? {
148 EventKind::SecretDeleteResponse { success: true, .. } => {
149 println!("Secret '{key}' deleted from profile '{profile}'.");
150 }
151 EventKind::SecretDeleteResponse {
152 success: false,
153 denial,
154 } => {
155 if let Some(reason) = denial {
156 anyhow::bail!("{}", format_denial_reason(&reason, key, &profile));
157 }
158 anyhow::bail!("failed to delete secret");
159 }
160 other => anyhow::bail!("unexpected response: {other:?}"),
161 }
162
163 Ok(())
164}
165
166pub(crate) async fn cmd_secret_list(profile: &str) -> anyhow::Result<()> {
167 validate_profile_in_config(profile)?;
168 let client = connect().await?;
169 let profile = TrustProfileName::try_from(profile).map_err(|e| anyhow::anyhow!("{e}"))?;
170
171 let event = EventKind::SecretList {
172 profile: profile.clone(),
173 };
174
175 match rpc(&client, event, SecurityLevel::SecretsOnly).await? {
176 EventKind::SecretListResponse { keys, denial } => {
177 if let Some(reason) = denial {
178 anyhow::bail!("{}", format_denial_reason(&reason, "", &profile));
179 }
180 if keys.is_empty() {
181 println!("{}", "No secrets in this profile.".dimmed());
182 } else {
183 println!("Secrets in profile '{}':", profile.as_ref().bold());
184 for k in &keys {
185 println!(" - {k}");
186 }
187 }
188 }
189 other => anyhow::bail!("unexpected response: {other:?}"),
190 }
191
192 Ok(())
193}