1use ratatui::{
2 layout::Constraint,
3 style::{Color, Modifier, Style},
4 text::{Line, Span, Text},
5 widgets::{Block, Cell, Row, Table},
6};
7use vta_sdk::prelude::*;
8
9use crate::render::{is_full_display, print_full_entry, print_full_list_title, print_widget};
10
11pub async fn cmd_key_create(
12 client: &VtaClient,
13 key_type: &str,
14 derivation_path: Option<String>,
15 mnemonic: Option<String>,
16 label: Option<String>,
17 context_id: Option<String>,
18) -> Result<(), Box<dyn std::error::Error>> {
19 let key_type = match key_type {
20 "ed25519" => KeyType::Ed25519,
21 "x25519" => KeyType::X25519,
22 "p256" => KeyType::P256,
23 other => {
24 return Err(
25 format!("unknown key type '{other}', expected ed25519, x25519, or p256").into(),
26 );
27 }
28 };
29 let mut req = CreateKeyRequest::new(key_type);
30 if let Some(p) = derivation_path {
31 req = req.derivation_path(p);
32 }
33 if let Some(m) = mnemonic {
34 req = req.mnemonic(m);
35 }
36 if let Some(l) = label {
37 req = req.label(l);
38 }
39 if let Some(c) = context_id {
40 req = req.context(c);
41 }
42 let resp = client.create_key(req).await?;
43 println!("Key created:");
44 println!(" Key ID: {}", resp.key_id);
45 println!(" Key Type: {}", resp.key_type);
46 println!(" Derivation Path: {}", resp.derivation_path);
47 println!(" Public Key: {}", resp.public_key);
48 println!(" Status: {}", resp.status);
49 if let Some(label) = &resp.label {
50 println!(" Label: {label}");
51 }
52 println!(
53 " Created At: {}",
54 crate::duration::format_local_datetime(resp.created_at)
55 );
56 Ok(())
57}
58
59pub async fn cmd_key_import(
60 client: &VtaClient,
61 key_type: &str,
62 private_key: Option<String>,
63 private_key_file: Option<std::path::PathBuf>,
64 label: Option<String>,
65 context_id: Option<String>,
66) -> Result<(), Box<dyn std::error::Error>> {
67 let key_type = match key_type {
68 "ed25519" => KeyType::Ed25519,
69 "x25519" => KeyType::X25519,
70 "p256" => KeyType::P256,
71 other => {
72 return Err(
73 format!("unknown key type '{other}', expected ed25519, x25519, or p256").into(),
74 );
75 }
76 };
77
78 let private_key_multibase = if let Some(key_str) = private_key {
80 key_str
81 } else if let Some(path) = private_key_file {
82 let bytes = std::fs::read(&path)
83 .map_err(|e| format!("failed to read key file '{}': {e}", path.display()))?;
84 match String::from_utf8(bytes.clone()) {
86 Ok(s) if s.starts_with('z') || s.starts_with('f') || s.starts_with('u') => {
87 s.trim().to_string()
88 }
89 _ => multibase::encode(multibase::Base::Base58Btc, &bytes),
90 }
91 } else {
92 return Err("either --private-key or --private-key-file is required".into());
93 };
94
95 let wrapping_key = client.get_wrapping_key().await.map_err(|e| {
104 format!(
105 "failed to fetch ephemeral wrapping key from {}/keys/import/wrapping-key: {e} \
106 — the VTA must support sealed-transfer key import (vta-sdk ≥ 0.8); \
107 raw `private_key_multibase` over REST is no longer accepted",
108 client.endpoint_label()
109 )
110 })?;
111 let sealed = seal_private_key(&wrapping_key.x, &key_type, &private_key_multibase).await?;
112
113 let req = ImportKeyRequest {
114 key_type,
115 private_key_sealed: Some(sealed),
116 private_key_jwe: None,
117 private_key_multibase: None,
118 label,
119 context_id,
120 };
121 let resp = client.import_key(req).await?;
122
123 println!("Key imported successfully:");
124 println!(" Key ID: {}", resp.key_id);
125 println!(" Key Type: {}", resp.key_type);
126 println!(" Public Key: {}", resp.public_key);
127 println!(" Status: {}", resp.status);
128 println!(" Origin: imported");
129 if let Some(label) = &resp.label {
130 println!(" Label: {label}");
131 }
132 println!(
133 " Created At: {}",
134 crate::duration::format_local_datetime(resp.created_at)
135 );
136 eprintln!();
137 eprintln!(
138 "\x1b[1;33mWarning: securely delete the source key material \u{2014} the VTA now holds this secret.\x1b[0m"
139 );
140
141 Ok(())
142}
143
144async fn seal_private_key(
148 vta_pub_b64: &str,
149 key_type: &KeyType,
150 private_key_multibase: &str,
151) -> Result<String, Box<dyn std::error::Error>> {
152 use base64::Engine;
153 use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64URL;
154 use vta_sdk::sealed_transfer::{
155 AssertionProof, InMemoryNonceStore, ProducerAssertion, RawPrivateKey, SealedPayloadV1,
156 armor, generate_ed25519_keypair, seal_payload,
157 };
158
159 let vta_pub_bytes: [u8; 32] = B64URL
162 .decode(vta_pub_b64)?
163 .try_into()
164 .map_err(|_| "wrapping public key must be 32 bytes")?;
165
166 let (_, key_bytes) = multibase::decode(private_key_multibase)?;
167
168 let payload = SealedPayloadV1::RawPrivateKey(RawPrivateKey {
169 key_type: key_type.to_string(),
170 key_bytes_b64: B64URL.encode(&key_bytes),
171 });
172
173 let (_seed, producer_ed_pub) = generate_ed25519_keypair();
178 let producer = ProducerAssertion {
179 producer_did: affinidi_crypto::did_key::ed25519_pub_to_did_key(&producer_ed_pub),
180 proof: AssertionProof::PinnedOnly,
181 };
182
183 let bundle_id: [u8; 16] = rand::random();
184
185 let nonce_store = InMemoryNonceStore::new();
186 let bundle = seal_payload(&vta_pub_bytes, bundle_id, producer, &payload, &nonce_store).await?;
187 Ok(armor::encode(&bundle))
188}
189
190pub async fn cmd_key_get(
191 client: &VtaClient,
192 key_id: &str,
193 secret: bool,
194) -> Result<(), Box<dyn std::error::Error>> {
195 if secret {
196 let resp = client.get_key_secret(key_id).await?;
197 println!("Key ID: {}", resp.key_id);
198 println!("Key Type: {}", resp.key_type);
199 println!("Public Key Multibase: {}", resp.public_key_multibase);
200 println!("Secret Key Multibase: {}", resp.private_key_multibase);
201 } else {
202 let resp = client.get_key(key_id).await?;
203 println!("Key ID: {}", resp.key_id);
204 println!("Key Type: {}", resp.key_type);
205 println!("Derivation Path: {}", resp.derivation_path);
206 println!("Public Key: {}", resp.public_key);
207 println!("Status: {}", resp.status);
208 if let Some(label) = &resp.label {
209 println!("Label: {label}");
210 }
211 println!(
212 "Created At: {}",
213 crate::duration::format_local_datetime(resp.created_at)
214 );
215 println!(
216 "Updated At: {}",
217 crate::duration::format_local_datetime(resp.updated_at)
218 );
219 }
220 Ok(())
221}
222
223pub async fn cmd_key_revoke(
224 client: &VtaClient,
225 key_id: &str,
226) -> Result<(), Box<dyn std::error::Error>> {
227 let resp = client.invalidate_key(key_id).await?;
228 println!("Key revoked:");
229 println!(" Key ID: {}", resp.key_id);
230 println!(" Status: {}", resp.status);
231 println!(
232 " Updated At: {}",
233 crate::duration::format_local_datetime(resp.updated_at)
234 );
235 Ok(())
236}
237
238pub async fn cmd_key_rename(
239 client: &VtaClient,
240 key_id: &str,
241 new_key_id: &str,
242) -> Result<(), Box<dyn std::error::Error>> {
243 let resp = client.rename_key(key_id, new_key_id).await?;
244 println!("Key renamed:");
245 println!(" Key ID: {}", resp.key_id);
246 println!(
247 " Updated At: {}",
248 crate::duration::format_local_datetime(resp.updated_at)
249 );
250 Ok(())
251}
252
253pub async fn cmd_key_list(
254 client: &VtaClient,
255 offset: u64,
256 limit: u64,
257 status: Option<String>,
258 context_id: Option<String>,
259) -> Result<(), Box<dyn std::error::Error>> {
260 let resp = client
261 .list_keys(offset, limit, status.as_deref(), context_id.as_deref())
262 .await?;
263
264 if crate::render::is_json_output() {
265 crate::render::print_json(&resp)?;
266 return Ok(());
267 }
268
269 if resp.keys.is_empty() {
270 println!("No keys found.");
271 return Ok(());
272 }
273
274 let end = (offset + resp.keys.len() as u64).min(resp.total);
275
276 if is_full_display() {
277 print_full_list_title(
278 &format!("Keys (showing {}..{} of {}", offset + 1, end, resp.total),
279 resp.keys.len(),
280 );
281 for key in &resp.keys {
282 let label = key.label.as_deref().unwrap_or("—");
283 let created = key
284 .created_at
285 .with_timezone(&chrono::Local)
286 .format("%Y-%m-%d %H:%M:%S %:z")
287 .to_string();
288 let status = key.status.to_string();
289 let key_type = key.key_type.to_string();
290 print_full_entry(&[
291 ("Key ID", &key.key_id),
292 ("Label", label),
293 ("Type", &key_type),
294 ("Status", &status),
295 ("Derivation", &key.derivation_path),
296 ("Created", &created),
297 ]);
298 }
299 return Ok(());
300 }
301
302 let dim = Style::default().fg(Color::DarkGray);
303 let bold = Style::default()
304 .fg(Color::White)
305 .add_modifier(Modifier::BOLD);
306
307 let rows: Vec<Row> = resp
308 .keys
309 .iter()
310 .map(|key| {
311 let label = key.label.clone().unwrap_or_else(|| "\u{2014}".into());
312 let created = key
313 .created_at
314 .with_timezone(&chrono::Local)
315 .format("%Y-%m-%d")
316 .to_string();
317
318 let status_span = match key.status {
319 vta_sdk::keys::KeyStatus::Active => {
320 Span::styled(key.status.to_string(), Style::default().fg(Color::Green))
321 }
322 vta_sdk::keys::KeyStatus::Revoked => {
323 Span::styled(key.status.to_string(), Style::default().fg(Color::Red))
324 }
325 };
326
327 let id_line = Line::from(vec![
328 Span::styled("\u{25b8} ", Style::default().fg(Color::Cyan)),
329 Span::styled(key.key_id.clone(), bold),
330 ]);
331
332 let detail_line = Line::from(vec![
333 Span::raw(" "),
334 Span::styled(label, Style::default().fg(Color::Yellow)),
335 Span::styled(" \u{2502} ", dim),
336 Span::raw(key.key_type.to_string()),
337 Span::styled(" \u{2502} ", dim),
338 status_span,
339 Span::styled(" \u{2502} ", dim),
340 Span::styled(key.derivation_path.clone(), dim),
341 Span::styled(" \u{2502} ", dim),
342 Span::styled(created, dim),
343 ]);
344
345 Row::new(vec![Cell::from(Text::from(vec![id_line, detail_line]))])
346 .height(2)
347 .bottom_margin(1)
348 })
349 .collect();
350
351 let title = format!(" Keys ({}\u{2013}{} of {}) ", offset + 1, end, resp.total);
352
353 let table = Table::new(rows, [Constraint::Min(1)])
354 .block(Block::bordered().title(title).border_style(dim));
355
356 let height = (resp.keys.len() as u16 * 3).saturating_sub(1) + 2;
357 print_widget(table, height);
358
359 Ok(())
360}
361
362pub async fn cmd_seeds_list(client: &VtaClient) -> Result<(), Box<dyn std::error::Error>> {
363 let resp = client.list_seeds().await?;
364
365 if resp.seeds.is_empty() {
366 println!("No seed records found.");
367 println!(" (pre-rotation state: using external seed store as generation 0)");
368 println!(" Active seed ID: {}", resp.active_seed_id);
369 return Ok(());
370 }
371
372 println!("{} seed generation(s):\n", resp.seeds.len());
373 for seed in &resp.seeds {
374 println!(" Seed ID: {}", seed.id);
375 println!(" Status: {}", seed.status);
376 println!(
377 " Created: {}",
378 crate::duration::format_local_datetime(seed.created_at)
379 );
380 if let Some(retired_at) = seed.retired_at {
381 println!(
382 " Retired: {}",
383 crate::duration::format_local_datetime(retired_at)
384 );
385 }
386 println!();
387 }
388 println!("Active seed ID: {}", resp.active_seed_id);
389
390 Ok(())
391}
392
393pub async fn cmd_seeds_rotate(
394 client: &VtaClient,
395 mnemonic: Option<String>,
396) -> Result<(), Box<dyn std::error::Error>> {
397 let resp = client.rotate_seed(mnemonic).await?;
398
399 println!("Seed rotated successfully.");
400 println!(" Previous seed ID: {} (retired)", resp.previous_seed_id);
401 println!(" New active seed ID: {}", resp.new_seed_id);
402
403 Ok(())
404}
405
406pub async fn cmd_key_bundle(
407 client: &VtaClient,
408 context: &str,
409 recipient: crate::sealed_producer::SealedRecipient,
410) -> Result<(), Box<dyn std::error::Error>> {
411 let bundle = client.fetch_did_secrets_bundle(context).await?;
412 crate::sealed_producer::emit_did_secrets_bundle(bundle, &recipient, context, None).await
413}
414
415pub async fn cmd_key_secrets(
416 client: &VtaClient,
417 key_ids: Vec<String>,
418 context: Option<String>,
419) -> Result<(), Box<dyn std::error::Error>> {
420 let key_ids = if key_ids.is_empty() {
421 let ctx = context.as_deref().ok_or(
422 "provide key IDs as arguments, or use --context to export all active keys in a context",
423 )?;
424 let resp = client
425 .list_keys(0, 10000, Some("active"), Some(ctx))
426 .await?;
427 resp.keys.into_iter().map(|k| k.key_id).collect()
428 } else {
429 key_ids
430 };
431 if key_ids.is_empty() {
432 println!("No active keys found.");
433 return Ok(());
434 }
435 for (i, key_id) in key_ids.iter().enumerate() {
436 if i > 0 {
437 println!();
438 }
439 let resp = client.get_key_secret(key_id).await?;
440 println!("Key ID: {}", resp.key_id);
441 println!("Key Type: {}", resp.key_type);
442 println!("Public Key Multibase: {}", resp.public_key_multibase);
443 println!("Secret Key Multibase: {}", resp.private_key_multibase);
444 }
445 Ok(())
446}