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
11#[allow(clippy::too_many_arguments)]
12pub async fn cmd_key_create(
13 client: &VtaClient,
14 key_type: &str,
15 derivation_path: Option<String>,
16 mnemonic: Option<String>,
17 label: Option<String>,
18 context_id: Option<String>,
19 internal: bool,
20 yes: bool,
21) -> Result<(), Box<dyn std::error::Error>> {
22 let key_type = match key_type {
23 "ed25519" => KeyType::Ed25519,
24 "x25519" => KeyType::X25519,
25 "p256" => KeyType::P256,
26 other => {
27 return Err(
28 format!("unknown key type '{other}', expected ed25519, x25519, or p256").into(),
29 );
30 }
31 };
32 if internal {
38 eprintln!();
39 eprintln!(" ⚠ You are about to create a NON-RECOVERABLE internal key.");
40 eprintln!();
41 eprintln!(" • Its material is generated from the system CSPRNG and is NOT");
42 eprintln!(" derived from your BIP-39 seed. Your 24-word mnemonic will NOT");
43 eprintln!(" recover it.");
44 eprintln!(" • It is excluded from `pnm backup export`. A restored VTA will");
45 eprintln!(" NOT have it.");
46 eprintln!(" • It can never be exported — not by you, not by an admin, not");
47 eprintln!(" under internal authority. The VTA will only sign with it.");
48 eprintln!(" • If this VTA's storage is lost, every signature this key was");
49 eprintln!(" the sole authority for becomes unproducible, permanently.");
50 eprintln!();
51 eprintln!(" It CANNOT be used to sign did:webvh log entries, precisely");
52 eprintln!(" because losing it would freeze that DID forever. It CAN be a");
53 eprintln!(" signing verificationMethod inside a DID document.");
54 eprintln!();
55
56 if !yes {
57 eprint!(" Type 'i understand this key cannot be recovered' to continue: ");
58 use std::io::{BufRead, Write};
59 std::io::stderr().flush().ok();
60 let mut line = String::new();
61 std::io::stdin().lock().read_line(&mut line)?;
62 if !line
63 .trim()
64 .eq_ignore_ascii_case("i understand this key cannot be recovered")
65 {
66 return Err("aborted: confirmation phrase not matched".into());
67 }
68 }
69 }
70
71 let mut req = CreateKeyRequest::new(key_type);
72 if internal {
73 req.internal = Some(true);
74 }
75 if let Some(p) = derivation_path {
76 req = req.derivation_path(p);
77 }
78 if let Some(m) = mnemonic {
79 req = req.mnemonic(m);
80 }
81 if let Some(l) = label {
82 req = req.label(l);
83 }
84 if let Some(c) = context_id {
85 req = req.context(c);
86 }
87 let resp = client.create_key(req).await?;
88 println!("Key created:");
89 println!(" Key ID: {}", resp.key_id);
90 println!(" Key Type: {}", resp.key_type);
91 println!(" Derivation Path: {}", resp.derivation_path);
92 println!(" Public Key: {}", resp.public_key);
93 println!(" Status: {}", resp.status);
94 if resp.origin == vta_sdk::keys::KeyOrigin::Internal {
95 println!();
96 println!(" ⚠ This is an internal key. It cannot be exported, is excluded from");
97 println!(" backups, and cannot be recovered from your mnemonic. If this VTA's");
98 println!(" storage is lost, this key is gone.");
99 }
100 if let Some(label) = &resp.label {
101 println!(" Label: {label}");
102 }
103 println!(
104 " Created At: {}",
105 crate::duration::format_local_datetime(resp.created_at)
106 );
107 Ok(())
108}
109
110pub async fn cmd_key_import(
111 client: &VtaClient,
112 key_type: &str,
113 private_key: Option<String>,
114 private_key_file: Option<std::path::PathBuf>,
115 label: Option<String>,
116 context_id: Option<String>,
117) -> Result<(), Box<dyn std::error::Error>> {
118 let key_type = match key_type {
119 "ed25519" => KeyType::Ed25519,
120 "x25519" => KeyType::X25519,
121 "p256" => KeyType::P256,
122 other => {
123 return Err(
124 format!("unknown key type '{other}', expected ed25519, x25519, or p256").into(),
125 );
126 }
127 };
128
129 let private_key_multibase = if let Some(key_str) = private_key {
131 key_str
132 } else if let Some(path) = private_key_file {
133 let bytes = std::fs::read(&path)
134 .map_err(|e| format!("failed to read key file '{}': {e}", path.display()))?;
135 match String::from_utf8(bytes.clone()) {
137 Ok(s) if s.starts_with('z') || s.starts_with('f') || s.starts_with('u') => {
138 s.trim().to_string()
139 }
140 _ => multibase::encode(multibase::Base::Base58Btc, &bytes),
141 }
142 } else {
143 return Err("either --private-key or --private-key-file is required".into());
144 };
145
146 let wrapping_key = client.get_wrapping_key().await.map_err(|e| {
155 format!(
156 "failed to fetch ephemeral wrapping key from {}/keys/import/wrapping-key: {e} \
157 — the VTA must support sealed-transfer key import (vta-sdk ≥ 0.8); \
158 raw `private_key_multibase` over REST is no longer accepted",
159 client.endpoint_label()
160 )
161 })?;
162 let sealed = seal_private_key(&wrapping_key.x, &key_type, &private_key_multibase).await?;
163
164 let req = ImportKeyRequest {
165 key_type,
166 private_key_sealed: Some(sealed),
167 private_key_jwe: None,
168 private_key_multibase: None,
169 label,
170 context_id,
171 };
172 let resp = client.import_key(req).await?;
173
174 println!("Key imported successfully:");
175 println!(" Key ID: {}", resp.key_id);
176 println!(" Key Type: {}", resp.key_type);
177 println!(" Public Key: {}", resp.public_key);
178 println!(" Status: {}", resp.status);
179 println!(" Origin: imported");
180 if let Some(label) = &resp.label {
181 println!(" Label: {label}");
182 }
183 println!(
184 " Created At: {}",
185 crate::duration::format_local_datetime(resp.created_at)
186 );
187 eprintln!();
188 eprintln!(
189 "\x1b[1;33mWarning: securely delete the source key material \u{2014} the VTA now holds this secret.\x1b[0m"
190 );
191
192 Ok(())
193}
194
195async fn seal_private_key(
199 vta_pub_b64: &str,
200 key_type: &KeyType,
201 private_key_multibase: &str,
202) -> Result<String, Box<dyn std::error::Error>> {
203 use base64::Engine;
204 use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64URL;
205 use vta_sdk::sealed_transfer::{
206 AssertionProof, InMemoryNonceStore, ProducerAssertion, RawPrivateKey, SealedPayloadV1,
207 armor, generate_ed25519_keypair, seal_payload,
208 };
209
210 let vta_pub_bytes: [u8; 32] = B64URL
213 .decode(vta_pub_b64)?
214 .try_into()
215 .map_err(|_| "wrapping public key must be 32 bytes")?;
216
217 let (_, key_bytes) = multibase::decode(private_key_multibase)?;
218
219 let payload = SealedPayloadV1::RawPrivateKey(RawPrivateKey {
220 key_type: key_type.to_string(),
221 key_bytes_b64: B64URL.encode(&key_bytes),
222 });
223
224 let (_seed, producer_ed_pub) = generate_ed25519_keypair();
229 let producer = ProducerAssertion {
230 producer_did: affinidi_crypto::did_key::ed25519_pub_to_did_key(&producer_ed_pub),
231 proof: AssertionProof::PinnedOnly,
232 };
233
234 let bundle_id: [u8; 16] = rand::random();
235
236 let nonce_store = InMemoryNonceStore::new();
237 let bundle = seal_payload(&vta_pub_bytes, bundle_id, producer, &payload, &nonce_store).await?;
238 Ok(armor::encode(&bundle))
239}
240
241pub async fn cmd_key_get(
242 client: &VtaClient,
243 key_id: &str,
244 secret: bool,
245) -> Result<(), Box<dyn std::error::Error>> {
246 if secret {
247 let resp = client.get_key_secret(key_id).await?;
248 println!("Key ID: {}", resp.key_id);
249 println!("Key Type: {}", resp.key_type);
250 println!("Public Key Multibase: {}", resp.public_key_multibase);
251 println!("Secret Key Multibase: {}", resp.private_key_multibase);
252 } else {
253 let resp = client.get_key(key_id).await?;
254 println!("Key ID: {}", resp.key_id);
255 println!("Key Type: {}", resp.key_type);
256 println!("Derivation Path: {}", resp.derivation_path);
257 println!("Public Key: {}", resp.public_key);
258 println!("Status: {}", resp.status);
259 if let Some(label) = &resp.label {
260 println!("Label: {label}");
261 }
262 println!(
263 "Created At: {}",
264 crate::duration::format_local_datetime(resp.created_at)
265 );
266 println!(
267 "Updated At: {}",
268 crate::duration::format_local_datetime(resp.updated_at)
269 );
270 }
271 Ok(())
272}
273
274pub async fn cmd_key_revoke(
275 client: &VtaClient,
276 key_id: &str,
277) -> Result<(), Box<dyn std::error::Error>> {
278 let resp = client.invalidate_key(key_id).await?;
279 println!("Key revoked:");
280 println!(" Key ID: {}", resp.key_id);
281 println!(" Status: {}", resp.status);
282 println!(
283 " Updated At: {}",
284 crate::duration::format_local_datetime(resp.updated_at)
285 );
286 Ok(())
287}
288
289pub async fn cmd_key_rename(
290 client: &VtaClient,
291 key_id: &str,
292 new_key_id: &str,
293) -> Result<(), Box<dyn std::error::Error>> {
294 let resp = client.rename_key(key_id, new_key_id).await?;
295 println!("Key renamed:");
296 println!(" Key ID: {}", resp.key_id);
297 println!(
298 " Updated At: {}",
299 crate::duration::format_local_datetime(resp.updated_at)
300 );
301 Ok(())
302}
303
304pub async fn cmd_key_list(
305 client: &VtaClient,
306 offset: u64,
307 limit: u64,
308 status: Option<String>,
309 context_id: Option<String>,
310) -> Result<(), Box<dyn std::error::Error>> {
311 let resp = client
312 .list_keys(offset, limit, status.as_deref(), context_id.as_deref())
313 .await?;
314
315 if crate::render::is_json_output() {
316 crate::render::print_json(&resp)?;
317 return Ok(());
318 }
319
320 if resp.keys.is_empty() {
321 println!("No keys found.");
322 return Ok(());
323 }
324
325 let end = (offset + resp.keys.len() as u64).min(resp.total);
326
327 if is_full_display() {
328 print_full_list_title(
329 &format!("Keys (showing {}..{} of {}", offset + 1, end, resp.total),
330 resp.keys.len(),
331 );
332 for key in &resp.keys {
333 let label = key.label.as_deref().unwrap_or("—");
334 let created = key
335 .created_at
336 .with_timezone(&chrono::Local)
337 .format("%Y-%m-%d %H:%M:%S %:z")
338 .to_string();
339 let status = key.status.to_string();
340 let key_type = key.key_type.to_string();
341 print_full_entry(&[
342 ("Key ID", &key.key_id),
343 ("Label", label),
344 ("Type", &key_type),
345 ("Status", &status),
346 ("Derivation", &key.derivation_path),
347 ("Created", &created),
348 ]);
349 }
350 return Ok(());
351 }
352
353 let dim = Style::default().fg(Color::DarkGray);
354 let bold = Style::default()
355 .fg(Color::White)
356 .add_modifier(Modifier::BOLD);
357
358 let rows: Vec<Row> = resp
359 .keys
360 .iter()
361 .map(|key| {
362 let label = key.label.clone().unwrap_or_else(|| "\u{2014}".into());
363 let created = key
364 .created_at
365 .with_timezone(&chrono::Local)
366 .format("%Y-%m-%d")
367 .to_string();
368
369 let status_span = match key.status {
370 vta_sdk::keys::KeyStatus::Active => {
371 Span::styled(key.status.to_string(), Style::default().fg(Color::Green))
372 }
373 vta_sdk::keys::KeyStatus::Revoked => {
374 Span::styled(key.status.to_string(), Style::default().fg(Color::Red))
375 }
376 };
377
378 let id_line = Line::from(vec![
379 Span::styled("\u{25b8} ", Style::default().fg(Color::Cyan)),
380 Span::styled(key.key_id.clone(), bold),
381 ]);
382
383 let detail_line = Line::from(vec![
384 Span::raw(" "),
385 Span::styled(label, Style::default().fg(Color::Yellow)),
386 Span::styled(" \u{2502} ", dim),
387 Span::raw(key.key_type.to_string()),
388 Span::styled(" \u{2502} ", dim),
389 status_span,
390 Span::styled(" \u{2502} ", dim),
391 Span::styled(key.derivation_path.clone(), dim),
392 Span::styled(" \u{2502} ", dim),
393 Span::styled(created, dim),
394 ]);
395
396 Row::new(vec![Cell::from(Text::from(vec![id_line, detail_line]))])
397 .height(2)
398 .bottom_margin(1)
399 })
400 .collect();
401
402 let title = format!(" Keys ({}\u{2013}{} of {}) ", offset + 1, end, resp.total);
403
404 let table = Table::new(rows, [Constraint::Min(1)])
405 .block(Block::bordered().title(title).border_style(dim));
406
407 let height = (resp.keys.len() as u16 * 3).saturating_sub(1) + 2;
408 print_widget(table, height);
409
410 Ok(())
411}
412
413pub async fn cmd_seeds_list(client: &VtaClient) -> Result<(), Box<dyn std::error::Error>> {
414 let resp = client.list_seeds().await?;
415
416 if resp.seeds.is_empty() {
417 println!("No seed records found.");
418 println!(" (pre-rotation state: using external seed store as generation 0)");
419 println!(" Active seed ID: {}", resp.active_seed_id);
420 return Ok(());
421 }
422
423 println!("{} seed generation(s):\n", resp.seeds.len());
424 for seed in &resp.seeds {
425 println!(" Seed ID: {}", seed.id);
426 println!(" Status: {}", seed.status);
427 println!(
428 " Created: {}",
429 crate::duration::format_local_datetime(seed.created_at)
430 );
431 if let Some(retired_at) = seed.retired_at {
432 println!(
433 " Retired: {}",
434 crate::duration::format_local_datetime(retired_at)
435 );
436 }
437 println!();
438 }
439 println!("Active seed ID: {}", resp.active_seed_id);
440
441 Ok(())
442}
443
444pub async fn cmd_seeds_rotate(
445 client: &VtaClient,
446 mnemonic: Option<String>,
447) -> Result<(), Box<dyn std::error::Error>> {
448 let resp = client.rotate_seed(mnemonic).await?;
449
450 println!("Seed rotated successfully.");
451 println!(" Previous seed ID: {} (retired)", resp.previous_seed_id);
452 println!(" New active seed ID: {}", resp.new_seed_id);
453
454 Ok(())
455}
456
457pub async fn cmd_key_bundle(
458 client: &VtaClient,
459 context: &str,
460 recipient: crate::sealed_producer::SealedRecipient,
461) -> Result<(), Box<dyn std::error::Error>> {
462 let bundle = client.fetch_did_secrets_bundle(context).await?;
463 crate::sealed_producer::emit_did_secrets_bundle(bundle, &recipient, context, None).await
464}
465
466pub async fn cmd_key_secrets(
467 client: &VtaClient,
468 key_ids: Vec<String>,
469 context: Option<String>,
470) -> Result<(), Box<dyn std::error::Error>> {
471 let key_ids = if key_ids.is_empty() {
472 let ctx = context.as_deref().ok_or(
473 "provide key IDs as arguments, or use --context to export all active keys in a context",
474 )?;
475 let resp = client
476 .list_keys(0, 10000, Some("active"), Some(ctx))
477 .await?;
478 resp.keys.into_iter().map(|k| k.key_id).collect()
479 } else {
480 key_ids
481 };
482 if key_ids.is_empty() {
483 println!("No active keys found.");
484 return Ok(());
485 }
486 for (i, key_id) in key_ids.iter().enumerate() {
487 if i > 0 {
488 println!();
489 }
490 let resp = client.get_key_secret(key_id).await?;
491 println!("Key ID: {}", resp.key_id);
492 println!("Key Type: {}", resp.key_type);
493 println!("Public Key Multibase: {}", resp.public_key_multibase);
494 println!("Secret Key Multibase: {}", resp.private_key_multibase);
495 }
496 Ok(())
497}