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