1use anyhow::{Context, Result};
2use colored::*;
3use k256::ecdsa::{SigningKey, VerifyingKey};
4use k256::SecretKey;
5use multibase::Base;
6use rand::rngs::OsRng;
7use serde_json::json;
8use std::path::PathBuf;
9use tokio::fs;
10
11/// Generate a new K256 private key
12pub fn generate_private_key() -> SigningKey {
13 SigningKey::random(&mut OsRng)
14}
15
16/// Load a private key from a file
17pub async fn load_private_key(path: &PathBuf) -> Result<SigningKey> {
18 let key_bytes = fs::read(path)
19 .await
20 .with_context(|| format!("Failed to read private key from {:?}", path))?;
21
22 if key_bytes.len() != 32 {
23 anyhow::bail!(
24 "Invalid private key length. Expected 32 bytes, got {}",
25 key_bytes.len()
26 );
27 }
28
29 let secret_key = SecretKey::from_slice(&key_bytes).context("Failed to parse private key")?;
30
31 Ok(SigningKey::from(secret_key))
32}
33
34/// Save a private key to a file
35pub async fn save_private_key(key: &SigningKey, path: &PathBuf) -> Result<()> {
36 let key_bytes = key.as_nonzero_scalar().to_bytes();
37
38 // Create parent directory if it doesn't exist
39 if let Some(parent) = path.parent() {
40 fs::create_dir_all(parent)
41 .await
42 .with_context(|| format!("Failed to create key directory: {:?}", parent))?;
43 }
44
45 fs::write(path, key_bytes)
46 .await
47 .with_context(|| format!("Failed to write private key to {:?}", path))?;
48
49 // Set restrictive permissions on Unix systems
50 #[cfg(unix)]
51 {
52 use std::os::unix::fs::PermissionsExt;
53 let mut perms = fs::metadata(path).await?.permissions();
54 perms.set_mode(0o600); // rw-------
55 fs::set_permissions(path, perms).await?;
56 }
57
58 Ok(())
59}
60
61/// Convert a public key to AT Protocol compatible multibase format
62pub fn public_key_to_multibase(public_key: &VerifyingKey) -> Result<String> {
63 // Get the compressed public key bytes (33 bytes)
64 let public_key_bytes = public_key.to_encoded_point(true).as_bytes().to_vec();
65
66 // Encode as multibase with base58btc (z prefix)
67 let multibase_string = multibase::encode(Base::Base58Btc, &public_key_bytes);
68
69 Ok(multibase_string)
70}
71
72/// Generate a new key pair and save to files
73pub async fn generate_key(
74 name: String,
75 keys_dir: PathBuf,
76 force: bool,
77 format: String,
78) -> Result<()> {
79 let private_key_path = keys_dir.join(format!("{}.key", name));
80 let public_key_path = keys_dir.join(format!("{}.pub", name));
81
82 // Check if files already exist
83 if !force && (private_key_path.exists() || public_key_path.exists()) {
84 anyhow::bail!(
85 "Key files already exist for '{}'. Use --force to overwrite.\n Private: {:?}\n Public: {:?}",
86 name,
87 private_key_path,
88 public_key_path
89 );
90 }
91
92 println!(
93 "{} Generating K256 key pair for '{}'...",
94 "🔐".blue(),
95 name.bold()
96 );
97
98 // Generate new private key
99 let private_key = generate_private_key();
100 let public_key = private_key.verifying_key();
101
102 // Save private key
103 save_private_key(&private_key, &private_key_path)
104 .await
105 .with_context(|| format!("Failed to save private key to {:?}", private_key_path))?;
106
107 // Generate public key multibase
108 let public_key_multibase =
109 public_key_to_multibase(public_key).context("Failed to generate public key multibase")?;
110
111 // Output based on format
112 match format.as_str() {
113 "json" => {
114 let output = json!({
115 "keyName": name,
116 "privateKeyPath": private_key_path,
117 "publicKeyPath": public_key_path,
118 "publicKeyMultibase": public_key_multibase,
119 "publicKeyHex": hex::encode(public_key.to_encoded_point(false).as_bytes()),
120 });
121 println!("{}", serde_json::to_string_pretty(&output)?);
122 }
123 "multibase" => {
124 println!("{}", public_key_multibase);
125 }
126 _ => {
127 // includes "files"
128 // Save public key multibase to file
129 fs::write(&public_key_path, &public_key_multibase)
130 .await
131 .with_context(|| format!("Failed to write public key to {:?}", public_key_path))?;
132
133 println!("{} Key pair generated successfully!", "✅".green());
134 println!(" {} {}", "Name:".bold(), name);
135 println!(" {} {:?}", "Private key:".bold(), private_key_path);
136 println!(" {} {:?}", "Public key:".bold(), public_key_path);
137 println!(
138 " {} {}",
139 "Multibase:".bold(),
140 public_key_multibase.bright_blue()
141 );
142 println!();
143 println!("{} Add this to your DID document:", "💡".yellow());
144 println!(" \"publicKeyMultibase\": \"{}\"", public_key_multibase);
145 }
146 }
147
148 Ok(())
149}
150
151/// Extract public key from private key file
152pub async fn extract_pubkey(private_key_path: PathBuf, format: String) -> Result<()> {
153 println!(
154 "{} Extracting public key from {:?}...",
155 "🔍".blue(),
156 private_key_path
157 );
158
159 let private_key = load_private_key(&private_key_path)
160 .await
161 .with_context(|| format!("Failed to load private key from {:?}", private_key_path))?;
162
163 let public_key = private_key.verifying_key();
164
165 match format.as_str() {
166 "multibase" => {
167 let multibase = public_key_to_multibase(public_key)?;
168 println!("{}", multibase);
169 }
170 "hex" => {
171 let hex = hex::encode(public_key.to_encoded_point(false).as_bytes());
172 println!("{}", hex);
173 }
174 "compressed-hex" => {
175 let hex = hex::encode(public_key.to_encoded_point(true).as_bytes());
176 println!("{}", hex);
177 }
178 "json" => {
179 let multibase = public_key_to_multibase(public_key)?;
180 let hex_uncompressed = hex::encode(public_key.to_encoded_point(false).as_bytes());
181 let hex_compressed = hex::encode(public_key.to_encoded_point(true).as_bytes());
182
183 let output = json!({
184 "publicKeyMultibase": multibase,
185 "publicKeyHex": hex_uncompressed,
186 "publicKeyHexCompressed": hex_compressed,
187 });
188 println!("{}", serde_json::to_string_pretty(&output)?);
189 }
190 _ => {
191 anyhow::bail!(
192 "Invalid format '{}'. Use: multibase, hex, compressed-hex, or json",
193 format
194 );
195 }
196 }
197
198 Ok(())
199}
200
201/// List available keys in directory
202pub async fn list_keys(keys_dir: PathBuf) -> Result<()> {
203 if !keys_dir.exists() {
204 println!("{} No keys directory found at {:?}", "ℹ️".blue(), keys_dir);
205 println!("Run 'teal gen-key' to create your first key.");
206 return Ok(());
207 }
208
209 let mut keys = Vec::new();
210 let mut entries = fs::read_dir(&keys_dir).await?;
211
212 while let Some(entry) = entries.next_entry().await? {
213 let path = entry.path();
214 if let Some(extension) = path.extension() {
215 if extension == "key" {
216 if let Some(stem) = path.file_stem() {
217 if let Some(name) = stem.to_str() {
218 keys.push(name.to_string());
219 }
220 }
221 }
222 }
223 }
224
225 if keys.is_empty() {
226 println!("{} No keys found in {:?}", "ℹ️".blue(), keys_dir);
227 println!("Run 'teal gen-key' to create your first key.");
228 return Ok(());
229 }
230
231 keys.sort();
232
233 println!("{} Available keys in {:?}:", "🔑".blue(), keys_dir);
234 println!();
235
236 let keys_count = keys.len();
237
238 for key_name in keys {
239 let private_path = keys_dir.join(format!("{}.key", key_name));
240 let public_path = keys_dir.join(format!("{}.pub", key_name));
241
242 let mut status_parts = Vec::new();
243
244 if private_path.exists() {
245 status_parts.push("private".green().to_string());
246 }
247
248 if public_path.exists() {
249 status_parts.push("public".cyan().to_string());
250
251 // Try to read and display the multibase
252 if let Ok(multibase) = fs::read_to_string(&public_path).await {
253 let multibase = multibase.trim();
254 println!(
255 " {} {} ({})",
256 "•".bold(),
257 key_name.bold(),
258 status_parts.join(", ")
259 );
260 println!(" {}: {}", "Multibase".dimmed(), multibase.bright_blue());
261 } else {
262 println!(
263 " {} {} ({})",
264 "•".bold(),
265 key_name.bold(),
266 status_parts.join(", ")
267 );
268 }
269 } else {
270 println!(
271 " {} {} ({})",
272 "•".bold(),
273 key_name.bold(),
274 status_parts.join(", ")
275 );
276 }
277
278 // Show file modification times
279 if let Ok(metadata) = fs::metadata(&private_path).await {
280 if let Ok(modified) = metadata.modified() {
281 let datetime = chrono::DateTime::<chrono::Local>::from(modified);
282 println!(
283 " {}: {}",
284 "Created".dimmed(),
285 datetime.format("%Y-%m-%d %H:%M:%S").to_string().dimmed()
286 );
287 }
288 }
289 println!();
290 }
291
292 println!(
293 "{} Total: {} key(s)",
294 "📊".blue(),
295 keys_count.to_string().bold()
296 );
297
298 Ok(())
299}
300
301/// Rotate a key (backup old, generate new)
302pub async fn rotate_key(
303 keys_dir: PathBuf,
304 name: String,
305 backup_dir: Option<PathBuf>,
306) -> Result<()> {
307 let private_key_path = keys_dir.join(format!("{}.key", name));
308
309 if !private_key_path.exists() {
310 anyhow::bail!("Key '{}' does not exist in {:?}", name, keys_dir);
311 }
312
313 println!("{} Rotating key '{}'...", "🔄".blue(), name.bold());
314
315 // Backup existing key
316 let backup_location = backup_dir.unwrap_or_else(|| keys_dir.join("backups"));
317
318 fs::create_dir_all(&backup_location).await?;
319
320 let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
321 let backup_private = backup_location.join(format!("{}_{}.key", name, timestamp));
322 let backup_public = backup_location.join(format!("{}_{}.pub", name, timestamp));
323
324 fs::copy(&private_key_path, &backup_private).await?;
325
326 let public_key_path = keys_dir.join(format!("{}.pub", name));
327 if public_key_path.exists() {
328 fs::copy(&public_key_path, &backup_public).await?;
329 }
330
331 println!("Backed up existing key to: {:?}", backup_private);
332
333 // Generate new key
334 let new_key = generate_private_key();
335 save_private_key(&new_key, &private_key_path).await?;
336
337 // Save new public key multibase
338 let public_key = new_key.verifying_key();
339 let multibase = public_key_to_multibase(public_key)?;
340 fs::write(&public_key_path, &multibase).await?;
341
342 println!("{} Key rotation completed!", "✅".green());
343 println!(" {} {}", "New multibase:".bold(), multibase.bright_blue());
344 println!();
345 println!("{} Update your DID document with:", "💡".yellow());
346 println!(" \"publicKeyMultibase\": \"{}\"", multibase);
347
348 Ok(())
349}