1use anyhow::{Context, Result};
2use clap::Parser;
3use colored::Colorize;
4use std::fs;
5use std::io::{self, Write};
6use std::path::PathBuf;
7use std::process::Command as ProcessCommand;
8
9#[derive(Parser, Debug, Clone)]
11pub struct LearnCommand {
12 #[clap(long, short, value_name = "SECTION")]
14 skip: Option<usize>,
15
16 #[clap(long)]
18 reset: bool,
19
20 #[clap(long)]
22 list: bool,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26enum Section {
27 WhatIsIdentity = 1,
28 CreatingIdentity = 2,
29 SigningCommit = 3,
30 VerifyingSignature = 4,
31 LinkingDevice = 5,
32 RevokingAccess = 6,
33}
34
35impl Section {
36 fn from_number(n: usize) -> Option<Section> {
37 match n {
38 1 => Some(Section::WhatIsIdentity),
39 2 => Some(Section::CreatingIdentity),
40 3 => Some(Section::SigningCommit),
41 4 => Some(Section::VerifyingSignature),
42 5 => Some(Section::LinkingDevice),
43 6 => Some(Section::RevokingAccess),
44 _ => None,
45 }
46 }
47
48 fn title(&self) -> &'static str {
49 match self {
50 Section::WhatIsIdentity => "What is a Cryptographic Identity?",
51 Section::CreatingIdentity => "Creating Your Identity",
52 Section::SigningCommit => "Signing a Commit",
53 Section::VerifyingSignature => "Verifying a Signature",
54 Section::LinkingDevice => "Linking a Second Device",
55 Section::RevokingAccess => "Revoking Access",
56 }
57 }
58
59 fn next(&self) -> Option<Section> {
60 Section::from_number(*self as usize + 1)
61 }
62}
63
64struct Tutorial {
65 sandbox_dir: PathBuf,
66 progress_file: PathBuf,
67}
68
69impl Tutorial {
70 fn new() -> Result<Self> {
71 let home = dirs::home_dir().context("Could not find home directory")?;
72 let sandbox_dir = home.join(".auths-tutorial");
73 let progress_file = sandbox_dir.join(".progress");
74
75 Ok(Self {
76 sandbox_dir,
77 progress_file,
78 })
79 }
80
81 fn setup_sandbox(&self) -> Result<()> {
82 if !self.sandbox_dir.exists() {
83 fs::create_dir_all(&self.sandbox_dir)?;
84 }
85 Ok(())
86 }
87
88 fn cleanup_sandbox(&self) -> Result<()> {
89 if self.sandbox_dir.exists() {
90 fs::remove_dir_all(&self.sandbox_dir)?;
91 }
92 Ok(())
93 }
94
95 fn load_progress(&self) -> usize {
96 if let Ok(content) = fs::read_to_string(&self.progress_file) {
97 content.trim().parse().unwrap_or(1)
98 } else {
99 1
100 }
101 }
102
103 fn save_progress(&self, section: usize) -> Result<()> {
104 fs::write(&self.progress_file, section.to_string())?;
105 Ok(())
106 }
107
108 fn reset_progress(&self) -> Result<()> {
109 if self.progress_file.exists() {
110 fs::remove_file(&self.progress_file)?;
111 }
112 self.cleanup_sandbox()?;
113 Ok(())
114 }
115}
116
117pub fn handle_learn(cmd: LearnCommand) -> Result<()> {
118 let tutorial = Tutorial::new()?;
119
120 if cmd.list {
121 list_sections();
122 return Ok(());
123 }
124
125 if cmd.reset {
126 tutorial.reset_progress()?;
127 println!("{}", "✓ Tutorial progress reset.".green());
128 return Ok(());
129 }
130
131 let start_section = if let Some(skip) = cmd.skip {
132 if !(1..=6).contains(&skip) {
133 anyhow::bail!("Section must be between 1 and 6");
134 }
135 skip
136 } else {
137 tutorial.load_progress()
138 };
139
140 println!();
141 println!(
142 "{}",
143 "╔════════════════════════════════════════════════════════════╗".cyan()
144 );
145 println!(
146 "{}",
147 "║ Welcome to Auths Tutorial ║".cyan()
148 );
149 println!(
150 "{}",
151 "╚════════════════════════════════════════════════════════════╝".cyan()
152 );
153 println!();
154
155 if start_section > 1 {
156 println!(" {} Resuming from section {}", "→".yellow(), start_section);
157 println!();
158 }
159
160 tutorial.setup_sandbox()?;
161
162 let mut current = Section::from_number(start_section).unwrap_or(Section::WhatIsIdentity);
163
164 loop {
165 run_section(current, &tutorial)?;
166 tutorial.save_progress(current as usize + 1)?;
167
168 if let Some(next) = current.next() {
169 println!();
170 print!(
171 " {} Press Enter to continue to the next section (or 'q' to quit): ",
172 "→".yellow()
173 );
174 io::stdout().flush()?;
175
176 let mut input = String::new();
177 io::stdin().read_line(&mut input)?;
178
179 if input.trim().to_lowercase() == "q" {
180 println!();
181 println!(
182 " {} Your progress has been saved. Run 'auths learn' to continue.",
183 "✓".green()
184 );
185 break;
186 }
187
188 current = next;
189 } else {
190 tutorial.cleanup_sandbox()?;
192 tutorial.reset_progress()?;
193
194 println!();
195 println!(
196 "{}",
197 "╔════════════════════════════════════════════════════════════╗".green()
198 );
199 println!(
200 "{}",
201 "║ Tutorial Complete! ║".green()
202 );
203 println!(
204 "{}",
205 "╚════════════════════════════════════════════════════════════╝".green()
206 );
207 println!();
208 println!(" You've learned the basics of Auths! Here's what to do next:");
209 println!();
210 println!(
211 " {} Run {} to create your real identity",
212 "1.".cyan(),
213 "auths init".bold()
214 );
215 println!(
216 " {} Run {} to start signing commits",
217 "2.".cyan(),
218 "auths git setup".bold()
219 );
220 println!(
221 " {} Check {} for advanced features",
222 "3.".cyan(),
223 "auths --help".bold()
224 );
225 println!();
226 break;
227 }
228 }
229
230 Ok(())
231}
232
233fn list_sections() {
234 println!();
235 println!("{}", "Tutorial Sections:".bold());
236 println!();
237
238 for i in 1..=6 {
239 if let Some(section) = Section::from_number(i) {
240 println!(" {} {}", format!("{}.", i).cyan(), section.title());
241 }
242 }
243
244 println!();
245 println!("Use {} to skip to a specific section", "--skip N".bold());
246 println!("Use {} to reset progress", "--reset".bold());
247 println!();
248}
249
250fn run_section(section: Section, tutorial: &Tutorial) -> Result<()> {
251 println!();
252 println!(
253 "{}",
254 "────────────────────────────────────────────────────────────".dimmed()
255 );
256 println!(
257 " {} {}",
258 format!("Section {}", section as usize).cyan().bold(),
259 section.title().bold()
260 );
261 println!(
262 "{}",
263 "────────────────────────────────────────────────────────────".dimmed()
264 );
265 println!();
266
267 match section {
268 Section::WhatIsIdentity => section_what_is_identity()?,
269 Section::CreatingIdentity => section_creating_identity(tutorial)?,
270 Section::SigningCommit => section_signing_commit(tutorial)?,
271 Section::VerifyingSignature => section_verifying_signature(tutorial)?,
272 Section::LinkingDevice => section_linking_device()?,
273 Section::RevokingAccess => section_revoking_access()?,
274 }
275
276 println!();
277 println!(" {} Section complete!", "✓".green());
278
279 Ok(())
280}
281
282fn section_what_is_identity() -> Result<()> {
283 println!(" A cryptographic identity lets you prove who you are without passwords.");
284 println!();
285 println!(" With Auths, your identity consists of:");
286 println!();
287 println!(
288 " {} A unique identifier called a {} (Decentralized Identifier)",
289 "•".cyan(),
290 "DID".bold()
291 );
292 println!(
293 " {} A {} stored in your device's secure storage",
294 "•".cyan(),
295 "signing key".bold()
296 );
297 println!(
298 " {} {} that authorize devices to sign on your behalf",
299 "•".cyan(),
300 "Attestations".bold()
301 );
302 println!();
303 println!(" Key benefits:");
304 println!();
305 println!(
306 " {} {} - No central server owns your identity",
307 "✓".green(),
308 "Decentralized".bold()
309 );
310 println!(
311 " {} {} - Keys never leave your device",
312 "✓".green(),
313 "Secure".bold()
314 );
315 println!(
316 " {} {} - Signatures are mathematically proven",
317 "✓".green(),
318 "Verifiable".bold()
319 );
320 println!(
321 " {} {} - Use the same identity across all your devices",
322 "✓".green(),
323 "Portable".bold()
324 );
325
326 wait_for_continue()?;
327 Ok(())
328}
329
330fn section_creating_identity(tutorial: &Tutorial) -> Result<()> {
331 println!(" Let's create a test identity in a sandbox environment.");
332 println!();
333 println!(" In a real scenario, you would run:");
334 println!();
335 println!(" {}", "$ auths init".cyan());
336 println!();
337 println!(" This creates your identity by:");
338 println!();
339 println!(" {} Generating a cryptographic key pair", "1.".cyan());
340 println!(
341 " {} Storing the private key in your keychain",
342 "2.".cyan()
343 );
344 println!(" {} Creating your DID from the public key", "3.".cyan());
345 println!(
346 " {} Recording everything in a Git repository",
347 "4.".cyan()
348 );
349 println!();
350
351 println!(" {} Creating sandbox identity...", "→".yellow());
353
354 let sandbox_repo = tutorial.sandbox_dir.join("identity");
355 if !sandbox_repo.exists() {
356 fs::create_dir_all(&sandbox_repo)?;
357
358 ProcessCommand::new("git")
360 .args(["init", "--quiet"])
361 .current_dir(&sandbox_repo)
362 .status()?;
363
364 ProcessCommand::new("git")
365 .args(["config", "user.email", "tutorial@auths.io"])
366 .current_dir(&sandbox_repo)
367 .status()?;
368
369 ProcessCommand::new("git")
370 .args(["config", "user.name", "Tutorial User"])
371 .current_dir(&sandbox_repo)
372 .status()?;
373 }
374
375 println!();
376 println!(" {} Sandbox identity created!", "✓".green());
377 println!();
378 println!(" Your sandbox DID would look like:");
379 println!(" {}", "did:keri:EExample123...".dimmed());
380 println!();
381 println!(" This DID is derived from your public key - it's mathematically");
382 println!(" guaranteed to be unique to you.");
383
384 wait_for_continue()?;
385 Ok(())
386}
387
388fn section_signing_commit(tutorial: &Tutorial) -> Result<()> {
389 println!(" Git commit signing proves that commits came from you.");
390 println!();
391 println!(" With Auths configured, Git automatically signs your commits:");
392 println!();
393 println!(" {}", "$ git commit -m \"Add feature\"".cyan());
394 println!(
395 " {}",
396 "[main abc1234] Add feature (auths-signed)".dimmed()
397 );
398 println!();
399 println!(" Behind the scenes, Auths:");
400 println!();
401 println!(" {} Creates a signature of the commit data", "1.".cyan());
402 println!(" {} Uses your key from the secure keychain", "2.".cyan());
403 println!(" {} Embeds the signature in the commit", "3.".cyan());
404 println!();
405
406 let sandbox_repo = tutorial.sandbox_dir.join("identity");
408 let test_file = sandbox_repo.join("test.txt");
409
410 fs::write(&test_file, "Hello from Auths tutorial!\n")?;
411
412 ProcessCommand::new("git")
413 .args(["add", "test.txt"])
414 .current_dir(&sandbox_repo)
415 .status()?;
416
417 ProcessCommand::new("git")
418 .args(["commit", "--quiet", "-m", "Tutorial: First signed commit"])
419 .current_dir(&sandbox_repo)
420 .status()?;
421
422 println!(" {} Created a test commit in the sandbox:", "→".yellow());
423 println!();
424
425 let output = ProcessCommand::new("git")
427 .args(["log", "--oneline", "-1"])
428 .current_dir(&sandbox_repo)
429 .output()?;
430
431 let log_output = String::from_utf8_lossy(&output.stdout);
432 println!(" {}", log_output.trim().dimmed());
433
434 wait_for_continue()?;
435 Ok(())
436}
437
438fn section_verifying_signature(_tutorial: &Tutorial) -> Result<()> {
439 println!(" Anyone can verify that a commit came from you.");
440 println!();
441 println!(" To verify a commit signature:");
442 println!();
443 println!(" {}", "$ auths verify-commit HEAD".cyan());
444 println!();
445 println!(" This checks:");
446 println!();
447 println!(" {} Is the signature mathematically valid?", "•".cyan());
448 println!(
449 " {} Does the signing key match an authorized device?",
450 "•".cyan()
451 );
452 println!(
453 " {} Was the device authorized at commit time?",
454 "•".cyan()
455 );
456 println!();
457 println!(" Verification is fast because it uses local Git data - no network");
458 println!(" calls needed.");
459 println!();
460
461 println!(" {} Example verification output:", "→".yellow());
463 println!();
464 println!(" {}", "✓ Signature valid".green());
465 println!(" {}", " Signer: did:keri:EExample123...".dimmed());
466 println!(" {}", " Device: MacBook Pro (active)".dimmed());
467 println!(" {}", " Signed: 2024-01-15 10:30:00 UTC".dimmed());
468
469 wait_for_continue()?;
470 Ok(())
471}
472
473fn section_linking_device() -> Result<()> {
474 println!(" Use the same identity across multiple devices.");
475 println!();
476 println!(" To link a new device:");
477 println!();
478 println!(" {} On your existing device:", "1.".cyan());
479 println!(" {}", "$ auths pair start".cyan());
480 println!(" {}", "Scan this QR code or enter: ABC123".dimmed());
481 println!();
482 println!(" {} On your new device:", "2.".cyan());
483 println!(" {}", "$ auths pair join --code ABC123".cyan());
484 println!();
485 println!(
486 " This creates an {} that authorizes the new device",
487 "attestation".bold()
488 );
489 println!(" to sign commits on behalf of your identity.");
490 println!();
491 println!(" {} The new device gets its own signing key", "•".cyan());
492 println!(" {} Your main device signs the authorization", "•".cyan());
493 println!(" {} The attestation is stored in Git", "•".cyan());
494 println!();
495 println!(" You can link phones, tablets, CI servers - any device that");
496 println!(" needs to sign commits as you.");
497
498 wait_for_continue()?;
499 Ok(())
500}
501
502fn section_revoking_access() -> Result<()> {
503 println!(" If a device is lost or compromised, revoke its access.");
504 println!();
505 println!(" To revoke a device:");
506 println!();
507 println!(" {}", "$ auths device revoke <device-did>".cyan());
508 println!();
509 println!(" This creates a revocation record that:");
510 println!();
511 println!(
512 " {} Marks the device as no longer authorized",
513 "•".cyan()
514 );
515 println!(" {} Is signed by your identity", "•".cyan());
516 println!(
517 " {} Is stored in Git and propagates automatically",
518 "•".cyan()
519 );
520 println!();
521 println!(" After revocation, signatures from that device will show as:");
522 println!();
523 println!(" {}", "✗ Device was revoked on 2024-01-20".red());
524 println!();
525 println!(
526 " {} If you suspect compromise, use emergency freeze:",
527 "!".red().bold()
528 );
529 println!();
530 println!(" {}", "$ auths emergency freeze".cyan());
531 println!();
532 println!(" This immediately suspends all signing until you investigate.");
533
534 wait_for_continue()?;
535 Ok(())
536}
537
538fn wait_for_continue() -> Result<()> {
539 println!();
540 print!(" {} Press Enter to continue...", "→".dimmed());
541 io::stdout().flush()?;
542
543 let mut input = String::new();
544 io::stdin().read_line(&mut input)?;
545
546 Ok(())
547}
548
549impl crate::commands::executable::ExecutableCommand for LearnCommand {
550 fn execute(&self, _ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
551 handle_learn(self.clone())
552 }
553}