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