From 66c93ce73bfb280261de3da100271dbc4b0dfe10 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 20 Nov 2024 19:36:22 +0000 Subject: [PATCH] feat: encrypt notification tokens --- src/accounts.rs | 2 +- src/imap.rs | 45 +++++++++++++++++++++++++++++---- src/push.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 103 insertions(+), 10 deletions(-) diff --git a/src/accounts.rs b/src/accounts.rs index 425139d71b..637684b206 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -372,7 +372,7 @@ impl Accounts { /// Sets notification token for Apple Push Notification service. pub async fn set_push_device_token(&self, token: &str) -> Result<()> { - self.push_subscriber.set_device_token(token).await; + self.push_subscriber.set_device_token(token).await?; Ok(()) } } diff --git a/src/imap.rs b/src/imap.rs index 4eb432599d..75e11d1714 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1565,11 +1565,27 @@ impl Session { .await? .context("INBOX is not configured")?; - self.run_command_and_check_ok(format!( - "SETMETADATA \"{folder}\" (/private/devicetoken \"{device_token}\")" - )) - .await - .context("SETMETADATA command failed")?; + // We expect that the server supporting `XDELTAPUSH` capability + // has non-synchronizing literals support as well: + // . + let device_token_len = device_token.len(); + + if device_token_len <= 4096 { + self.run_command_and_check_ok(&format_setmetadata(&folder, &device_token)) + .await + .context("SETMETADATA command failed")?; + } else { + // If Apple or Google (FCM) gives us a very large token, + // do not even try to give it to IMAP servers. + // + // Limit of 4096 is arbitrarily selected + // to be the same as required by LITERAL- IMAP extension. + // + // Dovecot supports LITERAL+ and non-synchronizing literals + // of any length, but there is no reason for tokens + // to be that large even after OpenPGP encryption. + warn!(context, "Device token is too long for LITERAL-, ignoring."); + } context.push_subscribed.store(true, Ordering::Relaxed); } else if !context.push_subscriber.heartbeat_subscribed().await { let context = context.clone(); @@ -1581,6 +1597,13 @@ impl Session { } } +fn format_setmetadata(folder: &str, device_token: &str) -> String { + let device_token_len = device_token.len(); + format!( + "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})" + ) +} + impl Session { /// Returns success if we successfully set the flag or we otherwise /// think add_flag should not be retried: Disconnection during setting @@ -2864,4 +2887,16 @@ mod tests { vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())] ); } + + #[test] + fn test_setmetadata_device_token() { + assert_eq!( + format_setmetadata("INBOX", "foobarbaz"), + "SETMETADATA \"INBOX\" (/private/devicetoken {9+}\r\nfoobarbaz)" + ); + assert_eq!( + format_setmetadata("INBOX", "foo\r\nbar\r\nbaz\r\n"), + "SETMETADATA \"INBOX\" (/private/devicetoken {15+}\r\nfoo\r\nbar\r\nbaz\r\n)" + ); + } } diff --git a/src/push.rs b/src/push.rs index fbdf7ff5ad..c74d30060a 100644 --- a/src/push.rs +++ b/src/push.rs @@ -1,10 +1,13 @@ use std::sync::atomic::Ordering; use std::sync::Arc; -use anyhow::Result; +use anyhow::{Context as _, Result}; +use pgp::crypto::sym::SymmetricKeyAlgorithm; +use rand::thread_rng; use tokio::sync::RwLock; use crate::context::Context; +use crate::key::DcKey; /// Manages subscription to Apple Push Notification services. /// @@ -24,20 +27,56 @@ pub struct PushSubscriber { inner: Arc>, } +/// The key was generated with +/// `rsop generate-key --profile rfc9580 | rsop extract-cert`. +const NOTIFIERS_PUBLIC_KEY: &str = "-----BEGIN PGP PUBLIC KEY BLOCK----- + +xioGZz5XrhsAAAAgArETOonYhhUGVk0fw0t8b4MbvoFJFHVKwD352OiEizPCsAYf +GwoAAABBBQJnPleuAhsDAh4JCAsJCAcKDQwLBRUKCQgLAhYCIiEGCeNFWH7kM/8A +XS8A2HouJVoSe2JbPKDxomei7cnPVg4AAAAAVtIgQI/MVJd3abu2ITUxpFMsTro8 +3sc5WC8dXB5Et5GNROsCQzfdmpmCb5RcVmjSirVnOIlxBvGg00ajCs76PKK0FUIk +xIQfFcNpKVnsqEcXja//Hq37Q5YQIHBswDTy4tUIzioGZz5XrhkAAAAgwgebLt0s +ZFniutxUyR+0wtgUjdPyqUiI5Tu9Qp4R/1fClQYYGwgAAAAsBQJnPleuAhsMIiEG +CeNFWH7kM/8AXS8A2HouJVoSe2JbPKDxomei7cnPVg4AAAAKCRAJ40VYfuQz/+nV +EJGsfYwfQw1ErVtGRNLHsrX/dmwpFlKS1fbCWXjxL8yPSfwvPPSKh0sMTbZ9X7NC +KvGiT8+VtKuphR9lga2BoUWnrQCXViPkJyPztVQSg0kO +=9WnP +-----END PGP PUBLIC KEY BLOCK-----"; + impl PushSubscriber { /// Creates new push notification subscriber. pub(crate) fn new() -> Self { Default::default() } - /// Sets device token for Apple Push Notification service. - pub(crate) async fn set_device_token(&self, token: &str) { - self.inner.write().await.device_token = Some(token.to_string()); + /// Sets device token for Apple Push Notification service + /// or Firebase Cloud Messaging. + /// + /// The token is encrypted with OpenPGP. + pub(crate) async fn set_device_token(&self, token: &str) -> Result<()> { + let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?.0; + let encryption_subkey = public_key + .public_subkeys + .first() + .context("No encryption subkey found")?; + let literal_message = pgp::composed::Message::new_literal("", token); + let mut rng = thread_rng(); + let encrypted_message = literal_message.encrypt_to_keys_seipdv1( + &mut rng, + SymmetricKeyAlgorithm::AES128, + &[&encryption_subkey], + )?; + let encoded_message = encrypted_message.to_armored_string(Default::default())?; + self.inner.write().await.device_token = Some(encoded_message); + Ok(()) } /// Retrieves device token. /// + /// The token is encrypted with OpenPGP. + /// /// Token may be not available if application is not running on Apple platform, + /// does not have Google Play services, /// failed to register for remote notifications or is in the process of registering. /// /// IMAP loop should periodically check if device token is available @@ -121,3 +160,22 @@ impl Context { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_set_device_key() { + let push_subscriber = PushSubscriber::new(); + assert_eq!(push_subscriber.device_token().await, None); + + push_subscriber + .set_device_token("some-token") + .await + .expect("Failed to set device token"); + let device_token = push_subscriber.device_token().await.unwrap(); + assert!(device_token.starts_with("-----BEGIN PGP MESSAGE-----")); + assert!(device_token.ends_with("-----END PGP MESSAGE-----\n")); + } +}