use std::{collections::BTreeMap, io::Read};
use ct_codecs::Base64UrlSafeNoPadding;
use http::uri::Uri;
use jwt_simple::prelude::*;
use serde_json::Value;
use crate::{
error::WebPushError,
message::SubscriptionInfo,
vapid::{signer::Claims, VapidKey, VapidSignature, VapidSigner},
};
pub struct VapidSignatureBuilder<'a> {
claims: Claims,
key: VapidKey,
subscription_info: &'a SubscriptionInfo,
}
impl<'a> VapidSignatureBuilder<'a> {
pub fn from_pem<R: Read>(
pk_pem: R,
subscription_info: &'a SubscriptionInfo,
) -> Result<VapidSignatureBuilder<'a>, WebPushError> {
let pr_key = Self::read_pem(pk_pem)?;
Ok(Self::from_ec(pr_key, subscription_info))
}
pub fn from_pem_no_sub<R: Read>(pk_pem: R) -> Result<PartialVapidSignatureBuilder, WebPushError> {
let pr_key = Self::read_pem(pk_pem)?;
Ok(PartialVapidSignatureBuilder {
key: VapidKey::new(pr_key),
})
}
pub fn from_der<R: Read>(
mut pk_der: R,
subscription_info: &'a SubscriptionInfo,
) -> Result<VapidSignatureBuilder<'a>, WebPushError> {
let mut der_key: Vec<u8> = Vec::new();
pk_der.read_to_end(&mut der_key)?;
Ok(Self::from_ec(
ES256KeyPair::from_bytes(
&sec1_decode::parse_der(&der_key)
.map_err(|_| WebPushError::InvalidCryptoKeys)?
.key,
)
.map_err(|_| WebPushError::InvalidCryptoKeys)?,
subscription_info,
))
}
pub fn from_der_no_sub<R: Read>(mut pk_der: R) -> Result<PartialVapidSignatureBuilder, WebPushError> {
let mut der_key: Vec<u8> = Vec::new();
pk_der.read_to_end(&mut der_key)?;
Ok(PartialVapidSignatureBuilder {
key: VapidKey::new(
ES256KeyPair::from_bytes(
&sec1_decode::parse_der(&der_key)
.map_err(|_| WebPushError::InvalidCryptoKeys)?
.key,
)
.map_err(|_| WebPushError::InvalidCryptoKeys)?,
),
})
}
pub fn from_base64(
encoded: &str,
subscription_info: &'a SubscriptionInfo,
) -> Result<VapidSignatureBuilder<'a>, WebPushError> {
let pr_key = ES256KeyPair::from_bytes(
&Base64UrlSafeNoPadding::decode_to_vec(encoded, None).map_err(|_| WebPushError::InvalidCryptoKeys)?,
)
.map_err(|_| WebPushError::InvalidCryptoKeys)?;
Ok(Self::from_ec(pr_key, subscription_info))
}
pub fn from_base64_no_sub(encoded: &str) -> Result<PartialVapidSignatureBuilder, WebPushError> {
let pr_key = ES256KeyPair::from_bytes(
&Base64UrlSafeNoPadding::decode_to_vec(encoded, None).map_err(|_| WebPushError::InvalidCryptoKeys)?,
)
.map_err(|_| WebPushError::InvalidCryptoKeys)?;
Ok(PartialVapidSignatureBuilder {
key: VapidKey::new(pr_key),
})
}
pub fn add_claim<V>(&mut self, key: &'a str, val: V)
where
V: Into<Value>,
{
self.claims.custom.insert(key.to_string(), val.into());
}
pub fn build(self) -> Result<VapidSignature, WebPushError> {
let endpoint: Uri = self.subscription_info.endpoint.parse()?;
let signature = VapidSigner::sign(self.key, &endpoint, self.claims)?;
Ok(signature)
}
fn from_ec(ec_key: ES256KeyPair, subscription_info: &'a SubscriptionInfo) -> VapidSignatureBuilder<'a> {
VapidSignatureBuilder {
claims: jwt_simple::prelude::Claims::with_custom_claims(BTreeMap::new(), Duration::from_hours(12)),
key: VapidKey::new(ec_key),
subscription_info,
}
}
pub(crate) fn read_pem<R: Read>(mut input: R) -> Result<ES256KeyPair, WebPushError> {
let mut buffer = String::new();
input.read_to_string(&mut buffer)?;
let parsed = pem::parse_many(&buffer).map_err(|_| WebPushError::InvalidCryptoKeys)?;
let found_pkcs8 = parsed.iter().any(|pem| pem.tag() == "PRIVATE KEY");
let found_sec1 = parsed.iter().any(|pem| pem.tag() == "EC PRIVATE KEY");
if found_sec1 {
let key = sec1_decode::parse_pem(buffer.as_bytes()).map_err(|_| WebPushError::InvalidCryptoKeys)?;
Ok(ES256KeyPair::from_bytes(&key.key).map_err(|_| WebPushError::InvalidCryptoKeys)?)
} else if found_pkcs8 {
Ok(ES256KeyPair::from_pem(&buffer).map_err(|_| WebPushError::InvalidCryptoKeys)?)
} else {
Err(WebPushError::MissingCryptoKeys)
}
}
}
#[derive(Clone)]
pub struct PartialVapidSignatureBuilder {
key: VapidKey,
}
impl PartialVapidSignatureBuilder {
pub fn add_sub_info(self, subscription_info: &SubscriptionInfo) -> VapidSignatureBuilder<'_> {
VapidSignatureBuilder {
key: self.key,
claims: jwt_simple::prelude::Claims::with_custom_claims(BTreeMap::new(), Duration::from_hours(12)),
subscription_info,
}
}
pub fn get_public_key(&self) -> Vec<u8> {
self.key.public_key()
}
}
#[cfg(test)]
mod tests {
use ct_codecs::{Base64UrlSafeNoPadding, Encoder};
use crate::{message::SubscriptionInfo, vapid::VapidSignatureBuilder};
static PRIVATE_PEM: &[u8] = include_bytes!("../../resources/vapid_test_key.pem");
static PRIVATE_DER: &[u8] = include_bytes!("../../resources/vapid_test_key.der");
static PRIVATE_BASE64: &str = "IQ9Ur0ykXoHS9gzfYX0aBjy9lvdrjx_PFUXmie9YRcY";
fn example_subscription_info() -> SubscriptionInfo {
serde_json::from_value(
serde_json::json!({
"endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABaso4Vajy4STM25r5y5oFfyN451rUmES6mhQngxABxbZB5q_o75WpG25oKdrlrh9KdgWFKdYBc-buLPhvCTqR5KdsK8iCZHQume-ndtZJWKOgJbQ20GjbxHmAT1IAv8AIxTwHO-JTQ2Np2hwkKISp2_KUtpnmwFzglLP7vlCd16hTNJ2I",
"keys": {
"auth": "sBXU5_tIYz-5w7G2B25BEw",
"p256dh": "BH1HTeKM7-NwaLGHEqxeu2IamQaVVLkcsFHPIHmsCnqxcBHPQBprF41bEMOr3O1hUQ2jU1opNEm1F_lZV_sxMP8"
}
})
).unwrap()
}
#[test]
fn test_builder_from_pem() {
let subscription_info = example_subscription_info();
let builder = VapidSignatureBuilder::from_pem(PRIVATE_PEM, &subscription_info).unwrap();
let signature = builder.build().unwrap();
assert_eq!(
"BMo1HqKF6skMZYykrte9duqYwBD08mDQKTunRkJdD3sTJ9E-yyN6sJlPWTpKNhp-y2KeS6oANHF-q3w37bClb7U",
Base64UrlSafeNoPadding::encode_to_string(&signature.auth_k).unwrap()
);
assert!(!signature.auth_t.is_empty());
}
#[test]
fn test_builder_from_der() {
let subscription_info = example_subscription_info();
let builder = VapidSignatureBuilder::from_der(PRIVATE_DER, &subscription_info).unwrap();
let signature = builder.build().unwrap();
assert_eq!(
"BMo1HqKF6skMZYykrte9duqYwBD08mDQKTunRkJdD3sTJ9E-yyN6sJlPWTpKNhp-y2KeS6oANHF-q3w37bClb7U",
Base64UrlSafeNoPadding::encode_to_string(&signature.auth_k).unwrap()
);
assert!(!signature.auth_t.is_empty());
}
#[test]
fn test_builder_from_base64() {
let subscription_info = example_subscription_info();
let builder = VapidSignatureBuilder::from_base64(PRIVATE_BASE64, &subscription_info).unwrap();
let signature = builder.build().unwrap();
assert_eq!(
"BMjQIp55pdbU8pfCBKyXcZjlmER_mXt5LqNrN1hrXbdBS5EnhIbMu3Au-RV53iIpztzNXkGI56BFB1udQ8Bq_H4",
Base64UrlSafeNoPadding::encode_to_string(&signature.auth_k).unwrap()
);
assert!(!signature.auth_t.is_empty());
}
}