1mod device;
23
24use std::fs;
25use std::io::{self, BufRead, Write};
26
27use anyhow::{bail, Context, Result};
28use clap::{Parser, Subcommand};
29
30use hsm_usb_protocol::commands::
31{
32 EMERGENCY_RESET_MAGIC,
33 LOCK_CONFIG_MAGIC,
34 LOCK_DATA_MAGIC,
35 LOCK_SLOT_MAGIC,
36};
37use hsm_usb_protocol::CommandOpcode;
38
39#[derive(Parser, Debug)]
40#[command(author, version, about, long_about = None)]
41struct Cli
42{
43 #[command(subcommand)]
44 command: Command,
45}
46
47#[derive(Subcommand, Debug)]
48enum Command
49{
50 Enumerate,
53
54 Info,
56
57 ReadConfig,
59
60 ReadConfigSlot
63 {
64 #[arg(long)]
65 slot: u8,
66 },
67
68 ReadSlotBlock
73 {
74 #[arg(long)]
75 slot: u8,
76 #[arg(long, default_value_t = 0)]
78 block: u8,
79 },
80
81 ReadSlotWord
83 {
84 #[arg(long)]
85 slot: u8,
86 #[arg(long, default_value_t = 0)]
88 block: u8,
89 #[arg(long, default_value_t = 0)]
91 offset: u8,
92 },
93
94 WriteConfig
97 {
98 #[arg(long)]
101 path: String,
102 },
103
104 ProvisionSlot
108 {
109 #[arg(long)]
110 slot: u8,
111 #[arg(long)]
113 value: String,
114 },
115
116 ProvisionToken
124 {
125 #[arg(long)]
129 secrets_file: String,
130 },
131
132 GetPubkey
134 {
135 #[arg(long)]
136 slot: u8,
137 },
138
139 Genkey
141 {
142 #[arg(long)]
143 slot: u8,
144 },
145
146 Sign
148 {
149 #[arg(long)]
150 slot: u8,
151 #[arg(long)]
153 challenge: String,
154 },
155
156 VerifyPin
158 {
159 #[arg(long)]
160 pin: String,
161 },
162
163 SetPin
165 {
166 #[arg(long)]
167 old: String,
168 #[arg(long)]
169 new: String,
170 #[arg(long)]
172 io_key: String,
173 },
174
175 UnblockPin
177 {
178 #[arg(long)]
179 puk: String,
180 #[arg(long)]
181 new_pin: String,
182 #[arg(long)]
184 io_key: String,
185 },
186
187 SetPuk
192 {
193 #[arg(long)]
194 old: String,
195 #[arg(long)]
196 new: String,
197 #[arg(long)]
199 io_key: String,
200 },
201
202 CloseSession,
207
208 #[command(name = "emergency-reset-DANGEROUS")]
215 EmergencyResetDangerous
216 {
217 #[arg(long)]
219 io_key: String,
220 },
221
222 PinStatus,
224
225 #[command(name = "read-counter")]
232 ReadCounter
233 {
234 #[arg(long)]
237 id: u8,
238 },
239
240 #[command(name = "lock-config-DANGEROUS")]
246 LockConfigDangerous,
247
248 #[command(name = "lock-data-DANGEROUS")]
253 LockDataDangerous,
254
255 #[command(name = "lock-slot-DANGEROUS")]
258 LockSlotDangerous
259 {
260 #[arg(long)]
261 slot: u8,
262 },
263}
264
265fn main() -> Result<()>
266{
267 let cli = Cli::parse();
268
269 match cli.command
270 {
271 Command::Enumerate => device::enumerate(),
272 Command::Info => cmd_info(),
273 Command::ReadConfig => cmd_read_config(),
274 Command::ReadConfigSlot { slot } => cmd_read_config_slot(slot),
275 Command::ReadSlotBlock { slot, block } => cmd_read_slot_block(slot, block),
276 Command::ReadSlotWord { slot, block, offset } => cmd_read_slot_word(slot, block, offset),
277 Command::WriteConfig { path } => cmd_write_config(&path),
278 Command::ProvisionSlot { slot, value } => cmd_provision_slot(slot, &value),
279 Command::ProvisionToken { secrets_file } => cmd_provision_token(&secrets_file),
280 Command::GetPubkey { slot } => cmd_get_pubkey(slot),
281 Command::Genkey { slot } => cmd_genkey(slot),
282 Command::Sign { slot, challenge } => cmd_sign(slot, &challenge),
283 Command::VerifyPin { pin } => cmd_verify_pin(&pin),
284 Command::SetPin { old, new, io_key } => cmd_set_pin(&old, &new, &io_key),
285 Command::UnblockPin { puk, new_pin, io_key } =>
286 {
287 cmd_unblock_pin(&puk, &new_pin, &io_key)
288 }
289 Command::SetPuk { old, new, io_key } => cmd_set_puk(&old, &new, &io_key),
290 Command::CloseSession => cmd_close_session(),
291 Command::EmergencyResetDangerous { io_key } =>
292 {
293 cmd_emergency_reset_dangerous(&io_key)
294 }
295 Command::PinStatus => cmd_pin_status(),
296 Command::ReadCounter { id } => cmd_read_counter(id),
297 Command::LockConfigDangerous => cmd_lock_config_dangerous(),
298 Command::LockDataDangerous => cmd_lock_data_dangerous(),
299 Command::LockSlotDangerous { slot } => cmd_lock_slot_dangerous(slot),
300 }
301}
302
303fn cmd_info() -> Result<()>
304{
305 let device = device::open()?;
306 let payload = device::send_command(&device, CommandOpcode::Info.as_u8(), &[])?;
307 if payload.len() != 14
308 {
309 bail!("unexpected Info payload length: {}", payload.len());
310 }
311 let revision: [u8; 4] = payload[0..4].try_into().unwrap();
312 let serial: [u8; 9] = payload[4..13].try_into().unwrap();
313 let is_provisioned = payload[13] != 0;
314 println!("Chip revision : {:02X} {:02X} {:02X} {:02X}",
315 revision[0], revision[1], revision[2], revision[3]);
316 println!("Serial number : {}", hex::encode_upper(serial));
317 println!("Provisioned : {}", if is_provisioned { "yes" } else { "no (zones unlocked)" });
318 Ok(())
319}
320
321fn read_full_config_zone(device: &hidapi::HidDevice) -> Result<[u8; 128]>
327{
328 let mut full = [0u8; 128];
329 for block in 0u8..=3
330 {
331 let payload = device::send_command
332 (
333 device,
334 CommandOpcode::ReadConfigZone.as_u8(),
335 &[block],
336 )?;
337 if payload.len() != 32
338 {
339 bail!("ReadConfigZone block {block}: expected 32 bytes, got {}", payload.len());
340 }
341 let start = (block as usize) * 32;
342 full[start..start + 32].copy_from_slice(&payload);
343 }
344 Ok(full)
345}
346
347fn cmd_read_config() -> Result<()>
348{
349 let device = device::open()?;
350 let full = read_full_config_zone(&device)?;
351 for (i, b) in full.iter().enumerate()
352 {
353 if i % 16 == 0
354 {
355 if i != 0 { println!(); }
356 print!("{:03}: ", i);
357 }
358 print!("{:02x} ", b);
359 }
360 println!();
361 Ok(())
362}
363
364fn cmd_write_config(path: &str) -> Result<()>
365{
366 let blob = fs::read(path)
367 .with_context(|| format!("failed to read config blob from {path}"))?;
368 if blob.len() != 128
369 {
370 bail!("config blob must be exactly 128 bytes, got {}", blob.len());
371 }
372 let device = device::open()?;
373 for block in 0u8..=3
375 {
376 let mut payload = [0u8; 33];
377 payload[0] = block;
378 let start = (block as usize) * 32;
379 payload[1..33].copy_from_slice(&blob[start..start + 32]);
380 device::send_command
381 (
382 &device,
383 CommandOpcode::WriteConfigZone.as_u8(),
384 &payload,
385 )?;
386 println!("wrote block {block}");
387 }
388 Ok(())
389}
390
391fn cmd_read_config_slot(slot: u8) -> Result<()>
392{
393 let device = device::open()?;
394 let payload = device::send_command
395 (
396 &device,
397 CommandOpcode::ReadConfigSlot.as_u8(),
398 &[slot],
399 )?;
400 if payload.len() != 4
401 {
402 bail!("unexpected ReadConfigSlot payload length: {}", payload.len());
403 }
404 let slot_config = u16::from_le_bytes([payload[0], payload[1]]);
405 let key_config = u16::from_le_bytes([payload[2], payload[3]]);
406 println!("Slot {slot} configuration:");
407 println!(" SlotConfig: 0x{:04X} ({} {})",
408 slot_config,
409 format_args!("{:08b}", payload[1]),
410 format_args!("{:08b}", payload[0]),
411 );
412 println!(" KeyConfig : 0x{:04X} ({} {})",
413 key_config,
414 format_args!("{:08b}", payload[3]),
415 format_args!("{:08b}", payload[2]),
416 );
417 Ok(())
418}
419
420fn cmd_read_slot_block(slot: u8, block: u8) -> Result<()>
421{
422 let device = device::open()?;
423 let payload = device::send_command
424 (
425 &device,
426 CommandOpcode::ReadSlotBlock.as_u8(),
427 &[slot, block],
428 )?;
429 if payload.len() != 32
430 {
431 bail!("unexpected ReadSlotBlock payload length: {}", payload.len());
432 }
433 println!("Slot {slot} block {block} (32 bytes):");
434 for (i, chunk) in payload.chunks(16).enumerate()
435 {
436 print!(" {:02}: ", i * 16);
437 for b in chunk
438 {
439 print!("{b:02X} ");
440 }
441 println!();
442 }
443 Ok(())
444}
445
446fn cmd_read_slot_word(slot: u8, block: u8, offset: u8) -> Result<()>
447{
448 let device = device::open()?;
449 let payload = device::send_command
450 (
451 &device,
452 CommandOpcode::ReadSlotWord.as_u8(),
453 &[slot, block, offset],
454 )?;
455 if payload.len() != 4
456 {
457 bail!("unexpected ReadSlotWord payload length: {}", payload.len());
458 }
459 println!
460 (
461 "Slot {slot} block {block} word {offset}: {:02X} {:02X} {:02X} {:02X}",
462 payload[0], payload[1], payload[2], payload[3],
463 );
464 Ok(())
465}
466
467fn cmd_close_session() -> Result<()>
468{
469 let device = device::open()?;
470 device::send_command
471 (
472 &device,
473 CommandOpcode::CloseSession.as_u8(),
474 &[],
475 )?;
476 println!("Session closed.");
477 Ok(())
478}
479
480fn cmd_provision_slot(slot: u8, value_hex: &str) -> Result<()>
481{
482 let value = parse_hex_array::<32>(value_hex, "value")?;
483 let mut payload = [0u8; 1 + 32];
484 payload[0] = slot;
485 payload[1..].copy_from_slice(&value);
486
487 let device = device::open()?;
488 device::send_command
489 (
490 &device,
491 CommandOpcode::ProvisionSlot.as_u8(),
492 &payload,
493 )?;
494 println!("Slot {slot} written (cleartext, {} bytes).", value.len());
495 Ok(())
496}
497
498fn cmd_provision_token(secrets_file_path: &str) -> Result<()>
515{
516 use std::path::Path;
517 let secrets_path = Path::new(secrets_file_path);
518 if secrets_path.exists()
519 {
520 bail!(
521 "secrets file `{secrets_file_path}` already exists, refusing to overwrite. \
522 Move it aside or pick a different path."
523 );
524 }
525
526 let device = device::open()?;
527
528 let info = device::send_command(&device, CommandOpcode::Info.as_u8(), &[])?;
530 if info.len() != 14
531 {
532 bail!("unexpected Info payload: {} bytes", info.len());
533 }
534 let serial_hex = hex::encode_upper(&info[4..13]);
535 let already_provisioned = info[13] != 0;
536 if already_provisioned
537 {
538 bail!("chip reports it is already provisioned (both zones locked). Refusing.");
539 }
540 println!("Chip serial : {serial_hex}");
541
542 println!("Generating IO key...");
544 let io_key_bytes = device::send_command(&device, CommandOpcode::ProvisionIoKey.as_u8(), &[])?;
545 if io_key_bytes.len() != 32
546 {
547 bail!("unexpected IO key length: {}", io_key_bytes.len());
548 }
549 let io_key_hex = hex::encode_upper(&io_key_bytes);
550 println!("IO key written to slot 8.");
551
552 println!("Writing default PIN hash to slot 5...");
554 device::send_command(&device, CommandOpcode::ProvisionInitialPin.as_u8(), &[])?;
555
556 println!("Generating PUK...");
558 let puk_bytes = device::send_command(
559 &device,
560 CommandOpcode::ProvisionInitialPuk.as_u8(),
561 &[],
562 )?;
563 if puk_bytes.len() != 8
564 {
565 bail!("unexpected PUK length: {}", puk_bytes.len());
566 }
567 let puk_str = core::str::from_utf8(&puk_bytes)
568 .context("PUK is not valid UTF-8")?
569 .to_string();
570
571 println!("Generating ECC P-256 key in slot 0...");
573 let pubkey = device::send_command(&device, CommandOpcode::GenKey.as_u8(), &[0u8])?;
574 if pubkey.len() != 64
575 {
576 bail!("unexpected pubkey length: {}", pubkey.len());
577 }
578 let pubkey_x = hex::encode_upper(&pubkey[..32]);
579 let pubkey_y = hex::encode_upper(&pubkey[32..]);
580
581 let json = format!
583 (
584 "{{\n \"chip_serial\": \"{serial_hex}\",\n \
585 \"io_key\": \"{io_key_hex}\",\n \
586 \"initial_puk\": \"{puk_str}\",\n \
587 \"primary_pubkey_x\": \"{pubkey_x}\",\n \
588 \"primary_pubkey_y\": \"{pubkey_y}\"\n}}\n"
589 );
590 fs::write(secrets_path, &json)
591 .with_context(|| format!("failed to write secrets to {secrets_file_path}"))?;
592
593 println!();
594 println!("=== PROVISIONING DONE ===");
595 println!("Chip serial : {serial_hex}");
596 println!("IO key : {io_key_hex}");
597 println!("Initial PIN : 0000");
598 println!("Initial PUK : {puk_str}");
599 println!("Primary pubkey X : {pubkey_x}");
600 println!("Primary pubkey Y : {pubkey_y}");
601 println!();
602 println!("Secrets written to: {secrets_file_path}");
603 println!();
604 println!("WRITE THE PUK AND IO KEY DOWN NOW. There is no way to recover");
605 println!("them after this command exits. The secrets file is your");
606 println!("only durable copy.");
607 println!();
608 println!("Next step: lock the data zone with");
609 println!(" hsm-host lock-data-DANGEROUS");
610 println!("once you have verified the slot contents via `read-config`.");
611 Ok(())
612}
613
614fn cmd_get_pubkey(slot: u8) -> Result<()>
615{
616 let device = device::open()?;
617 let payload = device::send_command
618 (
619 &device,
620 CommandOpcode::GetPubkey.as_u8(),
621 &[slot],
622 )?;
623 if payload.len() != 64
624 {
625 bail!("unexpected GetPubkey payload length: {}", payload.len());
626 }
627 println!("X: {}", hex::encode_upper(&payload[..32]));
628 println!("Y: {}", hex::encode_upper(&payload[32..]));
629 Ok(())
630}
631
632fn cmd_genkey(slot: u8) -> Result<()>
633{
634 let device = device::open()?;
635 let payload = device::send_command
636 (
637 &device,
638 CommandOpcode::GenKey.as_u8(),
639 &[slot],
640 )?;
641 if payload.len() != 64
642 {
643 bail!("unexpected GenKey payload length: {}", payload.len());
644 }
645 println!("New public key for slot {slot}:");
646 println!("X: {}", hex::encode_upper(&payload[..32]));
647 println!("Y: {}", hex::encode_upper(&payload[32..]));
648 Ok(())
649}
650
651fn cmd_sign(slot: u8, challenge: &str) -> Result<()>
652{
653 let digest = parse_hex_array::<32>(challenge, "challenge")?;
654 let mut payload = [0u8; 33];
655 payload[0] = slot;
656 payload[1..].copy_from_slice(&digest);
657
658 let device = device::open()?;
659 println!("touch the dongle within 30s...");
660 let response = device::send_command
661 (
662 &device,
663 CommandOpcode::Sign.as_u8(),
664 &payload,
665 )?;
666 if response.len() != 64
667 {
668 bail!("unexpected Sign response length: {}", response.len());
669 }
670 println!("R: {}", hex::encode_upper(&response[..32]));
671 println!("S: {}", hex::encode_upper(&response[32..]));
672 Ok(())
673}
674
675fn cmd_verify_pin(pin: &str) -> Result<()>
676{
677 let pin_bytes = pin.as_bytes();
678 if pin_bytes.len() != 4
679 {
680 bail!("PIN must be exactly 4 digits, got {}", pin_bytes.len());
681 }
682 let device = device::open()?;
683 device::send_command
684 (
685 &device,
686 CommandOpcode::VerifyPin.as_u8(),
687 pin_bytes,
688 )?;
689 println!("PIN accepted, session opened (30s window).");
690 Ok(())
691}
692
693fn cmd_set_pin(old: &str, new: &str, io_key_hex: &str) -> Result<()>
694{
695 let old_bytes = check_pin(old, "old")?;
696 let new_bytes = check_pin(new, "new")?;
697 let io_key = parse_hex_array::<32>(io_key_hex, "io-key")?;
698
699 let mut payload = [0u8; 4 + 4 + 32];
703 payload[..4].copy_from_slice(&old_bytes);
704 payload[4..8].copy_from_slice(&new_bytes);
705 payload[8..].copy_from_slice(&io_key);
706
707 let device = device::open()?;
708 device::send_command(&device, CommandOpcode::SetPin.as_u8(), &payload)?;
709 println!("PIN changed.");
710 Ok(())
711}
712
713fn cmd_unblock_pin(puk: &str, new_pin: &str, io_key_hex: &str) -> Result<()>
714{
715 let puk_bytes = check_puk(puk)?;
716 let new_pin_bytes = check_pin(new_pin, "new-pin")?;
717 let io_key = parse_hex_array::<32>(io_key_hex, "io-key")?;
718
719 let mut payload = [0u8; 8 + 4 + 32];
720 payload[..8].copy_from_slice(&puk_bytes);
721 payload[8..12].copy_from_slice(&new_pin_bytes);
722 payload[12..].copy_from_slice(&io_key);
723
724 let device = device::open()?;
725 device::send_command(&device, CommandOpcode::UnblockPin.as_u8(), &payload)?;
726 println!("PIN reset via PUK, fresh tries window granted.");
727 Ok(())
728}
729
730fn cmd_set_puk(old: &str, new: &str, io_key_hex: &str) -> Result<()>
731{
732 let old_bytes = check_puk(old)?;
733 let new_bytes = check_puk(new)?;
734 let io_key = parse_hex_array::<32>(io_key_hex, "io-key")?;
735
736 let mut payload = [0u8; 8 + 8 + 32];
737 payload[..8].copy_from_slice(&old_bytes);
738 payload[8..16].copy_from_slice(&new_bytes);
739 payload[16..].copy_from_slice(&io_key);
740
741 let device = device::open()?;
742 device::send_command(&device, CommandOpcode::SetPuk.as_u8(), &payload)?;
743 println!("PUK changed.");
744 Ok(())
745}
746
747fn cmd_emergency_reset_dangerous(io_key_hex: &str) -> Result<()>
748{
749 let io_key = parse_hex_array::<32>(io_key_hex, "io-key")?;
750
751 println!();
752 println!("=== EMERGENCY RESET -- LAST-CHANCE RECOVERY ===");
753 println!();
754 println!("This command is only intended for the case where you have");
755 println!("forgotten BOTH the PIN and the PUK, AND have tried enough times");
756 println!("on each to exhaust both retry batches. The token will refuse");
757 println!("this operation otherwise.");
758 println!();
759 println!("If you go through with it:");
760 println!(" - ALL identity ECC private keys (slots 0..=4 and 7) are");
761 println!(" destroyed. Any signature made under those keys cannot be");
762 println!(" reproduced. Public keys you published become useless.");
763 println!(" - PIN is reset to '0000'.");
764 println!(" - A fresh random PUK is generated and printed ONCE.");
765 println!(" - You get one fresh batch of PIN attempts and one fresh");
766 println!(" batch of PUK attempts. The chip's hardware counters are");
767 println!(" still consumed; you cannot do this indefinitely.");
768 println!();
769 println!("If you remember either the PIN or the PUK, STOP HERE and use");
770 println!("the appropriate command instead:");
771 println!(" PUK known -> `hsm-host unblock-pin`");
772 println!();
773 confirm_interactive("EMERGENCY-RESET")?;
774
775 let mut payload = [0u8; 4 + 32];
776 payload[..4].copy_from_slice(&EMERGENCY_RESET_MAGIC);
777 payload[4..].copy_from_slice(&io_key);
778
779 let device = device::open()?;
780 let response = device::send_command(
781 &device,
782 CommandOpcode::EmergencyReset.as_u8(),
783 &payload,
784 )?;
785 if response.len() != 8
786 {
787 bail!("unexpected response length: {} (expected 8 for the new PUK)", response.len());
788 }
789 let new_puk = core::str::from_utf8(&response)
790 .context("new PUK is not valid UTF-8")?;
791 println!();
792 println!("Emergency reset complete.");
793 println!(" - All identity keys regenerated (slots 0..=4 and 7).");
794 println!(" - PIN reset to: 0000");
795 println!(" - NEW PUK : {new_puk}");
796 println!();
797 println!("WRITE THE PUK DOWN NOW. It cannot be retrieved later.");
798 println!("Change the default PIN immediately.");
799 Ok(())
800}
801
802fn cmd_pin_status() -> Result<()>
803{
804 let device = device::open()?;
805 let payload = device::send_command
806 (
807 &device,
808 CommandOpcode::GetPinStatus.as_u8(),
809 &[],
810 )?;
811 if payload.len() != 3
812 {
813 bail!("unexpected PinStatus payload: {} bytes", payload.len());
814 }
815 println!("PIN tries remaining: {}", payload[0]);
816 println!("PUK tries remaining: {}", payload[1]);
817 println!("Session active : {}", payload[2] != 0);
818 Ok(())
819}
820
821fn cmd_read_counter(id: u8) -> Result<()>
822{
823 if id > 1
824 {
825 bail!("invalid counter id `{id}`: must be 0 (PIN) or 1 (PUK)");
826 }
827 let device = device::open()?;
828 let payload = device::send_command
829 (
830 &device,
831 CommandOpcode::ReadCounter.as_u8(),
832 &[id],
833 )?;
834 if payload.len() != 4
835 {
836 bail!("unexpected ReadCounter payload: {} bytes", payload.len());
837 }
838 let mut bytes = [0u8; 4];
841 bytes.copy_from_slice(&payload[..4]);
842 let value = u32::from_le_bytes(bytes);
843 let name = if id == 0 { "Counter0 (PIN)" } else { "Counter1 (PUK)" };
844 println!("{name} : {value} (0x{value:08X})");
845 println!("Raw bytes (LE) : {:02X} {:02X} {:02X} {:02X}", bytes[0], bytes[1], bytes[2], bytes[3]);
846 Ok(())
847}
848
849fn cmd_lock_config_dangerous() -> Result<()>
850{
851 let device = device::open()?;
852
853 let zone = read_full_config_zone(&device)?;
859 let crc = atecc608b::crc::crc16(&zone);
860
861 println!();
862 println!("=== LOCK CONFIG ZONE : IRREVERSIBLE ===");
863 println!();
864 println!("Current configuration zone CRC-16 (full 128 bytes): 0x{crc:04X}");
865 println!();
866 println!("This permanently freezes the configuration zone of the ATECC chip.");
867 println!("Slot policies, key configs, and counters can never be changed again.");
868 println!("The chip will recompute this CRC just before committing and refuse");
869 println!("the lock if it has drifted between this read and the lock command.");
870 println!();
871 confirm_interactive("LOCK-CONFIG")?;
872
873 let mut payload = [0u8; 6];
874 payload[..4].copy_from_slice(&LOCK_CONFIG_MAGIC);
875 payload[4..].copy_from_slice(&crc.to_le_bytes());
876
877 device::send_command(&device, CommandOpcode::LockConfigZone.as_u8(), &payload)?;
878 println!("Config zone locked.");
879 Ok(())
880}
881
882fn cmd_lock_data_dangerous() -> Result<()>
883{
884 println!();
885 println!("=== LOCK DATA ZONE : IRREVERSIBLE ===");
886 println!();
887 println!("This permanently freezes the data zone. Slots can no longer be");
888 println!("written in cleartext. Only the encrypted-write protocol against");
889 println!("the IO key (slot 8) remains, and only for slots whose WriteConfig");
890 println!("allows it. Make sure provisioning is complete (PIN hash, PUK hash,");
891 println!("IO key, identity keys) before doing this.");
892 println!();
893 println!("No CRC is checked at lock time: secret-bearing slots (IsSecret=1)");
894 println!("cannot be read back to compute one. The confirmation below is the");
895 println!("only safety beyond the magic-word check in the firmware.");
896 println!();
897 confirm_interactive("LOCK-DATA")?;
898
899 let mut payload = [0u8; 4];
900 payload.copy_from_slice(&LOCK_DATA_MAGIC);
901
902 let device = device::open()?;
903 device::send_command
904 (
905 &device,
906 CommandOpcode::LockDataZone.as_u8(),
907 &payload,
908 )?;
909 println!("Data zone locked.");
910 Ok(())
911}
912
913fn cmd_lock_slot_dangerous(slot: u8) -> Result<()>
914{
915 println!();
916 println!("=== LOCK SLOT {slot} : IRREVERSIBLE ===");
917 println!();
918 println!("Slot {slot} will no longer accept writes, even via the encrypted");
919 println!("write protocol. The current contents are frozen for life.");
920 println!();
921 confirm_interactive(&format!("LOCK-SLOT-{slot}"))?;
922
923 let mut payload = [0u8; 5];
924 payload[..4].copy_from_slice(&LOCK_SLOT_MAGIC);
925 payload[4] = slot;
926
927 let device = device::open()?;
928 device::send_command(&device, CommandOpcode::LockSlot.as_u8(), &payload)?;
929 println!("Slot {slot} locked.");
930 Ok(())
931}
932
933fn check_pin(pin: &str, name: &str) -> Result<[u8; 4]>
934{
935 if pin.len() != 4
936 {
937 bail!("{name} PIN must be 4 digits, got {}", pin.len());
938 }
939 let mut out = [0u8; 4];
940 out.copy_from_slice(pin.as_bytes());
941 Ok(out)
942}
943
944fn check_puk(puk: &str) -> Result<[u8; 8]>
945{
946 if puk.len() != 8
947 {
948 bail!("PUK must be 8 digits, got {}", puk.len());
949 }
950 let mut out = [0u8; 8];
951 out.copy_from_slice(puk.as_bytes());
952 Ok(out)
953}
954
955fn parse_hex_array<const N: usize>(s: &str, name: &str) -> Result<[u8; N]>
956{
957 let s = s.strip_prefix("0x").unwrap_or(s);
958 let bytes = hex::decode(s)
959 .with_context(|| format!("{name} is not valid hex"))?;
960 if bytes.len() != N
961 {
962 bail!("{name} must be {N} bytes ({} hex chars), got {} bytes", N * 2, bytes.len());
963 }
964 let mut out = [0u8; N];
965 out.copy_from_slice(&bytes);
966 Ok(out)
967}
968
969fn confirm_interactive(expected: &str) -> Result<()>
973{
974 print!("Type '{expected}' to confirm, anything else to abort: ");
975 io::stdout().flush().ok();
976 let mut buf = String::new();
977 io::stdin().lock().read_line(&mut buf).context("failed to read stdin")?;
978 let trimmed = buf.trim();
979 if trimmed != expected
980 {
981 bail!("confirmation did not match (got {trimmed:?}), aborting");
982 }
983 Ok(())
984}