pushicino/
subscription.rs1use crate::rfc8188;
2use crate::rfc8188::{EncodingKey, EncryptedPayload, InputKeyingMaterial, Keyid};
3use base64::Engine;
4use base64::prelude::BASE64_URL_SAFE_NO_PAD;
5use elliptic_curve::rand_core::OsRng;
6use elliptic_curve::sec1::ToEncodedPoint;
7use hkdf::InvalidLength;
8use p256::ecdh::EphemeralSecret;
9use reqwest::{Client, RequestBuilder};
10use serde::de::Error as DeError;
11use serde::{Deserialize, Serialize};
12use sha2::Sha256;
13use url::Url;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Keys {
17 pub auth: AuthenticationSecret,
18
19 #[serde(deserialize_with = "decode_p256dh")]
20 pub p256dh: p256::PublicKey,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct Subscription {
33 pub endpoint: Url,
34
35 pub expiration_time: Option<u64>,
36
37 pub keys: Keys,
38}
39
40impl EncodingKey for Subscription {
41 type Error = Error;
42
43 fn ikm(&self) -> Result<(Keyid, InputKeyingMaterial), Error> {
44 let user_agent_public_key = self.keys.p256dh;
45 let application_server_private_key = EphemeralSecret::random(&mut OsRng);
46 let application_service_public_key = application_server_private_key.public_key();
47 let ecdh_shared_secret =
48 application_server_private_key.diffie_hellman(&user_agent_public_key);
49
50 let mut key_info = Vec::new();
51 key_info.extend_from_slice(b"WebPush: info\0");
52 key_info.extend_from_slice(user_agent_public_key.to_encoded_point(false).as_bytes());
53 key_info.extend_from_slice(
54 application_service_public_key
55 .to_encoded_point(false)
56 .as_bytes(),
57 );
58
59 let mut ikm = [0u8; 32];
60
61 ecdh_shared_secret
62 .extract::<Sha256>(Some(self.keys.auth.as_ref()))
63 .expand(&key_info, &mut ikm)
64 .map_err(Error::InvalidOkmLength)?;
65
66 Ok((
67 Keyid(
68 *application_service_public_key
69 .to_encoded_point(false)
70 .to_bytes()
71 .as_array()
72 .unwrap(),
73 ),
74 InputKeyingMaterial(ikm),
75 ))
76 }
77}
78
79impl Subscription {
80 pub fn new(
82 endpoint: Url,
83 expiration_time: Option<u64>,
84 authentication_secret: AuthenticationSecret,
85 user_agent_public_key: p256::PublicKey,
86 ) -> Self {
87 Self {
88 endpoint,
89 expiration_time,
90 keys: Keys {
91 auth: authentication_secret,
92 p256dh: user_agent_public_key,
93 },
94 }
95 }
96
97 pub(crate) fn prepare_request<'a, 'b>(
98 &'a self,
99 client: &Client,
100 ttl: u64,
101 message: impl Into<&'b [u8]>,
102 ) -> Result<RequestBuilder, Error> {
103 let encoded_payload = self.encode_message(message)?;
104 Ok(client
105 .post(self.endpoint.as_str())
106 .header("TTL", ttl.to_string())
107 .header("Content-Type", "application/octet-stream")
108 .header("Content-Encoding", "aes128gcm")
109 .body::<Vec<u8>>((&encoded_payload).into()))
110 }
111
112 fn encode_message<'a, 'b>(
116 &'a self,
117 message: impl Into<&'b [u8]>,
118 ) -> Result<EncryptedPayload, Error> {
119 rfc8188::encode(self, message.into()).map_err(Error::FailedToEncryptPayload)
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
127pub struct AuthenticationSecret(
128 #[serde(deserialize_with = "decode_authentication_secret")] pub [u8; 16],
129);
130
131impl AsRef<[u8]> for AuthenticationSecret {
132 fn as_ref(&self) -> &[u8] {
133 &self.0
134 }
135}
136
137#[derive(Debug)]
138pub enum Error {
139 InvalidOkmLength(InvalidLength),
140 InvalidCekLength(InvalidLength),
141 InvalidNonceLength(InvalidLength),
142 FailedToEncryptPayload(rfc8188::Error),
143}
144
145fn decode_authentication_secret<'de, D>(deserializer: D) -> Result<[u8; 16], D::Error>
146where
147 D: serde::Deserializer<'de>,
148{
149 let s = String::deserialize(deserializer)?;
150 let bytes = BASE64_URL_SAFE_NO_PAD
151 .decode(s)
152 .map_err(|e| D::Error::custom(format!("Failed to decode base64: {}", e)))?;
153 bytes
154 .try_into()
155 .map_err(|_| D::Error::custom("Invalid authentication secret length"))
156}
157
158fn decode_p256dh<'de, D>(deserializer: D) -> Result<p256::PublicKey, D::Error>
159where
160 D: serde::Deserializer<'de>,
161{
162 let s = String::deserialize(deserializer)?;
163 p256::PublicKey::from_sec1_bytes(
164 BASE64_URL_SAFE_NO_PAD
165 .decode(s)
166 .map_err(|e| D::Error::custom(format!("Failed to decode base64: {}", e)))?
167 .as_slice(),
168 )
169 .map_err(|e| D::Error::custom(format!("Failed to parse p256dh: {}", e)))
170}
171
172#[cfg(test)]
173mod tests {}