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::parse::Parse;
41use openpgp::policy::StandardPolicy;
42use openpgp::serialize::SerializeInto;
43use openpgp::types::Curve;
44use sequoia_openpgp as openpgp;
45use serde::{Deserialize, Serialize};
46use std::path::{Path, PathBuf};
47use std::sync::Mutex;
48use std::time::{Duration, Instant, SystemTime};
49
50pub use openpgp::Cert as Certificate;
52pub use openpgp::crypto::Password;
54pub use openpgp::crypto::mem::Protected;
56pub use openpgp::types::HashAlgorithm;
58
59pub type PublicKey = Key<key::PublicParts, key::UnspecifiedRole>;
61
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "kebab-case")]
66pub enum Touch {
67 #[default]
69 Off,
70 On,
72 Fixed,
74 Cached,
76 CachedFixed,
78 Unknown,
80}
81
82impl Touch {
83 pub fn from_flag(flag: u8) -> Touch {
86 match flag {
87 0 => Touch::Off,
88 1 => Touch::On,
89 2 => Touch::Fixed,
90 3 => Touch::Cached,
91 4 => Touch::CachedFixed,
92 _ => Touch::Unknown,
93 }
94 }
95
96 pub fn asked(self) -> bool {
98 self != Touch::Off
99 }
100
101 pub fn words(self, tr: &Translator) -> Option<String> {
107 match self {
108 Touch::Off => None,
109 Touch::Unknown => Some(tr.text("seckey-touch-maybe", None)),
110 _ => Some(tr.text("seckey-touch", None)),
111 }
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum PinMode {
119 Sign,
121 Decrypt,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum Purpose {
128 Sign,
130 Open,
132 Read,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub enum CardPublic {
139 Rsa {
141 n: Vec<u8>,
143 e: Vec<u8>,
145 },
146 Ecc {
148 point: Vec<u8>,
150 },
151}
152
153#[derive(Debug, Clone, Default, PartialEq, Eq)]
155pub struct SlotInfo {
156 pub fingerprint: String,
159 pub created: u32,
161 pub algorithm: String,
163 pub touch: Touch,
165 pub public: Option<CardPublic>,
167}
168
169#[derive(Debug, Clone, Default, PartialEq, Eq)]
171pub struct CardInfo {
172 pub ident: String,
175 pub manufacturer: u16,
177 pub serial: u32,
179 pub holder: String,
181 pub url: String,
183 pub sign: Option<SlotInfo>,
185 pub decrypt: Option<SlotInfo>,
187 pub pin_tries: u8,
189 pub reset_tries: u8,
191 pub admin_tries: u8,
193 pub pin_each_signature: bool,
195 pub signatures: u32,
197 pub reader: String,
200}
201
202impl CardInfo {
203 pub fn label(&self, tr: &Translator) -> String {
205 label(self.manufacturer, self.serial, tr)
206 }
207
208 pub fn slot_of(&self, fingerprint: &str) -> Option<&SlotInfo> {
210 [&self.sign, &self.decrypt].into_iter().flatten().find(|s| s.fingerprint.eq_ignore_ascii_case(fingerprint))
211 }
212}
213
214pub fn label(manufacturer: u16, serial: u32, tr: &Translator) -> String {
217 let maker = match manufacturer {
218 0x0006 => "YubiKey".to_string(),
219 0x000F => "Nitrokey".to_string(),
220 0xF1D0 => "CanoKey".to_string(),
221 0xF517 => "Gnuk".to_string(),
222 0x0005 => "ZeitControl".to_string(),
223 _ => tr.text("seckey-generic", None),
224 };
225 format!("{maker} {}", serial_text(serial))
226}
227
228pub fn serial_text(serial: u32) -> String {
232 let digits = format!("{serial:08X}");
233 let digits = digits.trim_start_matches('0');
234 let digits = if digits.is_empty() { "0" } else { digits };
235 if !digits.chars().all(|c| c.is_ascii_digit()) {
236 return digits.to_string();
237 }
238 let mut out = String::new();
239 for (i, c) in digits.chars().enumerate() {
240 if i > 0 && (digits.len() - i) % 3 == 0 {
241 out.push('\u{202F}');
242 }
243 out.push(c);
244 }
245 out
246}
247
248pub fn algorithm_name(key: &PublicKey) -> String {
251 let curve = |curve: &Curve| match curve {
252 Curve::Ed25519 => "Ed25519".to_string(),
253 Curve::Cv25519 => "Curve25519".to_string(),
254 Curve::NistP256 => "NIST P-256".to_string(),
255 Curve::NistP384 => "NIST P-384".to_string(),
256 Curve::NistP521 => "NIST P-521".to_string(),
257 Curve::BrainpoolP256 => "brainpoolP256r1".to_string(),
258 Curve::BrainpoolP384 => "brainpoolP384r1".to_string(),
259 Curve::BrainpoolP512 => "brainpoolP512r1".to_string(),
260 other => other.to_string(),
261 };
262 match key.mpis() {
263 mpi::PublicKey::RSA { n, .. } => format!("RSA {}", n.bits()),
264 mpi::PublicKey::EdDSA { curve: c, .. } | mpi::PublicKey::ECDSA { curve: c, .. } | mpi::PublicKey::ECDH { curve: c, .. } => curve(c),
265 mpi::PublicKey::Ed25519 { .. } => "Ed25519".into(),
266 mpi::PublicKey::X25519 { .. } => "X25519".into(),
267 _ => key.pk_algo().to_string(),
268 }
269}
270
271#[derive(Debug, Clone, PartialEq, Eq)]
273pub enum CardError {
274 Unsupported,
276 NoService,
279 Absent,
281 Busy,
284 WrongPin {
286 left: u8,
288 },
289 Blocked,
291 TouchMissed,
293 Removed,
295 NotOpenPgp,
297 NotYours,
299 NoNfc,
301 NfcOff,
303 LastTry,
306 UsbRefused,
308 UsbBusy,
310 OtherKeys,
312 BadSignature,
314 Algorithm(String),
316 Other(String),
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub enum Remedy {
323 None,
325 Again,
327 Release,
329 Pin,
331 NfcSettings,
333}
334
335impl CardError {
336 pub fn sentence(&self, tr: &Translator, purpose: Purpose) -> String {
338 match self {
339 CardError::Unsupported => tr.text("seckey-unsupported", None),
340 CardError::NoService => say(tr, "seckey-no-service", &[("hint", no_service_hint(tr))]),
341 CardError::Absent => tr.text(
342 match purpose {
343 Purpose::Sign => "seckey-absent-sign",
344 Purpose::Open => "seckey-absent-open",
345 Purpose::Read => "seckey-absent",
346 },
347 None,
348 ),
349 CardError::Busy => tr.text("seckey-busy", None),
350 CardError::WrongPin { left: 0 } | CardError::Blocked => tr.text("seckey-blocked", None),
351 CardError::WrongPin { left } => tr.text("seckey-wrong-pin", Some(&tr.counted(usize::from(*left)))),
352 CardError::TouchMissed => tr.text("seckey-touch-missed", None),
353 CardError::Removed => tr.text(if purpose == Purpose::Sign { "seckey-removed-sign" } else { "seckey-removed" }, None),
354 CardError::NotOpenPgp => tr.text("seckey-not-openpgp", None),
355 CardError::NotYours => tr.text("seckey-not-yours", None),
356 CardError::NoNfc => tr.text("seckey-no-nfc", None),
357 CardError::NfcOff => tr.text("seckey-nfc-off", None),
358 CardError::LastTry => tr.text("seckey-last-try", None),
359 CardError::UsbRefused => tr.text("seckey-usb-refused", None),
360 CardError::UsbBusy => tr.text("seckey-usb-busy", None),
361 CardError::OtherKeys => tr.text("seckey-other-keys", None),
362 CardError::BadSignature => tr.text("seckey-bad-signature", None),
363 CardError::Algorithm(algorithm) => say(tr, "seckey-algorithm", &[("algorithm", algorithm.clone())]),
364 CardError::Other(detail) => say(tr, "seckey-other", &[("detail", detail.clone())]),
365 }
366 }
367
368 pub fn remedy(&self) -> Remedy {
370 match self {
371 CardError::Busy => Remedy::Release,
372 CardError::WrongPin { left } if *left > 0 => Remedy::Pin,
373 CardError::LastTry => Remedy::Pin,
374 CardError::NoService | CardError::Absent | CardError::TouchMissed | CardError::Removed | CardError::NotYours | CardError::NotOpenPgp | CardError::UsbRefused | CardError::UsbBusy | CardError::Other(_) => Remedy::Again,
375 CardError::NfcOff => Remedy::NfcSettings,
376 _ => Remedy::None,
377 }
378 }
379}
380
381impl std::fmt::Display for CardError {
382 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383 write!(f, "{self:?}")
384 }
385}
386
387impl std::error::Error for CardError {}
388
389fn say(tr: &Translator, id: &str, pairs: &[(&str, String)]) -> String {
390 let mut args = crate::i18n::args();
391 for (name, value) in pairs {
392 args.set(*name, value.clone());
393 }
394 tr.text(id, Some(&args))
395}
396
397fn no_service_hint(tr: &Translator) -> String {
399 let id = if cfg!(windows) {
400 "seckey-hint-windows"
401 } else if cfg!(target_os = "macos") {
402 "seckey-hint-macos"
403 } else if Path::new("/.flatpak-info").exists() {
404 "seckey-hint-flatpak"
405 } else {
406 hint_for_os_release(&std::fs::read_to_string("/etc/os-release").unwrap_or_default())
407 };
408 tr.text(id, None)
409}
410
411fn hint_for_os_release(text: &str) -> &'static str {
413 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();
414 let family = format!("{} {}", value("ID"), value("ID_LIKE"));
415 let has = |words: &[&str]| family.split_whitespace().any(|w| words.contains(&w));
416 if has(&["fedora", "rhel", "centos"]) {
417 "seckey-hint-fedora"
418 } else if has(&["debian", "ubuntu"]) {
419 "seckey-hint-debian"
420 } else if has(&["arch"]) {
421 "seckey-hint-arch"
422 } else if has(&["suse", "opensuse", "opensuse-leap", "opensuse-tumbleweed"]) {
423 "seckey-hint-suse"
424 } else {
425 "seckey-hint-linux"
426 }
427}
428
429#[derive(Debug, Clone, Copy)]
434pub enum ToSign<'a> {
435 Digest(&'a [u8]),
437 Rsa {
439 hash: HashAlgorithm,
441 digest: &'a [u8],
443 },
444}
445
446#[derive(Debug, Clone, Copy)]
450pub enum ToDecipher<'a> {
451 Rsa(&'a [u8]),
453 Ecdh(&'a [u8]),
455}
456
457pub trait Card {
460 fn info(&mut self) -> Result<CardInfo, CardError>;
462 fn verify(&mut self, pin: &Password, mode: PinMode) -> Result<(), CardError>;
464 fn sign(&mut self, input: ToSign<'_>) -> Result<Vec<u8>, CardError>;
466 fn decipher(&mut self, input: ToDecipher<'_>) -> Result<Protected, CardError>;
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
472pub enum Visit {
473 Next,
475 Done,
477}
478
479#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
481pub struct Visited {
482 pub opened: usize,
484 pub busy: usize,
486}
487
488pub trait Readers: Send + Sync {
491 fn present(&self) -> Result<Vec<String>, CardError>;
494 fn visit(&self, visit: &mut dyn FnMut(&mut dyn Card) -> Visit) -> Result<Visited, CardError>;
497 fn wait_change(&self, timeout: Duration) {
500 std::thread::sleep(timeout);
501 }
502 fn taps(&self) -> bool {
506 false
507 }
508 fn want(&self, _wanted: bool) {}
511 fn by_cable(&self) -> bool {
516 false
517 }
518 fn reach_note(&self) -> Option<CardError> {
521 None
522 }
523}
524
525pub struct NoReaders;
527
528impl Readers for NoReaders {
529 fn present(&self) -> Result<Vec<String>, CardError> {
530 Err(CardError::Unsupported)
531 }
532
533 fn visit(&self, _visit: &mut dyn FnMut(&mut dyn Card) -> Visit) -> Result<Visited, CardError> {
534 Err(CardError::Unsupported)
535 }
536}
537
538pub fn cards(readers: &dyn Readers) -> Result<Vec<CardInfo>, CardError> {
540 let mut found = Vec::new();
541 let mut failure = None;
542 let visited = readers.visit(&mut |card| {
543 match card.info() {
544 Ok(info) => found.push(info),
545 Err(e) => failure = Some(e),
546 }
547 Visit::Next
548 })?;
549 if found.is_empty() {
550 return Err(if visited.busy > 0 {
551 CardError::Busy
552 } else if let Some(e) = failure {
553 e
554 } else {
555 CardError::Absent
556 });
557 }
558 Ok(found)
559}
560
561pub fn with_card<R>(readers: &dyn Readers, ident: &str, work: impl FnOnce(&mut dyn Card, &CardInfo) -> Result<R, CardError>) -> Result<R, CardError> {
564 let mut work = Some(work);
565 let mut result = None;
566 let visited = readers.visit(&mut |card| {
567 let Ok(info) = card.info() else { return Visit::Next };
568 if !info.ident.eq_ignore_ascii_case(ident) {
569 return Visit::Next;
570 }
571 if let Some(work) = work.take() {
572 result = Some(work(card, &info));
573 }
574 Visit::Done
575 })?;
576 match result {
577 Some(result) => result,
578 None if visited.busy > 0 => Err(CardError::Busy),
579 None if visited.opened > 0 && readers.taps() => Err(CardError::NotYours),
581 None => Err(CardError::Absent),
582 }
583}
584
585pub struct CardAccess<'a> {
588 pub readers: &'a dyn Readers,
590 pub pin: &'a Password,
592 pub touch: &'a (dyn Fn() + Sync),
594 failure: Mutex<Option<CardError>>,
596 reader: Mutex<Option<String>>,
598 accepted: std::sync::atomic::AtomicBool,
600 last_try: std::sync::atomic::AtomicBool,
602}
603
604impl<'a> CardAccess<'a> {
605 pub fn new(readers: &'a dyn Readers, pin: &'a Password, touch: &'a (dyn Fn() + Sync)) -> CardAccess<'a> {
607 CardAccess {
608 readers,
609 pin,
610 touch,
611 failure: Mutex::new(None),
612 reader: Mutex::new(None),
613 accepted: std::sync::atomic::AtomicBool::new(false),
614 last_try: std::sync::atomic::AtomicBool::new(false),
615 }
616 }
617
618 pub fn allow_last_try(&self) {
621 self.last_try.store(true, std::sync::atomic::Ordering::Relaxed);
622 }
623
624 pub fn last_try_allowed(&self) -> bool {
626 self.last_try.load(std::sync::atomic::Ordering::Relaxed)
627 }
628
629 pub fn failure(&self) -> Option<CardError> {
631 self.failure.lock().ok().and_then(|mut f| f.take())
632 }
633
634 pub fn reader(&self) -> Option<String> {
636 self.reader.lock().ok().and_then(|r| r.clone())
637 }
638
639 pub fn pin_accepted(&self) -> bool {
642 self.accepted.load(std::sync::atomic::Ordering::Relaxed)
643 }
644
645 fn fail(&self, error: CardError) -> CardError {
647 if let Ok(mut failure) = self.failure.lock() {
648 *failure = Some(error.clone());
649 }
650 error
651 }
652
653 fn run<R>(&self, ident: &str, public: &PublicKey, mode: PinMode, work: impl FnOnce(&mut dyn Card) -> Result<R, CardError>) -> Result<R, CardError> {
656 let fingerprint = public.fingerprint().to_hex();
657 with_card(self.readers, ident, |card, info| {
658 let slot = match mode {
659 PinMode::Sign => info.sign.as_ref(),
660 PinMode::Decrypt => info.decrypt.as_ref(),
661 };
662 let slot = slot.ok_or(CardError::OtherKeys)?;
663 if !slot.fingerprint.eq_ignore_ascii_case(&fingerprint) {
664 return Err(CardError::OtherKeys);
665 }
666 if info.pin_tries == 1 && !self.last_try.load(std::sync::atomic::Ordering::Relaxed) {
669 return Err(CardError::LastTry);
670 }
671 card.verify(self.pin, mode)?;
672 self.accepted.store(true, std::sync::atomic::Ordering::Relaxed);
673 if let Ok(mut reader) = self.reader.lock() {
674 *reader = Some(info.reader.clone());
675 }
676 if slot.touch.asked() {
677 (self.touch)();
678 }
679 work(card)
680 })
681 }
682}
683
684pub struct CardSigner<'a> {
688 access: &'a CardAccess<'a>,
689 ident: String,
690 public: PublicKey,
691}
692
693impl<'a> CardSigner<'a> {
694 pub fn new(access: &'a CardAccess<'a>, ident: &str, public: PublicKey) -> CardSigner<'a> {
696 CardSigner { access, ident: ident.to_string(), public }
697 }
698
699 fn sign_on_card(&self, hash: HashAlgorithm, digest: &[u8]) -> Result<mpi::Signature, CardError> {
700 let input = match self.public.mpis() {
701 mpi::PublicKey::RSA { .. } => match hash {
702 HashAlgorithm::SHA256 | HashAlgorithm::SHA384 | HashAlgorithm::SHA512 => ToSign::Rsa { hash, digest },
703 other => return Err(CardError::Algorithm(format!("RSA, {other}"))),
704 },
705 mpi::PublicKey::EdDSA { .. } | mpi::PublicKey::Ed25519 { .. } => ToSign::Digest(digest),
706 mpi::PublicKey::ECDSA { curve, .. } => {
707 let size = curve.field_size().map_err(|e| CardError::Algorithm(e.to_string()))?;
708 ToSign::Digest(&digest[..digest.len().min(size)])
709 }
710 _ => return Err(CardError::Algorithm(algorithm_name(&self.public))),
711 };
712 let raw = self.access.run(&self.ident, &self.public, PinMode::Sign, |card| card.sign(input))?;
713 let half = raw.len() / 2;
714 let signature = match self.public.mpis() {
715 mpi::PublicKey::RSA { .. } => mpi::Signature::RSA { s: mpi::MPI::new(&raw) },
716 mpi::PublicKey::EdDSA { .. } if raw.len() == 64 => mpi::Signature::EdDSA { r: mpi::MPI::new(&raw[..32]), s: mpi::MPI::new(&raw[32..]) },
717 mpi::PublicKey::Ed25519 { .. } => mpi::Signature::Ed25519 { s: Box::new(<[u8; 64]>::try_from(raw.as_slice()).map_err(|_| CardError::BadSignature)?) },
718 mpi::PublicKey::ECDSA { .. } if raw.len() % 2 == 0 => mpi::Signature::ECDSA { r: mpi::MPI::new(&raw[..half]), s: mpi::MPI::new(&raw[half..]) },
719 _ => return Err(CardError::BadSignature),
720 };
721 self.public.verify(&signature, hash, digest).map_err(|_| CardError::BadSignature)?;
723 Ok(signature)
724 }
725}
726
727impl crypto::Signer for CardSigner<'_> {
728 fn public(&self) -> &PublicKey {
729 &self.public
730 }
731
732 fn acceptable_hashes(&self) -> &[HashAlgorithm] {
733 match self.public.mpis() {
734 mpi::PublicKey::RSA { .. } => &[HashAlgorithm::SHA512, HashAlgorithm::SHA384, HashAlgorithm::SHA256],
736 _ => &[HashAlgorithm::SHA512, HashAlgorithm::SHA384, HashAlgorithm::SHA256, HashAlgorithm::SHA224],
737 }
738 }
739
740 fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) -> openpgp::Result<mpi::Signature> {
741 self.sign_on_card(hash_algo, digest).map_err(|e| self.access.fail(e).into())
742 }
743}
744
745pub struct CardDecryptor<'a> {
748 access: &'a CardAccess<'a>,
749 ident: String,
750 public: PublicKey,
751}
752
753impl<'a> CardDecryptor<'a> {
754 pub fn new(access: &'a CardAccess<'a>, ident: &str, public: PublicKey) -> CardDecryptor<'a> {
756 CardDecryptor { access, ident: ident.to_string(), public }
757 }
758
759 fn decrypt_on_card(&self, ciphertext: &mpi::Ciphertext, plaintext_len: Option<usize>) -> Result<SessionKey, CardError> {
760 match (ciphertext, self.public.mpis()) {
761 (mpi::Ciphertext::RSA { c }, mpi::PublicKey::RSA { .. }) => {
762 let opened = self.access.run(&self.ident, &self.public, PinMode::Decrypt, |card| card.decipher(ToDecipher::Rsa(c.value())))?;
763 Ok(SessionKey::from(&opened[..]))
764 }
765 (mpi::Ciphertext::ECDH { e, .. }, mpi::PublicKey::ECDH { curve, .. }) => {
766 let point = match curve {
768 Curve::Cv25519 => e.value().strip_prefix(&[0x40]).ok_or_else(|| CardError::Other("malformed Curve25519 point".into()))?,
769 _ => e.value(),
770 };
771 let shared = self.access.run(&self.ident, &self.public, PinMode::Decrypt, |card| card.decipher(ToDecipher::Ecdh(point)))?;
772 let shared = if *curve == Curve::NistP256 && shared.len() == 65 && shared[0] == 0x04 { Protected::from(&shared[1..33]) } else { shared };
774 crypto::ecdh::decrypt_unwrap(&self.public, &shared, ciphertext, plaintext_len).map_err(|e| CardError::Other(e.to_string()))
775 }
776 _ => Err(CardError::Algorithm(algorithm_name(&self.public))),
777 }
778 }
779}
780
781impl crypto::Decryptor for CardDecryptor<'_> {
782 fn public(&self) -> &PublicKey {
783 &self.public
784 }
785
786 fn decrypt(&mut self, ciphertext: &mpi::Ciphertext, plaintext_len: Option<usize>) -> openpgp::Result<SessionKey> {
787 self.decrypt_on_card(ciphertext, plaintext_len).map_err(|e| self.access.fail(e).into())
788 }
789}
790
791pub const PIN_QUIET: Duration = Duration::from_secs(15 * 60);
793
794#[derive(Default)]
799pub struct PinMemory {
800 held: Option<Held>,
801}
802
803struct Held {
804 ident: String,
805 pin: Password,
806 used: Instant,
807 reader: String,
808}
809
810impl PinMemory {
811 pub const fn new() -> PinMemory {
813 PinMemory { held: None }
814 }
815
816 pub fn keep(&mut self, ident: &str, pin: Password, reader: &str, now: Instant) {
818 self.held = Some(Held { ident: ident.to_string(), pin, used: now, reader: reader.to_string() });
819 }
820
821 pub fn get(&mut self, ident: &str, now: Instant) -> Option<Password> {
823 self.expire(now);
824 self.held.as_ref().filter(|h| h.ident.eq_ignore_ascii_case(ident)).map(|h| h.pin.clone())
825 }
826
827 pub fn used(&mut self, ident: &str, now: Instant) {
829 if let Some(held) = self.held.as_mut().filter(|h| h.ident.eq_ignore_ascii_case(ident)) {
830 held.used = now;
831 }
832 }
833
834 pub fn forget(&mut self) {
836 self.held = None;
837 }
838
839 pub fn expire(&mut self, now: Instant) -> bool {
841 let stale = self.held.as_ref().is_some_and(|h| now.saturating_duration_since(h.used) >= PIN_QUIET);
842 if stale {
843 self.held = None;
844 }
845 stale
846 }
847
848 pub fn went_away(&mut self, present: &[String]) -> bool {
850 let gone = self.held.as_ref().is_some_and(|h| !h.reader.is_empty() && !present.iter().any(|r| *r == h.reader));
851 if gone {
852 self.held = None;
853 }
854 gone
855 }
856
857 pub fn holder(&self) -> Option<&str> {
859 self.held.as_ref().map(|h| h.ident.as_str())
860 }
861}
862
863#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
866pub struct KnownCard {
867 pub ident: String,
869 #[serde(default)]
871 pub manufacturer: u16,
872 #[serde(default)]
874 pub serial: u32,
875 #[serde(default)]
877 pub holder: String,
878 #[serde(default)]
880 pub sign: String,
881 #[serde(default)]
883 pub decrypt: String,
884 pub cert: String,
886 #[serde(default)]
888 pub addresses: Vec<String>,
889 #[serde(default)]
891 pub source: String,
892 #[serde(default)]
894 pub checked: i64,
895 #[serde(default)]
897 pub pin_tries_full: u8,
898 #[serde(default)]
900 pub touch_sign: Touch,
901 #[serde(default)]
903 pub touch_decrypt: Touch,
904 #[serde(default)]
906 pub pin_each_signature: bool,
907}
908
909impl KnownCard {
910 pub fn serves(&self, address: &str) -> bool {
912 self.addresses.iter().any(|a| a.eq_ignore_ascii_case(address.trim()))
913 }
914
915 pub fn label(&self, tr: &Translator) -> String {
917 label(self.manufacturer, self.serial, tr)
918 }
919
920 pub fn update(&mut self, info: &CardInfo) {
923 self.holder = info.holder.clone();
924 self.touch_sign = info.sign.as_ref().map(|s| s.touch).unwrap_or_default();
925 self.touch_decrypt = info.decrypt.as_ref().map(|s| s.touch).unwrap_or_default();
926 self.pin_each_signature = info.pin_each_signature;
927 self.pin_tries_full = self.pin_tries_full.max(info.pin_tries).max(3);
928 }
929
930 pub fn tries_line(&self, left: u8, tr: &Translator) -> Option<String> {
932 (left < self.pin_tries_full.max(3)).then(|| tr.text("seckey-tries-left", Some(&tr.counted(usize::from(left)))))
933 }
934}
935
936#[derive(Default, Serialize, Deserialize)]
937struct CardsFile {
938 #[serde(default, rename = "card")]
939 cards: Vec<KnownCard>,
940}
941
942pub fn cards_path() -> PathBuf {
944 data_dir().join("pgp").join("cards.toml")
945}
946
947pub fn known_cards() -> Vec<KnownCard> {
949 read_known(&cards_path())
950}
951
952pub fn known_cards_in(path: &Path) -> Vec<KnownCard> {
954 read_known(path)
955}
956
957fn read_known(path: &Path) -> Vec<KnownCard> {
958 std::fs::read_to_string(path).ok().and_then(|text| toml::from_str::<CardsFile>(&text).ok()).map(|f| f.cards).unwrap_or_default()
959}
960
961pub fn change_known<T>(change: impl FnOnce(&mut Vec<KnownCard>) -> T) -> Result<T, String> {
965 let out = change_known_at(&cards_path(), change)?;
966 share_cards()?;
967 Ok(out)
968}
969
970pub fn shared_cards_dir() -> PathBuf {
978 data_dir().join("pgp").join(SHARED_CARDS)
979}
980
981pub const SHARED_CARDS: &str = "cards";
983
984#[derive(Debug, Clone, Default, Serialize, Deserialize)]
987pub struct SharedCard {
988 pub card: KnownCard,
989 #[serde(default)]
990 pub certificate: String,
991}
992
993fn shared_name(ident: &str) -> String {
995 let name: String = ident.chars().map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }).collect();
996 format!("{name}.toml")
997}
998
999pub fn vouched(card: &KnownCard, certificate: &str) -> Option<Cert> {
1012 let cert = Cert::from_bytes(certificate.as_bytes()).ok()?.strip_secret_key_material();
1013 if card.cert.is_empty() || !cert.fingerprint().to_hex().eq_ignore_ascii_case(&card.cert) || (card.sign.is_empty() && card.decrypt.is_empty()) {
1014 return None;
1015 }
1016 let slot = |fingerprint: &str| (!fingerprint.is_empty()).then(|| SlotInfo { fingerprint: fingerprint.to_string(), ..SlotInfo::default() });
1017 let info = CardInfo { ident: card.ident.clone(), sign: slot(&card.sign), decrypt: slot(&card.decrypt), ..CardInfo::default() };
1018 check(&cert, &info).is_ok().then_some(cert)
1019}
1020
1021pub fn shared_cards() -> Vec<(KnownCard, Cert)> {
1026 shared_cards_in(&shared_cards_dir())
1027}
1028
1029pub fn shared_cards_in(dir: &Path) -> Vec<(KnownCard, Cert)> {
1031 let Ok(entries) = std::fs::read_dir(dir) else { return Vec::new() };
1032 let mut found: Vec<(KnownCard, Cert)> = entries
1033 .filter_map(Result::ok)
1034 .map(|e| e.path())
1035 .filter(|p| p.extension().is_some_and(|x| x == "toml"))
1036 .filter_map(|p| std::fs::read_to_string(p).ok())
1037 .filter_map(|text| toml::from_str::<SharedCard>(&text).ok())
1038 .filter_map(|shared| Some((vouched(&shared.card, &shared.certificate)?, shared.card)))
1039 .map(|(cert, card)| (card, cert))
1040 .collect();
1041 found.sort_by(|a, b| a.0.ident.cmp(&b.0.ident));
1042 found
1043}
1044
1045pub fn share_cards() -> Result<(), String> {
1053 share_cards_in(&cards_path(), &data_dir().join("pgp").join("own"), &shared_cards_dir(), false)?;
1054 for card in known_cards() {
1055 mark_shared(&marks_path(), &card.ident)?;
1056 }
1057 Ok(())
1058}
1059
1060pub fn share_cards_at_exchange() -> Result<(), String> {
1068 share_cards_marked_in(&cards_path(), &data_dir().join("pgp").join("own"), &shared_cards_dir(), &marks_path())
1069}
1070
1071pub fn marks_path() -> PathBuf {
1073 crate::config::state_dir().join("security-keys.toml")
1074}
1075
1076#[derive(Default, Serialize, Deserialize)]
1078struct Marks {
1079 #[serde(default)]
1081 shared: Vec<String>,
1082}
1083
1084fn read_marks(path: &Path) -> Marks {
1085 std::fs::read_to_string(path).ok().and_then(|text| toml::from_str(&text).ok()).unwrap_or_default()
1086}
1087
1088fn mark_shared(path: &Path, ident: &str) -> Result<(), String> {
1089 let mut marks = read_marks(path);
1090 if marks.shared.iter().any(|i| i.eq_ignore_ascii_case(ident)) {
1091 return Ok(());
1092 }
1093 marks.shared.push(ident.to_string());
1094 if let Some(parent) = path.parent() {
1095 std::fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
1096 }
1097 crate::filelock::replace(path, toml::to_string(&marks).map_err(|e| e.to_string())?.as_bytes()).map_err(|e| format!("{}: {e}", path.display()))
1098}
1099
1100pub fn share_cards_marked_in(cards: &Path, own: &Path, shared: &Path, marks: &Path) -> Result<(), String> {
1103 let known = read_known(cards);
1104 if known.is_empty() {
1105 return Ok(());
1106 }
1107 let marked = read_marks(marks);
1108 for card in known {
1109 let path = shared.join(shared_name(&card.ident));
1110 let first = !marked.shared.iter().any(|i| i.eq_ignore_ascii_case(&card.ident));
1111 if path.exists() {
1112 share_one(&card, own, shared, true)?;
1114 } else if first {
1115 share_one(&card, own, shared, false)?;
1116 }
1117 if first {
1118 mark_shared(marks, &card.ident)?;
1119 }
1120 }
1121 Ok(())
1122}
1123
1124pub fn stopped_in(root: &Path) -> Vec<String> {
1127 #[derive(Default, Deserialize)]
1128 struct Stopped {
1129 #[serde(default)]
1130 stopped: Vec<String>,
1131 }
1132 std::fs::read_to_string(root.join("stopped.toml")).ok().and_then(|text| toml::from_str::<Stopped>(&text).ok()).map(|s| s.stopped).unwrap_or_default()
1133}
1134
1135fn set_stopped_in(root: &Path, ident: &str, stopped: bool) -> Result<(), String> {
1137 let mut list = stopped_in(root);
1138 let before = list.len();
1139 list.retain(|i| !i.eq_ignore_ascii_case(ident));
1140 if stopped {
1141 list.push(ident.to_string());
1142 }
1143 if list.len() == before && !stopped {
1144 return Ok(());
1145 }
1146 std::fs::create_dir_all(root).map_err(|e| format!("{}: {e}", root.display()))?;
1147 let text = format!("# Sioul: the security keys this device stopped using; never shared.\nstopped = {}\n", toml::Value::Array(list.into_iter().map(toml::Value::String).collect()));
1148 crate::filelock::replace(&root.join("stopped.toml"), text.as_bytes()).map_err(|e| format!("{}: {e}", root.display()))
1149}
1150
1151pub fn share_cards_in(cards: &Path, own: &Path, shared: &Path, missing: bool) -> Result<(), String> {
1154 let known = read_known(cards);
1155 if known.is_empty() {
1156 return Ok(());
1157 }
1158 for card in known {
1159 if missing && shared.join(shared_name(&card.ident)).exists() {
1160 continue;
1161 }
1162 share_one(&card, own, shared, false)?;
1163 }
1164 Ok(())
1165}
1166
1167fn share_one(card: &KnownCard, own: &Path, shared: &Path, adds: bool) -> Result<(), String> {
1173 let fail = |path: &Path, e: std::io::Error| format!("{}: {e}", path.display());
1174 let path = shared.join(shared_name(&card.ident));
1175 let Some(mine) = std::fs::read(own.join(format!("{}.asc", card.cert))).ok().and_then(|bytes| Cert::from_bytes(&bytes).ok()) else { return Ok(()) };
1176 let mine = mine.strip_secret_key_material();
1177 let there = std::fs::read_to_string(&path).ok().and_then(|text| toml::from_str::<SharedCard>(&text).ok());
1178 let vouched_there = there.as_ref().and_then(|t| vouched(&t.card, &t.certificate)).filter(|t| t.fingerprint() == mine.fingerprint());
1179 let cert = match &vouched_there {
1180 Some(shared) => mine.clone().merge_public(shared.clone()).unwrap_or(mine),
1181 None => mine,
1182 };
1183 if adds {
1184 let Some(shared) = &vouched_there else { return Ok(()) };
1186 if cert.clone().into_packets().count() == shared.clone().into_packets().count() {
1187 return Ok(());
1188 }
1189 }
1190 let record = match (adds, &there) {
1191 (true, Some(there)) => there.card.clone(),
1192 _ => card.clone(),
1193 };
1194 let certificate = String::from_utf8(cert.armored().to_vec().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
1195 let text = format!(
1196 "# Sioul: one of your security keys, as a computer of yours read it, and its certificate: public parts only (docs/database.md).\n{}",
1197 toml::to_string(&SharedCard { card: record, certificate }).map_err(|e| e.to_string())?
1198 );
1199 if std::fs::read_to_string(&path).ok().as_deref() == Some(text.as_str()) {
1200 return Ok(());
1201 }
1202 std::fs::create_dir_all(shared).map_err(|e| fail(shared, e))?;
1203 let temporary = path.with_extension("toml.new");
1204 std::fs::write(&temporary, &text).map_err(|e| fail(&temporary, e))?;
1205 std::fs::rename(&temporary, &path).map_err(|e| fail(&path, e))
1206}
1207
1208fn unshare_card(shared: &Path, ident: &str) {
1211 let _ = std::fs::remove_file(shared.join(shared_name(ident)));
1212}
1213
1214pub(crate) fn change_known_at<T>(path: &Path, change: impl FnOnce(&mut Vec<KnownCard>) -> T) -> Result<T, String> {
1215 crate::filelock::with_lock(path, || {
1216 let mut cards = read_known(path);
1217 let result = change(&mut cards);
1218 let text = toml::to_string(&CardsFile { cards }).map_err(|e| e.to_string())?;
1219 let fail = |e: std::io::Error| format!("{}: {e}", path.display());
1220 if let Some(folder) = path.parent() {
1221 std::fs::create_dir_all(folder).map_err(fail)?;
1222 }
1223 let temporary = path.with_extension("toml.new");
1224 std::fs::write(&temporary, text).map_err(fail)?;
1225 std::fs::rename(&temporary, path).map_err(fail)?;
1226 Ok(result)
1227 })
1228}
1229
1230#[derive(Debug, Clone, PartialEq, Eq)]
1232pub struct CertCheck {
1233 pub cert: String,
1235 pub addresses: Vec<String>,
1237 pub expires: Option<i64>,
1240 pub expired: bool,
1242 pub signs: bool,
1244 pub decrypts: bool,
1246}
1247
1248#[derive(Debug, Clone, PartialEq, Eq)]
1250pub enum Mismatch {
1251 Empty,
1253 Missing,
1255 Revoked,
1257 NotForSigning,
1259 NotForEncryption,
1261 OtherPublicKey,
1264 Invalid(String),
1266}
1267
1268impl Mismatch {
1269 pub fn sentence(&self, tr: &Translator) -> String {
1271 match self {
1272 Mismatch::Empty => tr.text("seckey-mismatch-empty", None),
1273 Mismatch::Missing | Mismatch::OtherPublicKey => tr.text("seckey-mismatch", None),
1274 Mismatch::Revoked => tr.text("seckey-mismatch-revoked", None),
1275 Mismatch::NotForSigning | Mismatch::NotForEncryption => tr.text("seckey-mismatch-use", None),
1276 Mismatch::Invalid(why) => say(tr, "seckey-mismatch-invalid", &[("detail", why.clone())]),
1277 }
1278 }
1279}
1280
1281pub fn check(cert: &Cert, card: &CardInfo) -> Result<CertCheck, Mismatch> {
1285 let policy = StandardPolicy::new();
1286 let valid = cert.with_policy(&policy, None).map_err(|e| Mismatch::Invalid(e.to_string()))?;
1287 if matches!(valid.revocation_status(), openpgp::types::RevocationStatus::Revoked(_)) {
1288 return Err(Mismatch::Revoked);
1289 }
1290 if card.sign.is_none() && card.decrypt.is_none() {
1291 return Err(Mismatch::Empty);
1292 }
1293 let unix = |t: SystemTime| t.duration_since(SystemTime::UNIX_EPOCH).ok().and_then(|d| i64::try_from(d.as_secs()).ok());
1294 let mut expires: Option<i64> = valid.primary_key().key_expiration_time().and_then(unix);
1295 let mut earliest = |at: Option<i64>| {
1296 if let Some(at) = at {
1297 expires = Some(expires.map_or(at, |e| e.min(at)));
1298 }
1299 };
1300 let mut alive = valid.alive().is_ok();
1301 for (slot, signing) in [(&card.sign, true), (&card.decrypt, false)] {
1302 let Some(slot) = slot else { continue };
1303 let key = valid.keys().find(|k| k.key().fingerprint().to_hex().eq_ignore_ascii_case(&slot.fingerprint)).ok_or(Mismatch::Missing)?;
1304 if matches!(key.revocation_status(), openpgp::types::RevocationStatus::Revoked(_)) {
1305 return Err(Mismatch::Revoked);
1306 }
1307 let allowed = if signing { key.for_signing() } else { key.for_transport_encryption() || key.for_storage_encryption() };
1308 if !allowed {
1309 return Err(if signing { Mismatch::NotForSigning } else { Mismatch::NotForEncryption });
1310 }
1311 if let Some(public) = &slot.public
1312 && !same_public(key.key(), public)
1313 {
1314 return Err(Mismatch::OtherPublicKey);
1315 }
1316 earliest(key.key_expiration_time().and_then(unix));
1317 alive &= key.alive().is_ok();
1318 }
1319 let mut addresses: Vec<String> = valid.userids().filter_map(|u| u.userid().email().ok().flatten().map(|e| e.to_ascii_lowercase())).collect();
1320 addresses.sort();
1321 addresses.dedup();
1322 let now = unix(SystemTime::now()).unwrap_or(0);
1323 Ok(CertCheck {
1324 cert: cert.fingerprint().to_hex(),
1325 addresses,
1326 expires,
1327 expired: !alive || expires.is_some_and(|at| at <= now),
1328 signs: card.sign.is_some(),
1329 decrypts: card.decrypt.is_some(),
1330 })
1331}
1332
1333pub fn certificate_in(bytes: &[u8], card: &CardInfo) -> Result<(Cert, CertCheck), Mismatch> {
1337 let mut why = Mismatch::Missing;
1338 for cert in crate::pgp::certificates_of(bytes).map_err(Mismatch::Invalid)? {
1339 match check(&cert, card) {
1340 Ok(found) => return Ok((cert.strip_secret_key_material(), found)),
1341 Err(Mismatch::Missing) => {}
1343 Err(other) => why = other,
1344 }
1345 }
1346 Err(why)
1347}
1348
1349pub fn adopt(card: &CardInfo, cert: &Cert, source: &str, addresses: Vec<String>, now: i64) -> Result<KnownCard, String> {
1353 crate::pgp::keep_card_certificate(cert)?;
1354 let mut known = KnownCard {
1355 ident: card.ident.clone(),
1356 manufacturer: card.manufacturer,
1357 serial: card.serial,
1358 sign: card.sign.as_ref().map(|s| s.fingerprint.clone()).unwrap_or_default(),
1359 decrypt: card.decrypt.as_ref().map(|s| s.fingerprint.clone()).unwrap_or_default(),
1360 cert: cert.fingerprint().to_hex(),
1361 addresses: addresses.into_iter().map(|a| a.trim().to_ascii_lowercase()).collect(),
1362 source: source.to_string(),
1363 checked: now,
1364 ..KnownCard::default()
1365 };
1366 known.update(card);
1367 let kept = known.clone();
1368 set_stopped_in(&data_dir().join("pgp"), &known.ident, false)?;
1370 change_known(|cards| {
1371 cards.retain(|c| !c.ident.eq_ignore_ascii_case(&kept.ident));
1372 cards.push(kept);
1373 })?;
1374 Ok(known)
1375}
1376
1377pub fn forget(ident: &str) -> Result<bool, String> {
1386 forget_in(&data_dir().join("pgp"), ident)
1387}
1388
1389pub(crate) fn forget_in(root: &Path, ident: &str) -> Result<bool, String> {
1391 set_stopped_in(root, ident, true)?;
1392 let cards = root.join("cards.toml");
1393 if !read_known(&cards).iter().any(|c| c.ident.eq_ignore_ascii_case(ident)) {
1394 return Ok(false);
1395 }
1396 let cert = change_known_at(&cards, |cards| {
1397 let cert = cards.iter().find(|c| c.ident.eq_ignore_ascii_case(ident)).map(|c| c.cert.clone());
1398 cards.retain(|c| !c.ident.eq_ignore_ascii_case(ident));
1399 cert
1400 })?;
1401 unshare_card(&root.join(SHARED_CARDS), ident);
1402 match cert {
1403 Some(cert) => crate::pgp::remove_in(root, &cert).map(|()| true),
1404 None => Ok(true),
1405 }
1406}
1407
1408fn same_public(key: &PublicKey, card: &CardPublic) -> bool {
1410 fn native(point: &[u8]) -> &[u8] {
1412 if point.len() == 33 && point[0] == 0x40 { &point[1..] } else { point }
1413 }
1414 fn number(bytes: &[u8]) -> &[u8] {
1415 let zeros = bytes.iter().take_while(|&&b| b == 0).count();
1416 &bytes[zeros..]
1417 }
1418 match (key.mpis(), card) {
1419 (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),
1420 (mpi::PublicKey::EdDSA { q, .. } | mpi::PublicKey::ECDSA { q, .. } | mpi::PublicKey::ECDH { q, .. }, CardPublic::Ecc { point }) => native(q.value()) == native(point),
1421 (mpi::PublicKey::Ed25519 { a }, CardPublic::Ecc { point }) => a[..] == *native(point),
1422 (mpi::PublicKey::X25519 { u }, CardPublic::Ecc { point }) => u[..] == *native(point),
1423 _ => false,
1424 }
1425}
1426
1427pub fn expiry_of(cert: &Cert, fingerprints: &[&str]) -> Option<i64> {
1431 let policy = StandardPolicy::new();
1432 let valid = cert.with_policy(&policy, None).ok()?;
1433 let unix = |t: SystemTime| t.duration_since(SystemTime::UNIX_EPOCH).ok().and_then(|d| i64::try_from(d.as_secs()).ok());
1434 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));
1435 valid.primary_key().key_expiration_time().and_then(unix).into_iter().chain(keys).min()
1436}
1437
1438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1440pub enum Role {
1441 Primary,
1443 Signing,
1445 Encryption,
1447 Authentication,
1449 Other,
1451}
1452
1453#[derive(Debug, Clone, PartialEq, Eq)]
1456pub struct Part {
1457 pub role: Role,
1458 pub fingerprint: String,
1459 pub expires: Option<i64>,
1460}
1461
1462pub fn parts(cert: &Cert) -> Vec<Part> {
1466 let policy = StandardPolicy::new();
1467 let Ok(valid) = cert.with_policy(&policy, None) else { return Vec::new() };
1468 let unix = |t: SystemTime| t.duration_since(SystemTime::UNIX_EPOCH).ok().and_then(|d| i64::try_from(d.as_secs()).ok());
1469 let mut out = vec![Part { role: Role::Primary, fingerprint: cert.fingerprint().to_hex(), expires: valid.primary_key().key_expiration_time().and_then(unix) }];
1470 for key in valid.keys().subkeys() {
1471 if matches!(key.revocation_status(), openpgp::types::RevocationStatus::Revoked(_)) {
1472 continue;
1473 }
1474 let role = if key.for_transport_encryption() || key.for_storage_encryption() {
1475 Role::Encryption
1476 } else if key.for_signing() {
1477 Role::Signing
1478 } else if key.for_authentication() {
1479 Role::Authentication
1480 } else {
1481 Role::Other
1482 };
1483 out.push(Part { role, fingerprint: key.key().fingerprint().to_hex(), expires: key.key_expiration_time().and_then(unix) });
1484 }
1485 out.sort_by_key(|part| part.role as u8);
1487 out
1488}
1489
1490pub fn revoked_encryption(cert: &Cert) -> Option<String> {
1493 let policy = StandardPolicy::new();
1494 let valid = cert.with_policy(&policy, None).ok()?;
1495 if valid.keys().subkeys().revoked(false).for_transport_encryption().next().is_some() {
1496 return None;
1497 }
1498 valid.keys().subkeys().for_transport_encryption().find(|k| matches!(k.revocation_status(), openpgp::types::RevocationStatus::Revoked(_))).map(|k| k.key().fingerprint().to_hex())
1499}
1500
1501pub fn parts_in(bytes: &[u8]) -> Vec<Part> {
1504 crate::pgp::certificates_of(bytes).ok().and_then(|certs| certs.into_iter().next()).map(|cert| parts(&cert)).unwrap_or_default()
1505}
1506
1507pub fn short_fingerprint(fingerprint: &str) -> String {
1509 let hex: String = fingerprint.chars().filter(char::is_ascii_hexdigit).collect::<String>().to_ascii_uppercase();
1510 if hex.len() <= 8 { hex } else { format!("{}…{}", &hex[..4], &hex[hex.len() - 4..]) }
1511}
1512
1513pub fn expired_parts(tr: &Translator, parts: &[Part], now: i64) -> Vec<String> {
1517 parts
1518 .iter()
1519 .filter_map(|part| {
1520 let at = part.expires.filter(|at| *at <= now)?;
1521 let id = match part.role {
1522 Role::Primary => "seckey-part-expired-primary",
1523 Role::Signing => "seckey-part-expired-sign",
1524 Role::Encryption => "seckey-part-expired-encrypt",
1525 Role::Authentication => "seckey-part-expired-auth",
1526 Role::Other => "seckey-part-expired-other",
1527 };
1528 Some(say(tr, id, &[("fingerprint", short_fingerprint(&part.fingerprint)), ("date", full_date(tr, at))]))
1529 })
1530 .collect()
1531}
1532
1533pub fn parts_until(tr: &Translator, parts: &[Part]) -> String {
1536 parts
1537 .iter()
1538 .map(|part| {
1539 let name = tr.text(
1540 match part.role {
1541 Role::Primary => "seckey-part-primary",
1542 Role::Signing => "seckey-part-sign",
1543 Role::Encryption => "seckey-part-encrypt",
1544 Role::Authentication => "seckey-part-auth",
1545 Role::Other => "seckey-part-other",
1546 },
1547 None,
1548 );
1549 match part.expires {
1550 Some(at) => say(tr, "seckey-part-until", &[("part", name), ("date", full_date(tr, at))]),
1551 None => say(tr, "seckey-part-forever", &[("part", name)]),
1552 }
1553 })
1554 .collect::<Vec<_>>()
1555 .join(", ")
1556}
1557
1558pub fn expiry_words(tr: &Translator, cert: &Cert, expires: Option<i64>, now: i64) -> (String, bool, bool) {
1565 let parts = parts(cert);
1566 let expired = expired_parts(tr, &parts, now);
1567 if expired.is_empty() {
1568 return expiry_line(tr, expires, &cert.fingerprint().to_hex(), now).map_or((String::new(), false, false), |(line, late)| (line, late, false));
1569 }
1570 let primary_alive = parts.first().is_some_and(|p| p.expires.is_none_or(|at| at > now));
1571 let mut line = expired.join(" ");
1572 if primary_alive {
1573 line = format!("{line} {}", tr.text("seckey-subkeys-renew", None));
1574 }
1575 (line, true, true)
1576}
1577
1578pub fn subkeys_of(cert: &Cert) -> Vec<String> {
1581 parts(cert).into_iter().filter(|p| p.role != Role::Primary).map(|p| p.fingerprint).collect()
1582}
1583
1584pub fn full_date(tr: &Translator, at: i64) -> String {
1586 let Ok(stamp) = jiff::Timestamp::from_second(at) else { return String::new() };
1587 let date = stamp.to_zoned(jiff::tz::TimeZone::system()).date();
1588 format!("{} {}", tr.day_month(date), date.year())
1589}
1590
1591pub fn expiry_line(tr: &Translator, expires: Option<i64>, fingerprint: &str, now: i64) -> Option<(String, bool)> {
1594 let at = expires?;
1595 let date = full_date(tr, at);
1596 let pairs = [("date", date), ("fingerprint", fingerprint.to_string())];
1597 Some(if at <= now {
1598 (say(tr, "seckey-expired", &pairs), true)
1599 } else if at - now <= 31 * 24 * 3600 {
1600 (say(tr, "seckey-expires-soon", &pairs), true)
1601 } else {
1602 (say(tr, "seckey-valid-until", &pairs), false)
1603 })
1604}
1605
1606pub struct SoftCard {
1611 pub info: CardInfo,
1613 signer: Option<crypto::KeyPair>,
1614 decryptor: Option<crypto::KeyPair>,
1615 pin: Vec<u8>,
1616 x25519: Option<fn(&[u8; 32], &[u8; 32]) -> [u8; 32]>,
1618 verified_sign: bool,
1619 verified_decrypt: bool,
1620 pub fault: Option<CardError>,
1623 pub operations: usize,
1625 pub touch_delay: Duration,
1628}
1629
1630impl SoftCard {
1631 pub fn new(ident: &str, cert: &Cert, pin: &str) -> SoftCard {
1634 let policy = StandardPolicy::new();
1635 let pair = |signing: bool| {
1636 cert.keys()
1637 .unencrypted_secret()
1638 .with_policy(&policy, None)
1639 .find(|k| if signing { k.for_signing() } else { k.for_transport_encryption() })
1640 .and_then(|k| k.key().clone().into_keypair().ok())
1641 };
1642 let (signer, decryptor) = (pair(true), pair(false));
1643 let slot = |pair: &Option<crypto::KeyPair>| {
1644 pair.as_ref().map(|p| SlotInfo {
1645 fingerprint: p.public().fingerprint().to_hex(),
1646 created: openpgp::types::Timestamp::try_from(p.public().creation_time()).map(u32::from).unwrap_or(0),
1647 algorithm: algorithm_name(p.public()),
1648 touch: Touch::Off,
1649 public: card_public(p.public()),
1650 })
1651 };
1652 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)));
1653 SoftCard {
1654 info: CardInfo {
1655 ident: ident.to_string(),
1656 manufacturer,
1657 serial,
1658 holder: String::new(),
1659 url: String::new(),
1660 sign: slot(&signer),
1661 decrypt: slot(&decryptor),
1662 pin_tries: 3,
1663 reset_tries: 0,
1664 admin_tries: 3,
1665 pin_each_signature: false,
1666 signatures: 0,
1667 reader: format!("Soft reader {ident}"),
1668 },
1669 signer,
1670 decryptor,
1671 pin: pin.as_bytes().to_vec(),
1672 x25519: None,
1673 verified_sign: false,
1674 verified_decrypt: false,
1675 fault: None,
1676 operations: 0,
1677 touch_delay: Duration::ZERO,
1678 }
1679 }
1680
1681 pub fn generate(ident: &str, userids: &[&str], pin: &str) -> Result<(SoftCard, Vec<u8>), String> {
1686 SoftCard::generate_dated(ident, userids, pin, None)
1687 }
1688
1689 pub fn generate_rsa(ident: &str, userids: &[&str], pin: &str) -> Result<(SoftCard, Vec<u8>), String> {
1693 let mut builder = CertBuilder::new().set_cipher_suite(CipherSuite::RSA2k).set_primary_key_flags(openpgp::types::KeyFlags::empty().set_certification().set_signing()).add_transport_encryption_subkey();
1694 for userid in userids {
1695 builder = builder.add_userid(*userid);
1696 }
1697 let (cert, _) = builder.generate().map_err(|e| e.to_string())?;
1698 let public = cert.clone().strip_secret_key_material().armored().to_vec().map_err(|e| e.to_string())?;
1699 Ok((SoftCard::new(ident, &cert, pin), public))
1700 }
1701
1702 pub fn generate_dated(ident: &str, userids: &[&str], pin: &str, dated: Option<(SystemTime, Duration)>) -> Result<(SoftCard, Vec<u8>), String> {
1705 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();
1706 for userid in userids {
1707 builder = builder.add_userid(*userid);
1708 }
1709 if let Some((made, valid)) = dated {
1710 builder = builder.set_creation_time(made).set_validity_period(valid);
1711 }
1712 let (cert, _) = builder.generate().map_err(|e| e.to_string())?;
1713 let public = cert.clone().strip_secret_key_material().armored().to_vec().map_err(|e| e.to_string())?;
1714 Ok((SoftCard::new(ident, &cert, pin), public))
1715 }
1716
1717 pub fn with_x25519(mut self, x25519: fn(&[u8; 32], &[u8; 32]) -> [u8; 32]) -> SoftCard {
1719 self.x25519 = Some(x25519);
1720 self
1721 }
1722
1723 fn close(&mut self) {
1725 self.verified_sign = false;
1726 self.verified_decrypt = false;
1727 }
1728}
1729
1730fn card_public(key: &PublicKey) -> Option<CardPublic> {
1732 let native = |q: &mpi::MPI| {
1733 let v = q.value();
1734 if v.len() == 33 && v[0] == 0x40 { v[1..].to_vec() } else { v.to_vec() }
1735 };
1736 match key.mpis() {
1737 mpi::PublicKey::RSA { e, n } => Some(CardPublic::Rsa { n: n.value().to_vec(), e: e.value().to_vec() }),
1738 mpi::PublicKey::EdDSA { q, .. } | mpi::PublicKey::ECDSA { q, .. } | mpi::PublicKey::ECDH { q, .. } => Some(CardPublic::Ecc { point: native(q) }),
1739 mpi::PublicKey::Ed25519 { a } => Some(CardPublic::Ecc { point: a.to_vec() }),
1740 _ => None,
1741 }
1742}
1743
1744impl Card for SoftCard {
1745 fn info(&mut self) -> Result<CardInfo, CardError> {
1746 Ok(self.info.clone())
1747 }
1748
1749 fn verify(&mut self, pin: &Password, mode: PinMode) -> Result<(), CardError> {
1750 if self.info.pin_tries == 0 {
1751 return Err(CardError::Blocked);
1752 }
1753 if pin.map(|p| p[..] == self.pin[..]) {
1754 self.info.pin_tries = 3;
1755 match mode {
1756 PinMode::Sign => self.verified_sign = true,
1757 PinMode::Decrypt => self.verified_decrypt = true,
1758 }
1759 Ok(())
1760 } else {
1761 self.info.pin_tries -= 1;
1762 Err(CardError::WrongPin { left: self.info.pin_tries })
1763 }
1764 }
1765
1766 fn sign(&mut self, input: ToSign<'_>) -> Result<Vec<u8>, CardError> {
1767 if !self.verified_sign {
1768 return Err(CardError::Other("security status not satisfied".into()));
1769 }
1770 if let Some(fault) = self.fault.take() {
1771 return Err(fault);
1772 }
1773 if self.info.sign.as_ref().is_some_and(|s| s.touch.asked()) {
1774 std::thread::sleep(self.touch_delay);
1775 }
1776 let pair = self.signer.as_mut().ok_or(CardError::NotOpenPgp)?;
1777 let (hash, digest) = match input {
1778 ToSign::Digest(digest) => (HashAlgorithm::SHA512, digest),
1779 ToSign::Rsa { hash, digest } => (hash, digest),
1780 };
1781 let signature = crypto::Signer::sign(pair, hash, digest).map_err(|e| CardError::Other(e.to_string()))?;
1782 let pad = |m: &mpi::MPI, size: usize| m.value_padded(size).map(|v| v.to_vec()).map_err(|e| CardError::Other(e.to_string()));
1783 let raw = match (&signature, pair.public().mpis()) {
1784 (mpi::Signature::RSA { s }, mpi::PublicKey::RSA { n, .. }) => pad(s, n.value().len())?,
1785 (mpi::Signature::EdDSA { r, s }, _) => [pad(r, 32)?, pad(s, 32)?].concat(),
1786 (mpi::Signature::Ed25519 { s }, _) => s.to_vec(),
1787 (mpi::Signature::ECDSA { r, s }, mpi::PublicKey::ECDSA { curve, .. }) => {
1788 let size = curve.field_size().map_err(|e| CardError::Other(e.to_string()))?;
1789 [pad(r, size)?, pad(s, size)?].concat()
1790 }
1791 _ => return Err(CardError::Algorithm(algorithm_name(pair.public()))),
1792 };
1793 if self.info.pin_each_signature {
1794 self.verified_sign = false;
1795 }
1796 self.info.signatures += 1;
1797 self.operations += 1;
1798 Ok(raw)
1799 }
1800
1801 fn decipher(&mut self, input: ToDecipher<'_>) -> Result<Protected, CardError> {
1802 if !self.verified_decrypt {
1803 return Err(CardError::Other("security status not satisfied".into()));
1804 }
1805 if let Some(fault) = self.fault.take() {
1806 return Err(fault);
1807 }
1808 if self.info.decrypt.as_ref().is_some_and(|s| s.touch.asked()) {
1809 std::thread::sleep(self.touch_delay);
1810 }
1811 let pair = self.decryptor.as_mut().ok_or(CardError::NotOpenPgp)?;
1812 let opened = match (input, pair.public().mpis()) {
1813 (ToDecipher::Rsa(c), mpi::PublicKey::RSA { .. }) => {
1814 let session = crypto::Decryptor::decrypt(pair, &mpi::Ciphertext::RSA { c: mpi::MPI::new(c) }, None).map_err(|e| CardError::Other(e.to_string()))?;
1815 Protected::from(&session[..])
1816 }
1817 (ToDecipher::Ecdh(point), mpi::PublicKey::ECDH { curve: Curve::Cv25519, .. }) => {
1818 let x25519 = self.x25519.ok_or_else(|| CardError::Algorithm("Curve25519".into()))?;
1819 let scalar = pair.secret().map(|secret| match secret {
1821 mpi::SecretKeyMaterial::ECDH { scalar } => {
1822 let mut native = scalar.value_padded(32).to_vec();
1823 native.reverse();
1824 <[u8; 32]>::try_from(native.as_slice()).ok()
1825 }
1826 _ => None,
1827 });
1828 let scalar = scalar.ok_or_else(|| CardError::Other("no Curve25519 scalar".into()))?;
1829 let point = <[u8; 32]>::try_from(point).map_err(|_| CardError::Other("malformed point".into()))?;
1830 Protected::from(&x25519(&scalar, &point)[..])
1831 }
1832 _ => return Err(CardError::Algorithm(algorithm_name(pair.public()))),
1833 };
1834 self.operations += 1;
1835 Ok(opened)
1836 }
1837}
1838
1839#[derive(Default)]
1842pub struct SoftReaders {
1843 pub cards: Mutex<Vec<SoftCard>>,
1845 pub busy: usize,
1847 pub failure: Option<CardError>,
1849 pub taps: bool,
1851}
1852
1853impl SoftReaders {
1854 pub fn with(cards: Vec<SoftCard>) -> SoftReaders {
1856 SoftReaders { cards: Mutex::new(cards), busy: 0, failure: None, taps: false }
1857 }
1858
1859 pub fn card<R>(&self, ident: &str, f: impl FnOnce(&mut SoftCard) -> R) -> Option<R> {
1861 let mut cards = self.cards.lock().ok()?;
1862 cards.iter_mut().find(|c| c.info.ident == ident).map(f)
1863 }
1864}
1865
1866impl Readers for SoftReaders {
1867 fn present(&self) -> Result<Vec<String>, CardError> {
1868 if let Some(failure) = &self.failure {
1869 return Err(failure.clone());
1870 }
1871 Ok(self.cards.lock().map(|c| c.iter().map(|c| c.info.reader.clone()).collect()).unwrap_or_default())
1872 }
1873
1874 fn visit(&self, visit: &mut dyn FnMut(&mut dyn Card) -> Visit) -> Result<Visited, CardError> {
1875 if let Some(failure) = &self.failure {
1876 return Err(failure.clone());
1877 }
1878 let mut cards = self.cards.lock().map_err(|_| CardError::Other("poisoned".into()))?;
1879 let mut visited = Visited { opened: 0, busy: self.busy };
1880 for card in cards.iter_mut() {
1881 visited.opened += 1;
1882 let next = visit(card);
1883 card.close();
1884 if next == Visit::Done {
1885 break;
1886 }
1887 }
1888 Ok(visited)
1889 }
1890
1891 fn taps(&self) -> bool {
1892 self.taps
1893 }
1894}
1895
1896#[cfg(test)]
1897pub(crate) mod x25519 {
1898 type Field = [i64; 16];
1903
1904 const A24: Field = [0xDB41, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
1905
1906 fn carry(o: &mut Field) {
1907 for i in 0..16 {
1908 o[i] += 1 << 16;
1909 let c = o[i] >> 16;
1910 if i < 15 {
1911 o[i + 1] += c - 1;
1912 } else {
1913 o[0] += 38 * (c - 1);
1914 }
1915 o[i] -= c << 16;
1916 }
1917 }
1918
1919 fn swap(p: &mut Field, q: &mut Field, bit: i64) {
1920 let c = !(bit - 1);
1921 for i in 0..16 {
1922 let t = c & (p[i] ^ q[i]);
1923 p[i] ^= t;
1924 q[i] ^= t;
1925 }
1926 }
1927
1928 fn pack(n: &Field) -> [u8; 32] {
1929 let mut t = *n;
1930 carry(&mut t);
1931 carry(&mut t);
1932 carry(&mut t);
1933 for _ in 0..2 {
1934 let mut m = [0i64; 16];
1935 m[0] = t[0] - 0xffed;
1936 for i in 1..15 {
1937 m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
1938 m[i - 1] &= 0xffff;
1939 }
1940 m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
1941 let b = (m[15] >> 16) & 1;
1942 m[14] &= 0xffff;
1943 swap(&mut t, &mut m, 1 - b);
1944 }
1945 let mut o = [0u8; 32];
1946 for i in 0..16 {
1947 o[2 * i] = (t[i] & 0xff) as u8;
1948 o[2 * i + 1] = ((t[i] >> 8) & 0xff) as u8;
1949 }
1950 o
1951 }
1952
1953 fn unpack(n: &[u8; 32]) -> Field {
1954 let mut o = [0i64; 16];
1955 for i in 0..16 {
1956 o[i] = i64::from(n[2 * i]) + (i64::from(n[2 * i + 1]) << 8);
1957 }
1958 o[15] &= 0x7fff;
1959 o
1960 }
1961
1962 fn add(a: &Field, b: &Field) -> Field {
1963 std::array::from_fn(|i| a[i] + b[i])
1964 }
1965
1966 fn sub(a: &Field, b: &Field) -> Field {
1967 std::array::from_fn(|i| a[i] - b[i])
1968 }
1969
1970 fn mul(a: &Field, b: &Field) -> Field {
1971 let mut t = [0i64; 31];
1972 for i in 0..16 {
1973 for j in 0..16 {
1974 t[i + j] += a[i] * b[j];
1975 }
1976 }
1977 for i in 0..15 {
1978 t[i] += 38 * t[i + 16];
1979 }
1980 let mut o: Field = std::array::from_fn(|i| t[i]);
1981 carry(&mut o);
1982 carry(&mut o);
1983 o
1984 }
1985
1986 fn invert(i: &Field) -> Field {
1987 let mut c = *i;
1988 for a in (0..=253).rev() {
1989 c = mul(&c, &c);
1990 if a != 2 && a != 4 {
1991 c = mul(&c, i);
1992 }
1993 }
1994 c
1995 }
1996
1997 pub(crate) fn x25519(scalar: &[u8; 32], u: &[u8; 32]) -> [u8; 32] {
1999 let mut z = *scalar;
2000 z[31] = (z[31] & 127) | 64;
2001 z[0] &= 248;
2002 let x = unpack(u);
2003 let (mut a, mut b, mut c, mut d) = ([0i64; 16], x, [0i64; 16], [0i64; 16]);
2004 a[0] = 1;
2005 d[0] = 1;
2006 for i in (0..=254usize).rev() {
2007 let r = i64::from((z[i >> 3] >> (i & 7)) & 1);
2008 swap(&mut a, &mut b, r);
2009 swap(&mut c, &mut d, r);
2010 let mut e = add(&a, &c);
2011 a = sub(&a, &c);
2012 c = add(&b, &d);
2013 b = sub(&b, &d);
2014 d = mul(&e, &e);
2015 let f = mul(&a, &a);
2016 a = mul(&c, &a);
2017 c = mul(&b, &e);
2018 e = add(&a, &c);
2019 a = sub(&a, &c);
2020 b = mul(&a, &a);
2021 c = sub(&d, &f);
2022 a = mul(&c, &A24);
2023 a = add(&a, &d);
2024 c = mul(&c, &a);
2025 a = mul(&d, &f);
2026 d = mul(&b, &x);
2027 b = mul(&e, &e);
2028 swap(&mut a, &mut b, r);
2029 swap(&mut c, &mut d, r);
2030 }
2031 pack(&mul(&a, &invert(&c)))
2032 }
2033
2034 #[test]
2035 fn rfc_7748_vector() {
2036 let hex = |s: &str| -> [u8; 32] { std::array::from_fn(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap()) };
2037 let out = x25519(&hex("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4"), &hex("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c"));
2038 assert_eq!(out, hex("c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552"));
2039 }
2040}
2041
2042#[cfg(test)]
2043mod tests {
2044 use super::*;
2045 use openpgp::crypto::Signer as _;
2046 use openpgp::types::KeyFlags;
2047
2048 fn primary_signs(address: &str) -> Cert {
2050 CertBuilder::new()
2051 .add_userid(format!("Card Holder <{address}>"))
2052 .set_cipher_suite(CipherSuite::Cv25519)
2053 .set_primary_key_flags(KeyFlags::empty().set_certification().set_signing())
2054 .add_transport_encryption_subkey()
2055 .generate()
2056 .unwrap()
2057 .0
2058 }
2059
2060 fn touchless(_: &[u8; 32], _: &[u8; 32]) -> [u8; 32] {
2061 [0; 32]
2062 }
2063
2064 #[test]
2065 fn signs_and_checks_each_signature() {
2066 let cert = primary_signs("holder@example.org");
2067 let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456")]);
2068 let pin = Password::from("123456");
2069 let touched = std::sync::atomic::AtomicUsize::new(0);
2070 let touch = || {
2071 touched.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2072 };
2073 let access = CardAccess::new(&readers, &pin, &touch);
2074 let public = cert.primary_key().key().clone().role_into_unspecified();
2075 let mut signer = CardSigner::new(&access, "0006:12345678", public.clone());
2076 let digest = [7u8; 64];
2077 let signature = signer.sign(HashAlgorithm::SHA512, &digest).unwrap();
2078 public.verify(&signature, HashAlgorithm::SHA512, &digest).unwrap();
2079 assert_eq!(touched.load(std::sync::atomic::Ordering::Relaxed), 0, "no touch asked: none said");
2080 assert_eq!(access.reader().as_deref(), Some("Soft reader 0006:12345678"));
2081 readers.card("0006:12345678", |c| c.info.sign.as_mut().unwrap().touch = Touch::On);
2083 signer.sign(HashAlgorithm::SHA512, &digest).unwrap();
2084 assert_eq!(touched.load(std::sync::atomic::Ordering::Relaxed), 1);
2085 readers.card("0006:12345678", |c| c.fault = Some(CardError::TouchMissed));
2087 let fresh = CardAccess::new(&readers, &pin, &touch);
2088 assert!(CardSigner::new(&fresh, "0006:12345678", public.clone()).sign(HashAlgorithm::SHA512, &digest).is_err());
2089 assert_eq!(fresh.failure(), Some(CardError::TouchMissed));
2090 assert!(fresh.pin_accepted());
2092 let other = SoftReaders::with(vec![SoftCard::new("0006:87654321", &cert, "123456")]);
2094 let elsewhere = CardAccess::new(&other, &pin, &touch);
2095 assert!(CardSigner::new(&elsewhere, "0006:12345678", public).sign(HashAlgorithm::SHA512, &digest).is_err());
2096 assert_eq!(elsewhere.failure(), Some(CardError::Absent));
2097 }
2098
2099 #[test]
2104 fn a_key_with_one_try_left_takes_the_pin_only_once_confirmed() {
2105 let cert = primary_signs("holder@example.org");
2106 for taps in [false, true] {
2107 let mut card = SoftCard::new("0006:12345678", &cert, "123456");
2108 card.info.pin_tries = 1;
2109 let readers = SoftReaders { taps, ..SoftReaders::with(vec![card]) };
2110 let public = cert.primary_key().key().clone().role_into_unspecified();
2111 let right = Password::from("123456");
2112 let access = CardAccess::new(&readers, &right, &|| {});
2113 let mut signer = CardSigner::new(&access, "0006:12345678", public.clone());
2114 assert!(signer.sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
2115 assert_eq!(access.failure(), Some(CardError::LastTry));
2116 assert!(!access.pin_accepted());
2117 assert_eq!(readers.card("0006:12345678", |c| c.info.pin_tries), Some(1), "nothing sent");
2118 access.allow_last_try();
2119 let signature = signer.sign(HashAlgorithm::SHA512, &[1; 64]).unwrap();
2120 public.verify(&signature, HashAlgorithm::SHA512, &[1; 64]).unwrap();
2121 assert_eq!(readers.card("0006:12345678", |c| c.info.pin_tries), Some(3));
2122 }
2123 assert_eq!(CardError::LastTry.remedy(), Remedy::Pin);
2124 let tr = Translator::new("en");
2125 assert!(CardError::LastTry.sentence(&tr, Purpose::Sign).to_lowercase().contains("one pin try left"));
2126 }
2127
2128 #[test]
2129 fn a_wrong_pin_costs_a_try_and_three_lock_the_key() {
2130 let cert = primary_signs("holder@example.org");
2131 let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456")]);
2132 let public = cert.primary_key().key().clone().role_into_unspecified();
2133 let wrong = Password::from("000000");
2134 let access = CardAccess::new(&readers, &wrong, &|| {});
2135 let mut signer = CardSigner::new(&access, "0006:12345678", public.clone());
2136 for left in [2, 1] {
2137 assert!(signer.sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
2138 assert_eq!(access.failure(), Some(CardError::WrongPin { left }));
2139 }
2140 assert!(signer.sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
2142 assert_eq!(access.failure(), Some(CardError::LastTry));
2143 assert_eq!(readers.card("0006:12345678", |c| c.info.pin_tries), Some(1));
2144 access.allow_last_try();
2146 assert!(signer.sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
2147 assert_eq!(access.failure(), Some(CardError::WrongPin { left: 0 }));
2148 assert!(!access.pin_accepted());
2149 assert!(signer.sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
2150 assert_eq!(access.failure(), Some(CardError::Blocked));
2151 let right = Password::from("123456");
2153 let access = CardAccess::new(&readers, &right, &|| {});
2154 assert!(CardSigner::new(&access, "0006:12345678", public).sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
2155 assert_eq!(access.failure(), Some(CardError::Blocked));
2156 let tr = Translator::new("en");
2157 assert!(CardError::WrongPin { left: 2 }.sentence(&tr, Purpose::Sign).contains("2 tries left"));
2158 assert!(CardError::WrongPin { left: 1 }.sentence(&tr, Purpose::Sign).contains("one more mistake locks the key"));
2159 assert!(CardError::WrongPin { left: 0 }.sentence(&tr, Purpose::Sign).contains("Admin PIN"));
2160 assert_eq!(CardError::WrongPin { left: 1 }.remedy(), Remedy::Pin);
2161 assert_eq!(CardError::Blocked.remedy(), Remedy::None);
2162 }
2163
2164 #[test]
2165 fn the_pin_for_each_signature_is_given_each_time() {
2166 let cert = primary_signs("holder@example.org");
2167 let mut card = SoftCard::new("0006:12345678", &cert, "123456");
2168 card.info.pin_each_signature = true;
2169 let pin = Password::from("123456");
2170 card.verify(&pin, PinMode::Sign).unwrap();
2171 card.sign(ToSign::Digest(&[1; 64])).unwrap();
2172 assert!(card.sign(ToSign::Digest(&[1; 64])).is_err());
2174 let readers = SoftReaders::with(vec![card]);
2175 let access = CardAccess::new(&readers, &pin, &|| {});
2176 let mut signer = CardSigner::new(&access, "0006:12345678", cert.primary_key().key().clone().role_into_unspecified());
2177 signer.sign(HashAlgorithm::SHA512, &[2; 64]).unwrap();
2178 signer.sign(HashAlgorithm::SHA512, &[3; 64]).unwrap();
2179 }
2180
2181 #[test]
2182 fn rsa_keys_sign_with_a_digest_info() {
2183 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();
2184 let readers = SoftReaders::with(vec![SoftCard::new("000F:00001A2B", &cert, "123456")]);
2185 let pin = Password::from("123456");
2186 let access = CardAccess::new(&readers, &pin, &|| {});
2187 let public = cert.primary_key().key().clone().role_into_unspecified();
2188 let mut signer = CardSigner::new(&access, "000F:00001A2B", public.clone());
2189 assert!(!signer.acceptable_hashes().contains(&HashAlgorithm::SHA1));
2190 let digest = [9u8; 32];
2191 let signature = signer.sign(HashAlgorithm::SHA256, &digest).unwrap();
2192 public.verify(&signature, HashAlgorithm::SHA256, &digest).unwrap();
2193 let subkey = cert.keys().subkeys().next().unwrap().key().clone().role_into_unspecified();
2195 let session = SessionKey::from(&[1u8; 32][..]);
2196 let sealed = subkey.encrypt(&session).unwrap();
2197 let mut decryptor = CardDecryptor::new(&access, "000F:00001A2B", subkey);
2198 let opened = crypto::Decryptor::decrypt(&mut decryptor, &sealed, None).unwrap();
2199 assert_eq!(&opened[..], &session[..]);
2200 }
2201
2202 #[test]
2203 fn curve25519_opens_through_the_cards_shared_secret() {
2204 let cert = primary_signs("holder@example.org");
2205 let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456").with_x25519(x25519::x25519)]);
2206 let pin = Password::from("123456");
2207 let access = CardAccess::new(&readers, &pin, &|| {});
2208 let subkey = cert.keys().subkeys().next().unwrap().key().clone().role_into_unspecified();
2209 let session = SessionKey::from(&[5u8; 32][..]);
2210 let sealed = subkey.encrypt(&session).unwrap();
2211 let mut decryptor = CardDecryptor::new(&access, "0006:12345678", subkey.clone());
2212 let opened = crypto::Decryptor::decrypt(&mut decryptor, &sealed, None).unwrap();
2213 assert_eq!(&opened[..], &session[..]);
2214 let wrong = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456").with_x25519(touchless)]);
2216 let access = CardAccess::new(&wrong, &pin, &|| {});
2217 let mut decryptor = CardDecryptor::new(&access, "0006:12345678", subkey);
2218 assert!(crypto::Decryptor::decrypt(&mut decryptor, &sealed, None).is_err());
2219 }
2220
2221 #[test]
2222 fn another_key_held_to_the_phone_is_said_not_waited_for() {
2223 let cert = primary_signs("holder@example.org");
2224 let mut readers = SoftReaders::with(vec![SoftCard::new("0006:87654321", &cert, "123456")]);
2225 assert_eq!(with_card(&readers, "0006:12345678", |_, _| Ok(())), Err(CardError::Absent));
2227 readers.taps = true;
2229 assert_eq!(with_card(&readers, "0006:12345678", |_, _| Ok(())), Err(CardError::NotYours));
2230 assert_eq!(CardError::NotYours.remedy(), Remedy::Again);
2231 assert_eq!(CardError::NfcOff.remedy(), Remedy::NfcSettings);
2232 let empty = SoftReaders { taps: true, ..SoftReaders::with(vec![]) };
2234 assert_eq!(with_card(&empty, "0006:12345678", |_, _| Ok(())), Err(CardError::Absent));
2235 let tr = Translator::new("en");
2236 for error in [CardError::NotYours, CardError::NoNfc, CardError::NfcOff] {
2237 let said = error.sentence(&tr, Purpose::Open);
2238 assert!(!said.is_empty() && !said.contains("seckey-"), "{error:?}");
2239 }
2240 }
2241
2242 #[test]
2243 fn a_key_held_elsewhere_or_no_service_is_said() {
2244 let cert = primary_signs("holder@example.org");
2245 let mut readers = SoftReaders::with(vec![]);
2246 assert_eq!(cards(&readers), Err(CardError::Absent));
2247 readers.busy = 1;
2248 assert_eq!(cards(&readers), Err(CardError::Busy));
2249 assert_eq!(with_card(&readers, "0006:12345678", |_, _| Ok(())), Err(CardError::Busy));
2250 readers.failure = Some(CardError::NoService);
2251 assert_eq!(cards(&readers), Err(CardError::NoService));
2252 let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456")]);
2253 let found = cards(&readers).unwrap();
2254 assert_eq!(found[0].ident, "0006:12345678");
2255 assert_eq!(found[0].sign.as_ref().unwrap().algorithm, "Ed25519");
2256 assert_eq!(found[0].decrypt.as_ref().unwrap().algorithm, "Curve25519");
2257 let tr = Translator::new("en");
2258 assert_eq!(found[0].label(&tr), "YubiKey 12\u{202F}345\u{202F}678");
2259 assert_eq!(serial_text(0x0A1B2C3D), "A1B2C3D");
2260 assert_eq!(CardError::Busy.remedy(), Remedy::Release);
2261 assert!(CardError::NoService.sentence(&tr, Purpose::Sign).contains("smart card service"));
2262 }
2263
2264 #[test]
2265 fn the_system_names_its_own_command() {
2266 assert_eq!(hint_for_os_release("NAME=\"Fedora Linux\"\nID=fedora\n"), "seckey-hint-fedora");
2267 assert_eq!(hint_for_os_release("ID=linuxmint\nID_LIKE=\"ubuntu debian\"\n"), "seckey-hint-debian");
2268 assert_eq!(hint_for_os_release("ID=endeavouros\nID_LIKE=arch\n"), "seckey-hint-arch");
2269 assert_eq!(hint_for_os_release("ID=\"opensuse-tumbleweed\"\nID_LIKE=\"opensuse suse\"\n"), "seckey-hint-suse");
2270 assert_eq!(hint_for_os_release("ID=gentoo\n"), "seckey-hint-linux");
2271 }
2272
2273 #[test]
2274 fn a_certificate_is_checked_against_the_key() {
2275 let cert = primary_signs("holder@example.org");
2276 let card = SoftCard::new("0006:12345678", &cert, "123456");
2277 let found = check(&cert.clone().strip_secret_key_material(), &card.info).unwrap();
2278 assert_eq!(found.addresses, vec!["holder@example.org".to_string()]);
2279 assert!(found.signs && found.decrypts && !found.expired);
2280 assert_eq!(found.cert, cert.fingerprint().to_hex());
2281 let stranger = primary_signs("holder@example.org");
2283 assert_eq!(check(&stranger, &card.info), Err(Mismatch::Missing));
2284 let mut forged = card.info.clone();
2286 forged.sign.as_mut().unwrap().public = card_public(stranger.primary_key().key().role_as_unspecified());
2287 assert_eq!(check(&cert, &forged), Err(Mismatch::OtherPublicKey));
2288 let mut swapped = card.info.clone();
2290 swapped.sign = card.info.decrypt.clone();
2291 assert_eq!(check(&cert, &swapped), Err(Mismatch::NotForSigning));
2292 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();
2294 let card_of_revoked = SoftCard::new("0006:00000001", &revoked, "123456");
2295 let revoked = revoked.insert_packets(revocation).unwrap().0;
2296 assert_eq!(check(&revoked, &card_of_revoked.info), Err(Mismatch::Revoked));
2297 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();
2299 match check(&bare, &card.info) {
2300 Ok(found) => assert!(found.addresses.is_empty()),
2301 Err(Mismatch::Invalid(_)) => {}
2303 Err(other) => panic!("{other:?}"),
2304 }
2305 }
2306
2307 #[test]
2311 fn each_part_of_the_key_and_its_expiry_said_exactly() {
2312 let tr = Translator::new("en");
2313 let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64;
2314 let made = SystemTime::now() - Duration::from_secs(3 * 365 * 24 * 3600);
2315 let (old, _) = CertBuilder::new()
2316 .add_userid("Card Holder <late@example.org>")
2317 .set_cipher_suite(CipherSuite::Cv25519)
2318 .set_creation_time(made)
2319 .set_validity_period(Duration::from_secs(365 * 24 * 3600))
2320 .set_primary_key_flags(KeyFlags::empty().set_certification().set_signing())
2321 .add_transport_encryption_subkey()
2322 .add_authentication_subkey()
2323 .generate()
2324 .unwrap();
2325 let roles: Vec<Role> = parts(&old).iter().map(|p| p.role).collect();
2326 assert_eq!(roles, [Role::Primary, Role::Encryption, Role::Authentication]);
2327 let (line, warm, expired) = expiry_words(&tr, &old, None, now);
2329 assert!(warm && expired);
2330 let primary = short_fingerprint(&old.fingerprint().to_hex());
2331 assert!(line.starts_with(&format!("Its primary key ({primary}) expired on ")), "{line}");
2332 assert!(line.contains("Its encryption subkey (") && line.contains("nobody can encrypt to you with it.") && line.contains("Its authentication subkey ("), "{line}");
2333 assert!(!line.contains("Renew for two years"), "{line}");
2334 let mut signer = old.primary_key().key().clone().parts_into_secret().unwrap().into_keypair().unwrap();
2336 let policy = StandardPolicy::new();
2337 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();
2338 let half = old.clone().insert_packets(renewed).unwrap().0;
2339 let (line, warm, expired) = expiry_words(&tr, &half, None, now);
2340 assert!(warm && expired);
2341 let encryption = short_fingerprint(&parts(&half)[1].fingerprint);
2342 assert!(line.starts_with(&format!("Its encryption subkey ({encryption}) expired on ")) && !line.contains("Its primary key"), "{line}");
2343 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}");
2344 assert_eq!(subkeys_of(&half).len(), 2);
2345 let (fresh, _) = CertBuilder::general_purpose(Some("Card Holder <now@example.org>")).set_validity_period(Duration::from_secs(365 * 24 * 3600)).generate().unwrap();
2347 let (line, warm, expired) = expiry_words(&tr, &fresh, Some(now + 300 * 86_400), now);
2348 assert!(!warm && !expired && line.starts_with("Valid until "), "{line}");
2349 assert!(parts_until(&tr, &parts(&fresh)).starts_with("primary key until "));
2350 assert_eq!(short_fingerprint("b81a 2c3d 4e5f 6a7b 8c9d 0e1f 2a3b 4c5d 6e7f bf8c"), "B81A…BF8C");
2351 }
2352
2353 #[test]
2354 fn an_expired_key_is_still_its_key_and_says_so() {
2355 let made = SystemTime::now() - Duration::from_secs(3 * 365 * 24 * 3600);
2356 let (cert, _) = CertBuilder::new()
2357 .add_userid("Card Holder <late@example.org>")
2358 .set_cipher_suite(CipherSuite::Cv25519)
2359 .set_creation_time(made)
2360 .set_validity_period(Duration::from_secs(365 * 24 * 3600))
2361 .set_primary_key_flags(KeyFlags::empty().set_certification().set_signing())
2362 .add_transport_encryption_subkey()
2363 .generate()
2364 .unwrap();
2365 let card = SoftCard::new("0006:12345678", &cert, "123456");
2366 let found = check(&cert, &card.info).unwrap();
2367 assert!(found.expired);
2368 let tr = Translator::new("en");
2369 let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64;
2370 let (line, warm) = expiry_line(&tr, found.expires, &found.cert, now).unwrap();
2371 assert!(warm && line.contains("expired on") && line.contains("Accounts ▸ Encryption") && !line.contains("gpg "), "{line}");
2373 let (soon, warm) = expiry_line(&tr, Some(now + 10 * 24 * 3600), "F", now).unwrap();
2374 assert!(warm && soon.contains("expires on"), "{soon}");
2375 let (later, warm) = expiry_line(&tr, Some(now + 300 * 24 * 3600), "F", now).unwrap();
2376 assert!(!warm && later.contains("until"), "{later}");
2377 assert_eq!(expiry_line(&tr, None, "F", now), None);
2378 }
2379
2380 #[test]
2381 fn the_pin_is_held_fifteen_quiet_minutes() {
2382 let start = Instant::now();
2383 let mut memory = PinMemory::new();
2384 memory.keep("0006:12345678", Password::from("123456"), "Yubico YubiKey", start);
2385 assert!(memory.get("0006:12345678", start + Duration::from_secs(60)).is_some());
2386 assert!(memory.get("0006:87654321", start).is_none());
2387 memory.used("0006:12345678", start + Duration::from_secs(14 * 60));
2389 assert!(memory.get("0006:12345678", start + Duration::from_secs(28 * 60)).is_some());
2390 assert!(memory.get("0006:12345678", start + Duration::from_secs(29 * 60 + 1)).is_none());
2391 assert_eq!(memory.holder(), None);
2392 memory.keep("0006:12345678", Password::from("123456"), "Yubico YubiKey", start);
2394 assert!(!memory.went_away(&["Yubico YubiKey".to_string()]));
2395 assert!(memory.went_away(&[]));
2396 assert_eq!(memory.holder(), None);
2397 memory.keep("0006:12345678", Password::from("123456"), "Yubico YubiKey", start);
2398 memory.forget();
2399 assert!(memory.get("0006:12345678", start).is_none());
2400 }
2401
2402 #[test]
2403 fn known_keys_are_kept_without_secrets() {
2404 let dir = std::env::temp_dir().join(format!("sioul-cards-{}", std::process::id()));
2405 let path = dir.join("cards.toml");
2406 let _ = std::fs::remove_file(&path);
2407 change_known_at(&path, |cards| {
2408 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() });
2409 })
2410 .unwrap();
2411 let read = read_known(&path);
2412 assert_eq!(read.len(), 1);
2413 assert!(read[0].serves("holder@example.org") && !read[0].serves("other@example.org"));
2414 assert_eq!(read[0].touch_sign, Touch::On);
2415 let text = std::fs::read_to_string(&path).unwrap();
2416 assert!(text.contains("[[card]]") && text.contains("touch_sign = \"on\""), "{text}");
2417 let tr = Translator::new("en");
2418 let mut known = read[0].clone();
2419 assert_eq!(known.tries_line(3, &tr), None);
2420 assert!(known.tries_line(2, &tr).unwrap().contains("2 tries left"));
2421 known.pin_tries_full = 5;
2422 assert!(known.tries_line(4, &tr).is_some());
2423 std::fs::remove_dir_all(&dir).unwrap();
2424 }
2425
2426 #[test]
2427 fn every_failure_has_words_in_each_language() {
2428 for language in ["en", "fr"] {
2429 let tr = Translator::new(language);
2430 let failures = [
2431 CardError::Unsupported,
2432 CardError::NoService,
2433 CardError::Absent,
2434 CardError::Busy,
2435 CardError::WrongPin { left: 2 },
2436 CardError::WrongPin { left: 1 },
2437 CardError::Blocked,
2438 CardError::TouchMissed,
2439 CardError::Removed,
2440 CardError::NotOpenPgp,
2441 CardError::OtherKeys,
2442 CardError::BadSignature,
2443 CardError::Algorithm("X448".into()),
2444 CardError::Other("no reader".into()),
2445 ];
2446 for failure in failures {
2447 for purpose in [Purpose::Sign, Purpose::Open, Purpose::Read] {
2448 let said = failure.sentence(&tr, purpose);
2449 assert!(!said.is_empty() && !said.contains("seckey-") && !said.contains('!'), "{language} {failure:?}: {said}");
2450 }
2451 }
2452 for mismatch in [Mismatch::Empty, Mismatch::Missing, Mismatch::Revoked, Mismatch::NotForSigning, Mismatch::OtherPublicKey, Mismatch::Invalid("SHA-1".into())] {
2453 assert!(!mismatch.sentence(&tr).contains("seckey-"), "{language} {mismatch:?}");
2454 }
2455 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"] {
2456 assert_ne!(tr.text(hint, None), hint, "{language} {hint}");
2457 }
2458 }
2459 }
2460}