1use crate::backend::{json, load_config, say, tr};
10use sioul_core::compose::Draft;
11use sioul_core::pgp::{self, Keys, PgpView};
12use sioul_core::porch::Triaged;
13use sioul_core::trust::Trust;
14use sioul_sync::{SyncError, keys, secret};
15use std::path::{Path, PathBuf};
16
17fn looks_protected(raw: &[u8]) -> bool {
19 let text = String::from_utf8_lossy(&raw[..raw.len().min(512 * 1024)]);
20 text.contains("application/pgp-encrypted") || text.contains("application/pgp-signature") || text.contains("-----BEGIN PGP MESSAGE-----")
21}
22
23pub(crate) fn open(raw: &[u8]) -> Option<(Option<Vec<u8>>, PgpView)> {
29 if !looks_protected(raw) {
30 return None;
31 }
32 pgp::open(raw, &Keys::load(), &pgp::Unlock::new(&|fingerprint| secret::pgp_passphrase(&fingerprint.to_hex()), pgp::SessionKeys::global()))
33}
34
35pub(crate) fn readable(path: &Path) -> Option<Vec<u8>> {
37 let raw = std::fs::read(path).ok()?;
38 Some(open(&raw).and_then(|(opened, _)| opened).unwrap_or(raw))
39}
40
41pub(crate) fn learn_autocrypt(raw: &[u8], triaged: &Triaged) {
45 if triaged.trust != Trust::Verified {
46 return;
47 }
48 let Some(from) = triaged.card.from_address.as_deref() else { return };
49 if let Some(header) = triaged.card.headers.first("Autocrypt") {
50 let _ = raw;
51 pgp::learn_autocrypt(header, from);
52 }
53}
54
55#[derive(serde::Serialize, Default)]
57pub(crate) struct DraftProtection {
58 pub can_sign: bool,
59 pub can_encrypt: bool,
60 pub missing: Vec<String>,
62 pub expired: String,
65}
66
67pub(crate) fn draft_protection(draft: &Draft) -> DraftProtection {
68 let config = load_config();
69 let Some(address) = config.account(&draft.account).and_then(|a| a.address.clone()) else { return DraftProtection::default() };
70 let keys = Keys::load();
71 let missing: Vec<String> = draft.recipients().into_iter().filter(|r| keys.for_address(r).is_none()).collect();
72 let can_sign = keys.own_for(&address).is_some();
73 let expired = if can_sign { String::new() } else { crate::securitykey::expired_line(&keys, &address).unwrap_or_default() };
74 DraftProtection { can_sign, can_encrypt: !draft.recipients().is_empty() && missing.is_empty(), missing, expired }
75}
76
77pub(crate) fn protect_answer(draft: &mut Draft) {
81 let Some(original) = draft.original.clone() else { return };
82 let Ok(raw) = std::fs::read(&original) else { return };
83 let Some((opened, view)) = open(&raw) else { return };
84 if let Some(bytes) = opened {
85 let _ = std::fs::write(&original, bytes);
86 }
87 if view.encrypted {
88 draft.encrypt = draft_protection(draft).can_encrypt;
89 }
90}
91
92#[derive(serde::Serialize)]
93struct KeysView {
94 keys: Vec<pgp::KeySummary>,
95 without_key: Vec<String>,
97}
98
99pub(crate) fn keys_view() -> String {
101 let keys = Keys::load();
102 let config = load_config();
103 let without_key: Vec<String> = config.accounts.iter().filter(|a| a.syncs()).filter_map(|a| a.address.clone()).filter(|a| keys.own_for(a).is_none() && keys.card_for(a).is_none()).collect();
105 json(&KeysView { keys: pgp::summaries(), without_key })
106}
107
108pub(crate) fn generate(address: &str) -> Result<String, String> {
110 let config = load_config();
111 let name = config.accounts.iter().find(|a| a.address.as_deref() == Some(address)).and_then(|a| a.name.clone()).unwrap_or_default();
112 let passphrase = match secret::test_passphrase() {
113 Some(fixed) => fixed,
114 None => pgp::new_passphrase()?,
115 };
116 let made = pgp::generate(&name, address, &passphrase)?;
117 secret::save_pgp_passphrase(&made.fingerprint, &passphrase).map_err(|e| e.sentence(tr(), address))?;
118 Ok(say("pgp-made", &[("address", address.to_string())]))
119}
120
121pub(crate) fn import(url: &str, passphrase: &str) -> Result<String, String> {
124 let path = crate::backend::local_path(url);
125 let bytes = std::fs::read(&path).map_err(|e| format!("{}: {e}", path.display()))?;
126 let had: Vec<String> = pgp::summaries().into_iter().filter(|k| k.own).map(|k| k.fingerprint).collect();
128 let imported = pgp::import(&bytes)?;
129 for key in imported.iter().filter(|k| k.own) {
130 if !pgp::opens(&key.fingerprint, passphrase) {
131 if !had.contains(&key.fingerprint) {
132 let _ = pgp::remove(&key.fingerprint);
133 }
134 return Err(tr().text("pgp-wrong-passphrase", None));
135 }
136 secret::save_pgp_passphrase(&key.fingerprint, passphrase).map_err(|e| e.sentence(tr(), &key.fingerprint))?;
137 }
138 let mut args = tr().counted(imported.len());
139 args.set("n", imported.len());
140 Ok(tr().text("pgp-imported", Some(&args)))
141}
142
143pub(crate) fn export(fingerprint: &str, folder: &Path) -> Result<PathBuf, String> {
145 let armored = pgp::export_public(fingerprint).ok_or_else(|| tr().text("pgp-no-key", None))?;
146 std::fs::create_dir_all(folder).map_err(|e| e.to_string())?;
147 let short: String = fingerprint.chars().rev().take(16).collect::<Vec<_>>().into_iter().rev().collect();
148 let path = folder.join(format!("{short}.asc"));
149 std::fs::write(&path, armored).map_err(|e| e.to_string())?;
150 Ok(path)
151}
152
153pub(crate) fn lookup(draft: &Draft) -> String {
155 let missing = draft_protection(draft).missing;
156 let mut found = 0;
157 for address in &missing {
158 match keys::lookup(address) {
159 Ok(keys) if !keys.is_empty() => found += 1,
160 Ok(_) => {}
161 Err(SyncError::Network(e)) => return say("pgp-lookup-failed", &[("detail", e)]),
162 Err(e) => return e.sentence(tr(), address),
163 }
164 }
165 let still = missing.len() - found;
166 if still == 0 { tr().text("pgp-lookup-all", None) } else { say("pgp-lookup-some", &[("missing", draft_protection(draft).missing.join(", "))]) }
167}