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