use std::collections::BTreeMap;
use std::io::Read;
use crate::atomic_jwt::{self, prelude::*};
use base64::Engine;
use http::uri::Uri;
use serde_json::Value;
use crate::helpers::error::WebPushError;
use crate::helpers::message::SubscriptionInfo;
use crate::helpers::vapid::signer::Claims;
use crate::helpers::vapid::{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,
engine: base64::engine::general_purpose::GeneralPurpose,
subscription_info: &'a SubscriptionInfo,
) -> Result<VapidSignatureBuilder<'a>, WebPushError> {
let pr_key = ES256KeyPair::from_bytes(
&engine
.decode(encoded)
.map_err(|_| WebPushError::InvalidCryptoKeys)?,
)
.map_err(|_| WebPushError::InvalidCryptoKeys)?;
Ok(Self::from_ec(pr_key, subscription_info))
}
pub fn from_base64_no_sub(
encoded: &str,
engine: base64::engine::general_purpose::GeneralPurpose,
) -> Result<PartialVapidSignatureBuilder, WebPushError> {
let pr_key = ES256KeyPair::from_bytes(
&engine
.decode(encoded)
.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: atomic_jwt::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: atomic_jwt::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 std::fs::File;
use ::lazy_static::lazy_static;
use base64::engine::general_purpose;
use base64::Engine;
use crate::helpers::message::SubscriptionInfo;
use crate::helpers::vapid::VapidSignatureBuilder;
lazy_static! {
static ref PRIVATE_PEM: File = File::open("resources/vapid_test_key.pem").unwrap();
static ref PRIVATE_DER: File = File::open("resources/vapid_test_key.der").unwrap();
}
lazy_static! {
static ref 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();
}
static PRIVATE_BASE64: &str = "IQ9Ur0ykXoHS9gzfYX0aBjy9lvdrjx_PFUXmie9YRcY";
#[test]
fn test_builder_from_pem() {
let builder = VapidSignatureBuilder::from_pem(&*PRIVATE_PEM, &SUBSCRIPTION_INFO).unwrap();
let signature = builder.build().unwrap();
assert_eq!(
"BMo1HqKF6skMZYykrte9duqYwBD08mDQKTunRkJdD3sTJ9E-yyN6sJlPWTpKNhp-y2KeS6oANHF-q3w37bClb7U".to_string(),
general_purpose::URL_SAFE_NO_PAD.encode(&signature.auth_k)
);
assert!(!signature.auth_t.is_empty());
}
#[test]
fn test_builder_from_der() {
let builder = VapidSignatureBuilder::from_der(&*PRIVATE_DER, &SUBSCRIPTION_INFO).unwrap();
let signature = builder.build().unwrap();
assert_eq!(
"BMo1HqKF6skMZYykrte9duqYwBD08mDQKTunRkJdD3sTJ9E-yyN6sJlPWTpKNhp-y2KeS6oANHF-q3w37bClb7U".to_string(),
general_purpose::URL_SAFE_NO_PAD.encode(&signature.auth_k)
);
assert!(!signature.auth_t.is_empty());
}
#[test]
fn test_builder_from_base64() {
let builder = VapidSignatureBuilder::from_base64(
PRIVATE_BASE64,
general_purpose::URL_SAFE_NO_PAD,
&SUBSCRIPTION_INFO,
)
.unwrap();
let signature = builder.build().unwrap();
assert_eq!(
"BMjQIp55pdbU8pfCBKyXcZjlmER_mXt5LqNrN1hrXbdBS5EnhIbMu3Au-RV53iIpztzNXkGI56BFB1udQ8Bq_H4".to_string(),
general_purpose::URL_SAFE_NO_PAD.encode(&signature.auth_k)
);
assert!(!signature.auth_t.is_empty());
}
}