1use crate::did::KeyType;
23use crate::error::{Error, Result};
24use serde::Deserialize;
25use std::path::{Path, PathBuf};
26use std::process::Command;
27
28#[derive(Debug, Deserialize)]
30pub struct SecretHelperOutput {
31 pub private_key: String,
33 pub key_type: String,
35 #[serde(default = "default_encoding")]
37 pub encoding: String,
38}
39
40fn default_encoding() -> String {
41 "hex".to_string()
42}
43
44impl SecretHelperOutput {
45 pub fn decode(&self) -> Result<(Vec<u8>, KeyType)> {
47 let bytes = match self.encoding.as_str() {
48 "hex" => hex::decode(&self.private_key).map_err(|e| {
49 Error::Cryptography(format!("Failed to decode hex private key: {}", e))
50 })?,
51 "base64" => {
52 use base64::Engine;
53 base64::engine::general_purpose::STANDARD
54 .decode(&self.private_key)
55 .map_err(|e| {
56 Error::Cryptography(format!("Failed to decode base64 private key: {}", e))
57 })?
58 }
59 other => {
60 return Err(Error::Validation(format!(
61 "Unsupported encoding: {}",
62 other
63 )))
64 }
65 };
66
67 let key_type = match self.key_type.as_str() {
68 "Ed25519" => {
69 #[cfg(feature = "crypto-ed25519")]
70 {
71 KeyType::Ed25519
72 }
73 #[cfg(not(feature = "crypto-ed25519"))]
74 {
75 return Err(Error::Validation("Ed25519 support not enabled".to_string()));
76 }
77 }
78 "P256" => {
79 #[cfg(feature = "crypto-p256")]
80 {
81 KeyType::P256
82 }
83 #[cfg(not(feature = "crypto-p256"))]
84 {
85 return Err(Error::Validation("P256 support not enabled".to_string()));
86 }
87 }
88 "Secp256k1" => {
89 #[cfg(feature = "crypto-secp256k1")]
90 {
91 KeyType::Secp256k1
92 }
93 #[cfg(not(feature = "crypto-secp256k1"))]
94 {
95 return Err(Error::Validation(
96 "Secp256k1 support not enabled".to_string(),
97 ));
98 }
99 }
100 other => return Err(Error::Validation(format!("Unknown key type: {}", other))),
101 };
102
103 Ok((bytes, key_type))
104 }
105}
106
107#[derive(Debug, Clone)]
109pub struct SecretHelperConfig {
110 pub command: String,
112 pub args: Vec<String>,
114}
115
116impl SecretHelperConfig {
117 pub fn from_command_string(s: &str) -> Result<Self> {
122 let parts: Vec<&str> = s.split_whitespace().collect();
123 if parts.is_empty() {
124 return Err(Error::Validation(
125 "Secret helper command string is empty".to_string(),
126 ));
127 }
128
129 Ok(Self {
130 command: parts[0].to_string(),
131 args: parts[1..].iter().map(|s| s.to_string()).collect(),
132 })
133 }
134
135 pub fn get_key(&self, did: &str) -> Result<(Vec<u8>, KeyType)> {
137 let mut cmd = Command::new(&self.command);
138 for arg in &self.args {
139 cmd.arg(arg);
140 }
141 cmd.arg(did);
142
143 cmd.stderr(std::process::Stdio::inherit());
145
146 let output = cmd.output().map_err(|e| {
147 Error::Storage(format!(
148 "Failed to run secret helper '{}': {}",
149 self.command, e
150 ))
151 })?;
152
153 if !output.status.success() {
154 let code = output.status.code().unwrap_or(-1);
155 return Err(Error::Storage(format!(
156 "Secret helper '{}' exited with code {}",
157 self.command, code
158 )));
159 }
160
161 let stdout = String::from_utf8(output.stdout).map_err(|e| {
162 Error::Storage(format!(
163 "Secret helper produced invalid UTF-8 output: {}",
164 e
165 ))
166 })?;
167
168 let helper_output: SecretHelperOutput = serde_json::from_str(&stdout).map_err(|e| {
169 Error::Storage(format!("Failed to parse secret helper JSON output: {}", e))
170 })?;
171
172 helper_output.decode()
173 }
174}
175
176pub fn discover_agent_dids(tap_root: Option<&Path>) -> Result<Vec<String>> {
181 let tap_dir = if let Some(root) = tap_root {
182 root.to_path_buf()
183 } else if let Ok(tap_home) = std::env::var("TAP_HOME") {
184 PathBuf::from(tap_home)
185 } else if let Ok(test_dir) = std::env::var("TAP_TEST_DIR") {
186 PathBuf::from(test_dir).join(crate::storage::DEFAULT_TAP_DIR)
187 } else {
188 dirs::home_dir()
189 .ok_or_else(|| Error::Storage("Could not determine home directory".to_string()))?
190 .join(crate::storage::DEFAULT_TAP_DIR)
191 };
192
193 if !tap_dir.exists() {
194 return Ok(Vec::new());
195 }
196
197 let mut dids = Vec::new();
198 for entry in std::fs::read_dir(&tap_dir)
199 .map_err(|e| Error::Storage(format!("Failed to read TAP directory: {}", e)))?
200 {
201 let entry =
202 entry.map_err(|e| Error::Storage(format!("Failed to read directory entry: {}", e)))?;
203 let path = entry.path();
204 if path.is_dir() {
205 let dir_name = match path.file_name().and_then(|n| n.to_str()) {
206 Some(name) => name.to_string(),
207 None => continue,
208 };
209 if dir_name.starts_with("did_") {
211 let did = dir_name.replace('_', ":");
212 dids.push(did);
213 }
214 }
215 }
216
217 dids.sort();
218 Ok(dids)
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::key_manager::KeyManager;
225 use tempfile::TempDir;
226
227 #[cfg(unix)]
231 fn write_test_script(dir: &std::path::Path, name: &str, content: &str) -> std::path::PathBuf {
232 use std::os::unix::fs::PermissionsExt;
233 let tmp_path = dir.join(format!("{}.tmp", name));
234 let final_path = dir.join(name);
235 std::fs::write(&tmp_path, content).unwrap();
236 std::fs::set_permissions(&tmp_path, std::fs::Permissions::from_mode(0o755)).unwrap();
237 std::fs::rename(&tmp_path, &final_path).unwrap();
238 final_path
239 }
240
241 #[test]
242 fn test_from_command_string_simple() {
243 let config = SecretHelperConfig::from_command_string("my-helper").unwrap();
244 assert_eq!(config.command, "my-helper");
245 assert!(config.args.is_empty());
246 }
247
248 #[test]
249 fn test_from_command_string_with_args() {
250 let config = SecretHelperConfig::from_command_string(
251 "vault-helper --vault-addr https://vault.example.com",
252 )
253 .unwrap();
254 assert_eq!(config.command, "vault-helper");
255 assert_eq!(
256 config.args,
257 vec!["--vault-addr", "https://vault.example.com"]
258 );
259 }
260
261 #[test]
262 fn test_from_command_string_empty() {
263 let result = SecretHelperConfig::from_command_string("");
264 assert!(result.is_err());
265 }
266
267 #[test]
268 fn test_secret_helper_output_hex() {
269 let json = r#"{"private_key": "abcdef0123456789", "key_type": "Ed25519"}"#;
270 let output: SecretHelperOutput = serde_json::from_str(json).unwrap();
271 assert_eq!(output.encoding, "hex"); let (bytes, key_type) = output.decode().unwrap();
273 assert_eq!(bytes, hex::decode("abcdef0123456789").unwrap());
274 assert_eq!(key_type, KeyType::Ed25519);
275 }
276
277 #[test]
278 fn test_secret_helper_output_base64() {
279 use base64::Engine;
280 let key_bytes = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
281 let b64 = base64::engine::general_purpose::STANDARD.encode(&key_bytes);
282 let json = format!(
283 r#"{{"private_key": "{}", "key_type": "Ed25519", "encoding": "base64"}}"#,
284 b64
285 );
286 let output: SecretHelperOutput = serde_json::from_str(&json).unwrap();
287 let (bytes, _) = output.decode().unwrap();
288 assert_eq!(bytes, key_bytes);
289 }
290
291 #[test]
292 fn test_secret_helper_output_explicit_hex() {
293 let json = r#"{"private_key": "deadbeef", "key_type": "Ed25519", "encoding": "hex"}"#;
294 let output: SecretHelperOutput = serde_json::from_str(json).unwrap();
295 let (bytes, _) = output.decode().unwrap();
296 assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]);
297 }
298
299 #[test]
300 fn test_secret_helper_output_unknown_key_type() {
301 let json = r#"{"private_key": "abcd", "key_type": "RSA"}"#;
302 let output: SecretHelperOutput = serde_json::from_str(json).unwrap();
303 let result = output.decode();
304 assert!(result.is_err());
305 }
306
307 #[test]
308 fn test_secret_helper_output_unsupported_encoding() {
309 let json = r#"{"private_key": "abcd", "key_type": "Ed25519", "encoding": "raw"}"#;
310 let output: SecretHelperOutput = serde_json::from_str(json).unwrap();
311 let result = output.decode();
312 assert!(result.is_err());
313 }
314
315 #[test]
316 fn test_get_key_with_mock_script() {
317 let temp_dir = TempDir::new().unwrap();
318
319 let km = crate::agent_key_manager::AgentKeyManager::new();
321 let key = km
322 .generate_key(crate::did::DIDGenerationOptions {
323 key_type: KeyType::Ed25519,
324 })
325 .unwrap();
326 let hex_key = hex::encode(&key.private_key);
327
328 let script_path = write_test_script(
329 temp_dir.path(),
330 "helper.sh",
331 &format!(
332 "#!/bin/sh\necho '{{\"private_key\": \"{}\", \"key_type\": \"Ed25519\"}}'",
333 hex_key
334 ),
335 );
336
337 let config = SecretHelperConfig {
338 command: script_path.to_str().unwrap().to_string(),
339 args: vec![],
340 };
341
342 let (bytes, key_type) = config.get_key(&key.did).unwrap();
343 assert_eq!(bytes, key.private_key);
344 assert_eq!(key_type, KeyType::Ed25519);
345 }
346
347 #[tokio::test]
348 async fn test_secret_helper_roundtrip() {
349 let km = crate::agent_key_manager::AgentKeyManager::new();
350 let key = km
351 .generate_key(crate::did::DIDGenerationOptions {
352 key_type: KeyType::Ed25519,
353 })
354 .unwrap();
355 let hex_key = hex::encode(&key.private_key);
356
357 let temp_dir = TempDir::new().unwrap();
358 let script_path = write_test_script(
359 temp_dir.path(),
360 "helper.sh",
361 &format!(
362 "#!/bin/sh\necho '{{\"private_key\": \"{}\", \"key_type\": \"Ed25519\"}}'",
363 hex_key
364 ),
365 );
366
367 let config = SecretHelperConfig {
368 command: script_path.to_str().unwrap().to_string(),
369 args: vec![],
370 };
371
372 let (bytes, key_type) = config.get_key(&key.did).unwrap();
373 let (_agent, new_did) = crate::agent::TapAgent::from_private_key(&bytes, key_type, false)
374 .await
375 .unwrap();
376 assert_eq!(new_did, key.did);
377 }
378
379 #[test]
380 fn test_get_key_command_not_found() {
381 let config = SecretHelperConfig {
382 command: "/nonexistent/helper".to_string(),
383 args: vec![],
384 };
385 let result = config.get_key("did:key:test");
386 assert!(result.is_err());
387 }
388
389 #[test]
390 fn test_get_key_non_zero_exit() {
391 let temp_dir = TempDir::new().unwrap();
392 let script_path = write_test_script(temp_dir.path(), "fail.sh", "#!/bin/sh\nexit 1");
393
394 let config = SecretHelperConfig {
395 command: script_path.to_str().unwrap().to_string(),
396 args: vec![],
397 };
398 let result = config.get_key("did:key:test");
399 assert!(result.is_err());
400 }
401
402 #[test]
403 fn test_get_key_invalid_json() {
404 let temp_dir = TempDir::new().unwrap();
405 let script_path =
406 write_test_script(temp_dir.path(), "bad-json.sh", "#!/bin/sh\necho 'not json'");
407
408 let config = SecretHelperConfig {
409 command: script_path.to_str().unwrap().to_string(),
410 args: vec![],
411 };
412 let result = config.get_key("did:key:test");
413 assert!(result.is_err());
414 }
415
416 #[test]
417 fn test_discover_agent_dids() {
418 let temp_dir = TempDir::new().unwrap();
419 let tap_dir = temp_dir.path();
420
421 std::fs::create_dir(tap_dir.join("did_key_z6Mk1234")).unwrap();
423 std::fs::create_dir(tap_dir.join("did_web_example.com")).unwrap();
424 std::fs::create_dir(tap_dir.join("logs")).unwrap();
426 std::fs::write(tap_dir.join("keys.json"), "{}").unwrap();
428
429 let dids = discover_agent_dids(Some(tap_dir)).unwrap();
430 assert_eq!(dids.len(), 2);
431 assert!(dids.contains(&"did:key:z6Mk1234".to_string()));
432 assert!(dids.contains(&"did:web:example.com".to_string()));
433 }
434
435 #[test]
436 fn test_discover_agent_dids_empty() {
437 let temp_dir = TempDir::new().unwrap();
438 let dids = discover_agent_dids(Some(temp_dir.path())).unwrap();
439 assert!(dids.is_empty());
440 }
441
442 #[test]
443 fn test_discover_agent_dids_nonexistent() {
444 let dids = discover_agent_dids(Some(Path::new("/nonexistent/path"))).unwrap();
445 assert!(dids.is_empty());
446 }
447}