1use std::sync::atomic::Ordering;
10use std::sync::Arc;
11
12use anyhow::{Context as _, Result};
13use base64::Engine as _;
14use pgp::crypto::aead::AeadAlgorithm;
15use pgp::crypto::sym::SymmetricKeyAlgorithm;
16use pgp::ser::Serialize;
17use rand::thread_rng;
18use tokio::sync::RwLock;
19
20use crate::context::Context;
21use crate::key::DcKey;
22
23#[derive(Debug, Clone, Default)]
37pub struct PushSubscriber {
38 inner: Arc<RwLock<PushSubscriberState>>,
39}
40
41const NOTIFIERS_PUBLIC_KEY: &str = "-----BEGIN PGP PUBLIC KEY BLOCK-----
45
46xioGZ03cdhsAAAAg6PasQQylEuWAp9N5PXN93rqjZdqOqN3s9RJEU/K8FZzCsAYf
47GwoAAABBBQJnTdx2AhsDAh4JCAsJCAcKDQwLBRUKCQgLAhYCIiEGiJJktnCmEtXa
48qsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAAUfgg/sg0sR2mytzADFBpNAaY0Hyu
49aru8ics3eUkeNn2ziL4ZsIMx+4mcM5POvD0PG9LtH8Rz/y9iItD0c2aoRBab7iri
50/gDm6aQuj3xXgtAiXdaN9s+QPxR9gY/zG1t9iXgBzioGZ03cdhkAAAAgwJ0wQFsk
51MGH4jklfK1fFhYoQZMjEFCRBIk+r1S+WaSDClQYYGwgAAAAsBQJnTdx2AhsMIiEG
52iJJktnCmEtXaqsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAKCRCIkmS2cKYS1WdP
53EFerccH2BoIPNbrxi6hwvxxy7G1mHg//ofD90fqmeY9xTfKMYl16bqQh4R1PiYd5
54LMc5VqgXHgioqTYKbltlOtWC+HDt/PrymQsN4q/aEmsM
55=5jvt
56-----END PGP PUBLIC KEY BLOCK-----";
57
58fn pad_device_token(s: &str) -> String {
64 let expected_len: usize = 512;
66 let payload_len = s.len();
67 let padding_len = expected_len.saturating_sub(payload_len);
68 let padding = " ".repeat(padding_len);
69 let res = format!("{s}{padding}");
70 debug_assert_eq!(res.len(), expected_len);
71 res
72}
73
74pub(crate) fn encrypt_device_token(device_token: &str) -> Result<String> {
78 let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?.0;
79 let encryption_subkey = public_key
80 .public_subkeys
81 .first()
82 .context("No encryption subkey found")?;
83 let padded_device_token = pad_device_token(device_token);
84 let literal_message = pgp::composed::Message::new_literal("", &padded_device_token);
85 let mut rng = thread_rng();
86 let chunk_size = 8;
87
88 let encrypted_message = literal_message.encrypt_to_keys_seipdv2(
89 &mut rng,
90 SymmetricKeyAlgorithm::AES128,
91 AeadAlgorithm::Ocb,
92 chunk_size,
93 &[&encryption_subkey],
94 )?;
95 let encoded_message = encrypted_message.to_bytes()?;
96 Ok(format!(
97 "openpgp:{}",
98 base64::engine::general_purpose::STANDARD.encode(encoded_message)
99 ))
100}
101
102impl PushSubscriber {
103 pub(crate) fn new() -> Self {
105 Default::default()
106 }
107
108 pub(crate) async fn set_device_token(&self, token: &str) {
111 self.inner.write().await.device_token = Some(token.to_string());
112 }
113
114 pub(crate) async fn device_token(&self) -> Option<String> {
125 self.inner.read().await.device_token.clone()
126 }
127
128 #[cfg(target_os = "ios")]
130 pub(crate) async fn subscribe(&self, context: &Context) -> Result<()> {
131 use crate::net::http;
132
133 let mut state = self.inner.write().await;
134
135 if state.heartbeat_subscribed {
136 return Ok(());
137 }
138
139 let Some(ref token) = state.device_token else {
140 return Ok(());
141 };
142
143 if http::post_string(
144 context,
145 "https://notifications.delta.chat/register",
146 format!("{{\"token\":\"{token}\"}}"),
147 )
148 .await?
149 {
150 state.heartbeat_subscribed = true;
151 }
152 Ok(())
153 }
154
155 #[cfg(not(target_os = "ios"))]
157 pub(crate) async fn subscribe(&self, _context: &Context) -> Result<()> {
158 let mut state = self.inner.write().await;
159 state.heartbeat_subscribed = true;
160 Ok(())
161 }
162
163 pub(crate) async fn heartbeat_subscribed(&self) -> bool {
164 self.inner.read().await.heartbeat_subscribed
165 }
166}
167
168#[derive(Debug, Default)]
169pub(crate) struct PushSubscriberState {
170 device_token: Option<String>,
172
173 heartbeat_subscribed: bool,
175}
176
177#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
178#[repr(i8)]
179pub enum NotifyState {
180 #[default]
182 NotConnected = 0,
183
184 Heartbeat = 1,
186
187 Connected = 2,
189}
190
191impl Context {
192 pub async fn push_state(&self) -> NotifyState {
194 if self.push_subscribed.load(Ordering::Relaxed) {
195 NotifyState::Connected
196 } else if self.push_subscriber.heartbeat_subscribed().await {
197 NotifyState::Heartbeat
198 } else {
199 NotifyState::NotConnected
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
209 async fn test_set_device_token() {
210 let push_subscriber = PushSubscriber::new();
211 assert_eq!(push_subscriber.device_token().await, None);
212
213 push_subscriber.set_device_token("some-token").await;
214 let device_token = push_subscriber.device_token().await.unwrap();
215 assert_eq!(device_token, "some-token");
216 }
217
218 #[test]
219 fn test_pad_device_token() {
220 let apple_token = "0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894";
221 assert_eq!(pad_device_token(apple_token).trim(), apple_token);
222 }
223
224 #[test]
225 fn test_encrypt_device_token() {
226 let fcm_token = encrypt_device_token("fcm-chat.delta:c67DVcpVQN2rJHiSszKNDW:APA91bErcJV2b8qG0IT4aiuCqw6Al0_SbydSuz3V0CHBR1X7Fp8YzyvlpxNZIOGYVDFKejZGE1YiGSaqxmkr9ds0DuALmZNDwqIhuZWGKKrs3r7DTSkQ9MQ").unwrap();
227 let fcm_beta_token = encrypt_device_token("fcm-chat.delta.beta:chu-GhZCTLyzq1XseJp3na:APA91bFlsfDawdszWTyOLbxBy7KeRCrYM-SBFqutebF5ix0EZKMuCFUT_Y7R7Ex_eTQG_LbOu3Ky_z5UlTMJtI7ufpIp5wEvsFmVzQcOo3YhrUpbiSVGIlk").unwrap();
228 let apple_token = encrypt_device_token(
229 "0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894",
230 )
231 .unwrap();
232
233 assert_eq!(fcm_token.len(), fcm_beta_token.len());
234 assert_eq!(apple_token.len(), fcm_token.len());
235 }
236}