1use crate::config::data_dir;
35use crate::i18n::Translator;
36use openpgp::Cert;
37use openpgp::cert::prelude::*;
38use openpgp::crypto::{self, SessionKey, mpi};
39use openpgp::packet::{Key, key};
40use openpgp::policy::StandardPolicy;
41use openpgp::types::Curve;
42use sequoia_openpgp as openpgp;
43use serde::{Deserialize, Serialize};
44use std::path::{Path, PathBuf};
45use std::sync::Mutex;
46use std::time::{Duration, Instant, SystemTime};
47
48pub use openpgp::Cert as Certificate;
50pub use openpgp::crypto::Password;
52pub use openpgp::crypto::mem::Protected;
54pub use openpgp::types::HashAlgorithm;
56
57pub type PublicKey = Key<key::PublicParts, key::UnspecifiedRole>;
59
60#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "kebab-case")]
64pub enum Touch {
65 #[default]
67 Off,
68 On,
70 Fixed,
72 Cached,
74 CachedFixed,
76 Unknown,
78}
79
80impl Touch {
81 pub fn from_flag(flag: u8) -> Touch {
84 match flag {
85 0 => Touch::Off,
86 1 => Touch::On,
87 2 => Touch::Fixed,
88 3 => Touch::Cached,
89 4 => Touch::CachedFixed,
90 _ => Touch::Unknown,
91 }
92 }
93
94 pub fn asked(self) -> bool {
96 self != Touch::Off
97 }
98
99 pub fn words(self, tr: &Translator) -> Option<String> {
105 match self {
106 Touch::Off => None,
107 Touch::Unknown => Some(tr.text("seckey-touch-maybe", None)),
108 _ => Some(tr.text("seckey-touch", None)),
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum PinMode {
117 Sign,
119 Decrypt,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum Purpose {
126 Sign,
128 Open,
130 Read,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
136pub enum CardPublic {
137 Rsa {
139 n: Vec<u8>,
141 e: Vec<u8>,
143 },
144 Ecc {
146 point: Vec<u8>,
148 },
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct SlotInfo {
154 pub fingerprint: String,
157 pub created: u32,
159 pub algorithm: String,
161 pub touch: Touch,
163 pub public: Option<CardPublic>,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct CardInfo {
170 pub ident: String,
173 pub manufacturer: u16,
175 pub serial: u32,
177 pub holder: String,
179 pub url: String,
181 pub sign: Option<SlotInfo>,
183 pub decrypt: Option<SlotInfo>,
185 pub pin_tries: u8,
187 pub reset_tries: u8,
189 pub admin_tries: u8,
191 pub pin_each_signature: bool,
193 pub signatures: u32,
195 pub reader: String,
198}
199
200impl CardInfo {
201 pub fn label(&self, tr: &Translator) -> String {
203 label(self.manufacturer, self.serial, tr)
204 }
205
206 pub fn slot_of(&self, fingerprint: &str) -> Option<&SlotInfo> {
208 [&self.sign, &self.decrypt].into_iter().flatten().find(|s| s.fingerprint.eq_ignore_ascii_case(fingerprint))
209 }
210}
211
212pub fn label(manufacturer: u16, serial: u32, tr: &Translator) -> String {
215 let maker = match manufacturer {
216 0x0006 => "YubiKey".to_string(),
217 0x000F => "Nitrokey".to_string(),
218 0xF1D0 => "CanoKey".to_string(),
219 0xF517 => "Gnuk".to_string(),
220 0x0005 => "ZeitControl".to_string(),
221 _ => tr.text("seckey-generic", None),
222 };
223 format!("{maker} {}", serial_text(serial))
224}
225
226pub fn serial_text(serial: u32) -> String {
230 let digits = format!("{serial:08X}");
231 let digits = digits.trim_start_matches('0');
232 let digits = if digits.is_empty() { "0" } else { digits };
233 if !digits.chars().all(|c| c.is_ascii_digit()) {
234 return digits.to_string();
235 }
236 let mut out = String::new();
237 for (i, c) in digits.chars().enumerate() {
238 if i > 0 && (digits.len() - i) % 3 == 0 {
239 out.push('\u{202F}');
240 }
241 out.push(c);
242 }
243 out
244}
245
246pub fn algorithm_name(key: &PublicKey) -> String {
249 let curve = |curve: &Curve| match curve {
250 Curve::Ed25519 => "Ed25519".to_string(),
251 Curve::Cv25519 => "Curve25519".to_string(),
252 Curve::NistP256 => "NIST P-256".to_string(),
253 Curve::NistP384 => "NIST P-384".to_string(),
254 Curve::NistP521 => "NIST P-521".to_string(),
255 Curve::BrainpoolP256 => "brainpoolP256r1".to_string(),
256 Curve::BrainpoolP384 => "brainpoolP384r1".to_string(),
257 Curve::BrainpoolP512 => "brainpoolP512r1".to_string(),
258 other => other.to_string(),
259 };
260 match key.mpis() {
261 mpi::PublicKey::RSA { n, .. } => format!("RSA {}", n.bits()),
262 mpi::PublicKey::EdDSA { curve: c, .. } | mpi::PublicKey::ECDSA { curve: c, .. } | mpi::PublicKey::ECDH { curve: c, .. } => curve(c),
263 mpi::PublicKey::Ed25519 { .. } => "Ed25519".into(),
264 mpi::PublicKey::X25519 { .. } => "X25519".into(),
265 _ => key.pk_algo().to_string(),
266 }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
271pub enum CardError {
272 Unsupported,
274 NoService,
277 Absent,
279 Busy,
282 WrongPin {
284 left: u8,
286 },
287 Blocked,
289 TouchMissed,
291 Removed,
293 NotOpenPgp,
295 OtherKeys,
297 BadSignature,
299 Algorithm(String),
301 Other(String),
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
307pub enum Remedy {
308 None,
310 Again,
312 Release,
314 Pin,
316}
317
318impl CardError {
319 pub fn sentence(&self, tr: &Translator, purpose: Purpose) -> String {
321 match self {
322 CardError::Unsupported => tr.text("seckey-unsupported", None),
323 CardError::NoService => say(tr, "seckey-no-service", &[("hint", no_service_hint(tr))]),
324 CardError::Absent => tr.text(
325 match purpose {
326 Purpose::Sign => "seckey-absent-sign",
327 Purpose::Open => "seckey-absent-open",
328 Purpose::Read => "seckey-absent",
329 },
330 None,
331 ),
332 CardError::Busy => tr.text("seckey-busy", None),
333 CardError::WrongPin { left: 0 } | CardError::Blocked => tr.text("seckey-blocked", None),
334 CardError::WrongPin { left } => tr.text("seckey-wrong-pin", Some(&tr.counted(usize::from(*left)))),
335 CardError::TouchMissed => tr.text("seckey-touch-missed", None),
336 CardError::Removed => tr.text(if purpose == Purpose::Sign { "seckey-removed-sign" } else { "seckey-removed" }, None),
337 CardError::NotOpenPgp => tr.text("seckey-not-openpgp", None),
338 CardError::OtherKeys => tr.text("seckey-other-keys", None),
339 CardError::BadSignature => tr.text("seckey-bad-signature", None),
340 CardError::Algorithm(algorithm) => say(tr, "seckey-algorithm", &[("algorithm", algorithm.clone())]),
341 CardError::Other(detail) => say(tr, "seckey-other", &[("detail", detail.clone())]),
342 }
343 }
344
345 pub fn remedy(&self) -> Remedy {
347 match self {
348 CardError::Busy => Remedy::Release,
349 CardError::WrongPin { left } if *left > 0 => Remedy::Pin,
350 CardError::NoService | CardError::Absent | CardError::TouchMissed | CardError::Removed | CardError::Other(_) => Remedy::Again,
351 _ => Remedy::None,
352 }
353 }
354}
355
356impl std::fmt::Display for CardError {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 write!(f, "{self:?}")
359 }
360}
361
362impl std::error::Error for CardError {}
363
364fn say(tr: &Translator, id: &str, pairs: &[(&str, String)]) -> String {
365 let mut args = crate::i18n::args();
366 for (name, value) in pairs {
367 args.set(*name, value.clone());
368 }
369 tr.text(id, Some(&args))
370}
371
372fn no_service_hint(tr: &Translator) -> String {
374 let id = if cfg!(windows) {
375 "seckey-hint-windows"
376 } else if cfg!(target_os = "macos") {
377 "seckey-hint-macos"
378 } else if Path::new("/.flatpak-info").exists() {
379 "seckey-hint-flatpak"
380 } else {
381 hint_for_os_release(&std::fs::read_to_string("/etc/os-release").unwrap_or_default())
382 };
383 tr.text(id, None)
384}
385
386fn hint_for_os_release(text: &str) -> &'static str {
388 let value = |name: &str| text.lines().find_map(|l| l.strip_prefix(name)?.strip_prefix('=')).map(|v| v.trim_matches('"').to_ascii_lowercase()).unwrap_or_default();
389 let family = format!("{} {}", value("ID"), value("ID_LIKE"));
390 let has = |words: &[&str]| family.split_whitespace().any(|w| words.contains(&w));
391 if has(&["fedora", "rhel", "centos"]) {
392 "seckey-hint-fedora"
393 } else if has(&["debian", "ubuntu"]) {
394 "seckey-hint-debian"
395 } else if has(&["arch"]) {
396 "seckey-hint-arch"
397 } else if has(&["suse", "opensuse", "opensuse-leap", "opensuse-tumbleweed"]) {
398 "seckey-hint-suse"
399 } else {
400 "seckey-hint-linux"
401 }
402}
403
404#[derive(Debug, Clone, Copy)]
409pub enum ToSign<'a> {
410 Digest(&'a [u8]),
412 Rsa {
414 hash: HashAlgorithm,
416 digest: &'a [u8],
418 },
419}
420
421#[derive(Debug, Clone, Copy)]
425pub enum ToDecipher<'a> {
426 Rsa(&'a [u8]),
428 Ecdh(&'a [u8]),
430}
431
432pub trait Card {
435 fn info(&mut self) -> Result<CardInfo, CardError>;
437 fn verify(&mut self, pin: &Password, mode: PinMode) -> Result<(), CardError>;
439 fn sign(&mut self, input: ToSign<'_>) -> Result<Vec<u8>, CardError>;
441 fn decipher(&mut self, input: ToDecipher<'_>) -> Result<Protected, CardError>;
443}
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub enum Visit {
448 Next,
450 Done,
452}
453
454#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
456pub struct Visited {
457 pub opened: usize,
459 pub busy: usize,
461}
462
463pub trait Readers: Send + Sync {
466 fn present(&self) -> Result<Vec<String>, CardError>;
469 fn visit(&self, visit: &mut dyn FnMut(&mut dyn Card) -> Visit) -> Result<Visited, CardError>;
472}
473
474pub struct NoReaders;
476
477impl Readers for NoReaders {
478 fn present(&self) -> Result<Vec<String>, CardError> {
479 Err(CardError::Unsupported)
480 }
481
482 fn visit(&self, _visit: &mut dyn FnMut(&mut dyn Card) -> Visit) -> Result<Visited, CardError> {
483 Err(CardError::Unsupported)
484 }
485}
486
487pub fn cards(readers: &dyn Readers) -> Result<Vec<CardInfo>, CardError> {
489 let mut found = Vec::new();
490 let mut failure = None;
491 let visited = readers.visit(&mut |card| {
492 match card.info() {
493 Ok(info) => found.push(info),
494 Err(e) => failure = Some(e),
495 }
496 Visit::Next
497 })?;
498 if found.is_empty() {
499 return Err(if visited.busy > 0 {
500 CardError::Busy
501 } else if let Some(e) = failure {
502 e
503 } else {
504 CardError::Absent
505 });
506 }
507 Ok(found)
508}
509
510pub fn with_card<R>(readers: &dyn Readers, ident: &str, work: impl FnOnce(&mut dyn Card, &CardInfo) -> Result<R, CardError>) -> Result<R, CardError> {
513 let mut work = Some(work);
514 let mut result = None;
515 let visited = readers.visit(&mut |card| {
516 let Ok(info) = card.info() else { return Visit::Next };
517 if !info.ident.eq_ignore_ascii_case(ident) {
518 return Visit::Next;
519 }
520 if let Some(work) = work.take() {
521 result = Some(work(card, &info));
522 }
523 Visit::Done
524 })?;
525 match result {
526 Some(result) => result,
527 None if visited.busy > 0 => Err(CardError::Busy),
528 None => Err(CardError::Absent),
529 }
530}
531
532pub struct CardAccess<'a> {
535 pub readers: &'a dyn Readers,
537 pub pin: &'a Password,
539 pub touch: &'a (dyn Fn() + Sync),
541 failure: Mutex<Option<CardError>>,
543 reader: Mutex<Option<String>>,
545 accepted: std::sync::atomic::AtomicBool,
547}
548
549impl<'a> CardAccess<'a> {
550 pub fn new(readers: &'a dyn Readers, pin: &'a Password, touch: &'a (dyn Fn() + Sync)) -> CardAccess<'a> {
552 CardAccess { readers, pin, touch, failure: Mutex::new(None), reader: Mutex::new(None), accepted: std::sync::atomic::AtomicBool::new(false) }
553 }
554
555 pub fn failure(&self) -> Option<CardError> {
557 self.failure.lock().ok().and_then(|mut f| f.take())
558 }
559
560 pub fn reader(&self) -> Option<String> {
562 self.reader.lock().ok().and_then(|r| r.clone())
563 }
564
565 pub fn pin_accepted(&self) -> bool {
568 self.accepted.load(std::sync::atomic::Ordering::Relaxed)
569 }
570
571 fn fail(&self, error: CardError) -> CardError {
573 if let Ok(mut failure) = self.failure.lock() {
574 *failure = Some(error.clone());
575 }
576 error
577 }
578
579 fn run<R>(&self, ident: &str, public: &PublicKey, mode: PinMode, work: impl FnOnce(&mut dyn Card) -> Result<R, CardError>) -> Result<R, CardError> {
582 let fingerprint = public.fingerprint().to_hex();
583 with_card(self.readers, ident, |card, info| {
584 let slot = match mode {
585 PinMode::Sign => info.sign.as_ref(),
586 PinMode::Decrypt => info.decrypt.as_ref(),
587 };
588 let slot = slot.ok_or(CardError::OtherKeys)?;
589 if !slot.fingerprint.eq_ignore_ascii_case(&fingerprint) {
590 return Err(CardError::OtherKeys);
591 }
592 card.verify(self.pin, mode)?;
593 self.accepted.store(true, std::sync::atomic::Ordering::Relaxed);
594 if let Ok(mut reader) = self.reader.lock() {
595 *reader = Some(info.reader.clone());
596 }
597 if slot.touch.asked() {
598 (self.touch)();
599 }
600 work(card)
601 })
602 }
603}
604
605pub struct CardSigner<'a> {
609 access: &'a CardAccess<'a>,
610 ident: String,
611 public: PublicKey,
612}
613
614impl<'a> CardSigner<'a> {
615 pub fn new(access: &'a CardAccess<'a>, ident: &str, public: PublicKey) -> CardSigner<'a> {
617 CardSigner { access, ident: ident.to_string(), public }
618 }
619
620 fn sign_on_card(&self, hash: HashAlgorithm, digest: &[u8]) -> Result<mpi::Signature, CardError> {
621 let input = match self.public.mpis() {
622 mpi::PublicKey::RSA { .. } => match hash {
623 HashAlgorithm::SHA256 | HashAlgorithm::SHA384 | HashAlgorithm::SHA512 => ToSign::Rsa { hash, digest },
624 other => return Err(CardError::Algorithm(format!("RSA, {other}"))),
625 },
626 mpi::PublicKey::EdDSA { .. } | mpi::PublicKey::Ed25519 { .. } => ToSign::Digest(digest),
627 mpi::PublicKey::ECDSA { curve, .. } => {
628 let size = curve.field_size().map_err(|e| CardError::Algorithm(e.to_string()))?;
629 ToSign::Digest(&digest[..digest.len().min(size)])
630 }
631 _ => return Err(CardError::Algorithm(algorithm_name(&self.public))),
632 };
633 let raw = self.access.run(&self.ident, &self.public, PinMode::Sign, |card| card.sign(input))?;
634 let half = raw.len() / 2;
635 let signature = match self.public.mpis() {
636 mpi::PublicKey::RSA { .. } => mpi::Signature::RSA { s: mpi::MPI::new(&raw) },
637 mpi::PublicKey::EdDSA { .. } if raw.len() == 64 => mpi::Signature::EdDSA { r: mpi::MPI::new(&raw[..32]), s: mpi::MPI::new(&raw[32..]) },
638 mpi::PublicKey::Ed25519 { .. } => mpi::Signature::Ed25519 { s: Box::new(<[u8; 64]>::try_from(raw.as_slice()).map_err(|_| CardError::BadSignature)?) },
639 mpi::PublicKey::ECDSA { .. } if raw.len() % 2 == 0 => mpi::Signature::ECDSA { r: mpi::MPI::new(&raw[..half]), s: mpi::MPI::new(&raw[half..]) },
640 _ => return Err(CardError::BadSignature),
641 };
642 self.public.verify(&signature, hash, digest).map_err(|_| CardError::BadSignature)?;
644 Ok(signature)
645 }
646}
647
648impl crypto::Signer for CardSigner<'_> {
649 fn public(&self) -> &PublicKey {
650 &self.public
651 }
652
653 fn acceptable_hashes(&self) -> &[HashAlgorithm] {
654 match self.public.mpis() {
655 mpi::PublicKey::RSA { .. } => &[HashAlgorithm::SHA512, HashAlgorithm::SHA384, HashAlgorithm::SHA256],
657 _ => &[HashAlgorithm::SHA512, HashAlgorithm::SHA384, HashAlgorithm::SHA256, HashAlgorithm::SHA224],
658 }
659 }
660
661 fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) -> openpgp::Result<mpi::Signature> {
662 self.sign_on_card(hash_algo, digest).map_err(|e| self.access.fail(e).into())
663 }
664}
665
666pub struct CardDecryptor<'a> {
669 access: &'a CardAccess<'a>,
670 ident: String,
671 public: PublicKey,
672}
673
674impl<'a> CardDecryptor<'a> {
675 pub fn new(access: &'a CardAccess<'a>, ident: &str, public: PublicKey) -> CardDecryptor<'a> {
677 CardDecryptor { access, ident: ident.to_string(), public }
678 }
679
680 fn decrypt_on_card(&self, ciphertext: &mpi::Ciphertext, plaintext_len: Option<usize>) -> Result<SessionKey, CardError> {
681 match (ciphertext, self.public.mpis()) {
682 (mpi::Ciphertext::RSA { c }, mpi::PublicKey::RSA { .. }) => {
683 let opened = self.access.run(&self.ident, &self.public, PinMode::Decrypt, |card| card.decipher(ToDecipher::Rsa(c.value())))?;
684 Ok(SessionKey::from(&opened[..]))
685 }
686 (mpi::Ciphertext::ECDH { e, .. }, mpi::PublicKey::ECDH { curve, .. }) => {
687 let point = match curve {
689 Curve::Cv25519 => e.value().strip_prefix(&[0x40]).ok_or_else(|| CardError::Other("malformed Curve25519 point".into()))?,
690 _ => e.value(),
691 };
692 let shared = self.access.run(&self.ident, &self.public, PinMode::Decrypt, |card| card.decipher(ToDecipher::Ecdh(point)))?;
693 let shared = if *curve == Curve::NistP256 && shared.len() == 65 && shared[0] == 0x04 { Protected::from(&shared[1..33]) } else { shared };
695 crypto::ecdh::decrypt_unwrap(&self.public, &shared, ciphertext, plaintext_len).map_err(|e| CardError::Other(e.to_string()))
696 }
697 _ => Err(CardError::Algorithm(algorithm_name(&self.public))),
698 }
699 }
700}
701
702impl crypto::Decryptor for CardDecryptor<'_> {
703 fn public(&self) -> &PublicKey {
704 &self.public
705 }
706
707 fn decrypt(&mut self, ciphertext: &mpi::Ciphertext, plaintext_len: Option<usize>) -> openpgp::Result<SessionKey> {
708 self.decrypt_on_card(ciphertext, plaintext_len).map_err(|e| self.access.fail(e).into())
709 }
710}
711
712pub const PIN_QUIET: Duration = Duration::from_secs(15 * 60);
714
715#[derive(Default)]
720pub struct PinMemory {
721 held: Option<Held>,
722}
723
724struct Held {
725 ident: String,
726 pin: Password,
727 used: Instant,
728 reader: String,
729}
730
731impl PinMemory {
732 pub const fn new() -> PinMemory {
734 PinMemory { held: None }
735 }
736
737 pub fn keep(&mut self, ident: &str, pin: Password, reader: &str, now: Instant) {
739 self.held = Some(Held { ident: ident.to_string(), pin, used: now, reader: reader.to_string() });
740 }
741
742 pub fn get(&mut self, ident: &str, now: Instant) -> Option<Password> {
744 self.expire(now);
745 self.held.as_ref().filter(|h| h.ident.eq_ignore_ascii_case(ident)).map(|h| h.pin.clone())
746 }
747
748 pub fn used(&mut self, ident: &str, now: Instant) {
750 if let Some(held) = self.held.as_mut().filter(|h| h.ident.eq_ignore_ascii_case(ident)) {
751 held.used = now;
752 }
753 }
754
755 pub fn forget(&mut self) {
757 self.held = None;
758 }
759
760 pub fn expire(&mut self, now: Instant) -> bool {
762 let stale = self.held.as_ref().is_some_and(|h| now.saturating_duration_since(h.used) >= PIN_QUIET);
763 if stale {
764 self.held = None;
765 }
766 stale
767 }
768
769 pub fn went_away(&mut self, present: &[String]) -> bool {
771 let gone = self.held.as_ref().is_some_and(|h| !h.reader.is_empty() && !present.iter().any(|r| *r == h.reader));
772 if gone {
773 self.held = None;
774 }
775 gone
776 }
777
778 pub fn holder(&self) -> Option<&str> {
780 self.held.as_ref().map(|h| h.ident.as_str())
781 }
782}
783
784#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
787pub struct KnownCard {
788 pub ident: String,
790 #[serde(default)]
792 pub manufacturer: u16,
793 #[serde(default)]
795 pub serial: u32,
796 #[serde(default)]
798 pub holder: String,
799 #[serde(default)]
801 pub sign: String,
802 #[serde(default)]
804 pub decrypt: String,
805 pub cert: String,
807 #[serde(default)]
809 pub addresses: Vec<String>,
810 #[serde(default)]
812 pub source: String,
813 #[serde(default)]
815 pub checked: i64,
816 #[serde(default)]
818 pub pin_tries_full: u8,
819 #[serde(default)]
821 pub touch_sign: Touch,
822 #[serde(default)]
824 pub touch_decrypt: Touch,
825 #[serde(default)]
827 pub pin_each_signature: bool,
828}
829
830impl KnownCard {
831 pub fn serves(&self, address: &str) -> bool {
833 self.addresses.iter().any(|a| a.eq_ignore_ascii_case(address.trim()))
834 }
835
836 pub fn label(&self, tr: &Translator) -> String {
838 label(self.manufacturer, self.serial, tr)
839 }
840
841 pub fn update(&mut self, info: &CardInfo) {
844 self.holder = info.holder.clone();
845 self.touch_sign = info.sign.as_ref().map(|s| s.touch).unwrap_or_default();
846 self.touch_decrypt = info.decrypt.as_ref().map(|s| s.touch).unwrap_or_default();
847 self.pin_each_signature = info.pin_each_signature;
848 self.pin_tries_full = self.pin_tries_full.max(info.pin_tries).max(3);
849 }
850
851 pub fn tries_line(&self, left: u8, tr: &Translator) -> Option<String> {
853 (left < self.pin_tries_full.max(3)).then(|| tr.text("seckey-tries-left", Some(&tr.counted(usize::from(left)))))
854 }
855}
856
857#[derive(Default, Serialize, Deserialize)]
858struct CardsFile {
859 #[serde(default, rename = "card")]
860 cards: Vec<KnownCard>,
861}
862
863pub fn cards_path() -> PathBuf {
865 data_dir().join("pgp").join("cards.toml")
866}
867
868pub fn known_cards() -> Vec<KnownCard> {
870 read_known(&cards_path())
871}
872
873fn read_known(path: &Path) -> Vec<KnownCard> {
874 std::fs::read_to_string(path).ok().and_then(|text| toml::from_str::<CardsFile>(&text).ok()).map(|f| f.cards).unwrap_or_default()
875}
876
877pub fn change_known<T>(change: impl FnOnce(&mut Vec<KnownCard>) -> T) -> Result<T, String> {
879 change_known_at(&cards_path(), change)
880}
881
882fn change_known_at<T>(path: &Path, change: impl FnOnce(&mut Vec<KnownCard>) -> T) -> Result<T, String> {
883 crate::filelock::with_lock(path, || {
884 let mut cards = read_known(path);
885 let result = change(&mut cards);
886 let text = toml::to_string(&CardsFile { cards }).map_err(|e| e.to_string())?;
887 let fail = |e: std::io::Error| format!("{}: {e}", path.display());
888 if let Some(folder) = path.parent() {
889 std::fs::create_dir_all(folder).map_err(fail)?;
890 }
891 let temporary = path.with_extension("toml.new");
892 std::fs::write(&temporary, text).map_err(fail)?;
893 std::fs::rename(&temporary, path).map_err(fail)?;
894 Ok(result)
895 })
896}
897
898#[derive(Debug, Clone, PartialEq, Eq)]
900pub struct CertCheck {
901 pub cert: String,
903 pub addresses: Vec<String>,
905 pub expires: Option<i64>,
908 pub expired: bool,
910 pub signs: bool,
912 pub decrypts: bool,
914}
915
916#[derive(Debug, Clone, PartialEq, Eq)]
918pub enum Mismatch {
919 Empty,
921 Missing,
923 Revoked,
925 NotForSigning,
927 NotForEncryption,
929 OtherPublicKey,
932 Invalid(String),
934}
935
936impl Mismatch {
937 pub fn sentence(&self, tr: &Translator) -> String {
939 match self {
940 Mismatch::Empty => tr.text("seckey-mismatch-empty", None),
941 Mismatch::Missing | Mismatch::OtherPublicKey => tr.text("seckey-mismatch", None),
942 Mismatch::Revoked => tr.text("seckey-mismatch-revoked", None),
943 Mismatch::NotForSigning | Mismatch::NotForEncryption => tr.text("seckey-mismatch-use", None),
944 Mismatch::Invalid(why) => say(tr, "seckey-mismatch-invalid", &[("detail", why.clone())]),
945 }
946 }
947}
948
949pub fn check(cert: &Cert, card: &CardInfo) -> Result<CertCheck, Mismatch> {
953 let policy = StandardPolicy::new();
954 let valid = cert.with_policy(&policy, None).map_err(|e| Mismatch::Invalid(e.to_string()))?;
955 if matches!(valid.revocation_status(), openpgp::types::RevocationStatus::Revoked(_)) {
956 return Err(Mismatch::Revoked);
957 }
958 if card.sign.is_none() && card.decrypt.is_none() {
959 return Err(Mismatch::Empty);
960 }
961 let unix = |t: SystemTime| t.duration_since(SystemTime::UNIX_EPOCH).ok().and_then(|d| i64::try_from(d.as_secs()).ok());
962 let mut expires: Option<i64> = valid.primary_key().key_expiration_time().and_then(unix);
963 let mut earliest = |at: Option<i64>| {
964 if let Some(at) = at {
965 expires = Some(expires.map_or(at, |e| e.min(at)));
966 }
967 };
968 let mut alive = valid.alive().is_ok();
969 for (slot, signing) in [(&card.sign, true), (&card.decrypt, false)] {
970 let Some(slot) = slot else { continue };
971 let key = valid.keys().find(|k| k.key().fingerprint().to_hex().eq_ignore_ascii_case(&slot.fingerprint)).ok_or(Mismatch::Missing)?;
972 if matches!(key.revocation_status(), openpgp::types::RevocationStatus::Revoked(_)) {
973 return Err(Mismatch::Revoked);
974 }
975 let allowed = if signing { key.for_signing() } else { key.for_transport_encryption() || key.for_storage_encryption() };
976 if !allowed {
977 return Err(if signing { Mismatch::NotForSigning } else { Mismatch::NotForEncryption });
978 }
979 if let Some(public) = &slot.public
980 && !same_public(key.key(), public)
981 {
982 return Err(Mismatch::OtherPublicKey);
983 }
984 earliest(key.key_expiration_time().and_then(unix));
985 alive &= key.alive().is_ok();
986 }
987 let mut addresses: Vec<String> = valid.userids().filter_map(|u| u.userid().email().ok().flatten().map(|e| e.to_ascii_lowercase())).collect();
988 addresses.sort();
989 addresses.dedup();
990 let now = unix(SystemTime::now()).unwrap_or(0);
991 Ok(CertCheck {
992 cert: cert.fingerprint().to_hex(),
993 addresses,
994 expires,
995 expired: !alive || expires.is_some_and(|at| at <= now),
996 signs: card.sign.is_some(),
997 decrypts: card.decrypt.is_some(),
998 })
999}
1000
1001pub fn certificate_in(bytes: &[u8], card: &CardInfo) -> Result<(Cert, CertCheck), Mismatch> {
1004 use openpgp::parse::Parse;
1005 let mut why = Mismatch::Missing;
1006 let parser = openpgp::cert::CertParser::from_bytes(bytes).map_err(|e| Mismatch::Invalid(e.to_string()))?;
1007 for cert in parser.filter_map(Result::ok) {
1008 match check(&cert, card) {
1009 Ok(found) => return Ok((cert.strip_secret_key_material(), found)),
1010 Err(Mismatch::Missing) => {}
1012 Err(other) => why = other,
1013 }
1014 }
1015 Err(why)
1016}
1017
1018pub fn adopt(card: &CardInfo, cert: &Cert, source: &str, addresses: Vec<String>, now: i64) -> Result<KnownCard, String> {
1022 crate::pgp::keep_card_certificate(cert)?;
1023 let mut known = KnownCard {
1024 ident: card.ident.clone(),
1025 manufacturer: card.manufacturer,
1026 serial: card.serial,
1027 sign: card.sign.as_ref().map(|s| s.fingerprint.clone()).unwrap_or_default(),
1028 decrypt: card.decrypt.as_ref().map(|s| s.fingerprint.clone()).unwrap_or_default(),
1029 cert: cert.fingerprint().to_hex(),
1030 addresses: addresses.into_iter().map(|a| a.trim().to_ascii_lowercase()).collect(),
1031 source: source.to_string(),
1032 checked: now,
1033 ..KnownCard::default()
1034 };
1035 known.update(card);
1036 let kept = known.clone();
1037 change_known(|cards| {
1038 cards.retain(|c| !c.ident.eq_ignore_ascii_case(&kept.ident));
1039 cards.push(kept);
1040 })?;
1041 Ok(known)
1042}
1043
1044pub fn forget(ident: &str) -> Result<(), String> {
1047 let cert = change_known(|cards| {
1048 let cert = cards.iter().find(|c| c.ident.eq_ignore_ascii_case(ident)).map(|c| c.cert.clone());
1049 cards.retain(|c| !c.ident.eq_ignore_ascii_case(ident));
1050 cert
1051 })?;
1052 match cert {
1053 Some(cert) => crate::pgp::remove(&cert),
1054 None => Ok(()),
1055 }
1056}
1057
1058fn same_public(key: &PublicKey, card: &CardPublic) -> bool {
1060 fn native(point: &[u8]) -> &[u8] {
1062 if point.len() == 33 && point[0] == 0x40 { &point[1..] } else { point }
1063 }
1064 fn number(bytes: &[u8]) -> &[u8] {
1065 let zeros = bytes.iter().take_while(|&&b| b == 0).count();
1066 &bytes[zeros..]
1067 }
1068 match (key.mpis(), card) {
1069 (mpi::PublicKey::RSA { e, n }, CardPublic::Rsa { n: card_n, e: card_e }) => number(n.value()) == number(card_n) && number(e.value()) == number(card_e),
1070 (mpi::PublicKey::EdDSA { q, .. } | mpi::PublicKey::ECDSA { q, .. } | mpi::PublicKey::ECDH { q, .. }, CardPublic::Ecc { point }) => native(q.value()) == native(point),
1071 (mpi::PublicKey::Ed25519 { a }, CardPublic::Ecc { point }) => a[..] == *native(point),
1072 (mpi::PublicKey::X25519 { u }, CardPublic::Ecc { point }) => u[..] == *native(point),
1073 _ => false,
1074 }
1075}
1076
1077pub fn expiry_of(cert: &Cert, fingerprints: &[&str]) -> Option<i64> {
1081 let policy = StandardPolicy::new();
1082 let valid = cert.with_policy(&policy, None).ok()?;
1083 let unix = |t: SystemTime| t.duration_since(SystemTime::UNIX_EPOCH).ok().and_then(|d| i64::try_from(d.as_secs()).ok());
1084 let keys = valid.keys().filter(|k| fingerprints.iter().any(|f| !f.is_empty() && k.key().fingerprint().to_hex().eq_ignore_ascii_case(f))).filter_map(|k| k.key_expiration_time().and_then(unix));
1085 valid.primary_key().key_expiration_time().and_then(unix).into_iter().chain(keys).min()
1086}
1087
1088#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1090pub enum Role {
1091 Primary,
1093 Signing,
1095 Encryption,
1097 Authentication,
1099 Other,
1101}
1102
1103#[derive(Debug, Clone, PartialEq, Eq)]
1106pub struct Part {
1107 pub role: Role,
1108 pub fingerprint: String,
1109 pub expires: Option<i64>,
1110}
1111
1112pub fn parts(cert: &Cert) -> Vec<Part> {
1116 let policy = StandardPolicy::new();
1117 let Ok(valid) = cert.with_policy(&policy, None) else { return Vec::new() };
1118 let unix = |t: SystemTime| t.duration_since(SystemTime::UNIX_EPOCH).ok().and_then(|d| i64::try_from(d.as_secs()).ok());
1119 let mut out = vec![Part { role: Role::Primary, fingerprint: cert.fingerprint().to_hex(), expires: valid.primary_key().key_expiration_time().and_then(unix) }];
1120 for key in valid.keys().subkeys() {
1121 if matches!(key.revocation_status(), openpgp::types::RevocationStatus::Revoked(_)) {
1122 continue;
1123 }
1124 let role = if key.for_transport_encryption() || key.for_storage_encryption() {
1125 Role::Encryption
1126 } else if key.for_signing() {
1127 Role::Signing
1128 } else if key.for_authentication() {
1129 Role::Authentication
1130 } else {
1131 Role::Other
1132 };
1133 out.push(Part { role, fingerprint: key.key().fingerprint().to_hex(), expires: key.key_expiration_time().and_then(unix) });
1134 }
1135 out.sort_by_key(|part| part.role as u8);
1137 out
1138}
1139
1140pub fn parts_in(bytes: &[u8]) -> Vec<Part> {
1142 use openpgp::parse::Parse;
1143 openpgp::cert::CertParser::from_bytes(bytes).ok().and_then(|mut certs| certs.find_map(Result::ok)).map(|cert| parts(&cert)).unwrap_or_default()
1144}
1145
1146pub fn short_fingerprint(fingerprint: &str) -> String {
1148 let hex: String = fingerprint.chars().filter(char::is_ascii_hexdigit).collect::<String>().to_ascii_uppercase();
1149 if hex.len() <= 8 { hex } else { format!("{}…{}", &hex[..4], &hex[hex.len() - 4..]) }
1150}
1151
1152pub fn expired_parts(tr: &Translator, parts: &[Part], now: i64) -> Vec<String> {
1156 parts
1157 .iter()
1158 .filter_map(|part| {
1159 let at = part.expires.filter(|at| *at <= now)?;
1160 let id = match part.role {
1161 Role::Primary => "seckey-part-expired-primary",
1162 Role::Signing => "seckey-part-expired-sign",
1163 Role::Encryption => "seckey-part-expired-encrypt",
1164 Role::Authentication => "seckey-part-expired-auth",
1165 Role::Other => "seckey-part-expired-other",
1166 };
1167 Some(say(tr, id, &[("fingerprint", short_fingerprint(&part.fingerprint)), ("date", full_date(tr, at))]))
1168 })
1169 .collect()
1170}
1171
1172pub fn parts_until(tr: &Translator, parts: &[Part]) -> String {
1175 parts
1176 .iter()
1177 .map(|part| {
1178 let name = tr.text(
1179 match part.role {
1180 Role::Primary => "seckey-part-primary",
1181 Role::Signing => "seckey-part-sign",
1182 Role::Encryption => "seckey-part-encrypt",
1183 Role::Authentication => "seckey-part-auth",
1184 Role::Other => "seckey-part-other",
1185 },
1186 None,
1187 );
1188 match part.expires {
1189 Some(at) => say(tr, "seckey-part-until", &[("part", name), ("date", full_date(tr, at))]),
1190 None => say(tr, "seckey-part-forever", &[("part", name)]),
1191 }
1192 })
1193 .collect::<Vec<_>>()
1194 .join(", ")
1195}
1196
1197pub fn expiry_words(tr: &Translator, cert: &Cert, expires: Option<i64>, now: i64) -> (String, bool, bool) {
1204 let parts = parts(cert);
1205 let expired = expired_parts(tr, &parts, now);
1206 if expired.is_empty() {
1207 return expiry_line(tr, expires, &cert.fingerprint().to_hex(), now).map_or((String::new(), false, false), |(line, late)| (line, late, false));
1208 }
1209 let primary_alive = parts.first().is_some_and(|p| p.expires.is_none_or(|at| at > now));
1210 let mut line = expired.join(" ");
1211 if primary_alive {
1212 line = format!("{line} {}", tr.text("seckey-subkeys-renew", None));
1213 }
1214 (line, true, true)
1215}
1216
1217pub fn subkeys_of(cert: &Cert) -> Vec<String> {
1220 parts(cert).into_iter().filter(|p| p.role != Role::Primary).map(|p| p.fingerprint).collect()
1221}
1222
1223pub fn full_date(tr: &Translator, at: i64) -> String {
1225 let Ok(stamp) = jiff::Timestamp::from_second(at) else { return String::new() };
1226 let date = stamp.to_zoned(jiff::tz::TimeZone::system()).date();
1227 format!("{} {}", tr.day_month(date), date.year())
1228}
1229
1230pub fn expiry_line(tr: &Translator, expires: Option<i64>, fingerprint: &str, now: i64) -> Option<(String, bool)> {
1233 let at = expires?;
1234 let date = full_date(tr, at);
1235 let pairs = [("date", date), ("fingerprint", fingerprint.to_string())];
1236 Some(if at <= now {
1237 (say(tr, "seckey-expired", &pairs), true)
1238 } else if at - now <= 31 * 24 * 3600 {
1239 (say(tr, "seckey-expires-soon", &pairs), true)
1240 } else {
1241 (say(tr, "seckey-valid-until", &pairs), false)
1242 })
1243}
1244
1245pub struct SoftCard {
1250 pub info: CardInfo,
1252 signer: Option<crypto::KeyPair>,
1253 decryptor: Option<crypto::KeyPair>,
1254 pin: Vec<u8>,
1255 x25519: Option<fn(&[u8; 32], &[u8; 32]) -> [u8; 32]>,
1257 verified_sign: bool,
1258 verified_decrypt: bool,
1259 pub fault: Option<CardError>,
1262 pub operations: usize,
1264 pub touch_delay: Duration,
1267}
1268
1269impl SoftCard {
1270 pub fn new(ident: &str, cert: &Cert, pin: &str) -> SoftCard {
1273 let policy = StandardPolicy::new();
1274 let pair = |signing: bool| {
1275 cert.keys()
1276 .unencrypted_secret()
1277 .with_policy(&policy, None)
1278 .find(|k| if signing { k.for_signing() } else { k.for_transport_encryption() })
1279 .and_then(|k| k.key().clone().into_keypair().ok())
1280 };
1281 let (signer, decryptor) = (pair(true), pair(false));
1282 let slot = |pair: &Option<crypto::KeyPair>| {
1283 pair.as_ref().map(|p| SlotInfo {
1284 fingerprint: p.public().fingerprint().to_hex(),
1285 created: openpgp::types::Timestamp::try_from(p.public().creation_time()).map(u32::from).unwrap_or(0),
1286 algorithm: algorithm_name(p.public()),
1287 touch: Touch::Off,
1288 public: card_public(p.public()),
1289 })
1290 };
1291 let (manufacturer, serial) = ident.split_once(':').map_or((0, 0), |(m, s)| (u16::from_str_radix(m, 16).unwrap_or(0), u32::from_str_radix(s, 16).unwrap_or(0)));
1292 SoftCard {
1293 info: CardInfo {
1294 ident: ident.to_string(),
1295 manufacturer,
1296 serial,
1297 holder: String::new(),
1298 url: String::new(),
1299 sign: slot(&signer),
1300 decrypt: slot(&decryptor),
1301 pin_tries: 3,
1302 reset_tries: 0,
1303 admin_tries: 3,
1304 pin_each_signature: false,
1305 signatures: 0,
1306 reader: format!("Soft reader {ident}"),
1307 },
1308 signer,
1309 decryptor,
1310 pin: pin.as_bytes().to_vec(),
1311 x25519: None,
1312 verified_sign: false,
1313 verified_decrypt: false,
1314 fault: None,
1315 operations: 0,
1316 touch_delay: Duration::ZERO,
1317 }
1318 }
1319
1320 pub fn generate(ident: &str, userids: &[&str], pin: &str) -> Result<(SoftCard, Vec<u8>), String> {
1325 use openpgp::serialize::SerializeInto;
1326 let mut builder = CertBuilder::new().set_cipher_suite(CipherSuite::Cv25519).set_primary_key_flags(openpgp::types::KeyFlags::empty().set_certification().set_signing()).add_transport_encryption_subkey();
1327 for userid in userids {
1328 builder = builder.add_userid(*userid);
1329 }
1330 let (cert, _) = builder.generate().map_err(|e| e.to_string())?;
1331 let public = cert.clone().strip_secret_key_material().armored().to_vec().map_err(|e| e.to_string())?;
1332 Ok((SoftCard::new(ident, &cert, pin), public))
1333 }
1334
1335 pub fn with_x25519(mut self, x25519: fn(&[u8; 32], &[u8; 32]) -> [u8; 32]) -> SoftCard {
1337 self.x25519 = Some(x25519);
1338 self
1339 }
1340
1341 fn close(&mut self) {
1343 self.verified_sign = false;
1344 self.verified_decrypt = false;
1345 }
1346}
1347
1348fn card_public(key: &PublicKey) -> Option<CardPublic> {
1350 let native = |q: &mpi::MPI| {
1351 let v = q.value();
1352 if v.len() == 33 && v[0] == 0x40 { v[1..].to_vec() } else { v.to_vec() }
1353 };
1354 match key.mpis() {
1355 mpi::PublicKey::RSA { e, n } => Some(CardPublic::Rsa { n: n.value().to_vec(), e: e.value().to_vec() }),
1356 mpi::PublicKey::EdDSA { q, .. } | mpi::PublicKey::ECDSA { q, .. } | mpi::PublicKey::ECDH { q, .. } => Some(CardPublic::Ecc { point: native(q) }),
1357 mpi::PublicKey::Ed25519 { a } => Some(CardPublic::Ecc { point: a.to_vec() }),
1358 _ => None,
1359 }
1360}
1361
1362impl Card for SoftCard {
1363 fn info(&mut self) -> Result<CardInfo, CardError> {
1364 Ok(self.info.clone())
1365 }
1366
1367 fn verify(&mut self, pin: &Password, mode: PinMode) -> Result<(), CardError> {
1368 if self.info.pin_tries == 0 {
1369 return Err(CardError::Blocked);
1370 }
1371 if pin.map(|p| p[..] == self.pin[..]) {
1372 self.info.pin_tries = 3;
1373 match mode {
1374 PinMode::Sign => self.verified_sign = true,
1375 PinMode::Decrypt => self.verified_decrypt = true,
1376 }
1377 Ok(())
1378 } else {
1379 self.info.pin_tries -= 1;
1380 Err(CardError::WrongPin { left: self.info.pin_tries })
1381 }
1382 }
1383
1384 fn sign(&mut self, input: ToSign<'_>) -> Result<Vec<u8>, CardError> {
1385 if !self.verified_sign {
1386 return Err(CardError::Other("security status not satisfied".into()));
1387 }
1388 if let Some(fault) = self.fault.take() {
1389 return Err(fault);
1390 }
1391 if self.info.sign.as_ref().is_some_and(|s| s.touch.asked()) {
1392 std::thread::sleep(self.touch_delay);
1393 }
1394 let pair = self.signer.as_mut().ok_or(CardError::NotOpenPgp)?;
1395 let (hash, digest) = match input {
1396 ToSign::Digest(digest) => (HashAlgorithm::SHA512, digest),
1397 ToSign::Rsa { hash, digest } => (hash, digest),
1398 };
1399 let signature = crypto::Signer::sign(pair, hash, digest).map_err(|e| CardError::Other(e.to_string()))?;
1400 let pad = |m: &mpi::MPI, size: usize| m.value_padded(size).map(|v| v.to_vec()).map_err(|e| CardError::Other(e.to_string()));
1401 let raw = match (&signature, pair.public().mpis()) {
1402 (mpi::Signature::RSA { s }, mpi::PublicKey::RSA { n, .. }) => pad(s, n.value().len())?,
1403 (mpi::Signature::EdDSA { r, s }, _) => [pad(r, 32)?, pad(s, 32)?].concat(),
1404 (mpi::Signature::Ed25519 { s }, _) => s.to_vec(),
1405 (mpi::Signature::ECDSA { r, s }, mpi::PublicKey::ECDSA { curve, .. }) => {
1406 let size = curve.field_size().map_err(|e| CardError::Other(e.to_string()))?;
1407 [pad(r, size)?, pad(s, size)?].concat()
1408 }
1409 _ => return Err(CardError::Algorithm(algorithm_name(pair.public()))),
1410 };
1411 if self.info.pin_each_signature {
1412 self.verified_sign = false;
1413 }
1414 self.info.signatures += 1;
1415 self.operations += 1;
1416 Ok(raw)
1417 }
1418
1419 fn decipher(&mut self, input: ToDecipher<'_>) -> Result<Protected, CardError> {
1420 if !self.verified_decrypt {
1421 return Err(CardError::Other("security status not satisfied".into()));
1422 }
1423 if let Some(fault) = self.fault.take() {
1424 return Err(fault);
1425 }
1426 if self.info.decrypt.as_ref().is_some_and(|s| s.touch.asked()) {
1427 std::thread::sleep(self.touch_delay);
1428 }
1429 let pair = self.decryptor.as_mut().ok_or(CardError::NotOpenPgp)?;
1430 let opened = match (input, pair.public().mpis()) {
1431 (ToDecipher::Rsa(c), mpi::PublicKey::RSA { .. }) => {
1432 let session = crypto::Decryptor::decrypt(pair, &mpi::Ciphertext::RSA { c: mpi::MPI::new(c) }, None).map_err(|e| CardError::Other(e.to_string()))?;
1433 Protected::from(&session[..])
1434 }
1435 (ToDecipher::Ecdh(point), mpi::PublicKey::ECDH { curve: Curve::Cv25519, .. }) => {
1436 let x25519 = self.x25519.ok_or_else(|| CardError::Algorithm("Curve25519".into()))?;
1437 let scalar = pair.secret().map(|secret| match secret {
1439 mpi::SecretKeyMaterial::ECDH { scalar } => {
1440 let mut native = scalar.value_padded(32).to_vec();
1441 native.reverse();
1442 <[u8; 32]>::try_from(native.as_slice()).ok()
1443 }
1444 _ => None,
1445 });
1446 let scalar = scalar.ok_or_else(|| CardError::Other("no Curve25519 scalar".into()))?;
1447 let point = <[u8; 32]>::try_from(point).map_err(|_| CardError::Other("malformed point".into()))?;
1448 Protected::from(&x25519(&scalar, &point)[..])
1449 }
1450 _ => return Err(CardError::Algorithm(algorithm_name(pair.public()))),
1451 };
1452 self.operations += 1;
1453 Ok(opened)
1454 }
1455}
1456
1457#[derive(Default)]
1460pub struct SoftReaders {
1461 pub cards: Mutex<Vec<SoftCard>>,
1463 pub busy: usize,
1465 pub failure: Option<CardError>,
1467}
1468
1469impl SoftReaders {
1470 pub fn with(cards: Vec<SoftCard>) -> SoftReaders {
1472 SoftReaders { cards: Mutex::new(cards), busy: 0, failure: None }
1473 }
1474
1475 pub fn card<R>(&self, ident: &str, f: impl FnOnce(&mut SoftCard) -> R) -> Option<R> {
1477 let mut cards = self.cards.lock().ok()?;
1478 cards.iter_mut().find(|c| c.info.ident == ident).map(f)
1479 }
1480}
1481
1482impl Readers for SoftReaders {
1483 fn present(&self) -> Result<Vec<String>, CardError> {
1484 if let Some(failure) = &self.failure {
1485 return Err(failure.clone());
1486 }
1487 Ok(self.cards.lock().map(|c| c.iter().map(|c| c.info.reader.clone()).collect()).unwrap_or_default())
1488 }
1489
1490 fn visit(&self, visit: &mut dyn FnMut(&mut dyn Card) -> Visit) -> Result<Visited, CardError> {
1491 if let Some(failure) = &self.failure {
1492 return Err(failure.clone());
1493 }
1494 let mut cards = self.cards.lock().map_err(|_| CardError::Other("poisoned".into()))?;
1495 let mut visited = Visited { opened: 0, busy: self.busy };
1496 for card in cards.iter_mut() {
1497 visited.opened += 1;
1498 let next = visit(card);
1499 card.close();
1500 if next == Visit::Done {
1501 break;
1502 }
1503 }
1504 Ok(visited)
1505 }
1506}
1507
1508#[cfg(test)]
1509pub(crate) mod x25519 {
1510 type Field = [i64; 16];
1515
1516 const A24: Field = [0xDB41, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
1517
1518 fn carry(o: &mut Field) {
1519 for i in 0..16 {
1520 o[i] += 1 << 16;
1521 let c = o[i] >> 16;
1522 if i < 15 {
1523 o[i + 1] += c - 1;
1524 } else {
1525 o[0] += 38 * (c - 1);
1526 }
1527 o[i] -= c << 16;
1528 }
1529 }
1530
1531 fn swap(p: &mut Field, q: &mut Field, bit: i64) {
1532 let c = !(bit - 1);
1533 for i in 0..16 {
1534 let t = c & (p[i] ^ q[i]);
1535 p[i] ^= t;
1536 q[i] ^= t;
1537 }
1538 }
1539
1540 fn pack(n: &Field) -> [u8; 32] {
1541 let mut t = *n;
1542 carry(&mut t);
1543 carry(&mut t);
1544 carry(&mut t);
1545 for _ in 0..2 {
1546 let mut m = [0i64; 16];
1547 m[0] = t[0] - 0xffed;
1548 for i in 1..15 {
1549 m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
1550 m[i - 1] &= 0xffff;
1551 }
1552 m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
1553 let b = (m[15] >> 16) & 1;
1554 m[14] &= 0xffff;
1555 swap(&mut t, &mut m, 1 - b);
1556 }
1557 let mut o = [0u8; 32];
1558 for i in 0..16 {
1559 o[2 * i] = (t[i] & 0xff) as u8;
1560 o[2 * i + 1] = ((t[i] >> 8) & 0xff) as u8;
1561 }
1562 o
1563 }
1564
1565 fn unpack(n: &[u8; 32]) -> Field {
1566 let mut o = [0i64; 16];
1567 for i in 0..16 {
1568 o[i] = i64::from(n[2 * i]) + (i64::from(n[2 * i + 1]) << 8);
1569 }
1570 o[15] &= 0x7fff;
1571 o
1572 }
1573
1574 fn add(a: &Field, b: &Field) -> Field {
1575 std::array::from_fn(|i| a[i] + b[i])
1576 }
1577
1578 fn sub(a: &Field, b: &Field) -> Field {
1579 std::array::from_fn(|i| a[i] - b[i])
1580 }
1581
1582 fn mul(a: &Field, b: &Field) -> Field {
1583 let mut t = [0i64; 31];
1584 for i in 0..16 {
1585 for j in 0..16 {
1586 t[i + j] += a[i] * b[j];
1587 }
1588 }
1589 for i in 0..15 {
1590 t[i] += 38 * t[i + 16];
1591 }
1592 let mut o: Field = std::array::from_fn(|i| t[i]);
1593 carry(&mut o);
1594 carry(&mut o);
1595 o
1596 }
1597
1598 fn invert(i: &Field) -> Field {
1599 let mut c = *i;
1600 for a in (0..=253).rev() {
1601 c = mul(&c, &c);
1602 if a != 2 && a != 4 {
1603 c = mul(&c, i);
1604 }
1605 }
1606 c
1607 }
1608
1609 pub(crate) fn x25519(scalar: &[u8; 32], u: &[u8; 32]) -> [u8; 32] {
1611 let mut z = *scalar;
1612 z[31] = (z[31] & 127) | 64;
1613 z[0] &= 248;
1614 let x = unpack(u);
1615 let (mut a, mut b, mut c, mut d) = ([0i64; 16], x, [0i64; 16], [0i64; 16]);
1616 a[0] = 1;
1617 d[0] = 1;
1618 for i in (0..=254usize).rev() {
1619 let r = i64::from((z[i >> 3] >> (i & 7)) & 1);
1620 swap(&mut a, &mut b, r);
1621 swap(&mut c, &mut d, r);
1622 let mut e = add(&a, &c);
1623 a = sub(&a, &c);
1624 c = add(&b, &d);
1625 b = sub(&b, &d);
1626 d = mul(&e, &e);
1627 let f = mul(&a, &a);
1628 a = mul(&c, &a);
1629 c = mul(&b, &e);
1630 e = add(&a, &c);
1631 a = sub(&a, &c);
1632 b = mul(&a, &a);
1633 c = sub(&d, &f);
1634 a = mul(&c, &A24);
1635 a = add(&a, &d);
1636 c = mul(&c, &a);
1637 a = mul(&d, &f);
1638 d = mul(&b, &x);
1639 b = mul(&e, &e);
1640 swap(&mut a, &mut b, r);
1641 swap(&mut c, &mut d, r);
1642 }
1643 pack(&mul(&a, &invert(&c)))
1644 }
1645
1646 #[test]
1647 fn rfc_7748_vector() {
1648 let hex = |s: &str| -> [u8; 32] { std::array::from_fn(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap()) };
1649 let out = x25519(&hex("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4"), &hex("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c"));
1650 assert_eq!(out, hex("c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552"));
1651 }
1652}
1653
1654#[cfg(test)]
1655mod tests {
1656 use super::*;
1657 use openpgp::cert::prelude::*;
1658 use openpgp::crypto::Signer as _;
1659 use openpgp::types::KeyFlags;
1660
1661 fn primary_signs(address: &str) -> Cert {
1663 CertBuilder::new()
1664 .add_userid(format!("Card Holder <{address}>"))
1665 .set_cipher_suite(CipherSuite::Cv25519)
1666 .set_primary_key_flags(KeyFlags::empty().set_certification().set_signing())
1667 .add_transport_encryption_subkey()
1668 .generate()
1669 .unwrap()
1670 .0
1671 }
1672
1673 fn touchless(_: &[u8; 32], _: &[u8; 32]) -> [u8; 32] {
1674 [0; 32]
1675 }
1676
1677 #[test]
1678 fn signs_and_checks_each_signature() {
1679 let cert = primary_signs("holder@example.org");
1680 let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456")]);
1681 let pin = Password::from("123456");
1682 let touched = std::sync::atomic::AtomicUsize::new(0);
1683 let touch = || {
1684 touched.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1685 };
1686 let access = CardAccess::new(&readers, &pin, &touch);
1687 let public = cert.primary_key().key().clone().role_into_unspecified();
1688 let mut signer = CardSigner::new(&access, "0006:12345678", public.clone());
1689 let digest = [7u8; 64];
1690 let signature = signer.sign(HashAlgorithm::SHA512, &digest).unwrap();
1691 public.verify(&signature, HashAlgorithm::SHA512, &digest).unwrap();
1692 assert_eq!(touched.load(std::sync::atomic::Ordering::Relaxed), 0, "no touch asked: none said");
1693 assert_eq!(access.reader().as_deref(), Some("Soft reader 0006:12345678"));
1694 readers.card("0006:12345678", |c| c.info.sign.as_mut().unwrap().touch = Touch::On);
1696 signer.sign(HashAlgorithm::SHA512, &digest).unwrap();
1697 assert_eq!(touched.load(std::sync::atomic::Ordering::Relaxed), 1);
1698 readers.card("0006:12345678", |c| c.fault = Some(CardError::TouchMissed));
1700 let fresh = CardAccess::new(&readers, &pin, &touch);
1701 assert!(CardSigner::new(&fresh, "0006:12345678", public.clone()).sign(HashAlgorithm::SHA512, &digest).is_err());
1702 assert_eq!(fresh.failure(), Some(CardError::TouchMissed));
1703 assert!(fresh.pin_accepted());
1705 let other = SoftReaders::with(vec![SoftCard::new("0006:87654321", &cert, "123456")]);
1707 let elsewhere = CardAccess::new(&other, &pin, &touch);
1708 assert!(CardSigner::new(&elsewhere, "0006:12345678", public).sign(HashAlgorithm::SHA512, &digest).is_err());
1709 assert_eq!(elsewhere.failure(), Some(CardError::Absent));
1710 }
1711
1712 #[test]
1713 fn a_wrong_pin_costs_a_try_and_three_lock_the_key() {
1714 let cert = primary_signs("holder@example.org");
1715 let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456")]);
1716 let public = cert.primary_key().key().clone().role_into_unspecified();
1717 let wrong = Password::from("000000");
1718 let access = CardAccess::new(&readers, &wrong, &|| {});
1719 let mut signer = CardSigner::new(&access, "0006:12345678", public.clone());
1720 for left in [2, 1, 0] {
1721 assert!(signer.sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
1722 assert_eq!(access.failure(), Some(CardError::WrongPin { left }));
1723 }
1724 assert!(!access.pin_accepted());
1725 assert!(signer.sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
1726 assert_eq!(access.failure(), Some(CardError::Blocked));
1727 let right = Password::from("123456");
1729 let access = CardAccess::new(&readers, &right, &|| {});
1730 assert!(CardSigner::new(&access, "0006:12345678", public).sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
1731 assert_eq!(access.failure(), Some(CardError::Blocked));
1732 let tr = Translator::new("en");
1733 assert!(CardError::WrongPin { left: 2 }.sentence(&tr, Purpose::Sign).contains("2 tries left"));
1734 assert!(CardError::WrongPin { left: 1 }.sentence(&tr, Purpose::Sign).contains("one more mistake locks the key"));
1735 assert!(CardError::WrongPin { left: 0 }.sentence(&tr, Purpose::Sign).contains("Admin PIN"));
1736 assert_eq!(CardError::WrongPin { left: 1 }.remedy(), Remedy::Pin);
1737 assert_eq!(CardError::Blocked.remedy(), Remedy::None);
1738 }
1739
1740 #[test]
1741 fn the_pin_for_each_signature_is_given_each_time() {
1742 let cert = primary_signs("holder@example.org");
1743 let mut card = SoftCard::new("0006:12345678", &cert, "123456");
1744 card.info.pin_each_signature = true;
1745 let pin = Password::from("123456");
1746 card.verify(&pin, PinMode::Sign).unwrap();
1747 card.sign(ToSign::Digest(&[1; 64])).unwrap();
1748 assert!(card.sign(ToSign::Digest(&[1; 64])).is_err());
1750 let readers = SoftReaders::with(vec![card]);
1751 let access = CardAccess::new(&readers, &pin, &|| {});
1752 let mut signer = CardSigner::new(&access, "0006:12345678", cert.primary_key().key().clone().role_into_unspecified());
1753 signer.sign(HashAlgorithm::SHA512, &[2; 64]).unwrap();
1754 signer.sign(HashAlgorithm::SHA512, &[3; 64]).unwrap();
1755 }
1756
1757 #[test]
1758 fn rsa_keys_sign_with_a_digest_info() {
1759 let (cert, _) = CertBuilder::new().add_userid("Card Holder <rsa@example.org>").set_cipher_suite(CipherSuite::RSA2k).set_primary_key_flags(KeyFlags::empty().set_certification().set_signing()).add_transport_encryption_subkey().generate().unwrap();
1760 let readers = SoftReaders::with(vec![SoftCard::new("000F:00001A2B", &cert, "123456")]);
1761 let pin = Password::from("123456");
1762 let access = CardAccess::new(&readers, &pin, &|| {});
1763 let public = cert.primary_key().key().clone().role_into_unspecified();
1764 let mut signer = CardSigner::new(&access, "000F:00001A2B", public.clone());
1765 assert!(!signer.acceptable_hashes().contains(&HashAlgorithm::SHA1));
1766 let digest = [9u8; 32];
1767 let signature = signer.sign(HashAlgorithm::SHA256, &digest).unwrap();
1768 public.verify(&signature, HashAlgorithm::SHA256, &digest).unwrap();
1769 let subkey = cert.keys().subkeys().next().unwrap().key().clone().role_into_unspecified();
1771 let session = SessionKey::from(&[1u8; 32][..]);
1772 let sealed = subkey.encrypt(&session).unwrap();
1773 let mut decryptor = CardDecryptor::new(&access, "000F:00001A2B", subkey);
1774 let opened = crypto::Decryptor::decrypt(&mut decryptor, &sealed, None).unwrap();
1775 assert_eq!(&opened[..], &session[..]);
1776 }
1777
1778 #[test]
1779 fn curve25519_opens_through_the_cards_shared_secret() {
1780 let cert = primary_signs("holder@example.org");
1781 let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456").with_x25519(x25519::x25519)]);
1782 let pin = Password::from("123456");
1783 let access = CardAccess::new(&readers, &pin, &|| {});
1784 let subkey = cert.keys().subkeys().next().unwrap().key().clone().role_into_unspecified();
1785 let session = SessionKey::from(&[5u8; 32][..]);
1786 let sealed = subkey.encrypt(&session).unwrap();
1787 let mut decryptor = CardDecryptor::new(&access, "0006:12345678", subkey.clone());
1788 let opened = crypto::Decryptor::decrypt(&mut decryptor, &sealed, None).unwrap();
1789 assert_eq!(&opened[..], &session[..]);
1790 let wrong = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456").with_x25519(touchless)]);
1792 let access = CardAccess::new(&wrong, &pin, &|| {});
1793 let mut decryptor = CardDecryptor::new(&access, "0006:12345678", subkey);
1794 assert!(crypto::Decryptor::decrypt(&mut decryptor, &sealed, None).is_err());
1795 }
1796
1797 #[test]
1798 fn a_key_held_elsewhere_or_no_service_is_said() {
1799 let cert = primary_signs("holder@example.org");
1800 let mut readers = SoftReaders::with(vec![]);
1801 assert_eq!(cards(&readers), Err(CardError::Absent));
1802 readers.busy = 1;
1803 assert_eq!(cards(&readers), Err(CardError::Busy));
1804 assert_eq!(with_card(&readers, "0006:12345678", |_, _| Ok(())), Err(CardError::Busy));
1805 readers.failure = Some(CardError::NoService);
1806 assert_eq!(cards(&readers), Err(CardError::NoService));
1807 let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456")]);
1808 let found = cards(&readers).unwrap();
1809 assert_eq!(found[0].ident, "0006:12345678");
1810 assert_eq!(found[0].sign.as_ref().unwrap().algorithm, "Ed25519");
1811 assert_eq!(found[0].decrypt.as_ref().unwrap().algorithm, "Curve25519");
1812 let tr = Translator::new("en");
1813 assert_eq!(found[0].label(&tr), "YubiKey 12\u{202F}345\u{202F}678");
1814 assert_eq!(serial_text(0x0A1B2C3D), "A1B2C3D");
1815 assert_eq!(CardError::Busy.remedy(), Remedy::Release);
1816 assert!(CardError::NoService.sentence(&tr, Purpose::Sign).contains("smart card service"));
1817 }
1818
1819 #[test]
1820 fn the_system_names_its_own_command() {
1821 assert_eq!(hint_for_os_release("NAME=\"Fedora Linux\"\nID=fedora\n"), "seckey-hint-fedora");
1822 assert_eq!(hint_for_os_release("ID=linuxmint\nID_LIKE=\"ubuntu debian\"\n"), "seckey-hint-debian");
1823 assert_eq!(hint_for_os_release("ID=endeavouros\nID_LIKE=arch\n"), "seckey-hint-arch");
1824 assert_eq!(hint_for_os_release("ID=\"opensuse-tumbleweed\"\nID_LIKE=\"opensuse suse\"\n"), "seckey-hint-suse");
1825 assert_eq!(hint_for_os_release("ID=gentoo\n"), "seckey-hint-linux");
1826 }
1827
1828 #[test]
1829 fn a_certificate_is_checked_against_the_key() {
1830 let cert = primary_signs("holder@example.org");
1831 let card = SoftCard::new("0006:12345678", &cert, "123456");
1832 let found = check(&cert.clone().strip_secret_key_material(), &card.info).unwrap();
1833 assert_eq!(found.addresses, vec!["holder@example.org".to_string()]);
1834 assert!(found.signs && found.decrypts && !found.expired);
1835 assert_eq!(found.cert, cert.fingerprint().to_hex());
1836 let stranger = primary_signs("holder@example.org");
1838 assert_eq!(check(&stranger, &card.info), Err(Mismatch::Missing));
1839 let mut forged = card.info.clone();
1841 forged.sign.as_mut().unwrap().public = card_public(stranger.primary_key().key().role_as_unspecified());
1842 assert_eq!(check(&cert, &forged), Err(Mismatch::OtherPublicKey));
1843 let mut swapped = card.info.clone();
1845 swapped.sign = card.info.decrypt.clone();
1846 assert_eq!(check(&cert, &swapped), Err(Mismatch::NotForSigning));
1847 let (revoked, revocation) = CertBuilder::new().add_userid("<gone@example.org>").set_cipher_suite(CipherSuite::Cv25519).set_primary_key_flags(KeyFlags::empty().set_certification().set_signing()).add_transport_encryption_subkey().generate().unwrap();
1849 let card_of_revoked = SoftCard::new("0006:00000001", &revoked, "123456");
1850 let revoked = revoked.insert_packets(revocation).unwrap().0;
1851 assert_eq!(check(&revoked, &card_of_revoked.info), Err(Mismatch::Revoked));
1852 let bare = Cert::from_packets(cert.clone().strip_secret_key_material().into_packets().filter(|p| !matches!(p, openpgp::Packet::UserID(_)) && !matches!(p, openpgp::Packet::Signature(s) if s.typ() == openpgp::types::SignatureType::PositiveCertification))).unwrap();
1854 match check(&bare, &card.info) {
1855 Ok(found) => assert!(found.addresses.is_empty()),
1856 Err(Mismatch::Invalid(_)) => {}
1858 Err(other) => panic!("{other:?}"),
1859 }
1860 }
1861
1862 #[test]
1866 fn each_part_of_the_key_and_its_expiry_said_exactly() {
1867 let tr = Translator::new("en");
1868 let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64;
1869 let made = SystemTime::now() - Duration::from_secs(3 * 365 * 24 * 3600);
1870 let (old, _) = CertBuilder::new()
1871 .add_userid("Card Holder <late@example.org>")
1872 .set_cipher_suite(CipherSuite::Cv25519)
1873 .set_creation_time(made)
1874 .set_validity_period(Duration::from_secs(365 * 24 * 3600))
1875 .set_primary_key_flags(KeyFlags::empty().set_certification().set_signing())
1876 .add_transport_encryption_subkey()
1877 .add_authentication_subkey()
1878 .generate()
1879 .unwrap();
1880 let roles: Vec<Role> = parts(&old).iter().map(|p| p.role).collect();
1881 assert_eq!(roles, [Role::Primary, Role::Encryption, Role::Authentication]);
1882 let (line, warm, expired) = expiry_words(&tr, &old, None, now);
1884 assert!(warm && expired);
1885 let primary = short_fingerprint(&old.fingerprint().to_hex());
1886 assert!(line.starts_with(&format!("Its primary key ({primary}) expired on ")), "{line}");
1887 assert!(line.contains("Its encryption subkey (") && line.contains("nobody can encrypt to you with it.") && line.contains("Its authentication subkey ("), "{line}");
1888 assert!(!line.contains("Renew for two years"), "{line}");
1889 let mut signer = old.primary_key().key().clone().parts_into_secret().unwrap().into_keypair().unwrap();
1891 let policy = StandardPolicy::new();
1892 let renewed = old.primary_key().with_policy(&policy, None).unwrap().set_expiration_time(&mut signer, Some(SystemTime::now() + Duration::from_secs(2 * 365 * 24 * 3600))).unwrap();
1893 let half = old.clone().insert_packets(renewed).unwrap().0;
1894 let (line, warm, expired) = expiry_words(&tr, &half, None, now);
1895 assert!(warm && expired);
1896 let encryption = short_fingerprint(&parts(&half)[1].fingerprint);
1897 assert!(line.starts_with(&format!("Its encryption subkey ({encryption}) expired on ")) && !line.contains("Its primary key"), "{line}");
1898 assert!(line.ends_with("Renew for two years renews them with its primary key. By hand, name each subkey: GnuPG's \"*\" leaves those already expired as they are."), "{line}");
1899 assert_eq!(subkeys_of(&half).len(), 2);
1900 let (fresh, _) = CertBuilder::general_purpose(Some("Card Holder <now@example.org>")).set_validity_period(Duration::from_secs(365 * 24 * 3600)).generate().unwrap();
1902 let (line, warm, expired) = expiry_words(&tr, &fresh, Some(now + 300 * 86_400), now);
1903 assert!(!warm && !expired && line.starts_with("Valid until "), "{line}");
1904 assert!(parts_until(&tr, &parts(&fresh)).starts_with("primary key until "));
1905 assert_eq!(short_fingerprint("b81a 2c3d 4e5f 6a7b 8c9d 0e1f 2a3b 4c5d 6e7f bf8c"), "B81A…BF8C");
1906 }
1907
1908 #[test]
1909 fn an_expired_key_is_still_its_key_and_says_so() {
1910 let made = SystemTime::now() - Duration::from_secs(3 * 365 * 24 * 3600);
1911 let (cert, _) = CertBuilder::new()
1912 .add_userid("Card Holder <late@example.org>")
1913 .set_cipher_suite(CipherSuite::Cv25519)
1914 .set_creation_time(made)
1915 .set_validity_period(Duration::from_secs(365 * 24 * 3600))
1916 .set_primary_key_flags(KeyFlags::empty().set_certification().set_signing())
1917 .add_transport_encryption_subkey()
1918 .generate()
1919 .unwrap();
1920 let card = SoftCard::new("0006:12345678", &cert, "123456");
1921 let found = check(&cert, &card.info).unwrap();
1922 assert!(found.expired);
1923 let tr = Translator::new("en");
1924 let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64;
1925 let (line, warm) = expiry_line(&tr, found.expires, &found.cert, now).unwrap();
1926 assert!(warm && line.contains("expired on") && line.contains("Accounts ▸ Encryption") && !line.contains("gpg "), "{line}");
1928 let (soon, warm) = expiry_line(&tr, Some(now + 10 * 24 * 3600), "F", now).unwrap();
1929 assert!(warm && soon.contains("expires on"), "{soon}");
1930 let (later, warm) = expiry_line(&tr, Some(now + 300 * 24 * 3600), "F", now).unwrap();
1931 assert!(!warm && later.contains("until"), "{later}");
1932 assert_eq!(expiry_line(&tr, None, "F", now), None);
1933 }
1934
1935 #[test]
1936 fn the_pin_is_held_fifteen_quiet_minutes() {
1937 let start = Instant::now();
1938 let mut memory = PinMemory::new();
1939 memory.keep("0006:12345678", Password::from("123456"), "Yubico YubiKey", start);
1940 assert!(memory.get("0006:12345678", start + Duration::from_secs(60)).is_some());
1941 assert!(memory.get("0006:87654321", start).is_none());
1942 memory.used("0006:12345678", start + Duration::from_secs(14 * 60));
1944 assert!(memory.get("0006:12345678", start + Duration::from_secs(28 * 60)).is_some());
1945 assert!(memory.get("0006:12345678", start + Duration::from_secs(29 * 60 + 1)).is_none());
1946 assert_eq!(memory.holder(), None);
1947 memory.keep("0006:12345678", Password::from("123456"), "Yubico YubiKey", start);
1949 assert!(!memory.went_away(&["Yubico YubiKey".to_string()]));
1950 assert!(memory.went_away(&[]));
1951 assert_eq!(memory.holder(), None);
1952 memory.keep("0006:12345678", Password::from("123456"), "Yubico YubiKey", start);
1953 memory.forget();
1954 assert!(memory.get("0006:12345678", start).is_none());
1955 }
1956
1957 #[test]
1958 fn known_keys_are_kept_without_secrets() {
1959 let dir = std::env::temp_dir().join(format!("sioul-cards-{}", std::process::id()));
1960 let path = dir.join("cards.toml");
1961 let _ = std::fs::remove_file(&path);
1962 change_known_at(&path, |cards| {
1963 cards.push(KnownCard { ident: "0006:12345678".into(), manufacturer: 6, serial: 0x12345678, cert: "ABCD".into(), addresses: vec!["Holder@Example.org".into()], touch_sign: Touch::On, ..KnownCard::default() });
1964 })
1965 .unwrap();
1966 let read = read_known(&path);
1967 assert_eq!(read.len(), 1);
1968 assert!(read[0].serves("holder@example.org") && !read[0].serves("other@example.org"));
1969 assert_eq!(read[0].touch_sign, Touch::On);
1970 let text = std::fs::read_to_string(&path).unwrap();
1971 assert!(text.contains("[[card]]") && text.contains("touch_sign = \"on\""), "{text}");
1972 let tr = Translator::new("en");
1973 let mut known = read[0].clone();
1974 assert_eq!(known.tries_line(3, &tr), None);
1975 assert!(known.tries_line(2, &tr).unwrap().contains("2 tries left"));
1976 known.pin_tries_full = 5;
1977 assert!(known.tries_line(4, &tr).is_some());
1978 std::fs::remove_dir_all(&dir).unwrap();
1979 }
1980
1981 #[test]
1982 fn every_failure_has_words_in_each_language() {
1983 for language in ["en", "fr"] {
1984 let tr = Translator::new(language);
1985 let failures = [
1986 CardError::Unsupported,
1987 CardError::NoService,
1988 CardError::Absent,
1989 CardError::Busy,
1990 CardError::WrongPin { left: 2 },
1991 CardError::WrongPin { left: 1 },
1992 CardError::Blocked,
1993 CardError::TouchMissed,
1994 CardError::Removed,
1995 CardError::NotOpenPgp,
1996 CardError::OtherKeys,
1997 CardError::BadSignature,
1998 CardError::Algorithm("X448".into()),
1999 CardError::Other("no reader".into()),
2000 ];
2001 for failure in failures {
2002 for purpose in [Purpose::Sign, Purpose::Open, Purpose::Read] {
2003 let said = failure.sentence(&tr, purpose);
2004 assert!(!said.is_empty() && !said.contains("seckey-") && !said.contains('!'), "{language} {failure:?}: {said}");
2005 }
2006 }
2007 for mismatch in [Mismatch::Empty, Mismatch::Missing, Mismatch::Revoked, Mismatch::NotForSigning, Mismatch::OtherPublicKey, Mismatch::Invalid("SHA-1".into())] {
2008 assert!(!mismatch.sentence(&tr).contains("seckey-"), "{language} {mismatch:?}");
2009 }
2010 for hint in ["seckey-hint-fedora", "seckey-hint-debian", "seckey-hint-arch", "seckey-hint-suse", "seckey-hint-linux", "seckey-hint-flatpak", "seckey-hint-windows", "seckey-hint-macos"] {
2011 assert_ne!(tr.text(hint, None), hint, "{language} {hint}");
2012 }
2013 }
2014 }
2015}