summaryrefslogtreecommitdiffstats
path: root/third_party/rust/ece/src/common.rs
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/ece/src/common.rs')
-rw-r--r--third_party/rust/ece/src/common.rs251
1 files changed, 251 insertions, 0 deletions
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
+}