diff --git a/src/aes128gcm.rs b/src/aes128gcm.rs index c833a98..4dc4388 100644 --- a/src/aes128gcm.rs +++ b/src/aes128gcm.rs @@ -2,18 +2,23 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +//! Web Push encryption using the AES128GCM encoding scheme ([RFC8591](https://tools.ietf.org/html/rfc8291)). +//! +//! This module is meant for advanced use. For simple encryption/decryption, use the crate's top-level +//! [`encrypt`](crate::encrypt) and [`decrypt`](crate::decrypt) functions. + use crate::{ common::*, crypto::{self, LocalKeyPair, RemotePublicKey}, error::*, + Cryptographer, }; use byteorder::{BigEndian, ByteOrder}; +// Each record has a 16 byte authentication tag and 1 padding delimiter byte. +// Thus, a record size of less than 18 could never store any plaintext. const ECE_AES128GCM_MIN_RS: u32 = 18; const ECE_AES128GCM_HEADER_LENGTH: usize = 21; -// The max AES128GCM Key ID Length is 255 octets. We use far less of that because we use -// the "key_id" to store the exchanged public key since we don't cache the key_ids. -// Code fails if the key_id is not a public key length field. pub(crate) const ECE_AES128GCM_PAD_SIZE: usize = 1; const ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX: &str = "WebPush: info\0"; @@ -23,174 +28,393 @@ const ECE_WEBPUSH_IKM_LENGTH: usize = 32; const ECE_AES128GCM_KEY_INFO: &str = "Content-Encoding: aes128gcm\0"; const ECE_AES128GCM_NONCE_INFO: &str = "Content-Encoding: nonce\0"; -// TODO: When done, remove the aes128gcm prefixes and the EC_ ones. -// As for now it makes it easier to Ctrl + F into ecec :) - -/// Web Push encryption structure for the AES128GCM encoding scheme ([RFC8591](https://tools.ietf.org/html/rfc8291)) +/// Encrypts a Web Push message using the "aes128gcm" scheme, with an explicit sender key. /// -/// This structure is meant for advanced use. For simple encryption/decryption, use the top-level [`encrypt`](crate::encrypt) and [`decrypt`](crate::decrypt) functions. -pub(crate) struct Aes128GcmEceWebPush; -impl Aes128GcmEceWebPush { - /// Encrypts a Web Push message using the "aes128gcm" scheme, with an explicit - /// sender key. The sender key can be reused. - pub fn encrypt_with_keys( - local_prv_key: &dyn LocalKeyPair, - remote_pub_key: &dyn RemotePublicKey, - auth_secret: &[u8], - plaintext: &[u8], - params: WebPushParams, - ) -> Result> { - let cryptographer = crypto::holder::get_cryptographer(); - let salt = match params.salt { - Some(salt) => salt, - None => { - let mut salt = [0u8; ECE_SALT_LENGTH]; - cryptographer.random_bytes(&mut salt)?; - salt.to_vec() - } - }; - let mut header = vec![0u8; ECE_AES128GCM_HEADER_LENGTH + ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; - header[0..ECE_SALT_LENGTH].copy_from_slice(&salt); - BigEndian::write_u32(&mut header[ECE_SALT_LENGTH..], params.rs); - header[ECE_SALT_LENGTH + 4] = ECE_WEBPUSH_PUBLIC_KEY_LENGTH as u8; - let raw_local_pub_key = local_prv_key.pub_as_raw()?; - header[ECE_AES128GCM_HEADER_LENGTH - ..ECE_AES128GCM_HEADER_LENGTH + ECE_WEBPUSH_PUBLIC_KEY_LENGTH] - .copy_from_slice(&raw_local_pub_key); - let mut ciphertext = Self::common_encrypt( - local_prv_key, - remote_pub_key, - auth_secret, - &salt, +/// It is the caller's responsibility to ensure that this function is used correctly, +/// where "correctly" means important cryptographic details like: +/// +/// * use a new ephemeral local keypair for each encryption +/// * use a randomly-generated salt +/// +/// In general-purpose AES128GM ECE, the "keyid" field in the header may be up to 255 octects +/// and provides a string that allows the application to find the right key material in some +/// application-defined way. We only currently support the specific scheme used by WebPush, where +/// the "keyid" is an ephemeral ECDH public key and always has a fixed length. +/// +pub(crate) fn encrypt( + local_prv_key: &dyn LocalKeyPair, + remote_pub_key: &dyn RemotePublicKey, + auth_secret: &[u8], + plaintext: &[u8], + mut params: WebPushParams, +) -> Result> { + 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, + )?; + + // Encode the ephemeral public key in the "kid" header field. + let keyid = local_prv_key.pub_as_raw()?; + if keyid.len() != ECE_WEBPUSH_PUBLIC_KEY_LENGTH { + return Err(Error::InvalidKeyLength); + } + + let header = Header { + salt: &salt, + rs: params.rs, + keyid: &keyid, + }; + + // We always add at least one padding byte, for the delimiter. + let padding = std::cmp::max(params.pad_length, ECE_AES128GCM_PAD_SIZE); + + // For now, everything must fit in a single record. + // Calling code will ensure that this is the case. + if params.rs < ECE_AES128GCM_MIN_RS { + return Err(Error::InvalidRecordSize); + } + if plaintext.len() + padding + ECE_TAG_LENGTH > params.rs as usize { + dbg!(format!( + "Message content too long for a single record (rs={}, plaintext={}, padding={})", params.rs, - params.pad_length, - plaintext, + plaintext.len(), + padding + )); + return Err(Error::MultipleRecordsNotSupported); + } + let record = PlaintextRecord { + plaintext, + padding, + sequence_number: 0, + is_final: true, + }; + + let mut ciphertext = vec![0; header.encoded_size() + record.encrypted_size()]; + + header.write_into(&mut ciphertext); + record.encrypt_into( + cryptographer, + &key, + &nonce, + &mut ciphertext[header.encoded_size()..], + )?; + + Ok(ciphertext) +} + +/// Decrypts a Web Push message encrypted using the "aes128gcm" scheme. +/// +pub(crate) fn decrypt( + local_prv_key: &dyn LocalKeyPair, + auth_secret: &[u8], + ciphertext: &[u8], +) -> Result> { + let cryptographer = crypto::holder::get_cryptographer(); + if ciphertext.is_empty() { + return Err(Error::ZeroCiphertext); + } + + // Buffer into which to write the output. + // This will avoid any reallocations because plaintext will always be smaller than ciphertext. + // We could calculate a tighter bound if memory usage is an issue in future. + let mut output = Vec::::with_capacity(ciphertext.len()); + + let header = Header::read_from(ciphertext)?; + if ciphertext.len() == header.encoded_size() { + return Err(Error::ZeroCiphertext); + } + + // The `keyid` field must contain the serialized ephemeral public key. + if header.keyid.len() != ECE_WEBPUSH_PUBLIC_KEY_LENGTH { + return Err(Error::InvalidKeyLength); + } + let remote_pub_key = cryptographer.import_public_key(&header.keyid)?; + + let (key, nonce) = derive_key_and_nonce( + cryptographer, + EceMode::DECRYPT, + local_prv_key, + &*remote_pub_key, + auth_secret, + header.salt, + )?; + + // We'll re-use this buffer as scratch space for decrypting each record. + // This is nice for memory usage, but actually the main motivation is to have the decryption + // output a `PlaintextRecord` struct, which holds a borrowed slice of plaintext. + // TODO: pre-allocate the final output buffer, and let `decrypt_from` write directly into it. + let mut plaintext_buffer = vec![0u8; (header.rs as usize) - ECE_TAG_LENGTH]; + + let records = ciphertext[header.encoded_size()..].chunks(header.rs as usize); + + let mut seen_final_record = false; + for (sequence_number, ciphertext) in records.enumerate() { + // The record marked as final must actually be the final record. + // We check this inline in the loop because the loop consumes ownership of `records`, + // which means we can't do a separate "did we consume all the records?" check after loop termination. + // There's probably a way, but I didn't find it. + if seen_final_record { + return Err(Error::DecryptPadding); + } + let record = PlaintextRecord::decrypt_from( + cryptographer, + &key, + &nonce, + sequence_number, + ciphertext, + plaintext_buffer.as_mut_slice(), )?; - // TODO: Not efficient and probably allocates more, - // we should allocate the buffer upfront if possible. - header.append(&mut ciphertext); - Ok(header) - } - - /// Decrypts a Web Push message encrypted using the "aes128gcm" scheme. - pub fn decrypt( - local_prv_key: &dyn LocalKeyPair, - auth_secret: &[u8], - payload: &[u8], - ) -> Result> { - if payload.len() < ECE_AES128GCM_HEADER_LENGTH { + if record.is_final { + seen_final_record = true; + } + output.extend(record.plaintext) + } + if !seen_final_record { + return Err(Error::DecryptTruncated); + } + + Ok(output) +} + +/// Encapsulates header data for aes128gcm encryption scheme. +/// +/// The header is always written at the start of the encrypted data, like so: +/// +/// ```txt +/// +-----------+--------+-----------+---------------+ +/// | salt (16) | rs (4) | idlen (1) | keyid (idlen) | +/// +-----------+--------+-----------+---------------+ +/// ``` +/// +/// To avoid copying data when parsing, this struct stores references to its +/// field, borrowed from the underlying data. +/// +pub(crate) struct Header<'a> { + salt: &'a [u8], + rs: u32, + keyid: &'a [u8], +} + +impl<'a> Header<'a> { + /// Read a `Header` from the data at the start of the given input buffer. + /// + fn read_from(input: &'a [u8]) -> Result> { + if input.len() < ECE_AES128GCM_HEADER_LENGTH { return Err(Error::HeaderTooShort); } - let key_id_len = payload[ECE_SALT_LENGTH + 4] as usize; - if payload.len() < ECE_AES128GCM_HEADER_LENGTH + key_id_len { + let keyid_len = input[ECE_AES128GCM_HEADER_LENGTH - 1] as usize; + if input.len() < ECE_AES128GCM_HEADER_LENGTH + keyid_len { return Err(Error::HeaderTooShort); } - let rs = BigEndian::read_u32(&payload[ECE_SALT_LENGTH..]); + let salt = &input[0..ECE_SALT_LENGTH]; + let rs = BigEndian::read_u32(&input[ECE_SALT_LENGTH..]); if rs < ECE_AES128GCM_MIN_RS { return Err(Error::InvalidRecordSize); } + let keyid = &input[ECE_AES128GCM_HEADER_LENGTH..ECE_AES128GCM_HEADER_LENGTH + keyid_len]; - let salt = &payload[0..ECE_SALT_LENGTH]; - if key_id_len != ECE_WEBPUSH_PUBLIC_KEY_LENGTH { - return Err(Error::InvalidKeyLength); - } - let key_id_pos = ECE_AES128GCM_HEADER_LENGTH; - let key_id = &payload[key_id_pos..key_id_pos + key_id_len]; - - let ciphertext_start = ECE_AES128GCM_HEADER_LENGTH + key_id_len; - if payload.len() == ciphertext_start { - return Err(Error::ZeroCiphertext); - } - let ciphertext = &payload[ciphertext_start..]; - let cryptographer = crypto::holder::get_cryptographer(); - let key = cryptographer.import_public_key(key_id)?; - Self::common_decrypt(local_prv_key, &*key, auth_secret, salt, rs, ciphertext) + Ok(Header { salt, rs, keyid }) } -} -impl EceWebPush for Aes128GcmEceWebPush { - /// Always returns false because "aes128gcm" uses - /// a padding scheme that doesn't need a trailer. - fn needs_trailer(_: u32, _: usize) -> bool { - false + /// Write this `Header` at the start of the given output buffer. + /// + /// This assumes that the buffer has sufficient space for the data, and will + /// panic (via Rust's runtime safety checks) if it does not. + /// + pub fn write_into(&self, output: &mut [u8]) { + output[0..ECE_SALT_LENGTH].copy_from_slice(self.salt); + BigEndian::write_u32(&mut output[ECE_SALT_LENGTH..], self.rs); + output[ECE_AES128GCM_HEADER_LENGTH - 1] = self.keyid.len() as u8; + output[ECE_AES128GCM_HEADER_LENGTH..ECE_AES128GCM_HEADER_LENGTH + self.keyid.len()] + .copy_from_slice(self.keyid); } - fn allow_multiple_records() -> bool { - true + /// Get the size occupied by this header when written to the encrypted data. + /// + pub fn encoded_size(&self) -> usize { + ECE_AES128GCM_HEADER_LENGTH + self.keyid.len() } +} + +/// Struct representing an individual plaintext record. +/// +/// The encryption process splits up the input plaintext to fixed-size records, +/// each of which is encrypted independently. This struct encapsulates all the +/// data about a particular record. This diagram from the RFC may help you to +/// visualize how this data gets encrypted: +/// +/// ```txt +/// +-----------+ content +/// | data | any length up to rs-17 octets +/// +-----------+ +/// | +/// v +/// +-----------+-----+ add a delimiter octet (0x01 or 0x02) +/// | data | pad | then 0x00-valued octets to rs-16 +/// +-----------+-----+ (or less on the last record) +/// | +/// v +/// +--------------------+ encrypt with AEAD_AES_128_GCM; +/// | ciphertext | final size is rs; +/// +--------------------+ the last record can be smaller +/// ``` +/// +/// To avoid copying data when chunking a plaintext into multiple records, this struct +/// stores a reference to its portion of the plaintext, borrowed from the underlying data. +/// +struct PlaintextRecord<'a> { + /// The plaintext, to go at the start of the record. + plaintext: &'a [u8], + /// The amount of padding to be added to the end of the record. + /// Always >= 1 in practice, because the first byte of padding is a delimiter. + padding: usize, + /// The position of this record in the overall sequence of records for some data. + sequence_number: usize, + /// Whether this is the final record in the data. + is_final: bool, +} - fn pad_size() -> usize { - ECE_AES128GCM_PAD_SIZE +impl<'a> PlaintextRecord<'a> { + /// Decrypt a single record from the given ciphertext, into its corresponding plaintext. + /// + /// The caller must provide a buffer with sufficient space to store the decrypted plaintext, + /// and this method will panic (via Rust's runtime safety checks) if there is insufficient + /// space available. + /// + pub(crate) fn decrypt_from( + cryptographer: &dyn Cryptographer, + key: &[u8], + nonce: &[u8], + sequence_number: usize, + ciphertext: &[u8], + plaintext_buffer: &'a mut [u8], + ) -> Result { + if ciphertext.len() <= ECE_TAG_LENGTH { + return Err(Error::BlockTooShort); + } + let iv = generate_iv_for_record(&nonce, sequence_number); + // It would be nice if we could decrypt directly into `plaintext_buffer` here, + // but that will require some refactoring in the crypto backend. + let padded_plaintext = cryptographer.aes_gcm_128_decrypt(&key, &iv, &ciphertext)?; + // Scan backwards for the first non-zero byte from the end of the data, which delimits the padding. + let padding_delimiter_idx = padded_plaintext + .iter() + .rposition(|&b| b != 0u8) + .ok_or(Error::DecryptPadding)?; + // The padding delimiter tells is whether this is the final record. + let is_final = match padded_plaintext[padding_delimiter_idx] { + 1 => false, + 2 => true, + _ => return Err(Error::DecryptPadding), + }; + // Everything before the padding delimiter is the plaintext. + plaintext_buffer[0..padding_delimiter_idx] + .copy_from_slice(&padded_plaintext[0..padding_delimiter_idx]); + // That's it! + Ok(PlaintextRecord { + plaintext: &plaintext_buffer[0..padding_delimiter_idx], + padding: padded_plaintext.len() - padding_delimiter_idx, + sequence_number, + is_final, + }) } - fn min_block_pad_length(pad_len: usize, max_block_len: usize) -> usize { - ece_min_block_pad_length(pad_len, max_block_len) + /// Encrypt this record into the given output buffer. + /// + /// The caller must provide a buffer with sufficient space to store the encrypted data, + /// and this method will panic (via Rust's runtime safety checks) if there is insufficient + /// space available. + /// + pub(crate) fn encrypt_into( + &self, + cryptographer: &dyn Cryptographer, + key: &[u8], + nonce: &[u8], + output: &mut [u8], + ) -> Result { + // We're going to use the output buffer as scratch space for padding the plaintext. + // Since the ciphertext is always longer than the plaintext, there will definitely + // be enough space. + let padded_plaintext_len = self.plaintext.len() + self.padding; + // Plaintext goes at the start of the buffer. + output[0..self.plaintext.len()].copy_from_slice(self.plaintext); + // The first byte of padding is always the delimiter. + assert!(self.padding >= 1); + output[self.plaintext.len()] = if self.is_final { 2 } else { 1 }; + // And the rest of the padding is all zeroes. + output[self.plaintext.len() + 1..padded_plaintext_len].fill(0); + // Now we can encrypt! + let iv = generate_iv_for_record(&nonce, self.sequence_number); + let ciphertext = + cryptographer.aes_gcm_128_encrypt(&key, &iv, &output[0..padded_plaintext_len])?; + output[0..ciphertext.len()].copy_from_slice(&ciphertext); + Ok(ciphertext.len()) } - fn pad(plaintext: &[u8], block_pad_len: usize, last_record: bool) -> Result> { - let mut block = Vec::with_capacity(plaintext.len() + 1 /* delimiter */ + block_pad_len); - block.extend_from_slice(plaintext); - block.push(if last_record { 2 } else { 1 }); - let padding = vec![0u8; block_pad_len]; - block.extend(padding); - Ok(block) + pub(crate) fn encrypted_size(&self) -> usize { + self.plaintext.len() + self.padding + ECE_TAG_LENGTH } +} - fn unpad(block: &[u8], last_record: bool) -> Result<&[u8]> { - let pos = match block.iter().rposition(|&b| b != 0) { - Some(pos) => pos, - None => return Err(Error::ZeroCiphertext), - }; - let expected_delim = if last_record { 2 } else { 1 }; - if block[pos] != expected_delim { - return Err(Error::DecryptPadding); - } - Ok(&block[..pos]) - } - - /// Derives the "aes128gcm" decryption key and nonce given the receiver private - /// key, sender public key, authentication secret, and sender salt. - fn derive_key_and_nonce( - ece_mode: EceMode, - local_prv_key: &dyn LocalKeyPair, - remote_pub_key: &dyn RemotePublicKey, - auth_secret: &[u8], - salt: &[u8], - ) -> Result { - let cryptographer = crypto::holder::get_cryptographer(); - 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()?; - - // The new "aes128gcm" scheme includes the sender and receiver public keys in - // the info string when deriving the Web Push IKM. - let ikm_info = match ece_mode { - EceMode::ENCRYPT => generate_info(&raw_remote_pub_key, &raw_local_pub_key), - EceMode::DECRYPT => generate_info(&raw_local_pub_key, &raw_remote_pub_key), - }?; - let cryptographer = crypto::holder::get_cryptographer(); - let ikm = cryptographer.hkdf_sha256( - auth_secret, - &shared_secret, - &ikm_info, - ECE_WEBPUSH_IKM_LENGTH, - )?; - let key = cryptographer.hkdf_sha256( - salt, - &ikm, - ECE_AES128GCM_KEY_INFO.as_bytes(), - ECE_AES_KEY_LENGTH, - )?; - let nonce = cryptographer.hkdf_sha256( - salt, - &ikm, - ECE_AES128GCM_NONCE_INFO.as_bytes(), - ECE_NONCE_LENGTH, - )?; - Ok((key, nonce)) +/// Derives the "aes128gcm" decryption key and nonce given the receiver private +/// key, sender public key, authentication secret, and sender salt. +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 { + 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()?; + + // The "aes128gcm" scheme includes the sender and receiver public keys in + // the info string when deriving the Web Push IKM. + let ikm_info = match ece_mode { + EceMode::ENCRYPT => generate_info(&raw_remote_pub_key, &raw_local_pub_key), + EceMode::DECRYPT => generate_info(&raw_local_pub_key, &raw_remote_pub_key), + }?; + let ikm = cryptographer.hkdf_sha256( + auth_secret, + &shared_secret, + &ikm_info, + ECE_WEBPUSH_IKM_LENGTH, + )?; + let key = cryptographer.hkdf_sha256( + salt, + &ikm, + ECE_AES128GCM_KEY_INFO.as_bytes(), + ECE_AES_KEY_LENGTH, + )?; + let nonce = cryptographer.hkdf_sha256( + salt, + &ikm, + ECE_AES128GCM_NONCE_INFO.as_bytes(), + ECE_NONCE_LENGTH, + )?; + Ok((key, nonce)) } // The "aes128gcm" IKM info string is "WebPush: info\0", followed by the diff --git a/src/aesgcm.rs b/src/aesgcm.rs index 752b5a0..16b6fa6 100644 --- a/src/aesgcm.rs +++ b/src/aesgcm.rs @@ -9,11 +9,19 @@ * * */ +//! Web Push encryption structure for the legacy AESGCM encoding scheme +//! ([Web Push Encryption Draft 4](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04)) +//! +//! This module is meant for advanced use. For simple encryption/decryption, use the top-level +//! [`encrypt_aesgcm`](crate::legacy::encrypt_aesgcm) and [`decrypt_aesgcm`](crate::legacy::decrypt_aesgcm) +//! functions. + use crate::{ common::*, - crypto::{self, LocalKeyPair, RemotePublicKey}, + crypto::{self, Cryptographer, LocalKeyPair, RemotePublicKey}, error::*, }; +use byteorder::{BigEndian, ByteOrder}; pub(crate) const ECE_AESGCM_PAD_SIZE: usize = 2; @@ -24,6 +32,12 @@ 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; +/// Struct representing the result of encrypting with the "aesgcm" scheme. +/// +/// Since the "aesgcm" scheme needs to represent some data in HTTP headers and +/// other data in the encoded body, we need to represent it with a structure +/// rather than just with raw bytes. +/// pub struct AesGcmEncryptedBlock { pub(crate) dh: Vec, pub(crate) salt: Vec, @@ -91,137 +105,164 @@ impl AesGcmEncryptedBlock { } } -/// Web Push encryption structure for the legacy AESGCM encoding scheme -/// ([Web Push Encryption Draft 4](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04)) +/// Encrypts a Web Push message using the "aesgcm" scheme, with an explicit sender key. /// -/// This structure is meant for advanced use. For simple encryption/decryption, use the top-level -/// [`encrypt_aesgcm`](crate::legacy::encrypt_aesgcm) and [`decrypt_aesgcm`](crate::legacy::decrypt_aesgcm) -/// functions. -pub(crate) struct AesGcmEceWebPush; - -impl AesGcmEceWebPush { - /// Encrypts a Web Push message using the "aesgcm" scheme, with an explicit - /// sender key. The sender key can be reused. - pub fn encrypt_with_keys( - local_prv_key: &dyn LocalKeyPair, - remote_pub_key: &dyn RemotePublicKey, - auth_secret: &[u8], - plaintext: &[u8], - params: WebPushParams, - ) -> Result { - let cryptographer = crypto::holder::get_cryptographer(); - let salt = if let Some(salt) = params.salt { - salt - } else { - let mut salt = [0u8; ECE_SALT_LENGTH]; - cryptographer.random_bytes(&mut salt)?; - salt.to_vec() - }; - let raw_local_pub_key = local_prv_key.pub_as_raw()?; - let ciphertext = Self::common_encrypt( - local_prv_key, - remote_pub_key, - auth_secret, - &salt, - params.rs, - params.pad_length, - plaintext, - )?; - Ok(AesGcmEncryptedBlock { - salt, - dh: raw_local_pub_key, - rs: params.rs, - ciphertext, - }) +/// It is the caller's responsibility to ensure that this function is used correctly, +/// where "correctly" means important cryptographic details like: +/// +/// * use a new ephemeral local keypair for each encryption +/// * use a randomly-generated salt +/// +pub(crate) fn encrypt( + local_prv_key: &dyn LocalKeyPair, + remote_pub_key: &dyn RemotePublicKey, + auth_secret: &[u8], + plaintext: &[u8], + mut params: WebPushParams, +) -> Result { + // Check parameters, including doing the random salt thing. + // Probably could move into the WebPushParams struct? + let cryptographer = crypto::holder::get_cryptographer(); + + if plaintext.is_empty() { + return Err(Error::ZeroPlaintext); } - /// Decrypts a Web Push message encrypted using the "aesgcm" scheme. - pub fn decrypt( - local_prv_key: &dyn LocalKeyPair, - auth_secret: &[u8], - block: &AesGcmEncryptedBlock, - ) -> Result> { - let cryptographer = crypto::holder::get_cryptographer(); - let sender_key = cryptographer.import_public_key(&block.dh)?; - Self::common_decrypt( - local_prv_key, - &*sender_key, - auth_secret, - &block.salt, - block.rs, - &block.ciphertext, - ) + 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, + )?; + + // Each record must contain at least some padding, for recording the padding size. + let pad_length = std::cmp::max(params.pad_length, ECE_AESGCM_PAD_SIZE); + + // For this legacy scheme, we only support encrypting a single record. + // The record size in this scheme is the size of the plaintext plus padding, + // and the scheme requires that the final block be of a size less than `rs`. + if plaintext.len() + pad_length >= params.rs as usize { + return Err(Error::PlaintextTooLong); } + + // Pad out the plaintext. + // The first two bytes of padding are big-endian padding length, + // followed by the rest of the padding as zero bytes, + // followed by the plaintext. + 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); + + // Now we can encrypt it. + 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)?; + + // Encapsulate the crypto parameters in headers to return to caller. + let raw_local_pub_key = local_prv_key.pub_as_raw()?; + Ok(AesGcmEncryptedBlock { + salt, + dh: raw_local_pub_key, + rs: params.rs, + ciphertext, + }) } -impl EceWebPush for AesGcmEceWebPush { - fn needs_trailer(rs: u32, ciphertextlen: usize) -> bool { - ciphertextlen as u32 % rs == 0 - } +/// Decrypts a Web Push message encrypted using the "aesgcm" scheme. +/// +pub(crate) fn decrypt( + local_prv_key: &dyn LocalKeyPair, + auth_secret: &[u8], + block: &AesGcmEncryptedBlock, +) -> Result> { + let cryptographer = crypto::holder::get_cryptographer(); - /// Don't allow multiple records for this legacy scheme. - fn allow_multiple_records() -> bool { - false - } + let sender_key = cryptographer.import_public_key(&block.dh)?; - fn pad_size() -> usize { - ECE_AESGCM_PAD_SIZE - } + let (key, nonce) = derive_key_and_nonce( + cryptographer, + EceMode::DECRYPT, + local_prv_key, + &*sender_key, + auth_secret, + block.salt.as_ref(), + )?; - fn min_block_pad_length(pad_len: usize, max_block_len: usize) -> usize { - ece_min_block_pad_length(pad_len, max_block_len) + // We only support receipt of a single record for this legacy scheme. + // Recall that the final block must be strictly less than `rs` in size. + if block.ciphertext.len() - ECE_TAG_LENGTH >= block.rs as usize { + return Err(Error::MultipleRecordsNotSupported); } - - fn pad(plaintext: &[u8], _: usize, _: bool) -> Result> { - let plen = plaintext.len(); - let mut block = vec![0; plen + ECE_AESGCM_PAD_SIZE]; - block[2..].copy_from_slice(plaintext); - Ok(block) + if block.ciphertext.len() <= ECE_TAG_LENGTH + ECE_AESGCM_PAD_SIZE { + return Err(Error::BlockTooShort); } - fn unpad(block: &[u8], _: bool) -> Result<&[u8]> { - let padding_size = (((block[0] as u16) << 8) | block[1] as u16) as usize; - if padding_size >= block.len() - 2 { - return Err(Error::DecryptPadding); - } - if block[2..(2 + padding_size)].iter().any(|b| *b != 0u8) { - return Err(Error::DecryptPadding); - } - Ok(&block[(2 + padding_size)..]) + let iv = generate_iv_for_record(&nonce, 0); + let padded_plaintext = cryptographer.aes_gcm_128_decrypt(&key, &iv, &block.ciphertext)?; + + // The first two bytes are a big-endian u16 padding size, + // then that many zero bytes, + // then the plaintext. + 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); } - /// Derives the "aesgcm" decryption key and nonce given the receiver private - /// key, sender public key, authentication secret, and sender salt. - fn derive_key_and_nonce( - ece_mode: EceMode, - local_prv_key: &dyn LocalKeyPair, - remote_pub_key: &dyn RemotePublicKey, - auth_secret: &[u8], - salt: &[u8], - ) -> Result { - let cryptographer = crypto::holder::get_cryptographer(); - 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)) + Ok(padded_plaintext[(2 + num_padding_bytes)..].to_owned()) +} + +/// Derives the "aesgcm" decryption key and nonce given the receiver private +/// key, sender public key, authentication secret, and sender salt. +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 { + 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)) } +// Encode the input keys for inclusion in key-derivation info string. fn encode_keys(raw_key1: &[u8], raw_key2: &[u8]) -> Result> { let mut combined = vec![0u8; ECE_WEBPUSH_AESGCM_KEYPAIR_LENGTH]; diff --git a/src/common.rs b/src/common.rs index 72ac596..056e6ea 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2,18 +2,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::{ - crypto::{self, LocalKeyPair, RemotePublicKey}, - error::*, -}; +//! This module implements the parts of ECE that are currently shared by all +//! supported schemes, such as the actual AES-GCM encryption of a single record. +//! It can't be used in isolation; you must instead provide a concrete instantiation +//! of an ECE encryption scheme by implementing the `EncryptionScheme` trait. + +use crate::{crypto::Cryptographer, error::*}; use byteorder::{BigEndian, ByteOrder}; -use std::cmp::min; -// From keys.h: pub(crate) const ECE_AES_KEY_LENGTH: usize = 16; pub(crate) const ECE_NONCE_LENGTH: usize = 12; - -// From ece.h: pub(crate) const ECE_SALT_LENGTH: usize = 16; pub(crate) const ECE_TAG_LENGTH: usize = 16; pub(crate) const ECE_WEBPUSH_PUBLIC_KEY_LENGTH: usize = 65; @@ -21,16 +19,45 @@ pub(crate) const ECE_WEBPUSH_AUTH_SECRET_LENGTH: usize = 16; pub(crate) const ECE_WEBPUSH_DEFAULT_RS: u32 = 4096; pub(crate) const ECE_WEBPUSH_DEFAULT_PADDING_BLOCK_SIZE: usize = 128; -// TODO: Make it nicer to use with a builder pattern. +/// Parameters that control the details of the encryption process. +/// +/// These are the various configuration knobs that could potentially be +/// tweaked when encrypting a given piece of data, packaged together +/// in a struct for convenience. +/// pub(crate) struct WebPushParams { + /// The record size, for chunking the plaintext into multiple records. pub rs: u32, + /// The total amount of padding to add to the plaintext before encryption. pub pad_length: usize, + /// The salt to use when deriving keys. + /// The recommended and default value is `None`, which causes a new random + /// salt to be used for every encryption. Specifying a specific salt may + /// be useful for testing purposes. pub salt: Option>, } +impl WebPushParams { + /// Convenience method for getting an appropriate salt value. + /// + /// If we have a pre-configured salt then it is returned, transferring ownership + /// to ensure it is only used once. If we do not have a pre-configured salt then + /// a new random one is generated. + pub fn take_or_generate_salt(&mut self, cryptographer: &dyn Cryptographer) -> Result> { + Ok(match self.salt.take() { + Some(salt) => salt, + None => { + let mut salt = [0u8; ECE_SALT_LENGTH]; + cryptographer.random_bytes(&mut salt)?; + salt.to_vec() + } + }) + } +} + impl Default for WebPushParams { fn default() -> Self { - // Random salt, record size = 4096 and padding length = 0. + // Random salt, no padding, record size = 4096. Self { rs: ECE_WEBPUSH_DEFAULT_RS, pad_length: 0, @@ -63,210 +90,25 @@ impl WebPushParams { } } +/// Flag to indicate whether we're encrypting or decrypting. +/// Used when deriving keys. +/// pub(crate) enum EceMode { ENCRYPT, DECRYPT, } +/// Convenience tuple for "key" and "nonce" pair. +/// These are always derived as a pair. +/// pub(crate) type KeyAndNonce = (Vec, Vec); -pub(crate) trait EceWebPush { - fn common_encrypt( - local_prv_key: &dyn LocalKeyPair, - remote_pub_key: &dyn RemotePublicKey, - auth_secret: &[u8], - salt: &[u8], - rs: u32, - pad_len: usize, - plaintext: &[u8], - ) -> Result> { - if auth_secret.len() != ECE_WEBPUSH_AUTH_SECRET_LENGTH { - return Err(Error::InvalidAuthSecret); - } - if salt.len() != ECE_SALT_LENGTH { - return Err(Error::InvalidSalt); - } - if plaintext.is_empty() { - return Err(Error::ZeroPlaintext); - } - let (key, nonce) = Self::derive_key_and_nonce( - EceMode::ENCRYPT, - local_prv_key, - remote_pub_key, - auth_secret, - salt, - )?; - let overhead = (Self::pad_size() + ECE_TAG_LENGTH) as u32; - // The maximum amount of plaintext and padding that will fit into a full - // block. The last block can be smaller. - assert!(rs > overhead); - let max_block_len = (rs - overhead) as usize; - - // TODO: We should at least try to guess the capacity beforehand by - // re-implementing ece_ciphertext_max_length. - let mut ciphertext = Vec::with_capacity(plaintext.len()); - - // The offset at which to start reading the plaintext. - let mut plaintext_start = 0; - let mut pad_len = pad_len; - let mut last_record = false; - let mut counter = 0; - while !last_record { - let block_pad_len = Self::min_block_pad_length(pad_len, max_block_len); - assert!(block_pad_len <= pad_len); - pad_len -= block_pad_len; - - // Fill the rest of the block with plaintext. - assert!(block_pad_len <= max_block_len); - let max_block_plaintext_len = max_block_len - block_pad_len; - let plaintext_end = min(plaintext_start + max_block_plaintext_len, plaintext.len()); - - // The length of the plaintext. - assert!(plaintext_end >= plaintext_start); - let block_plaintext_len = plaintext_end - plaintext_start; - - // The length of the plaintext and padding. This should never overflow - // because `max_block_plaintext_len` accounts for `block_pad_len`. - assert!(block_plaintext_len <= max_block_plaintext_len); - let block_len = block_plaintext_len + block_pad_len; - - // The length of the full encrypted record, including the plaintext, - // padding, padding delimiter, and auth tag. This should never overflow - // because `max_block_len` accounts for `overhead`. - assert!(block_len <= max_block_len); - let record_len = block_len + overhead as usize; - - let plaintext_exhausted = plaintext_end >= plaintext.len(); - if pad_len == 0 - && plaintext_exhausted - && !Self::needs_trailer(rs, ciphertext.len() + record_len) - { - // We've reached the last record when the padding and plaintext are - // exhausted, and we don't need to write an empty trailing record. - last_record = true; - } - - if !last_record && block_len < max_block_len { - // We have padding left, but not enough plaintext to form a full record. - // Writing trailing padding-only records will still leak size information, - // so we force the caller to pick a smaller padding length. - return Err(Error::EncryptPadding); - } - - let iv = generate_iv(&nonce, counter); - let block = Self::pad( - &plaintext[plaintext_start..plaintext_end], - block_pad_len, - last_record, - )?; - let cryptographer = crypto::holder::get_cryptographer(); - let mut record = cryptographer.aes_gcm_128_encrypt(&key, &iv, &block)?; - ciphertext.append(&mut record); - plaintext_start = plaintext_end; - counter += 1; - } - // Cheap way to error out if the plaintext didn't fit in a single record. - // We're going to refactor away the multi-record stuff entirely in a future PR, - // but doing this here now lets us set API expectations for the caller. - if !Self::allow_multiple_records() && counter > 1 { - return Err(Error::PlaintextTooLong); - } - Ok(ciphertext) - } - - fn common_decrypt( - local_prv_key: &dyn LocalKeyPair, - remote_pub_key: &dyn RemotePublicKey, - auth_secret: &[u8], - salt: &[u8], - rs: u32, - ciphertext: &[u8], - ) -> Result> { - if auth_secret.len() != ECE_WEBPUSH_AUTH_SECRET_LENGTH { - return Err(Error::InvalidAuthSecret); - } - if salt.len() != ECE_SALT_LENGTH { - return Err(Error::InvalidSalt); - } - if ciphertext.is_empty() { - return Err(Error::ZeroCiphertext); - } - if Self::needs_trailer(rs, ciphertext.len()) { - // If we're missing a trailing block, the ciphertext is truncated. - return Err(Error::DecryptTruncated); - } - let (key, nonce) = Self::derive_key_and_nonce( - EceMode::DECRYPT, - local_prv_key, - remote_pub_key, - auth_secret, - salt, - )?; - let chunks = ciphertext.chunks(rs as usize); - let records_count = chunks.len(); - // Cheap way to error out if there are multiple records. - // We're going to refactor away the multi-record stuff entirely in a future PR, - // but doing this here now lets us set API expectations for the caller. - if !Self::allow_multiple_records() && records_count > 1 { - return Err(Error::MultipleRecordsNotSupported); - } - let items = chunks - .enumerate() - .map(|(count, record)| { - if record.len() <= ECE_TAG_LENGTH { - return Err(Error::BlockTooShort); - } - let iv = generate_iv(&nonce, count); - assert!(record.len() > ECE_TAG_LENGTH); - let cryptographer = crypto::holder::get_cryptographer(); - let plaintext = cryptographer.aes_gcm_128_decrypt(&key, &iv, record)?; - let last_record = count == records_count - 1; - if plaintext.len() < Self::pad_size() { - return Err(Error::BlockTooShort); - } - Ok(Self::unpad(&plaintext, last_record)?.to_vec()) - }) - .collect::>>>()?; - // TODO: There was a way to do it without this last line. - Ok(items.into_iter().flatten().collect::>()) - } - - fn pad_size() -> usize; - /// Calculates the padding so that the block contains at least one plaintext - /// byte. - fn min_block_pad_length(pad_len: usize, max_block_len: usize) -> usize; - fn needs_trailer(rs: u32, ciphertext_len: usize) -> bool; - fn allow_multiple_records() -> bool; - fn pad(plaintext: &[u8], block_pad_len: usize, last_record: bool) -> Result>; - fn unpad(block: &[u8], last_record: bool) -> Result<&[u8]>; - fn derive_key_and_nonce( - ece_mode: EceMode, - local_prv_key: &dyn LocalKeyPair, - remote_pub_key: &dyn RemotePublicKey, - auth_secret: &[u8], - salt: &[u8], - ) -> Result; -} - -// Calculates the padding so that the block contains at least one plaintext -// byte. -pub fn ece_min_block_pad_length(pad_len: usize, max_block_len: usize) -> usize { - assert!(max_block_len >= 1); - let mut block_pad_len = max_block_len - 1; - if pad_len > 0 && block_pad_len == 0 { - // If `max_block_len` is 1, we can only include 1 byte of data, so write - // the padding first. - block_pad_len += 1; - } - if block_pad_len > pad_len { - pad_len - } else { - block_pad_len - } -} - -/// Generates a 96-bit IV, 48 bits of which are populated. -fn generate_iv(nonce: &[u8], counter: usize) -> [u8; ECE_NONCE_LENGTH] { +/// Generates the AES-GCM IV to use for encrypting a single record. +/// +/// Each record in ECE is encrypted with a unique IV, that combines a "global" nonce +/// for the whole data with with the record's sequence number. +/// +pub(crate) fn generate_iv_for_record(nonce: &[u8], counter: usize) -> [u8; ECE_NONCE_LENGTH] { let mut iv = [0u8; ECE_NONCE_LENGTH]; let offset = ECE_NONCE_LENGTH - 8; iv[0..offset].copy_from_slice(&nonce[0..offset]); diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index a912ab6..c4553e5 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -111,7 +111,7 @@ pub trait Cryptographer: Send + Sync + 'static { /// #[cfg(any(test, feature = "backend-test-helper"))] pub fn test_cryptographer(cryptographer: T) { - use crate::{aes128gcm::Aes128GcmEceWebPush, common::WebPushParams}; + use crate::{aes128gcm, common::WebPushParams}; // These are test data from the RFC. let plaintext = "When I grow up, I want to be a watermelon"; @@ -135,7 +135,7 @@ pub fn test_cryptographer(cryptographer: T) { }; assert_eq!( - Aes128GcmEceWebPush::encrypt_with_keys( + aes128gcm::encrypt( &*local_key_pair, &*remote_pub_key, &auth_secret, @@ -154,7 +154,7 @@ pub fn test_cryptographer(cryptographer: T) { let local_key_pair = cryptographer.import_key_pair(&ec_key).unwrap(); assert_eq!( - Aes128GcmEceWebPush::decrypt(&*local_key_pair, &auth_secret, ciphertext.as_ref(),).unwrap(), + aes128gcm::decrypt(&*local_key_pair, &auth_secret, ciphertext.as_ref(),).unwrap(), plaintext.as_bytes() ); } diff --git a/src/legacy.rs b/src/legacy.rs index 36a3572..1a8014c 100644 --- a/src/legacy.rs +++ b/src/legacy.rs @@ -3,12 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ pub use crate::aesgcm::AesGcmEncryptedBlock; -use crate::{ - aesgcm::{AesGcmEceWebPush, ECE_AESGCM_PAD_SIZE}, - common::WebPushParams, - crypto::EcKeyComponents, - error::*, -}; +use crate::{aesgcm, common::WebPushParams, crypto::EcKeyComponents, error::*}; /// Encrypt a block using legacy AESGCM encoding. /// @@ -28,8 +23,8 @@ pub fn encrypt_aesgcm( let cryptographer = crate::crypto::holder::get_cryptographer(); let remote_key = cryptographer.import_public_key(remote_pub)?; let local_key_pair = cryptographer.generate_ephemeral_keypair()?; - let params = WebPushParams::new_for_plaintext(data, ECE_AESGCM_PAD_SIZE); - AesGcmEceWebPush::encrypt_with_keys(&*local_key_pair, &*remote_key, &remote_auth, data, params) + let params = WebPushParams::new_for_plaintext(data, aesgcm::ECE_AESGCM_PAD_SIZE); + aesgcm::encrypt(&*local_key_pair, &*remote_key, &remote_auth, data, params) } /// Decrypt a block using legacy AESGCM encoding. @@ -49,7 +44,7 @@ pub fn decrypt_aesgcm( ) -> Result> { let cryptographer = crate::crypto::holder::get_cryptographer(); let priv_key = cryptographer.import_key_pair(components).unwrap(); - AesGcmEceWebPush::decrypt(&*priv_key, &auth, data) + aesgcm::decrypt(&*priv_key, &auth, data) } #[cfg(all(test, feature = "backend-openssl"))] @@ -92,7 +87,7 @@ mod aesgcm_tests { pad_length, salt, }; - let encrypted_block = AesGcmEceWebPush::encrypt_with_keys( + let encrypted_block = aesgcm::encrypt( &*local_key_pair, &*remote_pub_key, &auth_secret, @@ -140,7 +135,7 @@ mod aesgcm_tests { .import_public_key(&remote_key.pub_as_raw().unwrap()) .unwrap(); let params = WebPushParams::default(); - let encrypted_block = AesGcmEceWebPush::encrypt_with_keys( + let encrypted_block = aesgcm::encrypt( &*local_key, &*remote_public, &auth_secret, @@ -148,8 +143,7 @@ mod aesgcm_tests { params, ) .unwrap(); - let decrypted = - AesGcmEceWebPush::decrypt(&*remote_key, &auth_secret, &encrypted_block).unwrap(); + let decrypted = aesgcm::decrypt(&*remote_key, &auth_secret, &encrypted_block).unwrap(); assert_eq!(decrypted, plaintext.to_vec()); } diff --git a/src/lib.rs b/src/lib.rs index f8bbdac..8a72477 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,8 +17,8 @@ pub use crate::{ }; use crate::{ - aes128gcm::{Aes128GcmEceWebPush, ECE_AES128GCM_PAD_SIZE}, - common::{WebPushParams, ECE_WEBPUSH_AUTH_SECRET_LENGTH}, + aes128gcm::ECE_AES128GCM_PAD_SIZE, + common::{WebPushParams, ECE_TAG_LENGTH, ECE_WEBPUSH_AUTH_SECRET_LENGTH}, }; /// Generate a local ECE key pair and authentication secret. @@ -45,14 +45,13 @@ pub fn encrypt(remote_pub: &[u8], remote_auth: &[u8], data: &[u8]) -> Result params.rs as usize { + params.rs = (data.len() + params.pad_length + ECE_TAG_LENGTH) as u32; + } + aes128gcm::encrypt(&*local_key_pair, &*remote_key, &remote_auth, data, params) } /// Decrypt a block using the AES128GCM encryption scheme. @@ -67,7 +66,7 @@ pub fn encrypt(remote_pub: &[u8], remote_auth: &[u8], data: &[u8]) -> Result Result> { let cryptographer = crypto::holder::get_cryptographer(); let priv_key = cryptographer.import_key_pair(components).unwrap(); - Aes128GcmEceWebPush::decrypt(&*priv_key, &auth, data) + aes128gcm::decrypt(&*priv_key, &auth, data) } /// Generate a pair of keys; useful for writing tests. @@ -111,7 +110,7 @@ mod aes128gcm_tests { pad_length, salt, }; - let ciphertext = Aes128GcmEceWebPush::encrypt_with_keys( + let ciphertext = aes128gcm::encrypt( &*local_key_pair, &*remote_pub_key, &auth_secret, @@ -145,26 +144,32 @@ mod aes128gcm_tests { } #[test] - fn test_e2e() { - let (local_key, remote_key) = generate_keys().unwrap(); + fn test_e2e_through_public_api() { + let (remote_key, auth_secret) = generate_keypair_and_auth_secret().unwrap(); let plaintext = b"When I grow up, I want to be a watermelon"; - let mut auth_secret = vec![0u8; 16]; - let cryptographer = crypto::holder::get_cryptographer(); - cryptographer.random_bytes(&mut auth_secret).unwrap(); - let remote_public = cryptographer - .import_public_key(&remote_key.pub_as_raw().unwrap()) - .unwrap(); - let params = WebPushParams::default(); - let ciphertext = Aes128GcmEceWebPush::encrypt_with_keys( - &*local_key, - &*remote_public, + let ciphertext = + encrypt(&remote_key.pub_as_raw().unwrap(), &auth_secret, plaintext).unwrap(); + let decrypted = decrypt( + &remote_key.raw_components().unwrap(), &auth_secret, - plaintext, - params, + &ciphertext, + ) + .unwrap(); + assert_eq!(decrypted, plaintext.to_vec()); + } + + #[test] + fn test_e2e_large_plaintext() { + let (remote_key, auth_secret) = generate_keypair_and_auth_secret().unwrap(); + let plaintext = [0; 5000]; + let ciphertext = + encrypt(&remote_key.pub_as_raw().unwrap(), &auth_secret, &plaintext).unwrap(); + let decrypted = decrypt( + &remote_key.raw_components().unwrap(), + &auth_secret, + &ciphertext, ) .unwrap(); - let decrypted = - Aes128GcmEceWebPush::decrypt(&*remote_key, &auth_secret, &ciphertext).unwrap(); assert_eq!(decrypted, plaintext.to_vec()); } @@ -226,7 +231,7 @@ mod aes128gcm_tests { .unwrap_err(); match err { Error::HeaderTooShort => {} - _ => unreachable!(), + _ => panic!("Unexpected error {:?}", err), }; } @@ -241,7 +246,7 @@ mod aes128gcm_tests { .unwrap_err(); match err { Error::InvalidKeyLength => {} - _ => unreachable!(), + _ => panic!("Unexpected error {:?}", err), }; } @@ -255,7 +260,7 @@ mod aes128gcm_tests { ).unwrap_err(); match err { Error::OpenSSLError(_) => {} - _ => panic!("{:?}", err), //unreachable!(), + _ => panic!("Unexpected error {:?}", err), }; } @@ -269,7 +274,7 @@ mod aes128gcm_tests { ).unwrap_err(); match err { Error::DecryptPadding => {} - _ => panic!("{:?}", err), //unreachable!(), + _ => panic!("Unexpected error {:?}", err), }; } }