use crate::Error;
use base64::prelude::*;
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
use p256::SecretKey;
use p256::ecdsa::SigningKey;
use p256::pkcs8::EncodePrivateKey;
use reqwest::header::HeaderValue;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Display;
use std::ops::Add;
use std::path::Path;
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use url::{Origin, Url};
#[derive(Debug)]
pub struct Vapid {
private_key: VapidKey,
subject: Subject,
token_lifetime: Duration,
cached_tokens: Mutex<HashMap<Origin, VapidAuthorizationHeader>>,
}
impl Vapid {
pub fn new(subject: Subject, private_key: VapidKey) -> Self {
Self::new_with_timeout(subject, private_key, Duration::from_mins(5))
}
pub fn new_with_timeout(
subject: Subject,
private_key: VapidKey,
token_lifetime: Duration,
) -> Self {
if token_lifetime < Duration::from_mins(5) {
panic!("Token lifetime cannot be less than 5 minutes");
}
if token_lifetime > Duration::from_hours(24) {
panic!("Token lifetime cannot exceed 24 hours");
}
Self {
private_key,
subject,
token_lifetime,
cached_tokens: Mutex::new(HashMap::new()),
}
}
pub(crate) fn public_key(&self) -> &p256::ecdsa::VerifyingKey {
self.private_key.0.verifying_key()
}
pub(crate) fn authorization_header(
&self,
origin: &Origin,
) -> Result<VapidAuthorizationHeader, Error> {
let mut cached_tokens = self
.cached_tokens
.lock()
.map_err(|_| Error::FailedToLockTokenCache)?;
let header = cached_tokens
.remove(origin)
.filter(|header| !header.requires_renewal())
.unwrap_or_else(|| {
let (token, expires_at) = self
.generate_token(origin)
.expect("Failed to generate VAPID token");
let key = self.key_parameter();
VapidAuthorizationHeader {
token,
key,
expires_at,
}
});
cached_tokens.insert(origin.clone(), header.clone());
Ok(header)
}
fn generate_token(&self, origin: &Origin) -> Result<(String, SystemTime), Error> {
let header = Header::new(Algorithm::ES256);
let now = SystemTime::now();
let exp = now.add(self.token_lifetime);
let claims = Claims {
aud: origin.unicode_serialization(),
sub: self.subject.to_string(),
exp: exp
.duration_since(UNIX_EPOCH)
.map_err(|_| Error::InvalidExpiryTime)?
.as_secs(),
};
let private_key_der = self
.private_key
.0
.to_pkcs8_der()
.map_err(Error::FailedToWritePem)?;
let encoding_key = EncodingKey::from_ec_der(private_key_der.to_bytes().as_slice());
encode(&header, &claims, &encoding_key)
.map(|token| (token, exp))
.map_err(Error::FailedToGenerateToken)
}
fn key_parameter(&self) -> String {
let verifying_key = self.private_key.0.verifying_key();
let public_key_bytes = verifying_key.to_sec1_bytes();
BASE64_STANDARD_NO_PAD.encode(public_key_bytes)
}
}
#[derive(Debug, Serialize)]
struct Claims {
pub aud: String,
pub sub: String,
pub exp: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct VapidAuthorizationHeader {
token: String,
key: String,
expires_at: SystemTime,
}
impl VapidAuthorizationHeader {
pub fn requires_renewal(&self) -> bool {
const RENEWAL_EARLY_PERIOD: Duration = Duration::from_mins(1);
self.expires_at - RENEWAL_EARLY_PERIOD < SystemTime::now()
}
}
impl From<VapidAuthorizationHeader> for HeaderValue {
fn from(value: VapidAuthorizationHeader) -> Self {
let header_value = format!("vapid t={},k={}", value.token, value.key);
HeaderValue::from_str(&header_value).expect("Failed to create HeaderValue")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Subject(Url);
impl Subject {
pub fn parse(value: &str) -> Result<Self, SubjectError> {
let url = Url::parse(value).map_err(SubjectError::InvalidUrl)?;
Self::try_from_url(url)
}
pub fn try_from_url(url: Url) -> Result<Self, SubjectError> {
if url.scheme() != "mailto" {
return Err(SubjectError::UrlNotMailto);
}
Ok(Subject(url))
}
}
impl TryFrom<&str> for Subject {
type Error = SubjectError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl TryFrom<Url> for Subject {
type Error = SubjectError;
fn try_from(value: Url) -> Result<Self, Self::Error> {
Self::try_from_url(value)
}
}
impl Display for Subject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl AsRef<Url> for Subject {
fn as_ref(&self) -> &Url {
&self.0
}
}
#[derive(Debug)]
pub enum SubjectError {
InvalidUrl(url::ParseError),
UrlNotMailto,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VapidKey(SigningKey);
impl VapidKey {
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, VapidKeyLoadFromFileError> {
let contents =
std::fs::read_to_string(path).map_err(VapidKeyLoadFromFileError::FileReadError)?;
Ok(Self::load_from_pem(&contents)?)
}
pub fn load_from_pem(contents: &str) -> Result<Self, VapidKeyLoadFromPemError> {
let key = SecretKey::from_sec1_pem(contents)
.map_err(VapidKeyLoadFromPemError::InvalidKeyFormat)?;
Ok(Self(key.into()))
}
}
impl AsRef<SigningKey> for VapidKey {
fn as_ref(&self) -> &SigningKey {
&self.0
}
}
impl<T: Into<SigningKey>> From<T> for VapidKey {
fn from(value: T) -> Self {
Self(value.into())
}
}
#[derive(Debug)]
pub enum VapidKeyLoadFromFileError {
FileReadError(std::io::Error),
InvalidKeyFormat(elliptic_curve::Error),
}
impl From<VapidKeyLoadFromPemError> for VapidKeyLoadFromFileError {
fn from(value: VapidKeyLoadFromPemError) -> Self {
match value {
VapidKeyLoadFromPemError::InvalidKeyFormat(e) => Self::InvalidKeyFormat(e),
}
}
}
pub enum VapidKeyLoadFromPemError {
InvalidKeyFormat(elliptic_curve::Error),
}