1
1
use std:: sync:: atomic:: Ordering ;
2
2
use std:: sync:: Arc ;
3
3
4
- use anyhow:: Result ;
4
+ use anyhow:: { Context as _, Result } ;
5
+ use base64:: Engine as _;
6
+ use pgp:: crypto:: aead:: AeadAlgorithm ;
7
+ use pgp:: crypto:: sym:: SymmetricKeyAlgorithm ;
8
+ use pgp:: ser:: Serialize ;
9
+ use rand:: thread_rng;
5
10
use tokio:: sync:: RwLock ;
6
11
7
12
use crate :: context:: Context ;
13
+ use crate :: key:: DcKey ;
8
14
9
15
/// Manages subscription to Apple Push Notification services.
10
16
///
@@ -24,20 +30,85 @@ pub struct PushSubscriber {
24
30
inner : Arc < RwLock < PushSubscriberState > > ,
25
31
}
26
32
33
+ /// The key was generated with
34
+ /// `rsop generate-key --profile rfc9580`
35
+ /// and public key was extracted with `rsop extract-cert`.
36
+ const NOTIFIERS_PUBLIC_KEY : & str = "-----BEGIN PGP PUBLIC KEY BLOCK-----
37
+
38
+ xioGZ03cdhsAAAAg6PasQQylEuWAp9N5PXN93rqjZdqOqN3s9RJEU/K8FZzCsAYf
39
+ GwoAAABBBQJnTdx2AhsDAh4JCAsJCAcKDQwLBRUKCQgLAhYCIiEGiJJktnCmEtXa
40
+ qsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAAUfgg/sg0sR2mytzADFBpNAaY0Hyu
41
+ aru8ics3eUkeNn2ziL4ZsIMx+4mcM5POvD0PG9LtH8Rz/y9iItD0c2aoRBab7iri
42
+ /gDm6aQuj3xXgtAiXdaN9s+QPxR9gY/zG1t9iXgBzioGZ03cdhkAAAAgwJ0wQFsk
43
+ MGH4jklfK1fFhYoQZMjEFCRBIk+r1S+WaSDClQYYGwgAAAAsBQJnTdx2AhsMIiEG
44
+ iJJktnCmEtXaqsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAKCRCIkmS2cKYS1WdP
45
+ EFerccH2BoIPNbrxi6hwvxxy7G1mHg//ofD90fqmeY9xTfKMYl16bqQh4R1PiYd5
46
+ LMc5VqgXHgioqTYKbltlOtWC+HDt/PrymQsN4q/aEmsM
47
+ =5jvt
48
+ -----END PGP PUBLIC KEY BLOCK-----" ;
49
+
50
+ /// Pads the token with spaces.
51
+ ///
52
+ /// This makes it impossible to tell
53
+ /// if the user is an Apple user with shorter tokens
54
+ /// or FCM user with longer tokens by the length of ciphertext.
55
+ fn pad_device_token ( s : & str ) -> String {
56
+ // 512 is larger than any token, tokens seen so far have not been larger than 200 bytes.
57
+ let expected_len: usize = 512 ;
58
+ let payload_len = s. len ( ) ;
59
+ let padding_len = expected_len. saturating_sub ( payload_len) ;
60
+ let padding = " " . repeat ( padding_len) ;
61
+ let res = format ! ( "{s}{padding}" ) ;
62
+ debug_assert_eq ! ( res. len( ) , expected_len) ;
63
+ res
64
+ }
65
+
66
+ /// Encrypts device token with OpenPGP.
67
+ ///
68
+ /// The result is base64-encoded and not ASCII armored to avoid dealing with newlines.
69
+ pub ( crate ) fn encrypt_device_token ( device_token : & str ) -> Result < String > {
70
+ let public_key = pgp:: composed:: SignedPublicKey :: from_asc ( NOTIFIERS_PUBLIC_KEY ) ?. 0 ;
71
+ let encryption_subkey = public_key
72
+ . public_subkeys
73
+ . first ( )
74
+ . context ( "No encryption subkey found" ) ?;
75
+ let padded_device_token = pad_device_token ( device_token) ;
76
+ let literal_message = pgp:: composed:: Message :: new_literal ( "" , & padded_device_token) ;
77
+ let mut rng = thread_rng ( ) ;
78
+ let chunk_size = 8 ;
79
+
80
+ let encrypted_message = literal_message. encrypt_to_keys_seipdv2 (
81
+ & mut rng,
82
+ SymmetricKeyAlgorithm :: AES128 ,
83
+ AeadAlgorithm :: Ocb ,
84
+ chunk_size,
85
+ & [ & encryption_subkey] ,
86
+ ) ?;
87
+ let encoded_message = encrypted_message. to_bytes ( ) ?;
88
+ Ok ( format ! (
89
+ "openpgp:{}" ,
90
+ base64:: engine:: general_purpose:: STANDARD . encode( encoded_message)
91
+ ) )
92
+ }
93
+
27
94
impl PushSubscriber {
28
95
/// Creates new push notification subscriber.
29
96
pub ( crate ) fn new ( ) -> Self {
30
97
Default :: default ( )
31
98
}
32
99
33
- /// Sets device token for Apple Push Notification service.
100
+ /// Sets device token for Apple Push Notification service
101
+ /// or Firebase Cloud Messaging.
34
102
pub ( crate ) async fn set_device_token ( & self , token : & str ) {
35
103
self . inner . write ( ) . await . device_token = Some ( token. to_string ( ) ) ;
36
104
}
37
105
38
106
/// Retrieves device token.
39
107
///
108
+ /// The token is encrypted with OpenPGP.
109
+ ///
40
110
/// Token may be not available if application is not running on Apple platform,
111
+ /// does not have Google Play services,
41
112
/// failed to register for remote notifications or is in the process of registering.
42
113
///
43
114
/// IMAP loop should periodically check if device token is available
@@ -121,3 +192,37 @@ impl Context {
121
192
}
122
193
}
123
194
}
195
+
196
+ #[ cfg( test) ]
197
+ mod tests {
198
+ use super :: * ;
199
+
200
+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
201
+ async fn test_set_device_token ( ) {
202
+ let push_subscriber = PushSubscriber :: new ( ) ;
203
+ assert_eq ! ( push_subscriber. device_token( ) . await , None ) ;
204
+
205
+ push_subscriber. set_device_token ( "some-token" ) . await ;
206
+ let device_token = push_subscriber. device_token ( ) . await . unwrap ( ) ;
207
+ assert_eq ! ( device_token, "some-token" ) ;
208
+ }
209
+
210
+ #[ test]
211
+ fn test_pad_device_token ( ) {
212
+ let apple_token = "0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894" ;
213
+ assert_eq ! ( pad_device_token( apple_token) . trim( ) , apple_token) ;
214
+ }
215
+
216
+ #[ test]
217
+ fn test_encrypt_device_token ( ) {
218
+ let fcm_token = encrypt_device_token ( "fcm-chat.delta:c67DVcpVQN2rJHiSszKNDW:APA91bErcJV2b8qG0IT4aiuCqw6Al0_SbydSuz3V0CHBR1X7Fp8YzyvlpxNZIOGYVDFKejZGE1YiGSaqxmkr9ds0DuALmZNDwqIhuZWGKKrs3r7DTSkQ9MQ" ) . unwrap ( ) ;
219
+ let fcm_beta_token = encrypt_device_token ( "fcm-chat.delta.beta:chu-GhZCTLyzq1XseJp3na:APA91bFlsfDawdszWTyOLbxBy7KeRCrYM-SBFqutebF5ix0EZKMuCFUT_Y7R7Ex_eTQG_LbOu3Ky_z5UlTMJtI7ufpIp5wEvsFmVzQcOo3YhrUpbiSVGIlk" ) . unwrap ( ) ;
220
+ let apple_token = encrypt_device_token (
221
+ "0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894" ,
222
+ )
223
+ . unwrap ( ) ;
224
+
225
+ assert_eq ! ( fcm_token. len( ) , fcm_beta_token. len( ) ) ;
226
+ assert_eq ! ( apple_token. len( ) , fcm_token. len( ) ) ;
227
+ }
228
+ }
0 commit comments