1use std::{
10 collections::BTreeMap,
11 hash::{BuildHasher, Hasher},
12 io::Cursor,
13 path::PathBuf,
14 time::{SystemTime, UNIX_EPOCH},
15};
16
17use base64::{Engine, engine::general_purpose::STANDARD};
18use kernoid_e2e::{
19 Kind, PublicKey, SecretKey,
20 compose::{self, Mailbox as Address},
21 keys::{normalize_recovery_code, recovery_code},
22 mime,
23};
24use serde::Deserialize;
25use serde_json::{Value, json};
26
27mod vault;
28
29use crate::{
30 Client, Error, MAIL, Result, SUBMISSION,
31 mail::{
32 BodyPart, BodyValue, Draft, Email, EmailAddress, EmailSummary, Identity, InlineFile, Inner, Mailbox, Sealed,
33 role,
34 },
35 set_error,
36};
37
38pub const OPENPGP: &str = "https://kernoid.dev/jmap/openpgp";
39
40#[derive(Debug, Clone, Deserialize)]
42pub struct KeyCopy {
43 #[serde(default)]
44 pub protected: String,
45 #[serde(default)]
46 pub salt: Option<String>,
47}
48
49#[derive(Debug, Clone, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct Key {
53 pub id: String,
55 #[serde(default)]
56 pub kind: String,
57 #[serde(default)]
58 pub public_key: String,
59 #[serde(default)]
60 pub retired: Option<String>,
61 #[serde(default)]
62 pub copies: BTreeMap<String, KeyCopy>,
63}
64
65#[derive(Debug, Clone, Default, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct Settings {
69 #[serde(default)]
71 pub mail: bool,
72 #[serde(default)]
73 pub policy: Policy,
74 #[serde(default)]
75 pub recovery_key: Option<RecoveryKey>,
76}
77
78#[derive(Debug, Clone, Default, Deserialize)]
80pub struct Policy {
81 #[serde(default)]
83 pub mail: String,
84 #[serde(default)]
86 pub vaults: String,
87 #[serde(default)]
89 pub recovery: bool,
90}
91
92#[derive(Debug, Clone, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct RecoveryKey {
96 #[serde(default)]
97 pub public_key: String,
98}
99
100#[derive(Debug, Clone)]
102pub enum Plan {
103 Sealed(Vec<PublicKey>),
105 Plain(String),
107}
108
109#[derive(Debug, Clone, Copy)]
111pub struct Outgoing<'a> {
112 pub account: &'a str,
113 pub identity: &'a Identity,
114 pub draft: &'a Draft,
115 pub files: &'a [PathBuf],
116 pub boxes: &'a [Mailbox],
117}
118
119#[derive(Default)]
122pub struct E2e {
123 pub available: bool,
124 pub keys: Vec<Key>,
125 pub settings: Settings,
126 current: Option<SecretKey>,
128 unlocked: Vec<SecretKey>,
130 known: BTreeMap<String, Vec<PublicKey>>,
132 vaults: BTreeMap<String, SecretKey>,
134}
135
136impl std::fmt::Debug for E2e {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 f.debug_struct("E2e")
139 .field("available", &self.available)
140 .field("keys", &self.keys.len())
141 .field("unlocked", &self.current.is_some())
142 .finish_non_exhaustive()
143 }
144}
145
146fn decode(text: &str) -> Result<Vec<u8>> {
147 STANDARD.decode(text).map_err(|_| Error::Protocol("not base64".into()))
148}
149
150fn list<T: serde::de::DeserializeOwned>(result: &Value) -> Result<Vec<T>> {
151 serde_json::from_value(result.get("list").cloned().unwrap_or(Value::Null))
152 .map_err(|error| Error::Protocol(error.to_string()))
153}
154
155impl E2e {
156 pub async fn load(client: &Client) -> Result<E2e> {
158 if !client.session.capabilities.contains_key(OPENPGP) {
159 return Ok(E2e::default());
160 }
161 let account = client.primary(OPENPGP)?;
162 let responses = client
163 .request(
164 &[OPENPGP],
165 vec![
166 ("OpenPgpKey/get", json!({ "accountId": account }), "k"),
167 ("E2eSettings/get", json!({ "accountId": account }), "s"),
168 ],
169 )
170 .await?;
171 let keys = list(responses.get("k").unwrap_or(&Value::Null))?;
172 let settings =
173 list::<Settings>(responses.get("s").unwrap_or(&Value::Null))?.into_iter().next().unwrap_or_default();
174 Ok(E2e { available: true, keys, settings, ..E2e::default() })
175 }
176
177 pub async fn refresh(&mut self, client: &Client) -> Result<()> {
179 let fresh = E2e::load(client).await?;
180 self.available = fresh.available;
181 self.keys = fresh.keys;
182 self.settings = fresh.settings;
183 Ok(())
184 }
185
186 pub async fn setup(&mut self, client: &Client, kind: Kind) -> Result<String> {
190 if !self.available {
191 return Err(Error::E2e("the server has no end-to-end encryption".into()));
192 }
193 if self.has_key() {
194 return Err(Error::E2e("the account has a key already".into()));
195 }
196 let account = client.primary(OPENPGP)?;
197 let mail_account = client.session.primary(MAIL).unwrap_or(&account).to_owned();
198 let mut addresses: Vec<String> =
199 match client.call(&[MAIL, SUBMISSION], "Identity/get", json!({ "accountId": mail_account })).await {
200 Ok(result) => list::<Identity>(&result)?.into_iter().map(|i| i.email.to_lowercase()).collect(),
201 Err(_) => Vec::new(),
202 };
203 addresses.push(client.session.username.to_lowercase());
204 addresses.sort();
205 addresses.dedup();
206 addresses.retain(|a| a.contains('@'));
207 let secret = tokio::task::spawn_blocking(move || {
209 let names: Vec<&str> = addresses.iter().map(String::as_str).collect();
210 SecretKey::generate(kind, &names)
211 })
212 .await
213 .map_err(|error| Error::E2e(error.to_string()))??;
214 let code = recovery_code();
215 let protected = STANDARD.encode(secret.protect(&normalize_recovery_code(&code))?);
216 let mut copies = json!({ "recovery": { "protected": protected, "salt": null } });
217 if self.settings.policy.recovery
218 && let Some(recovery) = &self.settings.recovery_key
219 {
220 let tenant = PublicKey::parse(&decode(recovery.public_key.trim())?)?;
221 let armored = secret.export()?;
222 let sealed = kernoid_e2e::message::encrypt(armored.as_bytes(), &[&tenant], None, false)?;
223 copies["tenant"] = json!({ "protected": STANDARD.encode(sealed), "salt": null });
224 }
225 let public = STANDARD.encode(secret.public().to_bytes()?);
226 let create = json!({ "create": { "key": { "publicKey": public, "copies": copies } } });
227 client.set(&[OPENPGP], "OpenPgpKey", &account, create, json!({})).await?;
228 self.refresh(client).await?;
229 self.unlocked = vec![secret.clone()];
230 self.current = Some(secret);
231 Ok(code)
232 }
233
234 pub fn current_key(&self) -> Option<&Key> {
236 self.keys.iter().find(|key| key.retired.is_none())
237 }
238
239 pub fn has_key(&self) -> bool {
240 self.current_key().is_some()
241 }
242
243 pub fn is_unlocked(&self) -> bool {
244 self.current.is_some()
245 }
246
247 pub fn unlock(&mut self, code: &str) -> Result<()> {
250 let key = self.current_key().ok_or_else(|| Error::E2e("this account has no key".into()))?;
251 let copy =
252 key.copies.get("recovery").ok_or_else(|| Error::E2e("the key has no copy for a recovery code".into()))?;
253 let code = normalize_recovery_code(code);
254 let current = SecretKey::unlock(&decode(©.protected)?, &code)?;
255 if !current.fingerprint().eq_ignore_ascii_case(&key.id) {
256 return Err(Error::E2e("the copy is not of the key in use".into()));
257 }
258 let mut unlocked = vec![current.clone()];
259 for key in self.keys.iter().filter(|key| key.retired.is_some()) {
260 let Some(copy) = key.copies.get("recovery") else { continue };
261 if let Ok(secret) = decode(©.protected).and_then(|p| Ok(SecretKey::unlock(&p, &code)?)) {
262 unlocked.push(secret);
263 }
264 }
265 self.unlocked = unlocked;
266 self.current = Some(current);
267 Ok(())
268 }
269
270 pub fn lock(&mut self) {
272 self.current = None;
273 self.unlocked.clear();
274 self.vaults.clear();
275 }
276
277 pub async fn lookup(&mut self, client: &Client, addresses: &[String]) -> Result<BTreeMap<String, Vec<PublicKey>>> {
279 let mut wanted: Vec<String> = addresses.iter().map(|a| a.to_lowercase()).collect();
280 wanted.sort();
281 wanted.dedup();
282 wanted.retain(|address| !self.known.contains_key(address));
283 if !wanted.is_empty() {
284 #[derive(Deserialize)]
285 #[serde(rename_all = "camelCase")]
286 struct Found {
287 public_key: String,
288 }
289 #[derive(Deserialize)]
290 struct Entry {
291 email: String,
292 #[serde(default)]
293 keys: Vec<Found>,
294 }
295 let account = client.primary(OPENPGP)?;
296 let result =
297 client.call(&[OPENPGP], "OpenPgpKey/lookup", json!({ "accountId": account, "emails": wanted })).await?;
298 for entry in list::<Entry>(&result)? {
299 let keys =
301 entry.keys.iter().filter_map(|key| PublicKey::parse(&decode(&key.public_key).ok()?).ok()).collect();
302 self.known.insert(entry.email.to_lowercase(), keys);
303 }
304 for address in wanted {
305 self.known.entry(address).or_default();
306 }
307 }
308 Ok(addresses
309 .iter()
310 .map(|a| (a.to_lowercase(), self.known.get(&a.to_lowercase()).cloned().unwrap_or_default()))
311 .collect())
312 }
313
314 fn secrets(&self) -> Vec<&SecretKey> {
315 self.unlocked.iter().collect()
316 }
317
318 pub fn open_summary(&self, email: &mut EmailSummary) {
320 if !email.encrypted || email.opened || self.unlocked.is_empty() {
321 return;
322 }
323 let Some(sealed) = email.encrypted_summary.as_deref().and_then(|s| STANDARD.decode(s).ok()) else { return };
324 if let Ok(summary) = mime::decrypt_summary(&sealed, &self.secrets()) {
326 email.subject = summary.subject;
327 email.preview = summary.preview;
328 email.has_attachment = summary.has_attachment;
329 email.opened = true;
330 }
331 }
332
333 pub async fn open(&mut self, client: &Client, account: &str, mut email: Email) -> Result<Email> {
336 if !email.summary.encrypted {
337 return Ok(email);
338 }
339 self.open_summary(&mut email.summary);
340 let mut locked = email.clone();
341 locked.text_body.clear();
342 locked.html_body.clear();
343 locked.attachments.clear();
344 locked.body_values.clear();
345 locked.sealed = Some(Sealed { locked: true, ..Sealed::default() });
346 let Some(blob) = email.blob_id.clone().filter(|_| self.is_unlocked()) else { return Ok(locked) };
347 let mut raw = Vec::new();
348 client.download(account, &blob, "message.eml", "message/rfc822", &mut raw, &mut |_, _| {}).await?;
349 let senders: Vec<String> = email.summary.from.iter().flatten().map(|a| a.email.clone()).collect();
350 let senders: Vec<PublicKey> = if senders.is_empty() {
351 Vec::new()
352 } else {
353 self.lookup(client, &senders).await.map(|found| found.into_values().flatten().collect()).unwrap_or_default()
354 };
355 let senders: Vec<&PublicKey> = senders.iter().collect();
356 let Ok(opened) = mime::decrypt_message(&raw, &self.secrets(), &senders) else { return Ok(locked) };
357 let Ok(parsed) = compose::parse(&opened.data) else { return Ok(locked) };
358 let addresses = |list: Vec<Address>| -> Vec<EmailAddress> {
359 list.into_iter().map(|a| EmailAddress { name: a.name, email: a.email }).collect()
360 };
361 let part = |id: &str, kind: &str, size: usize| BodyPart {
362 part_id: Some(id.to_owned()),
363 blob_id: None,
364 kind: kind.to_owned(),
365 name: None,
366 size: size as u64,
367 inner: None,
368 };
369 email.summary.subject = parsed.subject;
370 if !parsed.from.is_empty() {
371 email.summary.from = Some(addresses(parsed.from));
372 }
373 if !parsed.to.is_empty() {
374 email.summary.to = Some(addresses(parsed.to));
375 }
376 email.cc = Some(addresses(parsed.cc));
377 email.reply_to = Some(addresses(parsed.reply_to));
378 email.sent_at = parsed.sent_at.or(email.sent_at);
379 email.message_id = parsed.message_id.map(|id| vec![id]).or(email.message_id);
380 if !parsed.references.is_empty() {
381 email.references = Some(parsed.references);
382 }
383 email.text_body.clear();
384 email.html_body.clear();
385 email.body_values.clear();
386 if let Some(text) = parsed.text {
387 email.text_body.push(part("text", "text/plain", text.len()));
388 email.body_values.insert("text".into(), BodyValue { value: text });
389 }
390 if let Some(html) = parsed.html {
391 email.html_body.push(part("html", "text/html", html.len()));
392 email.body_values.insert("html".into(), BodyValue { value: html });
393 } else {
394 email.html_body = email.text_body.clone();
395 }
396 email.attachments = parsed
397 .attachments
398 .into_iter()
399 .map(|p| BodyPart {
400 part_id: None,
401 blob_id: None,
402 kind: p.media_type,
403 name: p.name,
404 size: p.size as u64,
405 inner: Some(p.index),
406 })
407 .collect();
408 email.summary.has_attachment = !email.attachments.is_empty();
409 email.inner = Some(Inner(opened.data));
410 email.sealed = Some(Sealed { locked: false, signed: opened.signed, verified: opened.verified_by.is_some() });
411 Ok(email)
412 }
413
414 fn own_key(&self, client: &Client, account: &str, identity: &Identity) -> std::result::Result<&SecretKey, String> {
417 if !self.available {
418 return Err("the server has no end-to-end encryption".into());
419 }
420 let Some(current) = &self.current else {
421 return Err(if self.has_key() {
422 "your key is locked (K unlocks it)".into()
423 } else {
424 "your account has no key (made in the web app)".into()
425 });
426 };
427 if client.session.primary(MAIL) != Some(account) {
428 return Err("it is not sent from your own account".into());
429 }
430 if !current.public().addresses().iter().any(|a| a.eq_ignore_ascii_case(&identity.email)) {
431 return Err(format!("your key is not for {}", identity.email));
432 }
433 Ok(current)
434 }
435
436 pub async fn plan(&mut self, client: &Client, account: &str, identity: &Identity, draft: &Draft) -> Plan {
439 let own = match self.own_key(client, account, identity) {
440 Ok(key) => key.public(),
441 Err(reason) => return Plan::Plain(reason),
442 };
443 if !draft.bcc.is_empty() {
445 return Plan::Plain("it has Bcc recipients, whom an encrypted message would show".into());
446 }
447 let addresses: Vec<String> = draft.to.iter().chain(&draft.cc).map(|a| a.email.clone()).collect();
448 if addresses.is_empty() {
449 return Plan::Plain("it has no recipients".into());
450 }
451 let found = match self.lookup(client, &addresses).await {
452 Ok(found) => found,
453 Err(error) => return Plan::Plain(format!("the keys could not be looked up: {error}")),
454 };
455 let mut keys = Vec::new();
456 for (address, list) in found {
457 let usable: Vec<PublicKey> = list.into_iter().filter(PublicKey::can_encrypt).collect();
458 if usable.is_empty() {
459 return Plan::Plain(format!("{address} has no key"));
460 }
461 keys.extend(usable);
462 }
463 keys.push(own);
464 Plan::Sealed(keys)
465 }
466
467 pub fn draft_plan(&self, client: &Client, account: &str, identity: &Identity, draft: &Draft) -> Plan {
469 match self.own_key(client, account, identity) {
470 Ok(_) if !draft.bcc.is_empty() => Plan::Plain("an encrypted draft cannot keep Bcc".into()),
471 Ok(key) => Plan::Sealed(vec![key.public()]),
472 Err(reason) => Plan::Plain(reason),
473 }
474 }
475
476 pub async fn send(&self, client: &Client, outgoing: Outgoing<'_>, recipients: &[PublicKey]) -> Result<()> {
480 self.store(client, outgoing, &[], recipients, true).await
481 }
482
483 pub async fn keep(&self, client: &Client, outgoing: Outgoing<'_>, keys: &[PublicKey]) -> Result<()> {
485 self.store(client, outgoing, &[], keys, false).await
486 }
487
488 pub async fn send_with(
491 &self,
492 client: &Client,
493 outgoing: Outgoing<'_>,
494 inline: &[InlineFile],
495 recipients: &[PublicKey],
496 ) -> Result<()> {
497 self.store(client, outgoing, inline, recipients, true).await
498 }
499
500 pub async fn keep_with(
502 &self,
503 client: &Client,
504 outgoing: Outgoing<'_>,
505 inline: &[InlineFile],
506 keys: &[PublicKey],
507 ) -> Result<()> {
508 self.store(client, outgoing, inline, keys, false).await
509 }
510
511 async fn store(
512 &self,
513 client: &Client,
514 outgoing: Outgoing<'_>,
515 inline: &[InlineFile],
516 recipients: &[PublicKey],
517 submit: bool,
518 ) -> Result<()> {
519 let Outgoing { account, identity, draft, files, boxes } = outgoing;
520 let own = self.own_key(client, account, identity).map_err(Error::E2e)?;
521 let drafts = role(boxes, "drafts").ok_or_else(|| Error::Protocol("there is no Drafts mailbox".into()))?;
522 let mut attachments = Vec::new();
523 for file in &draft.attachments {
525 let mut data = Vec::new();
526 client.download(account, &file.blob_id, &file.name, &file.kind, &mut data, &mut |_, _| {}).await?;
527 attachments.push(compose::Attachment {
528 name: file.name.clone(),
529 media_type: file.kind.clone(),
530 data: STANDARD.encode(data),
531 });
532 }
533 for path in files {
534 let data = std::fs::read(path).map_err(|error| Error::Protocol(format!("{}: {error}", path.display())))?;
535 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("attachment").to_owned();
536 let media_type = crate::files::media_type(&name).to_owned();
537 attachments.push(compose::Attachment { name, media_type, data: STANDARD.encode(data) });
538 }
539 for file in inline {
540 attachments.push(compose::Attachment {
541 name: file.name.clone(),
542 media_type: file.media_type.clone(),
543 data: STANDARD.encode(&file.data),
544 });
545 }
546 let address = |a: &EmailAddress| Address { name: a.name.clone(), email: a.email.clone() };
547 let strip = |ids: &[String]| -> Vec<String> {
548 ids.iter().map(|id| id.trim().trim_start_matches('<').trim_end_matches('>').to_owned()).collect()
549 };
550 let domain = identity.email.rsplit_once('@').map(|(_, d)| d).unwrap_or("localhost");
551 let message = compose::Draft {
552 from: Some(Address {
553 name: (!identity.name.is_empty()).then(|| identity.name.clone()),
554 email: identity.email.clone(),
555 }),
556 to: draft.to.iter().map(address).collect(),
557 cc: draft.cc.iter().map(address).collect(),
558 reply_to: Vec::new(),
559 subject: draft.subject.clone(),
560 message_id: format!("{}@{domain}", random_id()),
561 in_reply_to: strip(&draft.in_reply_to),
562 references: strip(&draft.references),
563 date: chrono::Local::now().to_rfc2822(),
564 text: draft.body.clone(),
565 html: None,
566 attachments,
567 };
568 let raw = compose::build(&message)?;
569 let recipients: Vec<&PublicKey> = recipients.iter().collect();
570 let encrypted = mime::encrypt_message(&raw, &recipients, Some(own))?;
571 let summary = mime::encrypted_summary(&raw, &[&own.public()])?;
572 let size = encrypted.len() as u64;
573 let upload = client.upload(account, "message/rfc822", Cursor::new(encrypted), size, |_, _| {}).await?;
574 let mut calls = vec![(
575 "Email/import",
576 json!({ "accountId": account, "emails": { "draft": {
577 "blobId": upload.blob_id, "mailboxIds": { drafts.id.as_str(): true },
578 "keywords": { "$draft": true, "$seen": true }, "kernoid:encryptedSummary": STANDARD.encode(summary),
579 } } }),
580 "e",
581 )];
582 if submit {
583 let mut update = json!({ "keywords/$draft": null });
584 if let Some(sent) = role(boxes, "sent") {
585 update["mailboxIds"] = json!({ sent.id.as_str(): true });
586 }
587 let rcpt: Vec<Value> =
588 draft.to.iter().chain(&draft.cc).map(|a| json!({ "email": a.email, "parameters": null })).collect();
589 calls.push((
590 "EmailSubmission/set",
591 json!({
592 "accountId": account,
593 "create": { "send": {
594 "identityId": identity.id, "emailId": "#draft",
595 "envelope": { "mailFrom": { "email": identity.email, "parameters": null }, "rcptTo": rcpt },
596 } },
597 "onSuccessUpdateEmail": { "#send": update },
598 }),
599 "s",
600 ));
601 }
602 let responses = client.request(&[MAIL, SUBMISSION, OPENPGP], calls).await?;
603 for (_, result, _) in &responses.0 {
604 set_error(result)?;
605 }
606 Ok(())
607 }
608}
609
610fn random_id() -> String {
613 let state = std::collections::hash_map::RandomState::new();
614 let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or_default();
615 let half = |n: u8| {
616 let mut hasher = state.build_hasher();
617 hasher.write_u128(now);
618 hasher.write_u32(std::process::id());
619 hasher.write_u8(n);
620 hasher.finish()
621 };
622 format!("{:016x}{:016x}", half(0), half(1))
623}