use crate::rfc8188;
use crate::rfc8188::{EncodingKey, EncryptedPayload, InputKeyingMaterial, Keyid};
use base64::Engine;
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use elliptic_curve::rand_core::OsRng;
use elliptic_curve::sec1::ToEncodedPoint;
use hkdf::InvalidLength;
use p256::ecdh::EphemeralSecret;
use reqwest::{Client, RequestBuilder};
use serde::de::Error as DeError;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use url::Url;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Keys {
pub auth: AuthenticationSecret,
#[serde(deserialize_with = "decode_p256dh")]
pub p256dh: p256::PublicKey,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Subscription {
pub endpoint: Url,
pub expiration_time: Option<u64>,
pub keys: Keys,
}
impl EncodingKey for Subscription {
type Error = Error;
fn ikm(&self) -> Result<(Keyid, InputKeyingMaterial), Error> {
let user_agent_public_key = self.keys.p256dh;
let application_server_private_key = EphemeralSecret::random(&mut OsRng);
let application_service_public_key = application_server_private_key.public_key();
let ecdh_shared_secret =
application_server_private_key.diffie_hellman(&user_agent_public_key);
let mut key_info = Vec::new();
key_info.extend_from_slice(b"WebPush: info\0");
key_info.extend_from_slice(user_agent_public_key.to_encoded_point(false).as_bytes());
key_info.extend_from_slice(
application_service_public_key
.to_encoded_point(false)
.as_bytes(),
);
let mut ikm = [0u8; 32];
ecdh_shared_secret
.extract::<Sha256>(Some(self.keys.auth.as_ref()))
.expand(&key_info, &mut ikm)
.map_err(Error::InvalidOkmLength)?;
Ok((
Keyid(
*application_service_public_key
.to_encoded_point(false)
.to_bytes()
.as_array()
.unwrap(),
),
InputKeyingMaterial(ikm),
))
}
}
impl Subscription {
pub fn new(
endpoint: Url,
expiration_time: Option<u64>,
authentication_secret: AuthenticationSecret,
user_agent_public_key: p256::PublicKey,
) -> Self {
Self {
endpoint,
expiration_time,
keys: Keys {
auth: authentication_secret,
p256dh: user_agent_public_key,
},
}
}
pub(crate) fn prepare_request<'a, 'b>(
&'a self,
client: &Client,
ttl: u64,
message: impl Into<&'b [u8]>,
) -> Result<RequestBuilder, Error> {
let encoded_payload = self.encode_message(message)?;
Ok(client
.post(self.endpoint.as_str())
.header("TTL", ttl.to_string())
.header("Content-Type", "application/octet-stream")
.header("Content-Encoding", "aes128gcm")
.body::<Vec<u8>>((&encoded_payload).into()))
}
fn encode_message<'a, 'b>(
&'a self,
message: impl Into<&'b [u8]>,
) -> Result<EncryptedPayload, Error> {
rfc8188::encode(self, message.into()).map_err(Error::FailedToEncryptPayload)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AuthenticationSecret(
#[serde(deserialize_with = "decode_authentication_secret")] pub [u8; 16],
);
impl AsRef<[u8]> for AuthenticationSecret {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
#[derive(Debug)]
pub enum Error {
InvalidOkmLength(InvalidLength),
InvalidCekLength(InvalidLength),
InvalidNonceLength(InvalidLength),
FailedToEncryptPayload(rfc8188::Error),
}
fn decode_authentication_secret<'de, D>(deserializer: D) -> Result<[u8; 16], D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let bytes = BASE64_URL_SAFE_NO_PAD
.decode(s)
.map_err(|e| D::Error::custom(format!("Failed to decode base64: {}", e)))?;
bytes
.try_into()
.map_err(|_| D::Error::custom("Invalid authentication secret length"))
}
fn decode_p256dh<'de, D>(deserializer: D) -> Result<p256::PublicKey, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
p256::PublicKey::from_sec1_bytes(
BASE64_URL_SAFE_NO_PAD
.decode(s)
.map_err(|e| D::Error::custom(format!("Failed to decode base64: {}", e)))?
.as_slice(),
)
.map_err(|e| D::Error::custom(format!("Failed to parse p256dh: {}", e)))
}
#[cfg(test)]
mod tests {}