diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/rust/ece/src | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/ece/src')
-rw-r--r-- | third_party/rust/ece/src/aes128gcm.rs | 222 | ||||
-rw-r--r-- | third_party/rust/ece/src/aesgcm.rs | 252 | ||||
-rw-r--r-- | third_party/rust/ece/src/common.rs | 251 | ||||
-rw-r--r-- | third_party/rust/ece/src/crypto/holder.rs | 50 | ||||
-rw-r--r-- | third_party/rust/ece/src/crypto/mod.rs | 105 | ||||
-rw-r--r-- | third_party/rust/ece/src/crypto/openssl.rs | 203 | ||||
-rw-r--r-- | third_party/rust/ece/src/error.rs | 51 | ||||
-rw-r--r-- | third_party/rust/ece/src/lib.rs | 441 |
8 files changed, 1575 insertions, 0 deletions
diff --git a/third_party/rust/ece/src/aes128gcm.rs b/third_party/rust/ece/src/aes128gcm.rs new file mode 100644 index 0000000000..041159fd96 --- /dev/null +++ b/third_party/rust/ece/src/aes128gcm.rs @@ -0,0 +1,222 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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::{ + common::*, + crypto::{self, LocalKeyPair, RemotePublicKey}, + error::*, +}; +use byteorder::{BigEndian, ByteOrder}; + +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. +const ECE_AES128GCM_PAD_SIZE: usize = 1; + +const ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX: &str = "WebPush: info\0"; +const ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH: usize = 144; // 14 (prefix len) + 65 (pub key len) * 2; + +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 :) + +pub struct Aes128GcmEceWebPush; +impl Aes128GcmEceWebPush { + /// Encrypts a Web Push message using the "aes128gcm" scheme. This function + /// automatically generates an ephemeral ECDH key pair. + pub fn encrypt( + remote_pub_key: &dyn RemotePublicKey, + auth_secret: &[u8], + plaintext: &[u8], + params: WebPushParams, + ) -> Result<Vec<u8>> { + let cryptographer = crypto::holder::get_cryptographer(); + let local_prv_key = cryptographer.generate_ephemeral_keypair()?; + Self::encrypt_with_keys( + &*local_prv_key, + remote_pub_key, + auth_secret, + plaintext, + params, + ) + } + + /// 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<Vec<u8>> { + 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, + params.rs, + params.pad_length, + plaintext, + )?; + // 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<Vec<u8>> { + if payload.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 { + return Err(Error::HeaderTooShort); + } + + let rs = BigEndian::read_u32(&payload[ECE_SALT_LENGTH..]); + if rs < ECE_AES128GCM_MIN_RS { + return Err(Error::InvalidRecordSize); + } + + 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) + } +} + +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 + } + + fn pad_size() -> usize { + ECE_AES128GCM_PAD_SIZE + } + + fn min_block_pad_length(pad_len: usize, max_block_len: usize) -> usize { + ece_min_block_pad_length(pad_len, max_block_len) + } + + fn pad(plaintext: &[u8], block_pad_len: usize, last_record: bool) -> Result<Vec<u8>> { + 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) + } + + 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<KeyAndNonce> { + 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)) + } +} + +// The "aes128gcm" IKM info string is "WebPush: info\0", followed by the +// receiver and sender public keys. +fn generate_info( + raw_recv_pub_key: &[u8], + raw_sender_pub_key: &[u8], +) -> Result<[u8; ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH]> { + let mut info = [0u8; ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH]; + let prefix = ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX.as_bytes(); + let mut offset = prefix.len(); + info[0..offset].copy_from_slice(prefix); + info[offset..offset + ECE_WEBPUSH_PUBLIC_KEY_LENGTH].copy_from_slice(raw_recv_pub_key); + offset += ECE_WEBPUSH_PUBLIC_KEY_LENGTH; + info[offset..].copy_from_slice(raw_sender_pub_key); + Ok(info) +} diff --git a/third_party/rust/ece/src/aesgcm.rs b/third_party/rust/ece/src/aesgcm.rs new file mode 100644 index 0000000000..d994c114a1 --- /dev/null +++ b/third_party/rust/ece/src/aesgcm.rs @@ -0,0 +1,252 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. + * + * This supports the now obsolete HTTP-ECE Draft 02 "aesgcm" content + * type. There are a number of providers that still use this format, + * and there's no real mechanism to return the client supported crypto + * versions. + * + * */ + +use crate::{ + common::*, + crypto::{self, LocalKeyPair, RemotePublicKey}, + error::*, +}; +use std::collections::HashMap; + +const ECE_AESGCM_PAD_SIZE: usize = 2; + +const ECE_WEBPUSH_AESGCM_KEYPAIR_LENGTH: usize = 134; // (2 + Raw Key Length) * 2 +const ECE_WEBPUSH_AESGCM_AUTHINFO: &str = "Content-Encoding: auth\0"; + +// a DER prefixed key is "\04" + ECE_WEBPUSH_RAW_KEY_LENGTH +const ECE_WEBPUSH_RAW_KEY_LENGTH: usize = 65; +const ECE_WEBPUSH_IKM_LENGTH: usize = 32; + +pub struct AesGcmEncryptedBlock { + pub dh: Vec<u8>, + pub salt: Vec<u8>, + pub rs: u32, + pub ciphertext: Vec<u8>, +} + +impl AesGcmEncryptedBlock { + pub fn aesgcm_rs(rs: u32) -> u32 { + if rs > u32::max_value() - ECE_TAG_LENGTH as u32 { + return 0; + } + rs + ECE_TAG_LENGTH as u32 + } + + /// Create a new block from the various header strings and body content. + 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, + }) + } + + /// Return the headers Hash, NOTE you may need to merge Crypto-Key if there's + /// already a VAPID element present. + pub fn headers(self) -> HashMap<String, String> { + let mut result: HashMap<String, String> = HashMap::new(); + let mut rs = "".to_owned(); + result.insert( + "Crypto-Key".to_owned(), + format!( + "dh={}", + base64::encode_config(&self.dh, base64::URL_SAFE_NO_PAD) + ), + ); + if self.rs > 0 { + rs = format!(";rs={}", self.rs); + } + result.insert( + "Encryption".to_owned(), + format!( + "salt={}{}", + base64::encode_config(&self.salt, base64::URL_SAFE_NO_PAD), + rs + ), + ); + result + } + + /// Encode the body as a String. + /// If you need the bytes, probably just call .ciphertext directly + pub fn body(self) -> String { + base64::encode_config(&self.ciphertext, base64::URL_SAFE_NO_PAD) + } +} + +pub struct AesGcmEceWebPush; +impl AesGcmEceWebPush { + /// Encrypts a Web Push message using the "aesgcm" scheme. This function + /// automatically generates an ephemeral ECDH key pair. + pub fn encrypt( + remote_pub_key: &dyn RemotePublicKey, + auth_secret: &[u8], + plaintext: &[u8], + params: WebPushParams, + ) -> Result<AesGcmEncryptedBlock> { + let cryptographer = crypto::holder::get_cryptographer(); + let local_prv_key = cryptographer.generate_ephemeral_keypair()?; + Self::encrypt_with_keys( + &*local_prv_key, + remote_pub_key, + auth_secret, + plaintext, + params, + ) + } + + /// 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<AesGcmEncryptedBlock> { + let cryptographer = crypto::holder::get_cryptographer(); + let salt = { + 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, + }) + } + + /// Decrypts a Web Push message encrypted using the "aesgcm" scheme. + pub 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)?; + Self::common_decrypt( + local_prv_key, + &*sender_key, + auth_secret, + &block.salt, + block.rs, + &block.ciphertext, + ) + } +} + +impl EceWebPush for AesGcmEceWebPush { + fn needs_trailer(rs: u32, ciphertextlen: usize) -> bool { + ciphertextlen as u32 % rs == 0 + } + + fn pad_size() -> usize { + ECE_AESGCM_PAD_SIZE + } + + fn min_block_pad_length(pad_len: usize, max_block_len: usize) -> usize { + ece_min_block_pad_length(pad_len, max_block_len) + } + + fn pad(plaintext: &[u8], _: usize, _: bool) -> Result<Vec<u8>> { + let plen = plaintext.len(); + let mut block = vec![0; plen + ECE_AESGCM_PAD_SIZE]; + block[2..].copy_from_slice(plaintext); + Ok(block) + } + + 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)..]) + } + + /// Derives the "aesgcm" decryption keyn 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<KeyAndNonce> { + 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)) + } +} + +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); + } + // length prefix each key + 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) +} + +// The "aesgcm" IKM info string is "WebPush: info", followed by the +// receiver and sender public keys prefixed by their lengths. +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) +} diff --git a/third_party/rust/ece/src/common.rs b/third_party/rust/ece/src/common.rs new file mode 100644 index 0000000000..984e25430f --- /dev/null +++ b/third_party/rust/ece/src/common.rs @@ -0,0 +1,251 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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::*, +}; +use byteorder::{BigEndian, ByteOrder}; +use std::cmp::min; + +// From keys.h: +pub const ECE_AES_KEY_LENGTH: usize = 16; +pub const ECE_NONCE_LENGTH: usize = 12; + +// From ece.h: +pub const ECE_SALT_LENGTH: usize = 16; +pub const ECE_TAG_LENGTH: usize = 16; +//const ECE_WEBPUSH_PRIVATE_KEY_LENGTH: usize = 32; +pub const ECE_WEBPUSH_PUBLIC_KEY_LENGTH: usize = 65; +pub const ECE_WEBPUSH_AUTH_SECRET_LENGTH: usize = 16; +const ECE_WEBPUSH_DEFAULT_RS: u32 = 4096; + +// TODO: Make it nicer to use with a builder pattern. +pub struct WebPushParams { + pub rs: u32, + pub pad_length: usize, + pub salt: Option<Vec<u8>>, +} + +impl WebPushParams { + /// Random salt, record size = 4096 and padding length = 0. + pub fn default() -> Self { + Self { + rs: ECE_WEBPUSH_DEFAULT_RS, + pad_length: 2, + salt: None, + } + } + + /// Never use the same salt twice as it will derive the same content encryption + /// key for multiple messages if the same sender private key is used! + pub fn new(rs: u32, pad_length: usize, salt: Vec<u8>) -> Self { + Self { + rs, + pad_length, + salt: Some(salt), + } + } +} + +pub enum EceMode { + ENCRYPT, + DECRYPT, +} + +pub type KeyAndNonce = (Vec<u8>, Vec<u8>); + +pub 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<Vec<u8>> { + 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; + } + 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<Vec<u8>> { + 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(); + 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::<Result<Vec<Vec<u8>>>>()?; + // TODO: There was a way to do it without this last line. + Ok(items.into_iter().flatten().collect::<Vec<u8>>()) + } + + 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 pad(plaintext: &[u8], block_pad_len: usize, last_record: bool) -> Result<Vec<u8>>; + 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<KeyAndNonce>; +} + +// 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] { + let mut iv = [0u8; ECE_NONCE_LENGTH]; + let offset = ECE_NONCE_LENGTH - 8; + iv[0..offset].copy_from_slice(&nonce[0..offset]); + // Combine the remaining unsigned 64-bit integer with the record sequence + // number using XOR. See the "nonce derivation" section of the draft. + let mask = BigEndian::read_u64(&nonce[offset..]); + BigEndian::write_u64(&mut iv[offset..], mask ^ (counter as u64)); + iv +} diff --git a/third_party/rust/ece/src/crypto/holder.rs b/third_party/rust/ece/src/crypto/holder.rs new file mode 100644 index 0000000000..fb78a73f52 --- /dev/null +++ b/third_party/rust/ece/src/crypto/holder.rs @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 super::Cryptographer; +use once_cell::sync::OnceCell; + +static CRYPTOGRAPHER: OnceCell<&'static dyn Cryptographer> = OnceCell::new(); + +#[derive(Debug, thiserror::Error)] +#[error("Cryptographer already initialized")] +pub struct SetCryptographerError(()); + +/// Sets the global object that will be used for cryptographic operations. +/// +/// This is a convenience wrapper over [`set_cryptographer`], +/// but takes a `Box<dyn Cryptographer>` instead. +#[cfg(not(feature = "backend-openssl"))] +pub fn set_boxed_cryptographer(c: Box<dyn Cryptographer>) -> Result<(), SetCryptographerError> { + // Just leak the Box. It wouldn't be freed as a `static` anyway, and we + // never allow this to be re-assigned (so it's not a meaningful memory leak). + set_cryptographer(Box::leak(c)) +} + +/// Sets the global object that will be used for cryptographic operations. +/// +/// This function may only be called once in the lifetime of a program. +/// +/// Any calls into this crate that perform cryptography prior to calling this +/// function will panic. +pub fn set_cryptographer(c: &'static dyn Cryptographer) -> Result<(), SetCryptographerError> { + CRYPTOGRAPHER.set(c).map_err(|_| SetCryptographerError(())) +} + +pub(crate) fn get_cryptographer() -> &'static dyn Cryptographer { + autoinit_crypto(); + *CRYPTOGRAPHER + .get() + .expect("`rust-ece` cryptographer not initialized!") +} + +#[cfg(feature = "backend-openssl")] +#[inline] +fn autoinit_crypto() { + let _ = set_cryptographer(&super::openssl::OpensslCryptographer); +} + +#[cfg(not(feature = "backend-openssl"))] +#[inline] +fn autoinit_crypto() {} diff --git a/third_party/rust/ece/src/crypto/mod.rs b/third_party/rust/ece/src/crypto/mod.rs new file mode 100644 index 0000000000..1f3648179b --- /dev/null +++ b/third_party/rust/ece/src/crypto/mod.rs @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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::error::*; +use std::any::Any; + +pub(crate) mod holder; +#[cfg(feature = "backend-openssl")] +mod openssl; + +#[cfg(not(feature = "backend-openssl"))] +pub use holder::{set_boxed_cryptographer, set_cryptographer}; + +pub trait RemotePublicKey: Send + Sync + 'static { + /// Export the key component in the + /// binary uncompressed point representation. + fn as_raw(&self) -> Result<Vec<u8>>; + /// For downcasting purposes. + fn as_any(&self) -> &dyn Any; +} + +pub trait LocalKeyPair: Send + Sync + 'static { + /// Export the public key component in the + /// binary uncompressed point representation. + fn pub_as_raw(&self) -> Result<Vec<u8>>; + /// Export the raw components of the keypair. + fn raw_components(&self) -> Result<EcKeyComponents>; + /// For downcasting purposes. + fn as_any(&self) -> &dyn Any; +} + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "serializable-keys", + derive(serde::Serialize, serde::Deserialize) +)] +pub enum EcCurve { + P256, +} + +impl Default for EcCurve { + fn default() -> Self { + EcCurve::P256 + } +} + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "serializable-keys", + derive(serde::Serialize, serde::Deserialize) +)] +pub struct EcKeyComponents { + // The curve is only kept in case the ECE standard changes in the future. + curve: EcCurve, + // The `d` value of the EC Key. + private_key: Vec<u8>, + // The uncompressed x,y-representation of the public component of the EC Key. + public_key: Vec<u8>, +} + +impl EcKeyComponents { + pub fn new<T: Into<Vec<u8>>>(private_key: T, public_key: T) -> Self { + EcKeyComponents { + private_key: private_key.into(), + public_key: public_key.into(), + curve: Default::default(), + } + } + pub fn curve(&self) -> &EcCurve { + &self.curve + } + /// The `d` value of the EC Key. + pub fn private_key(&self) -> &[u8] { + &self.private_key + } + /// The uncompressed x,y-representation of the public component of the EC Key. + pub fn public_key(&self) -> &[u8] { + &self.public_key + } +} + +pub trait Cryptographer: Send + Sync + 'static { + /// Generate a random ephemeral local key pair. + fn generate_ephemeral_keypair(&self) -> Result<Box<dyn LocalKeyPair>>; + /// Import a local keypair from its raw components. + fn import_key_pair(&self, components: &EcKeyComponents) -> Result<Box<dyn LocalKeyPair>>; + /// Import the public key component in the binary uncompressed point representation. + fn import_public_key(&self, raw: &[u8]) -> Result<Box<dyn RemotePublicKey>>; + fn compute_ecdh_secret( + &self, + remote: &dyn RemotePublicKey, + local: &dyn LocalKeyPair, + ) -> Result<Vec<u8>>; + fn hkdf_sha256(&self, salt: &[u8], secret: &[u8], info: &[u8], len: usize) -> Result<Vec<u8>>; + /// Should return [ciphertext, auth_tag]. + fn aes_gcm_128_encrypt(&self, key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>>; + fn aes_gcm_128_decrypt( + &self, + key: &[u8], + iv: &[u8], + ciphertext_and_tag: &[u8], + ) -> Result<Vec<u8>>; + fn random_bytes(&self, dest: &mut [u8]) -> Result<()>; +} diff --git a/third_party/rust/ece/src/crypto/openssl.rs b/third_party/rust/ece/src/crypto/openssl.rs new file mode 100644 index 0000000000..039f94b547 --- /dev/null +++ b/third_party/rust/ece/src/crypto/openssl.rs @@ -0,0 +1,203 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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::{Cryptographer, EcKeyComponents, LocalKeyPair, RemotePublicKey}, + error::*, +}; +use hkdf::Hkdf; +use lazy_static::lazy_static; +use openssl::{ + bn::{BigNum, BigNumContext}, + derive::Deriver, + ec::{EcGroup, EcKey, EcPoint, PointConversionForm}, + nid::Nid, + pkey::{PKey, Private, Public}, + rand::rand_bytes, + symm::{Cipher, Crypter, Mode}, +}; +use sha2::Sha256; +use std::{any::Any, fmt}; + +lazy_static! { + static ref GROUP_P256: EcGroup = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); +} +const AES_GCM_TAG_LENGTH: usize = 16; + +#[derive(Clone, Debug)] +pub struct OpenSSLRemotePublicKey { + raw_pub_key: Vec<u8>, +} + +impl OpenSSLRemotePublicKey { + fn from_raw(raw: &[u8]) -> Result<Self> { + Ok(OpenSSLRemotePublicKey { + raw_pub_key: raw.to_vec(), + }) + } + + fn to_pkey(&self) -> Result<PKey<Public>> { + let mut bn_ctx = BigNumContext::new()?; + let point = EcPoint::from_bytes(&GROUP_P256, &self.raw_pub_key, &mut bn_ctx)?; + let ec = EcKey::from_public_key(&GROUP_P256, &point)?; + PKey::from_ec_key(ec).map_err(std::convert::Into::into) + } +} + +impl RemotePublicKey for OpenSSLRemotePublicKey { + fn as_raw(&self) -> Result<Vec<u8>> { + Ok(self.raw_pub_key.to_vec()) + } + fn as_any(&self) -> &dyn Any { + self + } +} + +#[derive(Clone)] +pub struct OpenSSLLocalKeyPair { + ec_key: EcKey<Private>, +} + +impl fmt::Debug for OpenSSLLocalKeyPair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{:?}", + base64::encode_config(&self.ec_key.private_key().to_vec(), base64::URL_SAFE) + ) + } +} + +impl OpenSSLLocalKeyPair { + /// Generate a random local key pair using OpenSSL `ECKey::generate`. + fn generate_random() -> Result<Self> { + let ec_key = EcKey::generate(&GROUP_P256)?; + Ok(OpenSSLLocalKeyPair { ec_key }) + } + + fn to_pkey(&self) -> Result<PKey<Private>> { + PKey::from_ec_key(self.ec_key.clone()).map_err(std::convert::Into::into) + } + + fn from_raw_components(components: &EcKeyComponents) -> Result<Self> { + let d = BigNum::from_slice(&components.private_key())?; + let mut bn_ctx = BigNumContext::new()?; + let ec_point = EcPoint::from_bytes(&GROUP_P256, &components.public_key(), &mut bn_ctx)?; + let mut x = BigNum::new()?; + let mut y = BigNum::new()?; + ec_point.affine_coordinates_gfp(&GROUP_P256, &mut x, &mut y, &mut bn_ctx)?; + let public_key = EcKey::from_public_key_affine_coordinates(&GROUP_P256, &x, &y)?; + let private_key = EcKey::from_private_components(&GROUP_P256, &d, public_key.public_key())?; + Ok(Self { + ec_key: private_key, + }) + } +} + +impl LocalKeyPair for OpenSSLLocalKeyPair { + /// Export the public key component in the binary uncompressed point representation + /// using OpenSSL `PointConversionForm::UNCOMPRESSED`. + fn pub_as_raw(&self) -> Result<Vec<u8>> { + let pub_key_point = self.ec_key.public_key(); + let mut bn_ctx = BigNumContext::new()?; + let uncompressed = + pub_key_point.to_bytes(&GROUP_P256, PointConversionForm::UNCOMPRESSED, &mut bn_ctx)?; + Ok(uncompressed) + } + + fn raw_components(&self) -> Result<EcKeyComponents> { + let private_key = self.ec_key.private_key(); + Ok(EcKeyComponents::new( + private_key.to_vec(), + self.pub_as_raw()?, + )) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl From<EcKey<Private>> for OpenSSLLocalKeyPair { + fn from(key: EcKey<Private>) -> OpenSSLLocalKeyPair { + OpenSSLLocalKeyPair { ec_key: key } + } +} + +pub struct OpensslCryptographer; +impl Cryptographer for OpensslCryptographer { + fn generate_ephemeral_keypair(&self) -> Result<Box<dyn LocalKeyPair>> { + Ok(Box::new(OpenSSLLocalKeyPair::generate_random()?)) + } + + fn import_key_pair(&self, components: &EcKeyComponents) -> Result<Box<dyn LocalKeyPair>> { + Ok(Box::new(OpenSSLLocalKeyPair::from_raw_components( + components, + )?)) + } + + fn import_public_key(&self, raw: &[u8]) -> Result<Box<dyn RemotePublicKey>> { + Ok(Box::new(OpenSSLRemotePublicKey::from_raw(raw)?)) + } + + fn compute_ecdh_secret( + &self, + remote: &dyn RemotePublicKey, + local: &dyn LocalKeyPair, + ) -> Result<Vec<u8>> { + let local_any = local.as_any(); + let local = local_any.downcast_ref::<OpenSSLLocalKeyPair>().unwrap(); + let private = local.to_pkey()?; + let remote_any = remote.as_any(); + let remote = remote_any.downcast_ref::<OpenSSLRemotePublicKey>().unwrap(); + let public = remote.to_pkey()?; + let mut deriver = Deriver::new(&private)?; + deriver.set_peer(&public)?; + let shared_key = deriver.derive_to_vec()?; + Ok(shared_key) + } + + fn hkdf_sha256(&self, salt: &[u8], secret: &[u8], info: &[u8], len: usize) -> Result<Vec<u8>> { + let (_, hk) = Hkdf::<Sha256>::extract(Some(&salt[..]), &secret); + let mut okm = vec![0u8; len]; + hk.expand(&info, &mut okm).unwrap(); + Ok(okm) + } + + fn aes_gcm_128_encrypt(&self, key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>> { + let cipher = Cipher::aes_128_gcm(); + let mut c = Crypter::new(cipher, Mode::Encrypt, key, Some(iv))?; + let mut out = vec![0u8; data.len() + cipher.block_size()]; + let count = c.update(data, &mut out)?; + let rest = c.finalize(&mut out[count..])?; + let mut tag = vec![0u8; AES_GCM_TAG_LENGTH]; + c.get_tag(&mut tag)?; + out.truncate(count + rest); + out.append(&mut tag); + Ok(out) + } + + fn aes_gcm_128_decrypt( + &self, + key: &[u8], + iv: &[u8], + ciphertext_and_tag: &[u8], + ) -> Result<Vec<u8>> { + let block_len = ciphertext_and_tag.len() - AES_GCM_TAG_LENGTH; + let ciphertext = &ciphertext_and_tag[0..block_len]; + let tag = &ciphertext_and_tag[block_len..]; + let cipher = Cipher::aes_128_gcm(); + let mut c = Crypter::new(cipher, Mode::Decrypt, key, Some(iv))?; + let mut out = vec![0u8; ciphertext.len() + cipher.block_size()]; + let count = c.update(ciphertext, &mut out)?; + c.set_tag(tag)?; + let rest = c.finalize(&mut out[count..])?; + out.truncate(count + rest); + Ok(out) + } + + fn random_bytes(&self, dest: &mut [u8]) -> Result<()> { + Ok(rand_bytes(dest)?) + } +} diff --git a/third_party/rust/ece/src/error.rs b/third_party/rust/ece/src/error.rs new file mode 100644 index 0000000000..f4b6a6b05a --- /dev/null +++ b/third_party/rust/ece/src/error.rs @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +pub type Result<T> = std::result::Result<T, Error>; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Invalid auth secret")] + InvalidAuthSecret, + + #[error("Invalid salt")] + InvalidSalt, + + #[error("Invalid key length")] + InvalidKeyLength, + + #[error("Invalid record size")] + InvalidRecordSize, + + #[error("Invalid header size (too short)")] + HeaderTooShort, + + #[error("Truncated ciphertext")] + DecryptTruncated, + + #[error("Zero-length ciphertext")] + ZeroCiphertext, + + #[error("Zero-length plaintext")] + ZeroPlaintext, + + #[error("Block too short")] + BlockTooShort, + + #[error("Invalid decryption padding")] + DecryptPadding, + + #[error("Invalid encryption padding")] + EncryptPadding, + + #[error("Could not decode base64 entry")] + DecodeError(#[from] base64::DecodeError), + + #[error("Crypto backend error")] + CryptoError, + + #[cfg(feature = "backend-openssl")] + #[error("OpenSSL error: {0}")] + OpenSSLError(#[from] openssl::error::ErrorStack), +} diff --git a/third_party/rust/ece/src/lib.rs b/third_party/rust/ece/src/lib.rs new file mode 100644 index 0000000000..db00a99cae --- /dev/null +++ b/third_party/rust/ece/src/lib.rs @@ -0,0 +1,441 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +#![warn(rust_2018_idioms)] +mod aes128gcm; +mod aesgcm; +mod common; +pub mod crypto; +mod error; + +pub use crate::{ + aes128gcm::Aes128GcmEceWebPush, + aesgcm::{AesGcmEceWebPush, AesGcmEncryptedBlock}, + common::{WebPushParams, ECE_WEBPUSH_AUTH_SECRET_LENGTH}, + crypto::{Cryptographer, EcKeyComponents, LocalKeyPair, RemotePublicKey}, + error::*, +}; + +/// Generate a local ECE key pair and auth nonce. +pub fn generate_keypair_and_auth_secret( +) -> Result<(Box<dyn LocalKeyPair>, [u8; ECE_WEBPUSH_AUTH_SECRET_LENGTH])> { + let cryptographer = crypto::holder::get_cryptographer(); + let local_key_pair = cryptographer.generate_ephemeral_keypair()?; + let mut auth_secret = [0u8; ECE_WEBPUSH_AUTH_SECRET_LENGTH]; + cryptographer.random_bytes(&mut auth_secret)?; + Ok((local_key_pair, auth_secret)) +} + +/// Encrypt a block using default AES128GCM encoding. +/// +/// param remote_pub &[u8] - The remote public key +/// param remote_auth &u8 - The remote authorization token +/// param salt &[u8] - The locally generated random salt +/// param data &[u8] - The data to encrypt +/// +pub fn encrypt(remote_pub: &[u8], remote_auth: &[u8], salt: &[u8], data: &[u8]) -> Result<Vec<u8>> { + let cryptographer = crypto::holder::get_cryptographer(); + let remote_key = cryptographer.import_public_key(remote_pub)?; + let local_key_pair = cryptographer.generate_ephemeral_keypair()?; + let mut padr = [0u8; 2]; + cryptographer.random_bytes(&mut padr)?; + // since it's a sampled random, endian doesn't really matter. + let pad = ((usize::from(padr[0]) + (usize::from(padr[1]) << 8)) % 4095) + 1; + let params = WebPushParams::new(4096, pad, Vec::from(salt)); + Aes128GcmEceWebPush::encrypt_with_keys( + &*local_key_pair, + &*remote_key, + &remote_auth, + data, + params, + ) +} + +/// Decrypt a block using default AES128GCM encoding. +/// +/// param components &str - The locally generated private key components. +/// param auth &str - The locally generated auth token (this value was shared with the encryptor) +/// param data &[u8] - The encrypted data block +/// +pub fn decrypt(components: &EcKeyComponents, auth: &[u8], data: &[u8]) -> Result<Vec<u8>> { + let cryptographer = crypto::holder::get_cryptographer(); + let priv_key = cryptographer.import_key_pair(components).unwrap(); + Aes128GcmEceWebPush::decrypt(&*priv_key, &auth, data) +} + +#[cfg(all(test, feature = "backend-openssl"))] +fn generate_keys() -> Result<(Box<dyn LocalKeyPair>, Box<dyn LocalKeyPair>)> { + let cryptographer = crypto::holder::get_cryptographer(); + let local_key = cryptographer.generate_ephemeral_keypair()?; + let remote_key = cryptographer.generate_ephemeral_keypair()?; + Ok((local_key, remote_key)) +} + +#[cfg(all(test, feature = "backend-openssl"))] +mod aes128gcm_tests { + use super::*; + use hex; + + #[allow(clippy::too_many_arguments)] + fn try_encrypt( + private_key: &str, + public_key: &str, + remote_pub_key: &str, + auth_secret: &str, + salt: &str, + pad_length: usize, + rs: u32, + plaintext: &str, + ) -> Result<String> { + let cryptographer = crypto::holder::get_cryptographer(); + let private_key = hex::decode(private_key).unwrap(); + let public_key = hex::decode(public_key).unwrap(); + let ec_key = EcKeyComponents::new(private_key, public_key); + let local_key_pair = cryptographer.import_key_pair(&ec_key)?; + let remote_pub_key = hex::decode(remote_pub_key).unwrap(); + let remote_pub_key = cryptographer.import_public_key(&remote_pub_key).unwrap(); + let auth_secret = hex::decode(auth_secret).unwrap(); + let salt = hex::decode(salt).unwrap(); + let plaintext = plaintext.as_bytes(); + let params = WebPushParams::new(rs, pad_length, salt); + let ciphertext = Aes128GcmEceWebPush::encrypt_with_keys( + &*local_key_pair, + &*remote_pub_key, + &auth_secret, + &plaintext, + params, + )?; + Ok(hex::encode(ciphertext)) + } + + fn try_decrypt( + private_key: &str, + public_key: &str, + auth_secret: &str, + payload: &str, + ) -> Result<String> { + let private_key = hex::decode(private_key).unwrap(); + let public_key = hex::decode(public_key).unwrap(); + let ec_key = EcKeyComponents::new(private_key, public_key); + let plaintext = decrypt( + &ec_key, + &hex::decode(auth_secret).unwrap(), + &hex::decode(payload).unwrap(), + )?; + Ok(String::from_utf8(plaintext).unwrap()) + } + + #[test] + fn test_e2e() { + let (local_key, remote_key) = generate_keys().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, + &auth_secret, + plaintext, + params, + ) + .unwrap(); + let decrypted = + Aes128GcmEceWebPush::decrypt(&*remote_key, &auth_secret, &ciphertext).unwrap(); + assert_eq!(decrypted, plaintext.to_vec()); + } + + #[test] + fn test_conv_fn() -> Result<()> { + let (local_key, auth) = generate_keypair_and_auth_secret()?; + let plaintext = b"Mary had a little lamb, with some nice mint jelly"; + let mut salt = vec![0u8; 16]; + let cryptographer = crypto::holder::get_cryptographer(); + cryptographer.random_bytes(&mut salt)?; + let encoded = encrypt(&local_key.pub_as_raw()?, &auth, &salt, plaintext).unwrap(); + let decoded = decrypt(&local_key.raw_components()?, &auth, &encoded)?; + assert_eq!(decoded, plaintext.to_vec()); + Ok(()) + } + + #[test] + fn try_encrypt_ietf_rfc() { + let ciphertext = try_encrypt( + "c9f58f89813e9f8e872e71f42aa64e1757c9254dcc62b72ddc010bb4043ea11c", + "04fe33f4ab0dea71914db55823f73b54948f41306d920732dbb9a59a53286482200e597a7b7bc260ba1c227998580992e93973002f3012a28ae8f06bbb78e5ec0f", + "042571b2becdfde360551aaf1ed0f4cd366c11cebe555f89bcb7b186a53339173168ece2ebe018597bd30479b86e3c8f8eced577ca59187e9246990db682008b0e", + "05305932a1c7eabe13b6cec9fda48882", + "0c6bfaadad67958803092d454676f397", + 0, + 4096, + "When I grow up, I want to be a watermelon", + ).unwrap(); + assert_eq!(ciphertext, "0c6bfaadad67958803092d454676f397000010004104fe33f4ab0dea71914db55823f73b54948f41306d920732dbb9a59a53286482200e597a7b7bc260ba1c227998580992e93973002f3012a28ae8f06bbb78e5ec0ff297de5b429bba7153d3a4ae0caa091fd425f3b4b5414add8ab37a19c1bbb05cf5cb5b2a2e0562d558635641ec52812c6c8ff42e95ccb86be7cd"); + } + + #[test] + fn try_encrypt_rs_24_pad_6() { + let ciphertext = try_encrypt( + "0f28beaf7e27793c03638dc2973a15b0016e1b367cbffda8861ab175f31bce02", + "0430efcb1eb043b805e4e44bab35f82513c33fedb28700f7e568ac8b61e8d835665a51eb6679b2db228a10c0c3fe5077062848d9bb3d60279f93ce35484728aa1f", + "04c0d1a812b291291dd7beee358713c126c589f3633c26d1a201311de036dc10931e4ee142f61921a3ea5864e872a93841a52944e5b3f6accecce8c828fb04a4cd", + "9d7735d8de1962b98394b07ffe287e20", + "ff805030a108e114e6c17fad6186a1a6", + 6, + 24, + "I am the very model of a modern Major-General, I've information vegetable, animal, and mineral", + ).unwrap(); + assert_eq!(ciphertext, "ff805030a108e114e6c17fad6186a1a600000018410430efcb1eb043b805e4e44bab35f82513c33fedb28700f7e568ac8b61e8d835665a51eb6679b2db228a10c0c3fe5077062848d9bb3d60279f93ce35484728aa1fd2c1713949aec98f05096c7298fd3f51c4f818fafa1fe615d8447b3a05406031f6401ac24f2a775ca52456a921b83b9e0042c3a63e1afa1ae012774d9d775be8d19419451d37ff59ff592e84f07440a63fc17f5cabcb9a50eddaf75370db647f94447d3f166269d8711df0f57e56049576e1130a5a5e1f94ba8a5d0b0007c6c0fd2998429e7d63d4ef919798f46ecf5f0b28fb80f5b2439de26b8a52200bc7d6af7a4840721fe8be8524a691b6ef0edae90bb6f5927894819b831b45b53f8401fe022dbb64ed7565350904ac0b517135d7f8abbc98127fb163864d4d4a307425b2cd43db22af267d71c37146994a8c4805adc341bfba27af09fd80bd5eff51d877282a2fbfbfeb10199e7879e4b9d13a46d57fb7d786824853e1cc89cafbaf14de1e924c944feb8b626ce0207d6f9fa9d849eecac69b42d6e7a23bd5124d49622b44b35c5b15fb0e6a7781a503f1a4e062e015d557d95d44d9d8b0799b3aafce83d5d4"); + } + + #[test] + fn try_encrypt_rs_18_pad_31() { + // This test is also interesting because the data length (54) is a + // multiple of rs (18). We'll allocate memory to hold 4 records, but only + // write 3. + let ciphertext = try_encrypt( + "7830577bafcfc45828da0c40aab09fb227bfeae068aab8c064222acbe6effd34", + "0400b833e481a99aa330dcb277922d5f84af2e9ce611ad2ad3ed0f5b431912d35ea72fc5bf76b769d9526778f5abfa058650988da5e531ff82d1a7043794c71706", + "04c3d714cb42e2b0a1d6f98599e2f186b8c2ba6f6fab5e09a2abca865c0805892b2c3729330ef83dc9df4b44362b039a0609d36beb9321a431ec123506ddd90f24", + "e4d7b79decdede12c3e9d90d3e05730f", + "e49888d2b28f277f847bc5de96f0f81b", + 31, + 18, + "Push the button, Frank!", + ).unwrap(); + assert_eq!(ciphertext, "e49888d2b28f277f847bc5de96f0f81b00000012410400b833e481a99aa330dcb277922d5f84af2e9ce611ad2ad3ed0f5b431912d35ea72fc5bf76b769d9526778f5abfa058650988da5e531ff82d1a7043794c717063aeb958bf116bccf50742fd4d69bd0ea7e3f611c709bf2cdf5cd47c6426cb8323b5398c43c0d0b92cc982da1c24ce5fee2b203f7ad78ca44f0490f3407f5fee883266ee47035195de0fe6d8a75e487df256db597a75e45ae4fb55b8259cb0b2d19e7b05714267eb560ae072b7a665951917a068732df309be256f90f2adda32f05feaa5e9b0695bca2ccf22aaefc7da9ceebc5d40c12d32adb5c84cb320af944016095362febba4ffa4a99830e4958ea2bba508cb683a58d2027d4b74726a853b24b47ccba751abe9d9ab2da9ec2ba9c7ccf0cf17305bae314d38a687618b0772fcb71d4419027a4bf435cb721aad74efc179981b7169604bf97ecac41e73884456933734818132923b56c152d6c9e59aef995aca59de0bf2c803a07180889670a08e64a20d2bfa853e0112872947baaaffb510cc9e75d6310ed6aacbd2e0ba3a29be42c6532ea4e3346e1f0571646371c71665e3fac9d76faee1f122e64d490dd2a3e31816eab583f172841a075d205f318714a8c70ce0f327f4d92b8c9dcb813e6d24fe85633f1a9c7c1e4a1fb314dd5fe3e280e3908f36c8cbfb80b7d9243abaffa65c216cf1aa8b8d626a630dfe8186ce977a5b8f3649d3753b9176c367e4e07f220a175806138e88825a2f3498420582b96209658bbfa8f2ba6933a83c25edb269187796542e2ac49b8078636bddc268e11625e8bff9f0a343d3a4c06080ef0803b8dcd8e841d0e2759e483ea19b903324d9ec4d52f491acef3eeff441c37881c7593eac31621337a5e8659f93e20079b0e26ebfe56c10455d10971130bd2a2c159c74f48b2e526530a76f64cca2efb246e793d11fb75a668018e70c3107100f81ba3b16ae40a838f18d4c47f1d7132f174688ec5382394e0119921731a16879b858ff38f72851ea3d9f5263fec5a606d1271a89b84cca53ed73c5254e245bf8f2f27c2c1c87f39eea78c7017c8c6b5ab01663032b58da31057285e56c203f4e48d6789c66b2695a900e00482bd846559ecddd40264b38e279647d1ec0fccdc1881838bbe0c835e2690ef058b8f6a03e29cd9eb9584e97fbc309773c3688e5e03f9d38e3e4548738a5f569c59147d3e823cccac71d5e8825d5134ce9813cd0b8f9627a3dbfa45b83a59c83d2b4d3ad437778a3cb1bc77ba16c92306f4261a2a1f0d5c7edaecf926f92d7c9dfcae87513a68b8c7ef7c63264b858767c11aaa41d27c636f52e28551e93a969cdc96d43867b7cbd68fe0357bd33415faf22aaeebc957f4b5737a04ab7277b4ed4008f09edaff5a6db69f6cb06f3d0b76688906b2f53b27e63f3728ba2eda505fb1b32f81dddc6d305fd5949edd05490cb1618f0ce1430e9f5edf50012dc3"); + } + + #[test] + fn test_decrypt_rs_24_pad_0() { + let plaintext = try_decrypt( + "c899d11d32e2b7e6fe7498786f50f23b98ace5397ad261de39ba6449ecc12cad", + "04b3fc72e4365cbeb5c78862396eb5e66fd905b483a1b3eac04695f4b802e5b493c5e3b70eb427b6c728b2b204fc255fa218cb45f34d235242705e0d1ea87236e0", + "996fad8b50aa2d02b83f26412b2e2aee", + "495ce6c8de93a4539e862e8634993cbb0000001841043c3378a2c0ab954e1498718e85f08bb723fb7d25e135a663fe385884eb8192336bf90a54ed720f1c045c0b405e9bbc3a2142b16c89086734c374ebaf7099e6427e2d32c8ada5018703c54b10b481e1027d7209d8c6b43553fa133afa597f2ddc45a5ba8140944e6490bb8d6d99ba1d02e60d95f48ce644477c17231d95b97a4f95dd" + ).unwrap(); + assert_eq!(plaintext, "I am the walrus"); + } + + #[test] + fn test_decrypt_rs_49_pad_84_ciphertext_len_falls_on_record_boundary() { + let plaintext = try_decrypt( + "67004a4ea820deed8e49db5e9480e63d3ea3cce1ae8e1a60609713d527d001ef", + "04014e8f14b92da07ce083b93f96367e87b217a47f7ef2ee93a9d343aa063e575a9f30d59c690c6a39b3fc815b150ca7dd149601741337b53507a51f41b173a721", + "95f17570e508ef6a2b2ad1b4f5cade33", + "fb2883cec1c4fcadd6d1371f6ea491e00000003141042d441ee7f9ff6a0329a64927d0524fdbe7b22c6fb65e10ab4fdc038f94420a0ca3fa28dad36c84ec91a162eae078faad2c1ced78de8113e19602b20e894f4976b973e2fcf682fa0c8ccd9af3d5bff1ede16fad5a31ce19d38b5e1fe1f78a4fad842bbc10254c2c6cdd96a2b55284d972c53cad8c3bacb10f5f57eb0d4a4333b604102ba117cae29108fbd9f629a8ba6960dd01945b39ed37ba706c434a10fd2bd2094ff9249bcdad45135f5fe45fcd38071f8b2d3941afda439810d77aacaf7ce50b54325bf58c9503337d073785a323dfa343" + ).unwrap(); + assert_eq!(plaintext, "Hello, world"); + } + + #[test] + fn test_decrypt_ietf_rfc() { + let plaintext = try_decrypt( + "ab5757a70dd4a53e553a6bbf71ffefea2874ec07a6b379e3c48f895a02dc33de", + "042571b2becdfde360551aaf1ed0f4cd366c11cebe555f89bcb7b186a53339173168ece2ebe018597bd30479b86e3c8f8eced577ca59187e9246990db682008b0e", + "05305932a1c7eabe13b6cec9fda48882", + "0c6bfaadad67958803092d454676f397000010004104fe33f4ab0dea71914db55823f73b54948f41306d920732dbb9a59a53286482200e597a7b7bc260ba1c227998580992e93973002f3012a28ae8f06bbb78e5ec0ff297de5b429bba7153d3a4ae0caa091fd425f3b4b5414add8ab37a19c1bbb05cf5cb5b2a2e0562d558635641ec52812c6c8ff42e95ccb86be7cd" + ).unwrap(); + assert_eq!(plaintext, "When I grow up, I want to be a watermelon"); + } + + #[test] + fn test_decrypt_rs_18_pad_0() { + let plaintext = try_decrypt( + "27433fab8970b3cb5284b61183efb46286562cd2a7330d8cae960911a5571d0c", + "04515d4326355652399da24b2be9241e633b5cf14faf0cf3a6fd60317b954c0a2f4848548004b27b0cf7480bc810c6bec03a8fb79c8ea00fc8b05e00f8834563ef", + "d65a04df95f2db5e604839f717dcde79", + "7caebdbc20938ee340a946f1bd4f68f100000012410437cfdb5223d9f95eaa02f6ed940ff22eaf05b3622e949dc3ce9f335e6ef9b26aeaacca0f74080a8b364592f2ccc6d5eddd43004b70b91887d144d9fa93f16c3bc7ea68f4fd547a94eca84b16e138a6080177" + ).unwrap(); + assert_eq!(plaintext, "1"); + } + + #[test] + fn test_decrypt_missing_header_block() { + let err = try_decrypt( + "1be83f38332ef09681faf3f307b1ff2e10cab78cc7cdab683ac0ee92ac3f6ee1", + "04dba991ca215343f36bdd3e857cafde3d18bf57f1835b2833bad414f0884162051ac96a0b24490037d07cf528e4e18e100a1a64eb744748544bf1e220dabacf2c", + "3471bb98481e02533bf39542bcf3dba4", + "45b74d2b69be9b074de3b35aa87e7c15611d", + ) + .unwrap_err(); + match err { + Error::HeaderTooShort => {} + _ => unreachable!(), + }; + } + + #[test] + fn test_decrypt_truncated_sender_key() { + let err = try_decrypt( + "ce88e8e0b3057a4752eb4c8fa931eb621c302da5ad03b81af459cf6735560cae", + "04a325d99084c40de0ce722a042c448d94a32691721ca79e3cf745e78c69886194b02cea19224176795a9d4dbbb2073af2ccd6fa6f0a4c7c4968556be502a3ba81", + "5c31e0d96d9a139899ac0969d359f740", + "de5b696b87f1a15cb6adebdd79d6f99e000000120100b6bc1826c37c9f73dd6b4859c2b505181952", + ) + .unwrap_err(); + match err { + Error::InvalidKeyLength => {} + _ => unreachable!(), + }; + } + + #[test] + fn test_decrypt_truncated_auth_secret() { + let err = try_decrypt( + "60c7636a517de7039a0ac2d0e3064400794c78e7e049398129a227cee0f9a801", + "04fdd04128a85c05896d7f81fe118bdcb887b9f3c1ff4183adc4c824d128607300e986b2dfb5a610e5af43e408a00730584f93e3dfddfc44737d5f08fb2d6f8916", + "355a38cd6d9bef15990e2d3308dbd600", + "8115f4988b8c392a7bacb43c8f1ac5650000001241041994483c541e9bc39a6af03ff713aa7745c284e138a42a2435b797b20c4b698cf5118b4f8555317c190eabebfab749c164d3f6bdebe0d441719131a357d8890a13c4dbd4b16ff3dd5a83f7c91ad6e040ac42730a7f0b3cd3245e9f8d6ff31c751d410cfd" + ).unwrap_err(); + match err { + Error::OpenSSLError(_) => {} + _ => unreachable!(), + }; + } + + #[test] + fn test_decrypt_early_final_record() { + let err = try_decrypt( + "5dda1d918bc407ba3cda12cb8014d49aa7e0269002820304466bc80034ca9240", + "04c95c6520dad11e8f6a1bf8031a40c2a4ee1045c1903be06a1dfa7f829cceb2de02481ae6bd0476121b12c5532d0b231788077efa0683a5bfe0d62339b251cb35", + "40c241fde4269ee1e6d725592d982718", + "dbe215507d1ad3d2eaeabeae6e874d8f0000001241047bc4343f34a8348cdc4e462ffc7c40aa6a8c61a739c4c41d45125505f70e9fc5f9efa86852dd488dcf8e8ea2cafb75e07abd5ee7c9d5c038bafef079571b0bda294411ce98c76dd031c0e580577a4980a375e45ed30429be0e2ee9da7e6df8696d01b8ec" + ).unwrap_err(); + match err { + Error::DecryptPadding => {} + _ => unreachable!(), + }; + } +} + +// ===================== +#[cfg(all(test, feature = "backend-openssl"))] +mod aesgcm_tests { + use base64; + + use super::*; + + fn try_decrypt( + priv_key: &str, + pub_key: &str, + auth_secret: &str, + block: &AesGcmEncryptedBlock, + ) -> Result<String> { + // The AesGcmEncryptedBlock is composed from the `Crypto-Key` & `Encryption` headers, and post body + // The Block will attempt to decode the base64 strings for dh & salt, so no additional action needed. + // Since the body is most likely not encoded, it is expected to be a raw buffer of [u8] + let priv_key_raw = base64::decode_config(priv_key, base64::URL_SAFE_NO_PAD)?; + let pub_key_raw = base64::decode_config(pub_key, base64::URL_SAFE_NO_PAD)?; + let ec_key = EcKeyComponents::new(priv_key_raw, pub_key_raw); + let cryptographer = crypto::holder::get_cryptographer(); + let priv_key = cryptographer.import_key_pair(&ec_key)?; + let auth_secret = base64::decode_config(auth_secret, base64::URL_SAFE_NO_PAD)?; + let plaintext = AesGcmEceWebPush::decrypt(&*priv_key, &auth_secret, &block)?; + Ok(String::from_utf8(plaintext).unwrap()) + } + + #[test] + fn test_decode() { + // generated the content using pywebpush, which verified against the client. + let auth_raw = "LsuUOBKVQRY6-l7_Ajo-Ag"; + let priv_key_raw = "yerDmA9uNFoaUnSt2TkWWLwPseG1qtzS2zdjUl8Z7tc"; + let pub_key_raw = "BLBlTYure2QVhJCiDt4gRL0JNmUBMxtNB5B6Z1hDg5h-Epw6mVFV4whoYGBlWNY-ENR1FObkGFyMf7-6ZMHMAxw"; + + // Incoming Crypto-Key: dh= + let dh = "BJvcyzf8ocm6F7lbFePebtXU7OHkmylXN9FL2g-yBHwUKqo6cD-FP1h5SHEQQ-xEgJl-F0xEEmSaEx2-qeJHYmk"; + // Incoming Encryption-Key: salt= + let salt = "8qX1ZgkLD50LHgocZdPKZQ"; + // Incoming Body (this is normally raw bytes. It's encoded here for presentation) + let ciphertext = base64::decode_config("8Vyes671P_VDf3G2e6MgY6IaaydgR-vODZZ7L0ZHbpCJNVaf_2omEms2tiPJiU22L3BoECKJixiOxihcsxWMjTgAcplbvfu1g6LWeP4j8dMAzJionWs7OOLif6jBKN6LGm4EUw9e26EBv9hNhi87-HaEGbfBMGcLvm1bql1F", + base64::URL_SAFE_NO_PAD).unwrap(); + let plaintext = "Amidst the mists and coldest frosts I thrust my fists against the\nposts and still demand to see the ghosts.\n"; + + let block = AesGcmEncryptedBlock::new( + &base64::decode_config(dh, base64::URL_SAFE_NO_PAD).unwrap(), + &base64::decode_config(salt, base64::URL_SAFE_NO_PAD).unwrap(), + 4096, + ciphertext, + ) + .unwrap(); + + let result = try_decrypt(priv_key_raw, pub_key_raw, auth_raw, &block).unwrap(); + + assert!(result == plaintext) + } + + #[test] + fn test_decode_padding() { + // generated the content using pywebpush, which verified against the client. + let auth_raw = "LsuUOBKVQRY6-l7_Ajo-Ag"; + let priv_key_raw = "yerDmA9uNFoaUnSt2TkWWLwPseG1qtzS2zdjUl8Z7tc"; + let pub_key_raw = "BLBlTYure2QVhJCiDt4gRL0JNmUBMxtNB5B6Z1hDg5h-Epw6mVFV4whoYGBlWNY-ENR1FObkGFyMf7-6ZMHMAxw"; + + // Incoming Crypto-Key: dh= + let dh = "BCX7KJ_1Em-LjeB56E2KDoMjKDhTaDhjv8c6dwbvZQZ_Gsfp3AT54x2zYUPcBwd1GVyGsk55ProJ98cFrVxrPz4"; + // Incoming Encryption-Key: salt= + let salt = "x2I2OZpSCoe-Cc5UW36Nng"; + // Incoming Body (this is normally raw bytes. It's encoded here for presentation) + let ciphertext = base64::decode_config("Ua3-WW5kTbt11dBTiXBP6_hLBYhBNOtDFfue5QHMTd2DicL0wutDnt5z9pjRJ76w562egPq5qro95YLnsX0NWGmDQbsQ0Azds6jcBGsxHPt0p5GELAtR4AJj2OsB_LV7dTuGHN2SqsyXLARjTFN2wsF3xWhmuw", + base64::URL_SAFE_NO_PAD).unwrap(); + let plaintext = "Tabs are the real indent"; + + let block = AesGcmEncryptedBlock::new( + &base64::decode_config(dh, base64::URL_SAFE_NO_PAD).unwrap(), + &base64::decode_config(salt, base64::URL_SAFE_NO_PAD).unwrap(), + 4096, + ciphertext, + ) + .unwrap(); + + let result = try_decrypt(priv_key_raw, pub_key_raw, auth_raw, &block).unwrap(); + + println!( + "Result: b64={}", + base64::encode_config(&result, base64::URL_SAFE_NO_PAD) + ); + println!( + "Plaintext: b64={}", + base64::encode_config(&plaintext, base64::URL_SAFE_NO_PAD) + ); + assert!(result == plaintext) + } + + #[test] + fn test_e2e() { + let (local_key, remote_key) = generate_keys().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 = AesGcmEceWebPush::encrypt_with_keys( + &*local_key, + &*remote_public, + &auth_secret, + plaintext, + params, + ) + .unwrap(); + let decrypted = AesGcmEceWebPush::decrypt(&*remote_key, &auth_secret, &ciphertext).unwrap(); + assert_eq!(decrypted, plaintext.to_vec()); + } + + #[test] + fn test_keygen() { + let cryptographer = crypto::holder::get_cryptographer(); + cryptographer.generate_ephemeral_keypair().unwrap(); + } + + // If decode using externally validated data works, and e2e using the same decoder work, things + // should encode/decode. + // Other tests to be included if required, but skipping for now because of time constraints. +} |