use crate::{
common::*,
crypto::{self, Cryptographer, LocalKeyPair, RemotePublicKey},
error::*,
};
use base64::Engine;
use byteorder::{BigEndian, ByteOrder};
pub(crate) const ECE_AESGCM_PAD_SIZE: usize = 2;
const ECE_WEBPUSH_AESGCM_KEYPAIR_LENGTH: usize = 134; const ECE_WEBPUSH_AESGCM_AUTHINFO: &str = "Content-Encoding: auth\0";
const ECE_WEBPUSH_RAW_KEY_LENGTH: usize = 65;
const ECE_WEBPUSH_IKM_LENGTH: usize = 32;
pub struct AesGcmEncryptedBlock {
pub(crate) dh: Vec<u8>,
pub(crate) salt: Vec<u8>,
pub(crate) rs: u32,
pub(crate) ciphertext: Vec<u8>,
}
impl AesGcmEncryptedBlock {
fn aesgcm_rs(rs: u32) -> u32 {
if rs > u32::max_value() - ECE_TAG_LENGTH as u32 {
return 0;
}
rs + ECE_TAG_LENGTH as u32
}
pub fn new(
dh: &[u8],
salt: &[u8],
rs: u32,
ciphertext: Vec<u8>,
) -> Result<AesGcmEncryptedBlock> {
Ok(AesGcmEncryptedBlock {
dh: dh.to_owned(),
salt: salt.to_owned(),
rs: Self::aesgcm_rs(rs),
ciphertext,
})
}
pub fn headers(&self, vapid_public_key: Option<&[u8]>) -> Vec<(&'static str, String)> {
let mut result = Vec::new();
let mut rs = "".to_owned();
let dh = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&self.dh);
let crypto_key = match vapid_public_key {
Some(public_key) => format!(
"dh={}; p256ecdsa={}",
dh,
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(public_key)
),
None => format!("dh={}", dh),
};
result.push(("Crypto-Key", crypto_key));
if self.rs > 0 {
rs = format!(";rs={}", self.rs);
}
result.push((
"Encryption",
format!(
"salt={}{}",
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&self.salt),
rs
),
));
result
}
pub fn body(&self) -> String {
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&self.ciphertext)
}
}
pub(crate) fn encrypt(
local_prv_key: &dyn LocalKeyPair,
remote_pub_key: &dyn RemotePublicKey,
auth_secret: &[u8],
plaintext: &[u8],
mut params: WebPushParams,
) -> Result<AesGcmEncryptedBlock> {
let cryptographer = crypto::holder::get_cryptographer();
if plaintext.is_empty() {
return Err(Error::ZeroPlaintext);
}
let salt = params.take_or_generate_salt(cryptographer)?;
let (key, nonce) = derive_key_and_nonce(
cryptographer,
EceMode::Encrypt,
local_prv_key,
remote_pub_key,
auth_secret,
&salt,
)?;
let pad_length = std::cmp::max(params.pad_length, ECE_AESGCM_PAD_SIZE);
if plaintext.len() + pad_length >= params.rs as usize {
return Err(Error::PlaintextTooLong);
}
let mut padded_plaintext = vec![0; pad_length + plaintext.len()];
BigEndian::write_u16(
&mut padded_plaintext,
(pad_length - ECE_AESGCM_PAD_SIZE) as u16,
);
padded_plaintext[pad_length..].copy_from_slice(plaintext);
let iv = generate_iv_for_record(&nonce, 0);
let cryptographer = crypto::holder::get_cryptographer();
let ciphertext = cryptographer.aes_gcm_128_encrypt(&key, &iv, &padded_plaintext)?;
let raw_local_pub_key = local_prv_key.pub_as_raw()?;
Ok(AesGcmEncryptedBlock {
salt,
dh: raw_local_pub_key,
rs: params.rs,
ciphertext,
})
}
pub(crate) fn decrypt(
local_prv_key: &dyn LocalKeyPair,
auth_secret: &[u8],
block: &AesGcmEncryptedBlock,
) -> Result<Vec<u8>> {
let cryptographer = crypto::holder::get_cryptographer();
let sender_key = cryptographer.import_public_key(&block.dh)?;
let (key, nonce) = derive_key_and_nonce(
cryptographer,
EceMode::Decrypt,
local_prv_key,
&*sender_key,
auth_secret,
block.salt.as_ref(),
)?;
if block.ciphertext.len() - ECE_TAG_LENGTH >= block.rs as usize {
return Err(Error::MultipleRecordsNotSupported);
}
if block.ciphertext.len() <= ECE_TAG_LENGTH + ECE_AESGCM_PAD_SIZE {
return Err(Error::BlockTooShort);
}
let iv = generate_iv_for_record(&nonce, 0);
let padded_plaintext = cryptographer.aes_gcm_128_decrypt(&key, &iv, &block.ciphertext)?;
let num_padding_bytes =
(((padded_plaintext[0] as u16) << 8) | padded_plaintext[1] as u16) as usize;
if num_padding_bytes + 2 >= padded_plaintext.len() {
return Err(Error::DecryptPadding);
}
if padded_plaintext[2..(2 + num_padding_bytes)]
.iter()
.any(|b| *b != 0u8)
{
return Err(Error::DecryptPadding);
}
Ok(padded_plaintext[(2 + num_padding_bytes)..].to_owned())
}
fn derive_key_and_nonce(
cryptographer: &dyn Cryptographer,
ece_mode: EceMode,
local_prv_key: &dyn LocalKeyPair,
remote_pub_key: &dyn RemotePublicKey,
auth_secret: &[u8],
salt: &[u8],
) -> Result<KeyAndNonce> {
if auth_secret.len() != ECE_WEBPUSH_AUTH_SECRET_LENGTH {
return Err(Error::InvalidAuthSecret);
}
if salt.len() != ECE_SALT_LENGTH {
return Err(Error::InvalidSalt);
}
let shared_secret = cryptographer.compute_ecdh_secret(remote_pub_key, local_prv_key)?;
let raw_remote_pub_key = remote_pub_key.as_raw()?;
let raw_local_pub_key = local_prv_key.pub_as_raw()?;
let keypair = match ece_mode {
EceMode::Encrypt => encode_keys(&raw_remote_pub_key, &raw_local_pub_key),
EceMode::Decrypt => encode_keys(&raw_local_pub_key, &raw_remote_pub_key),
}?;
let keyinfo = generate_info("aesgcm", &keypair)?;
let nonceinfo = generate_info("nonce", &keypair)?;
let ikm = cryptographer.hkdf_sha256(
auth_secret,
&shared_secret,
ECE_WEBPUSH_AESGCM_AUTHINFO.as_bytes(),
ECE_WEBPUSH_IKM_LENGTH,
)?;
let key = cryptographer.hkdf_sha256(salt, &ikm, &keyinfo, ECE_AES_KEY_LENGTH)?;
let nonce = cryptographer.hkdf_sha256(salt, &ikm, &nonceinfo, ECE_NONCE_LENGTH)?;
Ok((key, nonce))
}
fn encode_keys(raw_key1: &[u8], raw_key2: &[u8]) -> Result<Vec<u8>> {
let mut combined = vec![0u8; ECE_WEBPUSH_AESGCM_KEYPAIR_LENGTH];
if raw_key1.len() > ECE_WEBPUSH_RAW_KEY_LENGTH || raw_key2.len() > ECE_WEBPUSH_RAW_KEY_LENGTH {
return Err(Error::InvalidKeyLength);
}
combined[0] = 0;
combined[1] = 65;
combined[2..67].copy_from_slice(raw_key1);
combined[67] = 0;
combined[68] = 65;
combined[69..].copy_from_slice(raw_key2);
Ok(combined)
}
fn generate_info(encoding: &str, keypair: &[u8]) -> Result<Vec<u8>> {
let info_str = format!("Content-Encoding: {}\0P-256\0", encoding);
let offset = info_str.len();
let mut info = vec![0u8; offset + keypair.len()];
info[0..offset].copy_from_slice(info_str.as_bytes());
info[offset..offset + ECE_WEBPUSH_AESGCM_KEYPAIR_LENGTH].copy_from_slice(keypair);
Ok(info)
}