Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto
at main 11 kB view raw
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}