// SPDX-License-Identifier: AGPL-3.0-or-later use base64::prelude::*; use hmac::{Hmac, KeyInit, Mac}; use p256::PublicKey; use p256::ecdsa::signature::Signer as _; use p256::ecdsa::{Signature, SigningKey}; use p256::elliptic_curve::sec1::ToEncodedPoint as _; use p256::pkcs8::DecodePrivateKey as _; use rand::Rng as _; use ring::aead::{AES_128_GCM, Aad, LessSafeKey, Nonce, UnboundKey}; use ring::rand::SystemRandom; use ring::signature::{RSA_PKCS1_SHA256, RsaKeyPair}; use serde_json::Value; use sha2::Sha256; use thiserror::Error; type HmacSha256 = Hmac; pub const ES256: &str = "ES256"; const SALT_LEN: usize = 16; const TAG_LEN: usize = 16; const RECORD_SIZE_LEN: usize = 4; const KEY_ID_LEN_LEN: usize = 1; const IKM_LEN: usize = 32; const CEK_LEN: usize = 16; const NONCE_LEN: usize = 12; const SCALAR_LEN: usize = 32; const UNCOMPRESSED_POINT_LEN: usize = 65; const UNCOMPRESSED_POINT_TAG: u8 = 0x04; const PADDING_DELIMITER: u8 = 0x02; const EPHEMERAL_KEY_TRIES: usize = 4; #[derive(Debug, Error)] pub enum CryptoError { #[error("subscription key is not base64")] InvalidSubscriptionKey, #[error("subscription public key is not an uncompressed P-256 point")] InvalidPeerKey, #[error("private key is not a P-256 key")] InvalidP256Key, #[error("private key is not an RSA key")] InvalidRsaKey, #[error("no ephemeral P-256 key could be generated")] EphemeralKey, #[error("record size {0} is too small to frame a payload")] RecordSizeTooSmall(usize), #[error("payload does not fit in a {0} byte record")] MaxPadExceeded(usize), #[error("sealing the payload failed")] SealFailed, #[error("signing failed")] SigningFailed, } pub fn decode_subscription_key(value: &str) -> Result, CryptoError> { let trimmed = value.trim().trim_end_matches('='); BASE64_URL_SAFE_NO_PAD .decode(trimmed) .or_else(|_| BASE64_STANDARD_NO_PAD.decode(trimmed)) .map_err(|_| CryptoError::InvalidSubscriptionKey) } pub fn parse_p256_private_scalar(value: &str) -> Result { let raw = decode_subscription_key(value)?; SigningKey::from_slice(&raw).map_err(|_| CryptoError::InvalidP256Key) } pub fn parse_p256_pkcs8_pem(pem: &str) -> Result { let secret = p256::SecretKey::from_pkcs8_pem(&expand_escaped_newlines(pem)) .map_err(|_| CryptoError::InvalidP256Key)?; Ok(SigningKey::from(&secret)) } pub fn parse_rsa_pkcs8_pem(pem: &str) -> Result { let der = pem_to_der(pem)?; RsaKeyPair::from_pkcs8(&der).map_err(|_| CryptoError::InvalidRsaKey) } pub fn es256_jwt(header: &Value, claims: &Value, key: &SigningKey) -> Result { let signing_input = signing_input(header, claims); let signature: Signature = key .try_sign(signing_input.as_bytes()) .map_err(|_| CryptoError::SigningFailed)?; Ok(format!( "{signing_input}.{}", BASE64_URL_SAFE_NO_PAD.encode(signature.to_bytes()) )) } pub fn rs256_jwt(header: &Value, claims: &Value, key: &RsaKeyPair) -> Result { let signing_input = signing_input(header, claims); let mut signature = vec![0u8; key.public().modulus_len()]; key.sign( &RSA_PKCS1_SHA256, &SystemRandom::new(), signing_input.as_bytes(), &mut signature, ) .map_err(|_| CryptoError::SigningFailed)?; Ok(format!( "{signing_input}.{}", BASE64_URL_SAFE_NO_PAD.encode(&signature) )) } pub fn encrypt_aes128gcm( message: &[u8], p256dh: &[u8], auth: &[u8], record_size: usize, ) -> Result, CryptoError> { let mut rng = rand::rng(); let mut salt = [0u8; SALT_LEN]; rng.fill_bytes(&mut salt); let local_secret = ephemeral_secret(&mut rng)?; seal(message, p256dh, auth, &local_secret, salt, record_size) } fn seal( message: &[u8], p256dh: &[u8], auth: &[u8], local_secret: &p256::SecretKey, salt: [u8; SALT_LEN], record_size: usize, ) -> Result, CryptoError> { if p256dh.len() != UNCOMPRESSED_POINT_LEN || p256dh[0] != UNCOMPRESSED_POINT_TAG { return Err(CryptoError::InvalidPeerKey); } let peer_public = PublicKey::from_sec1_bytes(p256dh).map_err(|_| CryptoError::InvalidPeerKey)?; let local_point = local_secret.public_key().to_encoded_point(false); let local_pub = local_point.as_bytes(); let shared = p256::ecdh::diffie_hellman(local_secret.to_nonzero_scalar(), peer_public.as_affine()); let mut info = Vec::with_capacity(b"WebPush: info\0".len() + p256dh.len() + local_pub.len()); info.extend_from_slice(b"WebPush: info\0"); info.extend_from_slice(p256dh); info.extend_from_slice(local_pub); let ikm = hkdf_sha256(shared.raw_secret_bytes(), auth, &info, IKM_LEN); let cek = hkdf_sha256(&ikm, &salt, b"Content-Encoding: aes128gcm\0", CEK_LEN); let nonce = hkdf_sha256(&ikm, &salt, b"Content-Encoding: nonce\0", NONCE_LEN); let header_len = SALT_LEN + RECORD_SIZE_LEN + KEY_ID_LEN_LEN + local_pub.len(); let required = record_size .checked_sub(TAG_LEN) .and_then(|record_len| record_len.checked_sub(header_len)) .ok_or(CryptoError::RecordSizeTooSmall(record_size))?; let record_size_be = u32::try_from(record_size) .map_err(|_| CryptoError::RecordSizeTooSmall(record_size))? .to_be_bytes(); let mut data = Vec::with_capacity(required + TAG_LEN); data.extend_from_slice(message); data.push(PADDING_DELIMITER); if data.len() > required { return Err(CryptoError::MaxPadExceeded(record_size)); } data.resize(required, 0); let key = LessSafeKey::new(UnboundKey::new(&AES_128_GCM, &cek).map_err(|_| CryptoError::SealFailed)?); let nonce = Nonce::try_assume_unique_for_key(&nonce).map_err(|_| CryptoError::SealFailed)?; key.seal_in_place_append_tag(nonce, Aad::empty(), &mut data) .map_err(|_| CryptoError::SealFailed)?; let mut body = Vec::with_capacity(header_len + data.len()); body.extend_from_slice(&salt); body.extend_from_slice(&record_size_be); body.push(u8::try_from(local_pub.len()).map_err(|_| CryptoError::InvalidP256Key)?); body.extend_from_slice(local_pub); body.append(&mut data); Ok(body) } fn ephemeral_secret(rng: &mut impl rand::Rng) -> Result { let mut scalar = [0u8; SCALAR_LEN]; for _ in 0..EPHEMERAL_KEY_TRIES { rng.fill_bytes(&mut scalar); if let Ok(secret) = p256::SecretKey::from_slice(&scalar) { return Ok(secret); } } Err(CryptoError::EphemeralKey) } fn hkdf_sha256(ikm: &[u8], salt: &[u8], info: &[u8], len: usize) -> Vec { let mut extract = HmacSha256::new_from_slice(salt).expect("hmac accepts any key length"); extract.update(ikm); let prk = extract.finalize().into_bytes(); let mut out = Vec::with_capacity(len + IKM_LEN); let mut block = Vec::new(); let mut counter: u8 = 1; while out.len() < len { let mut expand = HmacSha256::new_from_slice(&prk).expect("hmac accepts any key length"); expand.update(&block); expand.update(info); expand.update(&[counter]); block = expand.finalize().into_bytes().to_vec(); out.extend_from_slice(&block); counter = counter.wrapping_add(1); } out.truncate(len); out } fn signing_input(header: &Value, claims: &Value) -> String { format!("{}.{}", encode_segment(header), encode_segment(claims)) } fn encode_segment(value: &Value) -> String { BASE64_URL_SAFE_NO_PAD.encode(serde_json::to_vec(value).expect("a json value serialises")) } fn expand_escaped_newlines(value: &str) -> String { value.replace("\\n", "\n") } fn pem_to_der(pem: &str) -> Result, CryptoError> { let normalized = expand_escaped_newlines(pem); let body: String = normalized .lines() .filter(|line| !line.trim().starts_with("-----")) .flat_map(|line| line.chars().filter(|c| !c.is_ascii_whitespace())) .collect(); BASE64_STANDARD .decode(body) .map_err(|_| CryptoError::InvalidRsaKey) }