1use crate::backend::{QtThread, Shared, json, load_config, local_path, offline, say, tell, tr};
20use cxx_qt_lib::QString;
21use sioul_core::compose::{Draft, DraftKind};
22use sioul_core::i18n::Translator;
23use sioul_core::pgp::{self, Keys, Place};
24use sioul_core::securitykey::{self, CardAccess, CardError, CardInfo, KnownCard, NoReaders, Password, PinMemory, Purpose, Readers, Remedy, SoftCard, SoftReaders, Touch};
25use sioul_sync::securitykey::{Gnupg, GnupgProblem, GnupgStep, Looked, Released};
26use sioul_sync::send::BuildError;
27use std::collections::BTreeSet;
28use std::sync::atomic::{AtomicBool, Ordering};
29use std::sync::{Arc, Mutex, OnceLock};
30use std::time::{Duration, Instant};
31
32static PIN: Mutex<PinMemory> = Mutex::new(PinMemory::new());
34static WATCHING: AtomicBool = AtomicBool::new(false);
36static WAITING: Mutex<BTreeSet<String>> = Mutex::new(BTreeSet::new());
38static LAST_TRY: Mutex<BTreeSet<String>> = Mutex::new(BTreeSet::new());
40static SETUP: Mutex<Option<CardInfo>> = Mutex::new(None);
42static WINDOW: Mutex<Option<QtThread>> = Mutex::new(None);
44
45const LOOK_EVERY: Duration = Duration::from_millis(1500);
47const WAIT_AT_MOST: Duration = Duration::from_secs(5 * 60);
49const PIN_WAITS_AT_MOST: Duration = Duration::from_secs(60);
51const SETUP_BAND: &str = "setup";
53
54struct Demo {
57 readers: SoftReaders,
58 certificate: Vec<u8>,
59}
60
61fn demo() -> Option<&'static Demo> {
62 static DEMO: OnceLock<Option<Demo>> = OnceLock::new();
63 DEMO.get_or_init(|| {
64 let config = load_config();
65 let accounts: Vec<_> = config.accounts.iter().filter(|a| a.address.is_some()).collect();
66 let mut userids: Vec<String> = Vec::new();
68 for account in &accounts {
69 let address = account.address.clone().unwrap_or_default();
70 if userids.iter().any(|u| u.ends_with(&format!("<{address}>"))) {
71 continue;
72 }
73 let name = accounts.iter().filter(|a| a.address.as_deref() == Some(address.as_str())).find_map(|a| a.name.clone()).unwrap_or_default();
74 userids.push(format!("{name} <{address}>").trim().to_string());
75 }
76 let userids: Vec<&str> = userids.iter().map(String::as_str).collect();
77 let (mut card, certificate) = SoftCard::generate("0006:01234567", &userids, "123456").ok()?;
78 card.info.holder = accounts.first().and_then(|a| a.name.clone()).unwrap_or_default();
79 if let Some(slot) = card.info.sign.as_mut() {
80 slot.touch = Touch::On;
81 }
82 card.touch_delay = Duration::from_secs(4);
83 Some(Demo { readers: SoftReaders::with(vec![card]), certificate })
84 })
85 .as_ref()
86}
87
88fn readers() -> &'static dyn Readers {
90 static NONE: NoReaders = NoReaders;
91 if offline() {
92 return demo().map_or(&NONE as &dyn Readers, |d| &d.readers as &dyn Readers);
93 }
94 sioul_sync::securitykey::readers()
95}
96
97#[derive(serde::Serialize, Default)]
99struct Band {
100 state: &'static str,
102 line: String,
104 tries: String,
106 warm: bool,
108 action: &'static str,
111 fingerprint: String,
113 ident: String,
115 commands: Vec<String>,
117}
118
119fn tell_band(qt: &QtThread, context: &str, band: Band) {
120 if taps() {
122 readers().want(matches!(band.state, "pin" | "plug" | "working" | "touch"));
123 }
124 let (context, state) = (context.to_string(), json(&band));
125 let _ = qt.queue(move |mut sioul| sioul.as_mut().security_key_changed(QString::from(&context), QString::from(&state)));
126}
127
128fn working(qt: &QtThread, context: &str, id: &'static str) {
129 tell_band(qt, context, Band { state: "working", line: tr().text(working_words(&way(), id), None), ..Band::default() });
130}
131
132fn working_words(way: &Way, id: &'static str) -> &'static str {
135 if way.held() && matches!(id, "seckey-signing" | "seckey-opening") { "seckey-holding" } else { id }
136}
137
138#[derive(Debug, Clone, Default, PartialEq)]
142struct Way {
143 taps: bool,
145 cable: bool,
147 note: Option<CardError>,
149}
150
151impl Way {
152 fn of(readers: &dyn Readers) -> Way {
153 Way { taps: readers.taps(), cable: readers.by_cable(), note: readers.reach_note() }
154 }
155
156 fn held(&self) -> bool {
158 self.taps && !self.cable
159 }
160}
161
162fn way() -> Way {
163 Way::of(readers())
164}
165
166pub(crate) fn read_here_words(computer: &'static str) -> &'static str {
169 read_here_in(cfg!(target_os = "android"), computer)
170}
171
172fn read_here_in(phone: bool, computer: &'static str) -> &'static str {
173 if !phone {
174 return computer;
175 }
176 match computer {
177 "seckey-sign-read-here" => "seckey-sign-read-here-phone",
178 "seckey-source-shared-computer" => "seckey-source-shared-phone",
179 "pgp-read-card-here" => "pgp-read-card-here-phone",
180 other => other,
181 }
182}
183
184fn taps() -> bool {
187 readers().taps()
188}
189
190fn sentence(error: &CardError, purpose: Purpose) -> String {
194 sentence_in(&way(), error, purpose, tr())
195}
196
197fn sentence_in(way: &Way, error: &CardError, purpose: Purpose, words: &Translator) -> String {
198 let sign = purpose == Purpose::Sign;
199 if way.taps {
200 match error {
201 CardError::Removed if way.cable => return words.text(if sign { "seckey-unplugged-sign" } else { "seckey-unplugged" }, None),
202 CardError::Removed => return words.text(if sign { "seckey-moved-away-sign" } else { "seckey-moved-away" }, None),
203 CardError::TouchMissed if !way.cable => return words.text("seckey-nfc-touch", None),
204 CardError::Blocked | CardError::WrongPin { left: 0 } => return words.text("seckey-blocked-phone", None),
205 CardError::LastTry => return words.text("seckey-last-try-phone", None),
206 _ => {}
207 }
208 }
209 error.sentence(words, purpose)
210}
211
212fn waiting_line(purpose: Purpose) -> String {
215 waiting_in(&way(), purpose, tr())
216}
217
218fn waiting_in(way: &Way, purpose: Purpose, words: &Translator) -> String {
219 if !way.taps {
220 return CardError::Absent.sentence(words, purpose);
221 }
222 words.text(
223 match way.note {
224 Some(CardError::NoNfc) => "seckey-plug-phone",
225 Some(CardError::NfcOff) => "seckey-plug-nfc-off",
226 _ => "seckey-hold",
227 },
228 None,
229 )
230}
231
232fn waiting_action(way: &Way) -> &'static str {
234 if way.taps && way.note == Some(CardError::NfcOff) { "nfc-settings" } else { "" }
235}
236
237fn typed_by_the_key(typed: &str) -> bool {
242 (32..=64).contains(&typed.len()) && typed.bytes().all(|b| b"cbdefghijklnrtuv".contains(&b.to_ascii_lowercase()))
244}
245
246fn take_pin(mut typed: String) -> Option<Password> {
251 let pin = (!typed.is_empty()).then(|| Password::from(typed.as_str()));
252 let bytes = unsafe { typed.as_mut_vec() };
254 for byte in bytes.iter_mut() {
255 unsafe { std::ptr::write_volatile(byte, 0) };
258 }
259 typed.clear();
260 pin
261}
262
263fn pin_waited_too_long(typed: bool, waited: Duration) -> bool {
265 typed && waited > PIN_WAITS_AT_MOST
266}
267
268fn not_a_pin(qt: &QtThread, context: &str) {
270 tell_band(qt, context, Band { state: "pin", line: tr().text("seckey-pin-otp", None), warm: true, ..Band::default() });
271}
272
273fn problem_line(qt: &QtThread, context: &str, line: String, action: &'static str) {
274 tell_band(qt, context, Band { state: "problem", line, warm: true, action, ..Band::default() });
275}
276
277pub(crate) fn for_draft(id: &str) -> String {
287 let Some(draft) = Draft::by_id(id) else { return String::new() };
288 let Some(address) = load_config().account(&draft.account).and_then(|a| a.address.clone()) else { return String::new() };
289 let keys = Keys::load();
290 let here = sioul_sync::securitykey::usable_here();
291 let cannot = |id: &str| json(&serde_json::json!({ "cannot": tr().text(id, None) }));
292 if draft.kind == DraftKind::Forward
294 && let Some(id) = forward_refused(draft.encrypt, &crate::crypto::answered(crate::crypto::copy_of(&draft).as_deref()))
295 {
296 return cannot(id);
297 }
298 let opens = quote_card(&draft, &keys);
300 let label = |ident: &str| keys.all_cards().find(|c| c.ident == ident).map(|c| c.label(tr())).unwrap_or_default();
301 match draft.sign.then(|| keys.own_for(&address)) {
302 Some(Some(pgp::Own { place: Place::Card { .. }, .. })) if !here => cannot("seckey-sign-not-here"),
304 Some(Some(pgp::Own { place: Place::Card { ident }, .. })) => json(&serde_json::json!({ "ident": ident, "label": label(&ident), "signs": true })),
305 Some(None) if keys.card_for(&address).is_none() && keys.serving_card(&address).is_some() => cannot(if here { read_here_words("seckey-sign-read-here") } else { "seckey-sign-not-here" }),
307 signing => match matches!(signing, Some(None)).then(|| expired_line(&keys, &address)).flatten() {
309 Some(line) => json(&serde_json::json!({ "expired": line })),
310 None => opens.filter(|_| here).map(|ident| json(&serde_json::json!({ "ident": ident, "label": label(&ident), "signs": false }))).unwrap_or_default(),
311 },
312 }
313}
314
315fn forward_refused(encrypt: bool, answered: &crate::crypto::Answered) -> Option<&'static str> {
321 use crate::crypto::Answered;
322 match answered {
323 Answered::Clear => None,
324 _ if !encrypt => Some("pgp-forward-needs-encryption"),
325 Answered::Elsewhere => Some("seckey-forward-not-here"),
326 Answered::NoKey => Some("compose-forward-sealed-no-key"),
327 Answered::Opens(_) | Answered::AtSend => None,
328 }
329}
330
331pub(crate) fn quote_card(draft: &Draft, keys: &Keys) -> Option<String> {
336 if !draft.encrypt {
337 return None;
338 }
339 let raw = crate::crypto::copy_of(draft)?;
340 if !pgp::is_encrypted(&raw) {
341 return None;
342 }
343 let passphrase = |fingerprint: &pgp::Fingerprint| sioul_sync::secret::pgp_passphrase(&fingerprint.to_hex());
344 let unlock = pgp::Unlock::new(&passphrase, pgp::SessionKeys::global());
345 match pgp::open(&raw, keys, &unlock) {
346 Some((Some(_), _)) => None,
347 _ => unlock.wanted(),
348 }
349}
350
351pub(crate) fn expired_line(keys: &Keys, address: &str) -> Option<String> {
354 let (known, cert) = keys.card_for(address)?;
355 let expires = securitykey::expiry_of(cert, &[&known.sign, &known.decrypt]);
356 securitykey::expiry_line(tr(), expires, &cert.fingerprint().to_hex(), now_unix()).filter(|(_, warm)| *warm).map(|(line, _)| line)
357}
358
359fn now_unix() -> i64 {
360 jiff::Timestamp::now().as_second()
361}
362
363fn pin_for(ident: &str, typed: Option<&Password>, purpose: Purpose) -> Result<(CardInfo, Option<Password>), CardError> {
367 pin_from(readers(), || Keys::load().all_cards().find(|c| c.ident.eq_ignore_ascii_case(ident)).cloned(), ident, typed, purpose)
368}
369
370fn pin_from(readers: &dyn Readers, known: impl FnOnce() -> Option<KnownCard>, ident: &str, typed: Option<&Password>, purpose: Purpose) -> Result<(CardInfo, Option<Password>), CardError> {
372 if readers.taps() {
375 let known = known().ok_or(CardError::Absent)?;
376 return Ok((as_info(&known), typed.cloned()));
377 }
378 let info = securitykey::cards(readers)?.into_iter().find(|c| c.ident.eq_ignore_ascii_case(ident)).ok_or(CardError::Absent)?;
379 if info.pin_tries == 0 {
380 return Err(CardError::Blocked);
381 }
382 if let Some(typed) = typed {
383 return Ok((info, Some(typed.clone())));
384 }
385 if purpose == Purpose::Sign && info.pin_each_signature {
386 return Ok((info, None));
387 }
388 let held = PIN.lock().ok().and_then(|mut memory| memory.get(ident, Instant::now()));
389 Ok((info, held))
390}
391
392fn ask_pin(qt: &QtThread, context: &str, info: &CardInfo, purpose: Purpose) {
394 let known = securitykey::known_cards().into_iter().find(|c| c.ident.eq_ignore_ascii_case(&info.ident)).unwrap_or_default();
395 let tries = known.tries_line(info.pin_tries, tr()).unwrap_or_default();
396 let line = if purpose == Purpose::Sign && info.pin_each_signature { "seckey-pin-each" } else { "seckey-pin" };
397 tell_band(qt, context, Band { state: "pin", line: tr().text(line, None), warm: !tries.is_empty(), tries, ..Band::default() });
398}
399
400fn keys_changed() {
402 if let Some(qt) = WINDOW.lock().ok().and_then(|w| w.clone()) {
403 let _ = qt.queue(|mut sioul| sioul.as_mut().keys_changed());
404 }
405}
406
407fn remember(ident: &str, pin: Password, reader: Option<String>, info: &CardInfo) {
409 if keeps_pin(readers())
410 && let Ok(mut memory) = PIN.lock()
411 {
412 memory.keep(ident, pin, &reader.unwrap_or_else(|| info.reader.clone()), Instant::now());
413 }
414 keys_changed();
415 let mut known = securitykey::known_cards().into_iter().find(|c| c.ident.eq_ignore_ascii_case(ident));
416 if let Some(known) = known.as_mut() {
417 let before = known.clone();
418 known.update(info);
419 if *known != before {
420 let updated = known.clone();
421 let _ = securitykey::change_known(|cards| {
422 if let Some(card) = cards.iter_mut().find(|c| c.ident == updated.ident) {
423 *card = updated;
424 }
425 });
426 }
427 }
428 watch_pin();
429}
430
431fn keeps_pin(readers: &dyn Readers) -> bool {
434 !readers.taps()
435}
436
437fn watch_pin() {
440 if WATCHING.swap(true, Ordering::Relaxed) {
441 return;
442 }
443 std::thread::spawn(|| {
444 loop {
445 std::thread::sleep(LOOK_EVERY);
446 if !sioul_sync::securitykey::may_look() {
448 continue;
449 }
450 let present = readers().present();
451 let Ok(mut memory) = PIN.lock() else { break };
452 memory.expire(Instant::now());
453 if let Ok(present) = present {
454 memory.went_away(&present);
455 }
456 if memory.holder().is_none() {
457 break;
458 }
459 }
460 WATCHING.store(false, Ordering::Relaxed);
461 keys_changed();
462 });
463}
464
465pub(crate) fn forget_pin() -> String {
467 if let Ok(mut memory) = PIN.lock() {
468 memory.forget();
469 }
470 tr().text("seckey-pin-forgotten", None)
471}
472
473pub(crate) fn closing() {
475 if let Ok(mut memory) = PIN.lock() {
476 memory.forget();
477 }
478 pgp::SessionKeys::global().forget();
479}
480
481fn keep_taken(ident: &str, pin: &Password, access: &CardAccess, info: &CardInfo) {
484 if access.pin_accepted() {
485 remember(ident, pin.clone(), access.reader(), info);
486 }
487}
488
489fn confirm_last_try(access: &CardAccess, context: &str, typed: bool) {
494 let told = LAST_TRY.lock().is_ok_and(|mut told| told.remove(context));
495 if told || !typed {
496 access.allow_last_try();
497 }
498}
499
500fn failed(qt: &QtThread, context: &str, error: CardError, purpose: Purpose, typed: bool) {
501 if error == CardError::LastTry
502 && let Ok(mut told) = LAST_TRY.lock()
503 {
504 told.insert(context.to_string());
505 }
506 if matches!(error, CardError::WrongPin { .. } | CardError::Blocked) && !typed {
507 if let Ok(mut memory) = PIN.lock() {
509 memory.forget();
510 }
511 }
512 let line = sentence(&error, purpose);
513 match error.remedy() {
514 Remedy::Pin => tell_band(qt, context, Band { state: "pin", line, warm: true, ..Band::default() }),
515 Remedy::Again => problem_line(qt, context, line, "retry"),
516 Remedy::Release => problem_line(qt, context, line, "release"),
517 Remedy::NfcSettings => problem_line(qt, context, line, "nfc-settings"),
518 Remedy::None => problem_line(qt, context, line, ""),
519 }
520}
521
522fn know_window(qt: &QtThread) {
524 if let Ok(mut window) = WINDOW.lock() {
525 *window = Some(qt.clone());
526 }
527}
528
529pub(crate) fn sign_and_send(qt: &QtThread, shared: &Arc<Shared>, id: &str, typed: String) {
532 know_window(qt);
533 if typed_by_the_key(&typed) {
534 take_pin(typed);
535 return not_a_pin(qt, id);
536 }
537 let typed = take_pin(typed);
538 let (qt, shared, id) = (qt.clone(), Arc::clone(shared), id.to_string());
539 std::thread::spawn(move || sign_now(&qt, &shared, &id, typed));
540}
541
542fn sign_now(qt: &QtThread, shared: &Arc<Shared>, id: &str, typed: Option<Password>) {
543 working(qt, id, "seckey-reading");
544 let Some(draft) = Draft::by_id(id) else { return problem_line(qt, id, tr().text("mail-draft-gone", None), "") };
545 let config = load_config();
546 let Some(account) = config.account(&draft.account).filter(|a| a.syncs()).cloned() else {
547 return problem_line(qt, id, say("account-unknown", &[("id", draft.account.clone())]), "");
548 };
549 let keys = Keys::load();
550 let signs = match draft.sign.then(|| keys.own_for(account.address.as_deref().unwrap_or_default())).flatten() {
554 Some(pgp::Own { place: Place::Card { ident }, .. }) => Some(ident),
555 _ => None,
556 };
557 let opens = quote_card(&draft, &keys);
558 let ident = match (signs.as_deref(), opens.as_deref()) {
559 (Some(sign), Some(open)) if !same_key(&keys, sign, open) => return open_quote_now(qt, shared, &draft, open, typed),
563 (Some(ident), _) | (None, Some(ident)) => ident.to_string(),
564 (None, None) => {
566 match crate::mail::send(qt, shared, id) {
567 Some(problem) => problem_line(qt, id, problem, ""),
568 None => tell_band(qt, id, Band { state: "done", ..Band::default() }),
569 }
570 return;
571 }
572 };
573 let purpose = if signs.is_some() { Purpose::Sign } else { Purpose::Open };
574 let (info, pin) = match pin_for(&ident, typed.as_ref(), purpose) {
575 Ok(found) => found,
576 Err(CardError::Absent) => return wait_for(qt, shared, id, &ident, purpose, typed, sign_now),
577 Err(error) => return failed(qt, id, error, purpose, typed.is_some()),
578 };
579 let Some(pin) = pin else { return ask_pin(qt, id, &info, purpose) };
580 working(qt, id, if signs.is_some() { "seckey-signing" } else { "seckey-opening" });
581 let opening = opens.is_some().then_some(info.decrypt.as_ref()).flatten().filter(|s| s.touch.asked());
584 let slot = opening.or(if signs.is_some() { info.sign.as_ref() } else { info.decrypt.as_ref() });
585 let (touch_qt, context, line) = (qt.clone(), id.to_string(), touch_line(slot));
586 let touch = move || tell_band(&touch_qt, &context, Band { state: "touch", line: line.clone(), ..Band::default() });
587 let access = CardAccess::new(readers(), &pin, &touch);
588 confirm_last_try(&access, id, typed.is_some());
589 match sioul_sync::send::build_draft(&account, &draft, tr(), Some(&access)) {
590 Ok(outgoing) => {
591 remember(&ident, pin.clone(), access.reader(), &info);
592 crate::mail::send_signed(qt, shared, id, account, outgoing);
593 tell_band(qt, id, Band { state: "done", ..Band::default() });
594 lift(qt, shared);
595 }
596 Err(BuildError::Card(CardError::Absent)) => wait_for(qt, shared, id, &ident, purpose, typed, sign_now),
597 Err(BuildError::Card(error)) => {
598 keep_taken(&ident, &pin, &access, &info);
599 failed(qt, id, error, purpose, typed.is_some());
600 }
601 Err(other) => problem_line(qt, id, other.into_sync(tr()).sentence(tr(), &account.id).replace('`', "'"), ""),
603 }
604}
605
606fn same_key(keys: &Keys, a: &str, b: &str) -> bool {
609 let decrypt = |ident: &str| keys.all_cards().find(|c| c.ident.eq_ignore_ascii_case(ident)).map(|c| c.decrypt.to_ascii_lowercase()).filter(|f| !f.is_empty());
610 a.eq_ignore_ascii_case(b) || decrypt(a).is_some_and(|f| decrypt(b) == Some(f))
611}
612
613fn open_quote_now(qt: &QtThread, shared: &Arc<Shared>, draft: &Draft, ident: &str, typed: Option<Password>) {
617 let id = draft.id.as_str();
618 let (info, pin) = match pin_for(ident, typed.as_ref(), Purpose::Open) {
619 Ok(found) => found,
620 Err(CardError::Absent) => return wait_for(qt, shared, id, ident, Purpose::Open, typed, sign_now),
621 Err(error) => return failed(qt, id, error, Purpose::Open, typed.is_some()),
622 };
623 let Some(pin) = pin else { return ask_pin(qt, id, &info, Purpose::Open) };
624 working(qt, id, "seckey-opening");
625 let (touch_qt, context, line) = (qt.clone(), id.to_string(), touch_line(info.decrypt.as_ref()));
626 let touch = move || tell_band(&touch_qt, &context, Band { state: "touch", line: line.clone(), ..Band::default() });
627 let access = CardAccess::new(readers(), &pin, &touch);
628 confirm_last_try(&access, id, typed.is_some());
629 let passphrase = |fingerprint: &pgp::Fingerprint| sioul_sync::secret::pgp_passphrase(&fingerprint.to_hex());
630 let unlock = pgp::Unlock::new(&passphrase, pgp::SessionKeys::global()).with_card(&access);
631 let raw = crate::crypto::copy_of(draft);
632 match raw.and_then(|raw| pgp::open(&raw, &Keys::load(), &unlock)) {
633 Some((Some(_), _)) => {
634 remember(ident, pin.clone(), access.reader(), &info);
635 if quote_card(draft, &Keys::load()).is_some() {
637 return problem_line(qt, id, tr().text("pgp-quote-locked", None), "retry");
638 }
639 sign_now(qt, shared, id, None);
641 }
642 _ => match access.failure() {
643 Some(CardError::Absent) => wait_for(qt, shared, id, ident, Purpose::Open, typed, sign_now),
644 Some(error) => {
645 keep_taken(ident, &pin, &access, &info);
646 failed(qt, id, error, Purpose::Open, typed.is_some());
647 }
648 None if draft.kind == DraftKind::Forward => problem_line(qt, id, tr().text("pgp-forward-locked", None), ""),
649 None => problem_line(qt, id, tr().text("pgp-quote-locked", None), ""),
650 },
651 }
652}
653
654fn lift(qt: &QtThread, shared: &Arc<Shared>) {
656 let way = way();
657 if way.taps {
658 tell(qt, shared, tr().text(if way.cable { "seckey-done-unplug" } else { "seckey-done-lift" }, None));
659 }
660}
661
662fn touch_line(slot: Option<&securitykey::SlotInfo>) -> String {
667 if way().held() {
669 return tr().text("seckey-holding", None);
670 }
671 slot.map_or(Touch::Unknown, |s| s.touch).words(tr()).unwrap_or_else(|| tr().text("seckey-touch-maybe", None))
672}
673
674pub(crate) fn open_message(qt: &QtThread, shared: &Arc<Shared>, key: &str, typed: String) {
678 know_window(qt);
679 if typed_by_the_key(&typed) {
680 take_pin(typed);
681 return not_a_pin(qt, key);
682 }
683 let typed = take_pin(typed);
684 let (qt, shared, key) = (qt.clone(), Arc::clone(shared), key.to_string());
685 std::thread::spawn(move || open_now(&qt, &shared, &key, typed));
686}
687
688fn open_now(qt: &QtThread, shared: &Arc<Shared>, key: &str, typed: Option<Password>) {
689 working(qt, key, "seckey-reading");
690 let Some(raw) = crate::mail::locate(key).and_then(|(_, file)| std::fs::read(file).ok()) else {
691 return problem_line(qt, key, tr().text("mail-message-gone", None), "");
692 };
693 let ident = match crate::crypto::open(&raw) {
695 Some((Some(_), _)) | None => return tell_band(qt, key, Band { state: "done", ..Band::default() }),
696 Some((None, view)) => match view.security_key {
697 Some(ident) => ident,
698 None => return problem_line(qt, key, tr().text("pgp-not-opened", None), ""),
699 },
700 };
701 let (info, pin) = match pin_for(&ident, typed.as_ref(), Purpose::Open) {
702 Ok(found) => found,
703 Err(CardError::Absent) => return wait_for(qt, shared, key, &ident, Purpose::Open, typed, open_now),
704 Err(error) => return failed(qt, key, error, Purpose::Open, typed.is_some()),
705 };
706 let Some(pin) = pin else { return ask_pin(qt, key, &info, Purpose::Open) };
707 working(qt, key, "seckey-opening");
708 let (touch_qt, context, line) = (qt.clone(), key.to_string(), touch_line(info.decrypt.as_ref()));
709 let touch = move || tell_band(&touch_qt, &context, Band { state: "touch", line: line.clone(), ..Band::default() });
710 let access = CardAccess::new(readers(), &pin, &touch);
711 confirm_last_try(&access, key, typed.is_some());
712 let passphrase = |fingerprint: &pgp::Fingerprint| sioul_sync::secret::pgp_passphrase(&fingerprint.to_hex());
713 let unlock = pgp::Unlock::new(&passphrase, pgp::SessionKeys::global()).with_card(&access);
714 match pgp::open(&raw, &Keys::load(), &unlock) {
715 Some((Some(_), _)) => {
716 remember(&ident, pin.clone(), access.reader(), &info);
717 tell_band(qt, key, Band { state: "done", ..Band::default() });
718 lift(qt, shared);
719 }
720 _ => match access.failure() {
721 Some(CardError::Absent) => wait_for(qt, shared, key, &ident, Purpose::Open, typed, open_now),
722 Some(error) => {
723 keep_taken(&ident, &pin, &access, &info);
724 failed(qt, key, error, Purpose::Open, typed.is_some());
725 }
726 None => problem_line(qt, key, tr().text("pgp-not-opened", None), ""),
727 },
728 }
729}
730
731type Resume = fn(&QtThread, &Arc<Shared>, &str, Option<Password>);
734
735fn wait_for(qt: &QtThread, shared: &Arc<Shared>, context: &str, ident: &str, purpose: Purpose, typed: Option<Password>, resume: Resume) {
738 if let Ok(mut waiting) = WAITING.lock() {
739 waiting.insert(context.to_string());
740 }
741 tell_band(qt, context, Band { state: "plug", line: waiting_line(purpose), action: waiting_action(&way()), ..Band::default() });
742 let (qt, shared, context, ident) = (qt.clone(), Arc::clone(shared), context.to_string(), ident.to_string());
743 std::thread::spawn(move || {
744 let since = Instant::now();
745 let mut seen: Option<Vec<String>> = None;
746 loop {
747 readers().wait_change(LOOK_EVERY);
749 if !WAITING.lock().is_ok_and(|w| w.contains(&context)) {
750 return;
751 }
752 if since.elapsed() > WAIT_AT_MOST {
753 if let Ok(mut waiting) = WAITING.lock() {
754 waiting.remove(&context);
755 }
756 return problem_line(&qt, &context, waiting_line(purpose), "retry");
757 }
758 if pin_waited_too_long(typed.is_some(), since.elapsed()) {
760 if let Ok(mut waiting) = WAITING.lock() {
761 waiting.remove(&context);
762 }
763 drop(typed);
764 return tell_band(&qt, &context, Band { state: "pin", line: tr().text("seckey-pin-dropped", None), ..Band::default() });
765 }
766 if taps() {
768 match readers().present() {
769 Ok(held) if held.is_empty() => continue,
770 Ok(_) => {
771 if let Ok(mut waiting) = WAITING.lock() {
772 waiting.remove(&context);
773 }
774 return resume(&qt, &shared, &context, typed);
775 }
776 Err(error) => {
777 if let Ok(mut waiting) = WAITING.lock() {
778 waiting.remove(&context);
779 }
780 return failed(&qt, &context, error, purpose, typed.is_some());
781 }
782 }
783 }
784 if !sioul_sync::securitykey::may_look() {
786 continue;
787 }
788 let present = readers().present().ok();
790 if present == seen {
791 continue;
792 }
793 seen = present;
794 match key_in(readers(), &ident) {
795 Ok(_) => {
796 if let Ok(mut waiting) = WAITING.lock() {
797 waiting.remove(&context);
798 }
799 return resume(&qt, &shared, &context, typed);
800 }
801 Err(CardError::Busy) => {
803 if let Ok(mut waiting) = WAITING.lock() {
804 waiting.remove(&context);
805 }
806 return problem_line(&qt, &context, CardError::Busy.sentence(tr(), purpose), "release");
807 }
808 Err(_) => {}
809 }
810 }
811 });
812}
813
814pub(crate) fn not_now(context: &str) {
816 if let Ok(mut waiting) = WAITING.lock() {
817 waiting.remove(context);
818 }
819 if let Ok(mut told) = LAST_TRY.lock() {
820 told.remove(context);
821 }
822 if taps() {
823 readers().want(false);
824 }
825}
826
827pub(crate) fn open_nfc_settings() {
829 #[cfg(target_os = "android")]
830 crate::nfc::open_settings();
831}
832
833pub(crate) fn release() -> String {
836 let released = if offline() { Released::Done } else { sioul_sync::securitykey::release_gnupg() };
837 json(&serde_json::json!({ "line": released.sentence(tr()), "done": released == Released::Done }))
838}
839
840fn holder_name(written: &str) -> String {
843 match written.split_once("<<") {
844 Some((surname, given)) => format!("{} {}", given.replace('<', " ").trim(), surname.replace('<', " ").trim()).trim().to_string(),
845 None => written.replace('<', " ").trim().to_string(),
846 }
847}
848
849fn describe(info: &CardInfo) -> String {
851 let holder = holder_name(&info.holder);
852 let holder = if holder.is_empty() { String::new() } else { format!(", {holder}") };
853 let made = |slot: &securitykey::SlotInfo| match slot.created {
854 0 => slot.algorithm.clone(),
855 at => say("seckey-made-on", &[("algorithm", slot.algorithm.clone()), ("date", securitykey::full_date(tr(), i64::from(at)))]),
856 };
857 let (sign, decrypt) = (info.sign.as_ref().map(made), info.decrypt.as_ref().map(made));
858 let id = match (&sign, &decrypt) {
859 (Some(_), Some(_)) => "seckey-found-both",
860 (Some(_), None) => "seckey-found-sign",
861 _ => "seckey-found-decrypt",
862 };
863 say(id, &[("label", info.label(tr())), ("holder", holder), ("sign", sign.unwrap_or_default()), ("decrypt", decrypt.unwrap_or_default())])
864}
865
866pub(crate) fn read_for_setup(qt: &QtThread) {
868 let qt = qt.clone();
869 std::thread::spawn(move || {
870 working(&qt, SETUP_BAND, "seckey-reading");
871 if taps() && !held_for_setup(&qt) {
873 return;
874 }
875 match securitykey::cards(readers()) {
876 Ok(found) => {
877 let known = securitykey::known_cards();
878 let Some(info) = found.iter().find(|c| !known.iter().any(|k| k.ident == c.ident)).or(found.first()).cloned() else { return };
880 if let Some((cert, check, source)) = shared_certificate(&info) {
883 return adopt(&qt, &info, &cert, &check, &source);
884 }
885 let line = describe(&info);
886 let fingerprint = fingerprint_of(&info);
887 let commands = vec![sioul_sync::securitykey::export_command(&fingerprint)];
888 if let Ok(mut setup) = SETUP.lock() {
889 *setup = Some(info);
890 }
891 tell_band(&qt, SETUP_BAND, Band { state: "found", line, fingerprint, commands, ..Band::default() });
892 }
893 Err(error) => tell_band(&qt, SETUP_BAND, setup_problem(&error, tr())),
894 }
895 });
896}
897
898fn held_for_setup(qt: &QtThread) -> bool {
901 if let Ok(mut waiting) = WAITING.lock() {
902 waiting.insert(SETUP_BAND.to_string());
903 }
904 tell_band(qt, SETUP_BAND, Band { state: "plug", line: waiting_line(Purpose::Read), action: waiting_action(&way()), ..Band::default() });
905 let since = Instant::now();
906 loop {
907 match readers().present() {
908 Ok(held) if !held.is_empty() => break,
909 Ok(_) => {}
910 Err(error) => {
911 failed(qt, SETUP_BAND, error, Purpose::Read, false);
912 break;
913 }
914 }
915 if !WAITING.lock().is_ok_and(|w| w.contains(SETUP_BAND)) {
916 return false;
917 }
918 if since.elapsed() > WAIT_AT_MOST {
919 problem_line(qt, SETUP_BAND, waiting_line(Purpose::Read), "retry");
920 break;
921 }
922 readers().wait_change(LOOK_EVERY);
923 }
924 let came = WAITING.lock().is_ok_and(|mut w| w.remove(SETUP_BAND)) && readers().present().is_ok_and(|held| !held.is_empty());
925 if came {
926 working(qt, SETUP_BAND, "seckey-holding");
927 }
928 came
929}
930
931fn shared_certificate(info: &CardInfo) -> Option<(securitykey::Certificate, securitykey::CertCheck, String)> {
935 shared_in(&Keys::load(), info)
936}
937
938fn shared_in(keys: &Keys, info: &CardInfo) -> Option<(securitykey::Certificate, securitykey::CertCheck, String)> {
939 let record = keys.shared.iter().find(|c| c.ident.eq_ignore_ascii_case(&info.ident))?;
940 let cert = keys.cert(&record.cert)?;
941 let check = securitykey::check(cert, info).ok()?;
942 Some((cert.clone(), check, record.source.clone()))
943}
944
945fn fingerprint_of(info: &CardInfo) -> String {
947 info.sign.as_ref().or(info.decrypt.as_ref()).map(|s| s.fingerprint.clone()).unwrap_or_default()
948}
949
950fn card_for_lookup(ident: &str) -> Result<CardInfo, CardError> {
953 let read = SETUP.lock().ok().and_then(|s| s.clone()).filter(|c| ident.is_empty() || c.ident.eq_ignore_ascii_case(ident));
954 let known = || securitykey::known_cards().into_iter().find(|c| c.ident.eq_ignore_ascii_case(ident)).map(|known| as_info(&known));
955 match read.or_else(known) {
956 Some(info) => Ok(info),
957 None => key_in(readers(), ident),
959 }
960}
961
962fn key_in(readers: &dyn Readers, ident: &str) -> Result<CardInfo, CardError> {
964 securitykey::cards(readers)?.into_iter().find(|c| ident.is_empty() || c.ident.eq_ignore_ascii_case(ident)).ok_or(CardError::Absent)
965}
966
967fn setup_problem(error: &CardError, words: &Translator) -> Band {
971 setup_problem_in(&way(), error, words)
972}
973
974fn setup_problem_in(way: &Way, error: &CardError, words: &Translator) -> Band {
975 Band { state: "problem", line: sentence_in(way, error, Purpose::Read, words), warm: true, action: setup_action(error), ..Band::default() }
976}
977
978fn setup_action(error: &CardError) -> &'static str {
981 match error.remedy() {
982 Remedy::Release => "release",
983 Remedy::NfcSettings => "nfc-settings",
984 _ => "retry",
985 }
986}
987
988fn as_info(known: &KnownCard) -> CardInfo {
990 let slot = |fingerprint: &str, touch: Touch| (!fingerprint.is_empty()).then(|| securitykey::SlotInfo { fingerprint: fingerprint.to_string(), created: 0, algorithm: String::new(), touch, public: None });
991 CardInfo {
992 ident: known.ident.clone(),
993 manufacturer: known.manufacturer,
994 serial: known.serial,
995 holder: known.holder.clone(),
996 url: String::new(),
997 sign: slot(&known.sign, known.touch_sign),
998 decrypt: slot(&known.decrypt, known.touch_decrypt),
999 pin_tries: known.pin_tries_full,
1000 reset_tries: 0,
1001 admin_tries: 0,
1002 pin_each_signature: known.pin_each_signature,
1003 signatures: 0,
1004 reader: String::new(),
1005 }
1006}
1007
1008fn your_addresses() -> Vec<String> {
1010 load_config().accounts.iter().filter(|a| a.syncs()).filter_map(|a| a.address.clone()).map(|a| a.to_ascii_lowercase()).collect()
1011}
1012
1013pub(crate) fn look_for_certificate(qt: &QtThread, ident: &str) {
1016 let (qt, ident) = (qt.clone(), ident.to_string());
1017 std::thread::spawn(move || {
1018 let info = match card_for_lookup(&ident) {
1019 Ok(info) => info,
1020 Err(error) => return tell_band(&qt, SETUP_BAND, setup_problem(&error, tr())),
1021 };
1022 working(&qt, SETUP_BAND, "seckey-looking");
1023 let looked = if offline() {
1024 match demo().map(|d| securitykey::certificate_in(&d.certificate, &info)) {
1026 Some(Ok((cert, check))) => Ok(Looked::Found { cert, check, source: "https://keys.openpgp.org/".into() }),
1027 _ => Ok(Looked::NotFound),
1028 }
1029 } else {
1030 sioul_sync::securitykey::find_certificate(&info, &your_addresses())
1031 };
1032 match looked {
1033 Ok(Looked::Found { cert, check, source }) => adopt(&qt, &info, &cert, &check, &source),
1034 Ok(Looked::Mismatch(why)) => problem_line(&qt, SETUP_BAND, why.sentence(tr()), ""),
1035 Ok(Looked::NotFound) => {
1036 let fingerprint = fingerprint_of(&info);
1037 let commands = vec![sioul_sync::securitykey::export_command(&fingerprint)];
1038 tell_band(&qt, SETUP_BAND, Band { state: "missing", line: tr().text("seckey-not-found", None), fingerprint, commands, ..Band::default() })
1039 }
1040 Err(error) => problem_line(&qt, SETUP_BAND, error.sentence(tr(), ""), "lookup"),
1041 }
1042 });
1043}
1044
1045pub(crate) fn import_certificate(qt: &QtThread, ident: &str, url: &str) {
1047 let info = match card_for_lookup(ident) {
1048 Ok(info) => info,
1049 Err(error) => return tell_band(qt, SETUP_BAND, setup_problem(&error, tr())),
1050 };
1051 let path = local_path(url);
1052 match std::fs::read(&path) {
1053 Ok(bytes) => match securitykey::certificate_in(&bytes, &info) {
1054 Ok((cert, check)) => adopt(qt, &info, &cert, &check, "file"),
1055 Err(why) => problem_line(qt, SETUP_BAND, why.sentence(tr()), ""),
1056 },
1057 Err(e) => problem_line(qt, SETUP_BAND, format!("{}: {e}", path.display()), ""),
1058 }
1059}
1060
1061fn adopt(qt: &QtThread, info: &CardInfo, cert: &securitykey::Certificate, check: &securitykey::CertCheck, source: &str) {
1064 match keep(info, cert, check, source) {
1065 Ok(kept) => {
1066 let (action, commands) = if kept.expired && renewable() { ("renew-offer", kept.commands) } else if kept.expired { ("", kept.commands) } else { ("", Vec::new()) };
1068 tell_band(qt, SETUP_BAND, Band { state: "done", line: kept.line, warm: kept.warm, action, ident: info.ident.clone(), commands, ..Band::default() });
1069 let _ = qt.queue(|mut sioul| sioul.as_mut().keys_changed());
1070 }
1071 Err(e) => problem_line(qt, SETUP_BAND, e, ""),
1072 }
1073}
1074
1075struct Kept {
1077 line: String,
1078 warm: bool,
1079 expired: bool,
1081 commands: Vec<String>,
1083}
1084
1085
1086fn keep(info: &CardInfo, cert: &securitykey::Certificate, check: &securitykey::CertCheck, source: &str) -> Result<Kept, String> {
1089 let yours = your_addresses();
1090 let named: Vec<String> = check.addresses.iter().filter(|a| yours.contains(a)).cloned().collect();
1091 let addresses = if check.addresses.is_empty() { yours } else { named };
1094 securitykey::adopt(info, cert, source, addresses.clone(), now_unix())?;
1095 let names = cert.userids().map(|u| String::from_utf8_lossy(u.userid().value()).to_string()).collect::<Vec<_>>().join(", ");
1096 let mut line = if check.addresses.is_empty() {
1097 tr().text("seckey-kept-no-address", None)
1098 } else if addresses.is_empty() {
1099 say("seckey-kept-not-yours", &[("names", names)])
1100 } else {
1101 say("seckey-kept", &[("names", names), ("addresses", addresses.join(", "))])
1102 };
1103 let mut warm = addresses.is_empty();
1104 let (expiry, late, expired) = securitykey::expiry_words(tr(), cert, check.expires, now_unix());
1105 if !expiry.is_empty() {
1106 line = format!("{line} {expiry}");
1107 warm |= late;
1108 }
1109 if let Ok(mut setup) = SETUP.lock() {
1110 *setup = None;
1111 }
1112 Ok(Kept { line, warm, expired, commands: sioul_sync::securitykey::renew_commands(&check.cert, &securitykey::subkeys_of(cert)) })
1113}
1114
1115fn gnupg_sentence(problem: &GnupgProblem, fingerprint: &str) -> String {
1117 match problem {
1118 GnupgProblem::Missing => tr().text("seckey-gnupg-missing", None),
1119 GnupgProblem::Sandboxed => tr().text("seckey-gnupg-sandboxed", None),
1120 GnupgProblem::NoSuchKey => say("seckey-gnupg-no-key", &[("fingerprint", fingerprint.to_string())]),
1121 GnupgProblem::TouchMissed => tr().text("seckey-gnupg-touch-missed", None),
1122 GnupgProblem::Failed(detail) => say("seckey-gnupg-failed", &[("detail", detail.replace('`', "'"))]),
1124 }
1125}
1126
1127fn gnupg() -> Result<Gnupg, GnupgProblem> {
1129 if offline() { Err(GnupgProblem::Missing) } else { Gnupg::find() }
1130}
1131
1132pub(crate) fn import_from_gnupg(qt: &QtThread, ident: &str) {
1136 let (qt, ident) = (qt.clone(), ident.to_string());
1137 std::thread::spawn(move || {
1138 let info = match card_for_lookup(&ident) {
1139 Ok(info) => info,
1140 Err(error) => return tell_band(&qt, SETUP_BAND, setup_problem(&error, tr())),
1141 };
1142 working(&qt, SETUP_BAND, "seckey-gnupg-reading");
1143 let fingerprint = fingerprint_of(&info);
1144 let exported = match demo().filter(|_| offline()) {
1145 Some(demo) => Ok(demo.certificate.clone()),
1146 None => gnupg().and_then(|g| g.export(&fingerprint)),
1147 };
1148 match exported {
1149 Ok(bytes) => match securitykey::certificate_in(&bytes, &info) {
1150 Ok((cert, check)) => adopt(&qt, &info, &cert, &check, "gnupg"),
1151 Err(why) => problem_line(&qt, SETUP_BAND, why.sentence(tr()), ""),
1152 },
1153 Err(problem) => {
1154 let commands = vec![sioul_sync::securitykey::export_command(&fingerprint)];
1155 tell_band(&qt, SETUP_BAND, Band { state: "problem", line: gnupg_sentence(&problem, &fingerprint), warm: true, fingerprint, commands, ..Band::default() });
1156 }
1157 }
1158 });
1159}
1160
1161fn renewable() -> bool {
1165 gnupg().is_ok()
1166}
1167
1168pub(crate) fn renew(qt: &QtThread, ident: &str) {
1172 let (qt, ident) = (qt.clone(), ident.to_string());
1173 std::thread::spawn(move || {
1174 let Some(known) = securitykey::known_cards().into_iter().find(|c| c.ident.eq_ignore_ascii_case(&ident)) else { return problem_line(&qt, SETUP_BAND, CardError::Absent.sentence(tr(), Purpose::Read), "") };
1175 let info = as_info(&known);
1176 let subkeys = Keys::load().cert(&known.cert).map(securitykey::subkeys_of).unwrap_or_default();
1177 let commands: Vec<String> = sioul_sync::securitykey::renew_commands(&known.cert, &subkeys);
1178 let touch = known.touch_sign;
1180 tell_band(&qt, SETUP_BAND, Band { state: "working", line: tr().text("seckey-renewing", None), ident: ident.clone(), ..Band::default() });
1181 let watch = |step: GnupgStep| tell_band(&qt, SETUP_BAND, Band { state: "working", line: renew_line(step, touch, tr()), ident: ident.clone(), ..Band::default() });
1182 let renewed = gnupg().and_then(|g| {
1183 g.renew(&known.cert, "2y", &watch)?;
1184 g.export(&known.cert)
1185 });
1186 let failed = |line: String, action: &'static str| tell_band(&qt, SETUP_BAND, Band { state: "problem", line, warm: true, action, ident: ident.clone(), commands: commands.clone(), ..Band::default() });
1187 match renewed {
1188 Ok(bytes) => match securitykey::certificate_in(&bytes, &info) {
1189 Ok((cert, _)) if !securitykey::expired_parts(tr(), &securitykey::parts(&cert), now_unix()).is_empty() => {
1191 let still = securitykey::expired_parts(tr(), &securitykey::parts(&cert), now_unix()).join(" ");
1192 failed(say("seckey-renew-incomplete", &[("parts", still)]), "renew");
1193 }
1194 Ok((cert, check)) => match keep(&info, &cert, &check, "gnupg") {
1195 Ok(_) => {
1196 let parts = securitykey::parts_until(tr(), &securitykey::parts(&cert));
1197 tell_band(&qt, SETUP_BAND, Band { state: "done", line: say("seckey-renewed", &[("parts", parts)]), action: "publish", ident: ident.clone(), ..Band::default() });
1198 let _ = qt.queue(|mut sioul| sioul.as_mut().keys_changed());
1199 }
1200 Err(e) => failed(e, ""),
1201 },
1202 Err(why) => failed(why.sentence(tr()), ""),
1203 },
1204 Err(GnupgProblem::NoSuchKey) => failed(tr().text("seckey-renew-unknown", None), ""),
1206 Err(GnupgProblem::TouchMissed) => failed(gnupg_sentence(&GnupgProblem::TouchMissed, &known.cert), "renew"),
1208 Err(problem) => failed(gnupg_sentence(&problem, &known.cert), ""),
1209 }
1210 });
1211}
1212
1213fn renew_line(step: GnupgStep, touch: Touch, tr: &sioul_core::i18n::Translator) -> String {
1218 match (step, touch.words(tr)) {
1219 (GnupgStep::Pin, Some(_)) => format!("{} {}", tr.text("seckey-gnupg-pin", None), tr.text("seckey-gnupg-then-touch", None)),
1220 (GnupgStep::Pin, None) => tr.text("seckey-gnupg-pin", None),
1221 (GnupgStep::Card, Some(words)) => words,
1222 (GnupgStep::Card, None) => tr.text("seckey-renewing", None),
1223 }
1224}
1225
1226pub(crate) fn send_to_keys_openpgp(qt: &QtThread, ident: &str) {
1231 let (qt, ident) = (qt.clone(), ident.to_string());
1232 std::thread::spawn(move || {
1233 let Some(known) = securitykey::known_cards().into_iter().find(|c| c.ident.eq_ignore_ascii_case(&ident)) else { return problem_line(&qt, SETUP_BAND, CardError::Absent.sentence(tr(), Purpose::Read), "") };
1234 let Some(armored) = pgp::export_public(&known.cert) else { return problem_line(&qt, SETUP_BAND, tr().text("seckey-not-found", None), "") };
1235 working(&qt, SETUP_BAND, "seckey-sending");
1236 let sent = if offline() {
1237 Ok(sioul_sync::securitykey::Published { fingerprint: known.cert.clone(), mailed: known.addresses.clone(), published: Vec::new() })
1238 } else {
1239 sioul_sync::securitykey::send_to_keys_openpgp(armored.as_bytes(), tr().language())
1240 };
1241 match sent {
1242 Ok(published) => {
1243 let line = if !published.mailed.is_empty() {
1244 say("seckey-sent-mailed", &[("addresses", published.mailed.join(", "))])
1245 } else if !published.published.is_empty() {
1246 say("seckey-sent-published", &[("addresses", published.published.join(", "))])
1247 } else {
1248 tr().text("seckey-sent-no-address", None)
1249 };
1250 tell_band(&qt, SETUP_BAND, Band { state: "done", line, ..Band::default() });
1251 }
1252 Err(error) => problem_line(&qt, SETUP_BAND, error.sentence(tr(), ""), ""),
1253 }
1254 });
1255}
1256
1257pub(crate) fn stop_using(qt: &QtThread, shared: &Shared, ident: &str) {
1260 let read_here = Keys::load().cards.iter().any(|card| card.ident.eq_ignore_ascii_case(ident));
1264 if !may_stop(read_here, sioul_sync::securitykey::usable_here()) {
1265 tell(qt, shared, tr().text("seckey-stop-on-computer", None));
1266 return;
1267 }
1268 if PIN.lock().is_ok_and(|m| m.holder().is_some_and(|h| h.eq_ignore_ascii_case(ident))) {
1269 let _ = forget_pin();
1270 }
1271 let line = match securitykey::forget(ident) {
1272 Ok(true) => tr().text("seckey-stopped", None),
1274 Ok(false) => tr().text("seckey-stopped-shared", None),
1276 Err(e) => e,
1277 };
1278 tell(qt, shared, line);
1279 let _ = qt.queue(|mut sioul| sioul.as_mut().keys_changed());
1280}
1281
1282fn may_stop(read_here: bool, card_here: bool) -> bool {
1288 read_here || card_here
1289}
1290
1291#[derive(serde::Serialize)]
1293struct KnownView {
1294 ident: String,
1295 title: String,
1297 signs_for: String,
1299 source: String,
1301 expiry: String,
1303 late: bool,
1304 touch: String,
1306 pin_held: bool,
1308 renewable: bool,
1310 stoppable: bool,
1312 by_hand: Vec<String>,
1315}
1316
1317pub(crate) fn known_view() -> String {
1319 let keys = Keys::load();
1320 let holder = PIN.lock().ok().and_then(|m| m.holder().map(str::to_string));
1321 let here = sioul_sync::securitykey::usable_here();
1322 let cards: Vec<KnownView> = keys
1323 .all_cards()
1324 .map(|card| {
1325 let name = holder_name(&card.holder);
1326 let title = if name.is_empty() { card.label(tr()) } else { format!("{}, {name}", card.label(tr())) };
1327 let read_here = keys.cards.iter().any(|known| known.ident.eq_ignore_ascii_case(&card.ident));
1328 let source = match card.source.as_str() {
1329 _ if !read_here && here => tr().text(read_here_words("seckey-source-shared-computer"), None),
1331 _ if !read_here => tr().text("seckey-source-shared", None),
1332 "file" => say("seckey-source-file", &[("date", securitykey::full_date(tr(), card.checked))]),
1333 "gnupg" => say("seckey-source-gnupg", &[("date", securitykey::full_date(tr(), card.checked))]),
1334 url => say("seckey-source", &[("source", host_of(url)), ("date", securitykey::full_date(tr(), card.checked))]),
1335 };
1336 let cert = keys.cert(&card.cert);
1337 let (expiry, late, _) = cert.map(|cert| securitykey::expiry_words(tr(), cert, securitykey::expiry_of(cert, &[&card.sign, &card.decrypt]), now_unix())).unwrap_or_default();
1338 let subkeys = cert.map(securitykey::subkeys_of).unwrap_or_default();
1339 let unknown = (card.touch_sign == Touch::Unknown && !card.sign.is_empty()) || (card.touch_decrypt == Touch::Unknown && !card.decrypt.is_empty());
1340 let touch = match (card.touch_sign.asked() && !card.sign.is_empty(), card.touch_decrypt.asked() && !card.decrypt.is_empty()) {
1341 _ if unknown => "seckey-touch-unknown",
1342 (true, true) => "seckey-touch-both",
1343 (true, false) => "seckey-touch-sign",
1344 (false, true) => "seckey-touch-open",
1345 (false, false) => "seckey-touch-none",
1346 };
1347 KnownView {
1348 ident: card.ident.clone(),
1349 title,
1350 signs_for: if card.addresses.is_empty() { tr().text("seckey-signs-for-none", None) } else { say("seckey-signs-for", &[("addresses", card.addresses.join(", "))]) },
1351 source,
1352 expiry,
1353 late,
1354 touch: tr().text(touch, None),
1355 pin_held: holder.as_deref().is_some_and(|h| h.eq_ignore_ascii_case(&card.ident)),
1356 renewable: read_here && late && renewable(),
1359 stoppable: may_stop(read_here, here),
1360 by_hand: std::iter::once(sioul_sync::securitykey::export_command(&card.cert)).chain(if late { sioul_sync::securitykey::renew_commands(&card.cert, &subkeys) } else { Vec::new() }).collect(),
1361 }
1362 })
1363 .collect();
1364 json(&serde_json::json!({ "cards": cards }))
1365}
1366
1367fn host_of(url: &str) -> String {
1369 url.split_once("://").map_or(url, |(_, rest)| rest).split(['/', '?', '#']).next().unwrap_or(url).to_string()
1370}
1371
1372#[cfg(test)]
1373mod tests {
1374 use super::*;
1375
1376 #[test]
1380 fn a_shared_key_is_stopped_only_where_it_can_be_read_again() {
1381 assert!(!may_stop(false, false), "a phone, a key another device read");
1382 assert!(may_stop(false, true) && may_stop(true, true) && may_stop(true, false));
1383 }
1384
1385 #[test]
1389 fn a_forward_that_cannot_go_is_refused_at_send() {
1390 use crate::crypto::Answered;
1391 assert_eq!(forward_refused(false, &Answered::Clear), None);
1392 for answered in [Answered::Opens(Vec::new()), Answered::AtSend, Answered::Elsewhere, Answered::NoKey] {
1393 assert_eq!(forward_refused(false, &answered), Some("pgp-forward-needs-encryption"));
1394 }
1395 assert_eq!(forward_refused(true, &Answered::Elsewhere), Some("seckey-forward-not-here"));
1396 assert_eq!(forward_refused(true, &Answered::NoKey), Some("compose-forward-sealed-no-key"));
1397 assert_eq!(forward_refused(true, &Answered::Opens(Vec::new())), None);
1398 assert_eq!(forward_refused(true, &Answered::AtSend), None);
1399 }
1400
1401 #[test]
1406 fn a_phone_asks_the_pin_first_and_never_keeps_it() {
1407 let known = KnownCard { ident: "0006:12345678".into(), sign: "AB".repeat(20), decrypt: "CD".repeat(20), cert: "EF".repeat(20), pin_tries_full: 3, ..KnownCard::default() };
1408 let phone = SoftReaders { taps: true, failure: Some(CardError::NoService), ..SoftReaders::with(Vec::new()) };
1410 let (info, pin) = pin_from(&phone, || Some(known.clone()), "0006:12345678", None, Purpose::Sign).unwrap();
1411 assert_eq!((info.ident.as_str(), info.pin_tries, pin.is_none()), ("0006:12345678", 3, true), "the band asks for the PIN");
1412 assert_eq!(info.sign.map(|s| s.fingerprint), Some("AB".repeat(20)));
1413 let typed = Password::from("123456");
1414 let (_, pin) = pin_from(&phone, || Some(known.clone()), "0006:12345678", Some(&typed), Purpose::Open).unwrap();
1415 assert!(pin.is_some(), "the PIN typed goes with the tap");
1416 assert_eq!(pin_from(&phone, || None, "0006:12345678", None, Purpose::Open).map(|(i, _)| i.ident), Err(CardError::Absent), "a key not known here");
1417 assert!(!keeps_pin(&phone));
1418 let computer = SoftReaders { failure: Some(CardError::NoService), ..SoftReaders::with(Vec::new()) };
1420 assert_eq!(pin_from(&computer, || Some(known.clone()), "0006:12345678", None, Purpose::Sign).map(|(i, _)| i.ident), Err(CardError::NoService));
1421 assert!(keeps_pin(&SoftReaders::with(Vec::new())));
1422 }
1423
1424 #[test]
1430 fn a_phone_says_hold_or_plug_and_what_went_wrong() {
1431 let computer = Way::default();
1432 let held = Way { taps: true, ..Way::default() };
1433 let plugged = Way { taps: true, cable: true, ..Way::default() };
1434 for lang in ["en", "fr"] {
1435 let words = Translator::new(lang);
1436 assert_eq!(waiting_in(&held, Purpose::Sign, &words), words.text("seckey-hold", None));
1437 assert_eq!(waiting_in(&Way { note: Some(CardError::NoNfc), ..held.clone() }, Purpose::Sign, &words), words.text("seckey-plug-phone", None));
1438 let off = Way { note: Some(CardError::NfcOff), ..held.clone() };
1439 assert_eq!((waiting_in(&off, Purpose::Open, &words), waiting_action(&off)), (words.text("seckey-plug-nfc-off", None), "nfc-settings"));
1440 assert_eq!((waiting_in(&computer, Purpose::Sign, &words), waiting_action(&computer)), (CardError::Absent.sentence(&words, Purpose::Sign), ""));
1441 assert_eq!(waiting_action(&held), "");
1442 assert_eq!(sentence_in(&held, &CardError::Removed, Purpose::Sign, &words), words.text("seckey-moved-away-sign", None));
1443 assert_eq!(sentence_in(&held, &CardError::Removed, Purpose::Open, &words), words.text("seckey-moved-away", None));
1444 assert_eq!(sentence_in(&plugged, &CardError::Removed, Purpose::Sign, &words), words.text("seckey-unplugged-sign", None));
1445 assert_eq!(sentence_in(&plugged, &CardError::Removed, Purpose::Read, &words), words.text("seckey-unplugged", None));
1446 assert_eq!(sentence_in(&held, &CardError::TouchMissed, Purpose::Sign, &words), words.text("seckey-nfc-touch", None));
1447 assert_eq!(sentence_in(&plugged, &CardError::TouchMissed, Purpose::Sign, &words), CardError::TouchMissed.sentence(&words, Purpose::Sign));
1448 for way in [&held, &plugged] {
1449 assert_eq!(sentence_in(way, &CardError::WrongPin { left: 0 }, Purpose::Open, &words), words.text("seckey-blocked-phone", None));
1450 }
1451 assert_eq!(sentence_in(&held, &CardError::NotYours, Purpose::Open, &words), CardError::NotYours.sentence(&words, Purpose::Open));
1452 assert_eq!(sentence_in(&plugged, &CardError::UsbRefused, Purpose::Open, &words), words.text("seckey-usb-refused", None));
1453 assert_eq!(sentence_in(&plugged, &CardError::UsbBusy, Purpose::Open, &words), words.text("seckey-usb-busy", None));
1454 for error in [CardError::Removed, CardError::TouchMissed, CardError::Blocked] {
1455 assert_eq!(sentence_in(&computer, &error, Purpose::Sign, &words), error.sentence(&words, Purpose::Sign));
1456 }
1457 for id in ["seckey-hold", "seckey-holding", "seckey-done-lift", "seckey-done-unplug", "seckey-moved-away", "seckey-moved-away-sign", "seckey-unplugged", "seckey-unplugged-sign", "seckey-nfc-touch", "seckey-blocked-phone", "seckey-nfc-settings", "seckey-plug-phone", "seckey-plug-nfc-off", "seckey-usb-refused", "seckey-usb-busy", "seckey-pin-otp"] {
1458 let said = words.text(id, None);
1459 assert!(!said.is_empty() && !said.contains("seckey-"), "{lang} {id}");
1460 }
1461 let off = setup_problem_in(&held, &CardError::NfcOff, &words);
1462 assert_eq!((off.state, off.action, off.line), ("problem", "nfc-settings", words.text("seckey-nfc-off", None)));
1463 for error in [CardError::NoNfc, CardError::NotYours, CardError::UsbRefused, CardError::UsbBusy] {
1464 assert_eq!(setup_problem_in(&plugged, &error, &words).action, "retry", "{error:?}");
1465 }
1466 }
1467 assert_eq!(working_words(&held, "seckey-signing"), "seckey-holding");
1468 assert_eq!(working_words(&held, "seckey-opening"), "seckey-holding");
1469 assert_eq!(working_words(&held, "seckey-reading"), "seckey-reading");
1470 assert_eq!(working_words(&plugged, "seckey-signing"), "seckey-signing");
1471 assert_eq!(working_words(&computer, "seckey-signing"), "seckey-signing");
1472 assert_eq!(Way::of(&SoftReaders::with(Vec::new())), computer);
1473 }
1474
1475 #[test]
1479 fn a_code_the_key_typed_is_not_taken_for_its_pin() {
1480 assert!(typed_by_the_key("ccccccjlkgjhvbtnedkdfiricjgtilhkbhuelkljdlgf"));
1482 assert!(typed_by_the_key(&"c".repeat(32)) && typed_by_the_key(&"v".repeat(64)));
1483 assert!(typed_by_the_key("CCCCCCJLKGJHVBTNEDKDFIRICJGTILHKBHUELKLJDLGF"));
1485 assert!(typed_by_the_key("Ccccccjlkgjhvbtnedkdfiricjgtilhkbhuelkljdlgf"));
1486 for pin in ["123456", "12345678", "correct horse", "cbdefghijk", "CBDEFGHIJK", &"c".repeat(31), &"c".repeat(65), "ccccccjlkgjhvbtnedkdfiricjgtilhkbhuelkljdlga", "CCCCCCJLKGJHVBTNEDKDFIRICJGTILHKBHUELKLJDLGA"] {
1487 assert!(!typed_by_the_key(pin), "{pin}");
1488 }
1489 }
1490
1491 #[test]
1497 fn the_last_try_is_sent_only_once_said_and_a_waiting_pin_is_dropped() {
1498 let readers = SoftReaders::with(Vec::new());
1499 let pin = Password::from("123456");
1500 let first = CardAccess::new(&readers, &pin, &|| {});
1501 confirm_last_try(&first, "draft-f1", true);
1502 assert!(!first.last_try_allowed(), "typed, not told: not sent with one try left");
1503 LAST_TRY.lock().unwrap().insert("draft-f1".to_string());
1504 let other = CardAccess::new(&readers, &pin, &|| {});
1505 confirm_last_try(&other, "draft-other", true);
1506 assert!(!other.last_try_allowed(), "another band's word does not count");
1507 let again = CardAccess::new(&readers, &pin, &|| {});
1508 confirm_last_try(&again, "draft-f1", true);
1509 assert!(again.last_try_allowed(), "told, typed again: sent");
1510 let after = CardAccess::new(&readers, &pin, &|| {});
1511 confirm_last_try(&after, "draft-f1", true);
1512 assert!(!after.last_try_allowed(), "once only");
1513 let held = CardAccess::new(&readers, &pin, &|| {});
1514 confirm_last_try(&held, "draft-f1", false);
1515 assert!(held.last_try_allowed(), "a PIN held, which the key took before");
1516 assert!(!pin_waited_too_long(true, Duration::from_secs(59)));
1517 assert!(pin_waited_too_long(true, Duration::from_secs(61)));
1518 assert!(!pin_waited_too_long(false, Duration::from_secs(3600)), "no PIN typed: the band waits on");
1519 assert!(take_pin(String::new()).is_none());
1520 let taken = take_pin("123456".to_string()).unwrap();
1521 taken.map(|protected| assert_eq!(&protected[..], b"123456"));
1522 for lang in ["en", "fr"] {
1523 let words = Translator::new(lang);
1524 let phone = Way { taps: true, ..Way::default() };
1525 assert_eq!(sentence_in(&phone, &CardError::LastTry, Purpose::Sign, &words), words.text("seckey-last-try-phone", None));
1526 assert_eq!(sentence_in(&Way::default(), &CardError::LastTry, Purpose::Sign, &words), words.text("seckey-last-try", None));
1527 for id in ["seckey-last-try", "seckey-last-try-phone", "seckey-pin-dropped"] {
1528 assert!(!words.text(id, None).contains("seckey-"), "{lang} {id}");
1529 }
1530 }
1531 }
1532
1533 #[test]
1536 fn a_phone_says_to_hold_the_key_where_a_computer_says_to_plug_it() {
1537 for computer in ["seckey-sign-read-here", "seckey-source-shared-computer", "pgp-read-card-here"] {
1538 assert_eq!(read_here_in(false, computer), computer);
1539 let phone = read_here_in(true, computer);
1540 assert_eq!(phone, format!("{computer}-phone").replace("-computer-phone", "-phone"));
1541 for lang in ["en", "fr"] {
1542 let said = Translator::new(lang).text(phone, None);
1543 assert!(!said.is_empty() && !said.contains(phone), "{lang} {phone}");
1544 }
1545 }
1546 assert_eq!(read_here_in(true, "seckey-other"), "seckey-other");
1547 }
1548
1549 #[test]
1553 fn a_key_known_from_another_device_is_kept_when_held_to_the_phone() {
1554 let (card, bytes) = SoftCard::generate("0006:12345678", &["Jane <jane@example.org>"], "123456").unwrap();
1555 let (cert, check) = securitykey::certificate_in(&bytes, &card.info).unwrap();
1556 let record = KnownCard {
1557 ident: card.info.ident.clone(),
1558 sign: card.info.sign.as_ref().unwrap().fingerprint.clone(),
1559 decrypt: card.info.decrypt.as_ref().unwrap().fingerprint.clone(),
1560 cert: check.cert.clone(),
1561 source: "laptop".into(),
1562 ..KnownCard::default()
1563 };
1564 let keys = Keys { own: vec![cert.clone()], shared: vec![record], ..Keys::default() };
1565 let (kept, checked, source) = shared_in(&keys, &card.info).unwrap();
1566 assert_eq!((kept.fingerprint(), checked.cert, source.as_str()), (cert.fingerprint(), check.cert.clone(), "laptop"));
1567 assert!(checked.addresses.contains(&"jane@example.org".to_string()));
1568 let (other, _) = SoftCard::generate("0006:12345678", &["Jane <jane@example.org>"], "123456").unwrap();
1570 assert!(shared_in(&keys, &other.info).is_none());
1571 assert!(shared_in(&Keys { own: vec![cert], ..Keys::default() }, &card.info).is_none());
1573 }
1574
1575 #[test]
1576 fn the_holder_reads_in_the_usual_order() {
1577 assert_eq!(holder_name("Doe<<Jane<Marie"), "Jane Marie Doe");
1578 assert_eq!(holder_name("Jane Doe"), "Jane Doe");
1579 assert_eq!(holder_name(""), "");
1580 assert_eq!(host_of("https://keys.openpgp.org/vks/v1/by-fingerprint/ABC"), "keys.openpgp.org");
1581 assert_eq!(host_of("https://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/x?l=jane"), "openpgpkey.example.org");
1582 }
1583
1584 #[test]
1592 fn a_key_gnupg_holds_offers_to_release_it() {
1593 let tr = sioul_core::i18n::Translator::new("en");
1594 let held = SoftReaders { cards: std::sync::Mutex::new(Vec::new()), busy: 1, failure: None, taps: false };
1595 assert_eq!(key_in(&held, "0006:12345678").map(|c| c.ident), Err(CardError::Busy));
1596 assert_eq!(key_in(&held, "").map(|c| c.ident), Err(CardError::Busy), "the last step of adding it, the key not read yet");
1597 let band = setup_problem(&CardError::Busy, &tr);
1598 assert_eq!((band.state, band.action, band.warm), ("problem", "release", true));
1599 assert!(band.line.starts_with("Another program holds your security key") && band.line.ends_with("`pcsc-shared`"), "{}", band.line);
1600 assert_eq!(key_in(&SoftReaders::with(Vec::new()), "").map(|c| c.ident), Err(CardError::Absent));
1602 assert_eq!(setup_problem(&CardError::Absent, &tr).action, "retry");
1603 }
1604
1605 #[test]
1606 fn the_renewal_says_the_pin_then_the_touch() {
1607 let tr = sioul_core::i18n::Translator::new("en");
1608 let pin_then_touch = renew_line(GnupgStep::Pin, Touch::On, &tr);
1609 assert!(pin_then_touch.contains("PIN") && pin_then_touch.contains("hold your finger"), "{pin_then_touch}");
1610 let touch = renew_line(GnupgStep::Card, Touch::On, &tr);
1611 assert!(touch.starts_with("Touch your security key now") && touch.contains("15 seconds"), "{touch}");
1612 assert_eq!(renew_line(GnupgStep::Card, Touch::Fixed, &tr), touch);
1613 assert!(renew_line(GnupgStep::Card, Touch::Unknown, &tr).starts_with("If your key asks for a touch"));
1614 assert!(!renew_line(GnupgStep::Pin, Touch::Off, &tr).contains("touch"));
1615 assert_eq!(renew_line(GnupgStep::Card, Touch::Off, &tr), tr.text("seckey-renewing", None));
1616 let fr = sioul_core::i18n::Translator::new("fr");
1618 assert!(renew_line(GnupgStep::Card, Touch::On, &fr).starts_with("Touchez votre clé de sécurité"));
1619 }
1620}