From a50068a51107f9d1950806588eef6e03b806dd63 Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Thu, 23 May 2019 20:06:05 +1000 Subject: [PATCH] Update the public API to remove footguns, and document it. This is a significant refactor of the public API of the crate, simplifying the API surface and removing some of the footgun potential noted by Martin in his review at https://github.com/mozilla/application-services/issues/1068. In particular: * The public `encrypt` functions no longer take a `salt` parameter. The right thing to do is to generate a new random `salt` for each encryption so we just do that for you automatically. * Many internal implementation details are now `pub(crate)` rather than `pub`, to avoid potential confusion from consumers. * We refuse to encrypt or decrypt across multiple records, because our only consumer in practice is webpush, and webpush restricts consumers to using only a single record. We still have the code lying around to encrypt/decrypt across record boundaries, but we don't have high confidence that it works correctly and intend to remove it in a future commit. So, may as well adjust the interface to reflect that while we're in here making breaking changes. To go along with the revised interface, this commit also significantly expands to docs in order to help set consumer expectations and context. --- Cargo.toml | 4 +- README.md | 96 ++++++++++++++++--- src/aes128gcm.rs | 21 +---- src/aesgcm.rs | 48 ++++------ src/common.rs | 53 +++++------ src/crypto/mod.rs | 67 +++++++++++++ src/error.rs | 6 ++ src/legacy.rs | 125 +++++++++++++++++++++---- src/lib.rs | 234 +++++++++++----------------------------------- 9 files changed, 365 insertions(+), 289 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index acd624b..20a0251 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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,9 +20,6 @@ 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"] diff --git a/README.md b/README.md index 434abd1..b514acb 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,95 @@ [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_) + +## 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 @@ -33,5 +106,4 @@ make sure you have it installed and then: 2. Run `cargo release --dry-run -vv [major|minor|patch]` and check that the things 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. - +4. Make a PR from your `release-vX.Y.Z` branch to request it be merged to the main branch. \ No newline at end of file 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..dce81ca 100644 --- a/src/aesgcm.rs +++ b/src/aesgcm.rs @@ -25,22 +25,23 @@ 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 { + #[cfg(test)] + pub(crate) 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( + #[cfg(test)] + pub(crate) fn new( dh: &[u8], salt: &[u8], rs: u32, @@ -87,35 +88,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 +187,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..5b6e955 100644 --- a/src/common.rs +++ b/src/common.rs @@ -10,53 +10,42 @@ 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), - } - } } -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 +140,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 +179,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..f0af07e 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -12,6 +12,8 @@ mod openssl; #[cfg(not(feature = "backend-openssl"))] pub use holder::{set_boxed_cryptographer, set_cryptographer}; +use crate::{aes128gcm::Aes128GcmEceWebPush, common::WebPushParams}; + pub trait RemotePublicKey: Send + Sync + 'static { /// Export the key component in the /// binary uncompressed point representation. @@ -103,3 +105,68 @@ 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. +/// +pub fn test_cryptographer(cryptographer: T) { + // 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..5c7ca6f 100644 --- a/src/legacy.rs +++ b/src/legacy.rs @@ -11,33 +11,46 @@ use crate::{ /// 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()?; + // Randomly select a padding length, without exceeding the record size. + // The magic constants here will go away in a future refactor. 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 mut pad_length = ((usize::from(padr[0]) + (usize::from(padr[1]) << 8)) % 4095) + 1; + if data.len() + pad_length > 4095 { + pad_length = 4095 - data.len() + } + let params = WebPushParams { + pad_length, + ..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 +65,7 @@ pub fn decrypt_aesgcm( #[cfg(all(test, feature = "backend-openssl"))] mod aesgcm_tests { use super::*; + use base64; use hex; #[derive(Debug)] @@ -81,9 +95,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 +167,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 +208,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..180c5bf 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::{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,31 @@ 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()?; + // Randomly select a padding length, without exceeding the record size. + // The magic constants here will go away in a future refactor. 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 mut pad_length = ((usize::from(padr[0]) + (usize::from(padr[1]) << 8)) % 4095) + 1; + if data.len() + pad_length > 4095 { + pad_length = 4095 - data.len() + } + let params = WebPushParams { + pad_length, + ..Default::default() + }; Aes128GcmEceWebPush::encrypt_with_keys( &*local_key_pair, &*remote_key, @@ -54,19 +66,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 +115,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 +149,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 +183,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 +204,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 +256,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( @@ -296,6 +269,7 @@ mod aes128gcm_tests { "355a38cd6d9bef15990e2d3308dbd600", "8115f4988b8c392a7bacb43c8f1ac5650000001241041994483c541e9bc39a6af03ff713aa7745c284e138a42a2435b797b20c4b698cf5118b4f8555317c190eabebfab749c164d3f6bdebe0d441719131a357d8890a13c4dbd4b16ff3dd5a83f7c91ad6e040ac42730a7f0b3cd3245e9f8d6ff31c751d410cfd" ).unwrap_err(); + println!() match err { Error::OpenSSLError(_) => {} _ => unreachable!(), @@ -315,107 +289,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. + */ }