diff --git a/Cargo.toml b/Cargo.toml index acd624b..978a80c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ece" -version = "1.3.0" +version = "1.4.0-alpha1" authors = ["Firefox Sync Team ", "JR Conlin "] license = "MPL-2.0" edition = "2018" @@ -12,6 +12,7 @@ keywords = ["http-ece", "web-push"] byteorder = "1.3" thiserror = "1.0" base64 = "0.12" +hex = "0.4" hkdf = { version = "0.9", optional = true } lazy_static = { version = "1.4", optional = true } once_cell = "1.4" @@ -19,13 +20,11 @@ openssl = { version = "0.10", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } sha2 = { version = "0.9", optional = true } -[dev-dependencies] -hex = "0.4" - [features] default = ["backend-openssl", "serializable-keys"] serializable-keys = ["serde"] backend-openssl = ["openssl", "lazy_static", "hkdf", "sha2"] +backend-test-helper = [] [package.metadata.release] no-dev-version = true diff --git a/README.md b/README.md index 434abd1..f7e0dfe 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,98 @@ [Latest Version]: https://img.shields.io/crates/v/ece.svg [crates.io]: https://crates.io/crates/ece -*This crate has not been security reviewed yet, use at your own risk ([tracking issue](https://github.com/mozilla/rust-ece/issues/18))*. +*This crate has not been security reviewed yet, use at your own risk +([tracking issue](https://github.com/mozilla/rust-ece/issues/18))*. -[ece](https://crates.io/crates/ece) is a Rust implementation of the HTTP Encrypted Content-Encoding standard (RFC 8188). It is a port of the [ecec](https://github.com/web-push-libs/ecec) C library. -This crate is destined to be used by higher-level Web Push libraries, both on the server and the client side. +The [ece](https://crates.io/crates/ece) crate is a Rust implementation of Message Encryption for Web Push +([RFC8291](https://tools.ietf.org/html/rfc8291)) and the HTTP Encrypted Content-Encoding scheme +([RFC8188](https://tools.ietf.org/html/rfc8188)) on which it is based. -[Documentation](https://docs.rs/ece/) +It provides low-level cryptographic "plumbing" and is destined to be used by higher-level Web Push libraries, both on +the server and the client side. It is a port of the [ecec](https://github.com/web-push-libs/ecec) C library. -## Cryptographic backends - -This crate is designed to be used with different crypto backends. At the moment only [openssl](https://github.com/sfackler/rust-openssl) is supported. +[Full Documentation](https://docs.rs/ece/) ## Implemented schemes -Currently, two HTTP ece schemes are available to consumers of the crate: -- The newer [RFC8188](https://tools.ietf.org/html/rfc8188) `aes128gcm` standard. -- The legacy [draft-03](https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-03) `aesgcm` scheme. +This crate implements both the published Web Push Encryption scheme, and a legacy scheme from earlier drafts +that is still widely used in the wild: + +* `aes128gcm`: the scheme described in [RFC8291](https://tools.ietf.org/html/rfc8291) and + [RFC8188](https://tools.ietf.org/html/rfc8188) +* `aesgcm`: the draft scheme described in + [draft-ietf-webpush-encryption-04](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04) and + [draft-ietf-httpbis-encryption-encoding-03](https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-03_) + +It does not support, and we have no plans to ever support, the obsolete `aesgcm128` scheme +from [earlier drafts](https://tools.ietf.org/html/draft-thomson-http-encryption-02). + +## Usage + +To receive messages via WebPush, the receiver must generate an EC keypair and a symmetric authentication secret, +then distribute the public key and authentication secret to the sender: + +``` +let (keypair, auth_secret) = ece::generate_keypair_and_auth_secret()?; +let pubkey = keypair.pub_as_raw(); +// Base64-encode the `pubkey` and `auth_secret` bytes and distribute them to the sender. +``` + +The sender can encrypt a Web Push message to the receiver's public key: + +``` +let ciphertext = ece::encrypt(&pubkey, &auth_secret, b"payload")?; +``` + +And the receiver can decrypt it using their private key: + +``` +let plaintext = ece::decrypt(&keypair, &auth_secret, &ciphertext)?; +``` + +That's pretty much all there is to it! It's up to the higher-level library to manage distributing the encrypted payload, +typically by arranging for it to be included in a HTTP response with `Content-Encoding: aes128gcm` header. + +### Legacy `aesgcm` encryption + +The legacy `aesgcm` scheme is more complicated, because it communicates some encryption parameters in HTTP header fields +rather than as part of the encrypted payload. When used for encryption, the sender must deal with `Encryption` and +`Crypto-Key` headers in addition to the ciphertext: + +``` +let encrypted_block = ece::legacy::encrypt_aesgcm(pubkey, auth_secret, b"payload")?; +for (header, &value) in encrypted_block.headers().iter() { + // Set header to corresponding value +} +// Send encrypted_block.body() as the body +``` + +When receiving an `aesgcm` message, the receiver needs to parse encryption parameters from the `Encryption` +and `Crypto-Key` fields: + +``` +// Parse `rs`, `salt` and `dh` from the `Encryption` and `Crypto-Key` headers. +// You'll need to consult the spec for how to do this; we might add some helpers one day. +let encrypted_block = ece::AesGcmEncryptedBlock::new(dh, rs, salt, ciphertext); +let plaintext = ece::legacy::decrypt_aesgcm(keypair, auth_secret, encrypted_block)?; +``` + +### Unimplemented Features + +* We do not implement streaming encryption or decryption, although the ECE scheme is designed to permit it. +* We do not implement support for encrypting or decrypting across multiple records, although the ECE scheme is designed to permit it. +* We do not support customizing the record size parameter during encryption, but do check it during decryption. + * The default record size is 4096 bytes, which effectively limits the size of plaintext that can be encrypted to 3993 bytes. +* We do not support customizing the padding bytes added during encryption. + * We currently select the padding length at random for each encryption, but this is an implementation detail and + should not be relied on. + +These restrictions might be lifted in future, if it turns out that we need them. + +## Cryptographic backends + +This crate is designed to use pluggable backend implementations of low-level crypto primitives. different crypto +backends. At the moment only [openssl](https://github.com/sfackler/rust-openssl) is supported. ## Release process @@ -34,4 +110,3 @@ make sure you have it installed and then: it's proposing to do seem sensible. 3. Run `cargo release [major|minor|patch]` to prepare, commit, tag and publish the release. 4. Make a PR from your `release-vX.Y.Z` branch to request it be merged to the main branch. - diff --git a/src/aes128gcm.rs b/src/aes128gcm.rs index 34d7dc0..c81d406 100644 --- a/src/aes128gcm.rs +++ b/src/aes128gcm.rs @@ -29,27 +29,8 @@ const ECE_AES128GCM_NONCE_INFO: &str = "Content-Encoding: nonce\0"; /// Web Push encryption structure for the AES128GCM encoding scheme ([RFC8591](https://tools.ietf.org/html/rfc8291)) /// /// This structure is meant for advanced use. For simple encryption/decryption, use the top-level [`encrypt`](crate::encrypt) and [`decrypt`](crate::decrypt) functions. -pub struct Aes128GcmEceWebPush; +pub(crate) 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> { - 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( diff --git a/src/aesgcm.rs b/src/aesgcm.rs index aa9afba..3e96619 100644 --- a/src/aesgcm.rs +++ b/src/aesgcm.rs @@ -25,21 +25,20 @@ const ECE_WEBPUSH_RAW_KEY_LENGTH: usize = 65; const ECE_WEBPUSH_IKM_LENGTH: usize = 32; pub struct AesGcmEncryptedBlock { - pub dh: Vec, - pub salt: Vec, - pub rs: u32, - pub ciphertext: Vec, + pub(crate) dh: Vec, + pub(crate) salt: Vec, + pub(crate) rs: u32, + pub(crate) ciphertext: Vec, } impl AesGcmEncryptedBlock { - pub fn aesgcm_rs(rs: u32) -> u32 { + 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], @@ -87,35 +86,20 @@ impl AesGcmEncryptedBlock { } /// 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) } } -/// 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)) + +/// 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 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 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 { - 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, - ) - } +/// 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( @@ -201,7 +185,7 @@ impl EceWebPush for AesGcmEceWebPush { Ok(&block[(2 + padding_size)..]) } - /// Derives the "aesgcm" decryption keyn and nonce given the receiver private + /// 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, diff --git a/src/common.rs b/src/common.rs index 984e254..f6433ae 100644 --- a/src/common.rs +++ b/src/common.rs @@ -3,60 +3,67 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::{ - crypto::{self, LocalKeyPair, RemotePublicKey}, + crypto::{self, Cryptographer, 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; +pub(crate) const ECE_AES_KEY_LENGTH: usize = 16; +pub(crate) 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; +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; +pub(crate) const ECE_WEBPUSH_AUTH_SECRET_LENGTH: usize = 16; +pub(crate) const ECE_WEBPUSH_DEFAULT_RS: u32 = 4096; // TODO: Make it nicer to use with a builder pattern. -pub struct WebPushParams { +pub(crate) struct WebPushParams { pub rs: u32, pub pad_length: usize, pub salt: Option>, } -impl WebPushParams { - /// Random salt, record size = 4096 and padding length = 0. - pub fn default() -> Self { +impl Default for WebPushParams { + fn default() -> Self { + // Random salt, record size = 4096 and padding length = 0. Self { rs: ECE_WEBPUSH_DEFAULT_RS, - pad_length: 2, + pad_length: 0, 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) -> Self { - Self { - rs, - pad_length, - salt: Some(salt), - } +/// Randomly select a padding length to apply to the given plaintext. +/// +/// Some care is taken not to exceed the maximum record size. +/// +pub fn get_random_padding_length( + plaintext: &[u8], + cryptographer: &dyn Cryptographer, +) -> Result { + const MAX_SIZE: usize = (ECE_WEBPUSH_DEFAULT_RS - 1) as usize; + let mut padr = [0u8; 2]; + cryptographer.random_bytes(&mut padr)?; + let mut pad_length = ((usize::from(padr[0]) + (usize::from(padr[1]) << 8)) % MAX_SIZE) + 1; + if plaintext.len() + pad_length > MAX_SIZE { + pad_length = MAX_SIZE - plaintext.len(); } + Ok(pad_length) } -pub enum EceMode { +pub(crate) enum EceMode { ENCRYPT, DECRYPT, } -pub type KeyAndNonce = (Vec, Vec); +pub(crate) type KeyAndNonce = (Vec, Vec); -pub trait EceWebPush { +pub(crate) trait EceWebPush { fn common_encrypt( local_prv_key: &dyn LocalKeyPair, remote_pub_key: &dyn RemotePublicKey, @@ -151,6 +158,12 @@ pub trait EceWebPush { 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 counter > 1 { + return Err(Error::PlaintextTooLong); + } Ok(ciphertext) } @@ -184,6 +197,12 @@ pub trait EceWebPush { )?; 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 records_count > 1 { + return Err(Error::MultipleRecordsNotSupported); + } let items = chunks .enumerate() .map(|(count, record)| { diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 1f36481..a912ab6 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -103,3 +103,71 @@ pub trait Cryptographer: Send + Sync + 'static { ) -> Result>; fn random_bytes(&self, dest: &mut [u8]) -> Result<()>; } + +/// Run a small suite of tests to check that a `Cryptographer` backend is working correctly. +/// +/// You should only use this is you're implementing a custom `Cryptographer` and want to check +/// that it is working as intended. This function will panic if the tests fail. +/// +#[cfg(any(test, feature = "backend-test-helper"))] +pub fn test_cryptographer(cryptographer: T) { + use crate::{aes128gcm::Aes128GcmEceWebPush, common::WebPushParams}; + + // These are test data from the RFC. + let plaintext = "When I grow up, I want to be a watermelon"; + let ciphertext = hex::decode("0c6bfaadad67958803092d454676f397000010004104fe33f4ab0dea71914db55823f73b54948f41306d920732dbb9a59a53286482200e597a7b7bc260ba1c227998580992e93973002f3012a28ae8f06bbb78e5ec0ff297de5b429bba7153d3a4ae0caa091fd425f3b4b5414add8ab37a19c1bbb05cf5cb5b2a2e0562d558635641ec52812c6c8ff42e95ccb86be7cd").unwrap(); + + // First, a trial encryption. + let private_key = + hex::decode("c9f58f89813e9f8e872e71f42aa64e1757c9254dcc62b72ddc010bb4043ea11c").unwrap(); + let public_key = hex::decode("04fe33f4ab0dea71914db55823f73b54948f41306d920732dbb9a59a53286482200e597a7b7bc260ba1c227998580992e93973002f3012a28ae8f06bbb78e5ec0f").unwrap(); + let ec_key = EcKeyComponents::new(private_key, public_key); + let local_key_pair = cryptographer.import_key_pair(&ec_key).unwrap(); + + let remote_pub_key = hex::decode("042571b2becdfde360551aaf1ed0f4cd366c11cebe555f89bcb7b186a53339173168ece2ebe018597bd30479b86e3c8f8eced577ca59187e9246990db682008b0e").unwrap(); + let remote_pub_key = cryptographer.import_public_key(&remote_pub_key).unwrap(); + let auth_secret = hex::decode("05305932a1c7eabe13b6cec9fda48882").unwrap(); + + let params = WebPushParams { + rs: 4096, + pad_length: 0, + salt: Some(hex::decode("0c6bfaadad67958803092d454676f397").unwrap()), + }; + + assert_eq!( + Aes128GcmEceWebPush::encrypt_with_keys( + &*local_key_pair, + &*remote_pub_key, + &auth_secret, + plaintext.as_bytes(), + params, + ) + .unwrap(), + ciphertext + ); + + // Now, a trial decryption. + let private_key = + hex::decode("ab5757a70dd4a53e553a6bbf71ffefea2874ec07a6b379e3c48f895a02dc33de").unwrap(); + let public_key = hex::decode("042571b2becdfde360551aaf1ed0f4cd366c11cebe555f89bcb7b186a53339173168ece2ebe018597bd30479b86e3c8f8eced577ca59187e9246990db682008b0e").unwrap(); + let ec_key = EcKeyComponents::new(private_key, public_key); + let local_key_pair = cryptographer.import_key_pair(&ec_key).unwrap(); + + assert_eq!( + Aes128GcmEceWebPush::decrypt(&*local_key_pair, &auth_secret, ciphertext.as_ref(),).unwrap(), + plaintext.as_bytes() + ); +} + +#[cfg(all(test, feature = "backend-openssl"))] +mod tests { + use super::*; + + // All of the tests in this crate exercise the default backend, so running this here + // doesn't tell us anyting more about the default backend. Instead, it tells us whether + // the `test_cryptographer` function is working correctly! + #[test] + fn test_default_cryptograher() { + test_cryptographer(super::openssl::OpensslCryptographer); + } +} diff --git a/src/error.rs b/src/error.rs index f4b6a6b..74f143c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -33,6 +33,12 @@ pub enum Error { #[error("Block too short")] BlockTooShort, + #[error("Plaintext is too long to fit in a single block")] + PlaintextTooLong, + + #[error("Decryption across multiple records is not supported")] + MultipleRecordsNotSupported, + #[error("Invalid decryption padding")] DecryptPadding, diff --git a/src/legacy.rs b/src/legacy.rs index 72854cb..fdefe22 100644 --- a/src/legacy.rs +++ b/src/legacy.rs @@ -2,42 +2,48 @@ * 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 use crate::aesgcm::AesGcmEncryptedBlock; use crate::{ - aesgcm::{AesGcmEceWebPush, AesGcmEncryptedBlock}, - common::WebPushParams, + aesgcm::AesGcmEceWebPush, + common::{get_random_padding_length, WebPushParams}, crypto::EcKeyComponents, error::*, }; /// Encrypt a block using legacy AESGCM encoding. /// -/// * `remote_pub` : the remote public key -/// * `remote_auth` : the remote authorization token -/// * `salt` : the locally generated random salt +/// * `remote_pub` : The public key of the remote message recipient +/// * `remote_auth` : The authentication secret of the remote message recipient /// * `data` : the data to encrypt /// +/// You should only use this function if you know that you definitely need +/// to use the legacy format. The [`encrypt`](crate::encrypt) function should +/// be preferred where possible. +/// pub fn encrypt_aesgcm( remote_pub: &[u8], remote_auth: &[u8], - salt: &[u8], data: &[u8], ) -> Result { 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 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)); + let params = WebPushParams { + pad_length: get_random_padding_length(&data, cryptographer)?, + ..Default::default() + }; AesGcmEceWebPush::encrypt_with_keys(&*local_key_pair, &*remote_key, &remote_auth, data, params) } /// Decrypt a block using legacy AESGCM encoding. /// -/// * `components` : the locally generated private key components. -/// * `auth` : the locally generated auth token (this value was shared with the encryptor). -/// * `data` : the encrypted data block +/// * `components` : The public and private key components of the local message recipient +/// * `auth` : The authentication secret of the remote message recipient +/// * `data` : The encrypted data block +/// +/// You should only use this function if you know that you definitely need +/// to use the legacy format. The [`decrypt`](crate::decrypt) function should +/// be preferred where possible. /// pub fn decrypt_aesgcm( components: &EcKeyComponents, @@ -52,6 +58,7 @@ pub fn decrypt_aesgcm( #[cfg(all(test, feature = "backend-openssl"))] mod aesgcm_tests { use super::*; + use base64; use hex; #[derive(Debug)] @@ -81,9 +88,13 @@ mod aesgcm_tests { 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 salt = Some(hex::decode(salt).unwrap()); let plaintext = plaintext.as_bytes(); - let params = WebPushParams::new(rs, pad_length, salt); + let params = WebPushParams { + rs, + pad_length, + salt, + }; let encrypted_block = AesGcmEceWebPush::encrypt_with_keys( &*local_key_pair, &*remote_pub_key, @@ -149,10 +160,7 @@ mod aesgcm_tests { fn test_conv_fn() -> Result<()> { let (local_key, auth) = crate::generate_keypair_and_auth_secret()?; let plaintext = b"There was a little ship that had never sailed"; - let mut salt = vec![0u8; 16]; - let cryptographer = crate::crypto::holder::get_cryptographer(); - cryptographer.random_bytes(&mut salt)?; - let encoded = encrypt_aesgcm(&local_key.pub_as_raw()?, &auth, &salt, plaintext).unwrap(); + let encoded = encrypt_aesgcm(&local_key.pub_as_raw()?, &auth, plaintext).unwrap(); let decoded = decrypt_aesgcm(&local_key.raw_components()?, &auth, &encoded)?; assert_eq!(decoded, plaintext.to_vec()); Ok(()) @@ -193,4 +201,82 @@ mod aesgcm_tests { ).unwrap(); assert_eq!(plaintext, "I am the walrus"); } + + // We have some existing test data in b64, and some in hex, + // and it's easy to make a second `try_decrypt` helper function + // than to re-encode all the data. + fn try_decrypt_b64( + priv_key: &str, + pub_key: &str, + auth_secret: &str, + block: &AesGcmEncryptedBlock, + ) -> Result { + // 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 auth_secret = base64::decode_config(auth_secret, base64::URL_SAFE_NO_PAD)?; + let plaintext = decrypt_aesgcm(&ec_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_b64(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_b64(priv_key_raw, pub_key_raw, auth_raw, &block).unwrap(); + + assert!(result == plaintext) + } } diff --git a/src/lib.rs b/src/lib.rs index f9c62d9..1b6b676 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #![warn(rust_2018_idioms)] + mod aes128gcm; mod aesgcm; mod common; @@ -11,14 +12,17 @@ mod error; pub mod legacy; 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. +use crate::{ + aes128gcm::Aes128GcmEceWebPush, + common::{get_random_padding_length, WebPushParams, ECE_WEBPUSH_AUTH_SECRET_LENGTH}, +}; + +/// Generate a local ECE key pair and authentication secret. +/// pub fn generate_keypair_and_auth_secret( ) -> Result<(Box, [u8; ECE_WEBPUSH_AUTH_SECRET_LENGTH])> { let cryptographer = crypto::holder::get_cryptographer(); @@ -28,23 +32,23 @@ pub fn generate_keypair_and_auth_secret( Ok((local_key_pair, auth_secret)) } -/// Encrypt a block using default AES128GCM encoding. +/// Encrypt a block using the AES128GCM encryption scheme. /// -/// * `remote_pub` : The remote public key -/// * `remote_auth` : The remote authorization token -/// * `salt` : The locally generated random salt +/// * `remote_pub` : The public key of the remote message recipient +/// * `remote_auth` : The authentication secret of the remote message recipient /// * `data` : The data to encrypt /// -/// *For the legacy AESGCM version, go to* [`encrypt_aesgcm`](crate::legacy::encrypt_aesgcm) -pub fn encrypt(remote_pub: &[u8], remote_auth: &[u8], salt: &[u8], data: &[u8]) -> Result> { +/// For the equivalent function using legacy AESGCM encryption scheme +/// use [`legacy::encrypt_aesgcm`](crate::legacy::encrypt_aesgcm). +/// +pub fn encrypt(remote_pub: &[u8], remote_auth: &[u8], data: &[u8]) -> Result> { 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)); + let params = WebPushParams { + pad_length: get_random_padding_length(&data, cryptographer)?, + ..Default::default() + }; Aes128GcmEceWebPush::encrypt_with_keys( &*local_key_pair, &*remote_key, @@ -54,19 +58,23 @@ pub fn encrypt(remote_pub: &[u8], remote_auth: &[u8], salt: &[u8], data: &[u8]) ) } -/// Decrypt a block using default AES128GCM encoding. +/// Decrypt a block using the AES128GCM encryption scheme. /// -/// * `components` : The locally generated private key components. -/// * `auth` : The locally generated auth token (this value was shared with the encryptor) +/// * `components` : The public and private key components of the local message recipient +/// * `auth` : The authentication secret of the remote message recipient /// * `data` : The encrypted data block /// -/// *For the legacy AESGCM version, go to* [`decrypt_aesgcm`](crate::legacy::decrypt_aesgcm) +/// For the equivalent function using legacy AESGCM encryption scheme +/// use [`legacy::decrypt_aesgcm`](crate::legacy::decrypt_aesgcm). +/// pub fn decrypt(components: &EcKeyComponents, auth: &[u8], data: &[u8]) -> Result> { let cryptographer = crypto::holder::get_cryptographer(); let priv_key = cryptographer.import_key_pair(components).unwrap(); Aes128GcmEceWebPush::decrypt(&*priv_key, &auth, data) } +/// Generate a pair of keys; useful for writing tests. +/// #[cfg(all(test, feature = "backend-openssl"))] fn generate_keys() -> Result<(Box, Box)> { let cryptographer = crypto::holder::get_cryptographer(); @@ -99,9 +107,13 @@ mod aes128gcm_tests { 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 salt = Some(hex::decode(salt).unwrap()); let plaintext = plaintext.as_bytes(); - let params = WebPushParams::new(rs, pad_length, salt); + let params = WebPushParams { + rs, + pad_length, + salt, + }; let ciphertext = Aes128GcmEceWebPush::encrypt_with_keys( &*local_key_pair, &*remote_pub_key, @@ -129,6 +141,12 @@ mod aes128gcm_tests { Ok(String::from_utf8(plaintext).unwrap()) } + #[test] + fn test_keygen() { + let cryptographer = crypto::holder::get_cryptographer(); + cryptographer.generate_ephemeral_keypair().unwrap(); + } + #[test] fn test_e2e() { let (local_key, remote_key) = generate_keys().unwrap(); @@ -157,10 +175,7 @@ mod aes128gcm_tests { 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 encoded = encrypt(&local_key.pub_as_raw()?, &auth, plaintext).unwrap(); let decoded = decrypt(&local_key.raw_components()?, &auth, &encoded)?; assert_eq!(decoded, plaintext.to_vec()); Ok(()) @@ -181,61 +196,6 @@ mod aes128gcm_tests { 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( @@ -288,6 +248,11 @@ mod aes128gcm_tests { }; } + // TODO: when we refactor away the multi-record support, we need to restore single-record versions of these tests. + // IIUC they are testing important record-formatting edgecases that could still occur even with a single record, + // although I really with the test explained in more detail what the error was it's trying to test rather than + // just presenting a bunch of hex-encoded bytes... + /* #[test] fn test_decrypt_truncated_auth_secret() { let err = try_decrypt( @@ -315,107 +280,5 @@ mod aes128gcm_tests { _ => 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 { - // 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_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. + */ }