summaryrefslogtreecommitdiffstats
path: root/third_party/rust/ece/src/aes128gcm.rs
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/ece/src/aes128gcm.rs')
-rw-r--r--third_party/rust/ece/src/aes128gcm.rs222
1 files changed, 222 insertions, 0 deletions
diff --git a/third_party/rust/ece/src/aes128gcm.rs b/third_party/rust/ece/src/aes128gcm.rs
new file mode 100644
index 0000000000..041159fd96
--- /dev/null
+++ b/third_party/rust/ece/src/aes128gcm.rs
@@ -0,0 +1,222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::{
+ common::*,
+ crypto::{self, LocalKeyPair, RemotePublicKey},
+ error::*,
+};
+use byteorder::{BigEndian, ByteOrder};
+
+const ECE_AES128GCM_MIN_RS: u32 = 18;
+const ECE_AES128GCM_HEADER_LENGTH: usize = 21;
+// The max AES128GCM Key ID Length is 255 octets. We use far less of that because we use
+// the "key_id" to store the exchanged public key since we don't cache the key_ids.
+// Code fails if the key_id is not a public key length field.
+const ECE_AES128GCM_PAD_SIZE: usize = 1;
+
+const ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX: &str = "WebPush: info\0";
+const ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH: usize = 144; // 14 (prefix len) + 65 (pub key len) * 2;
+
+const ECE_WEBPUSH_IKM_LENGTH: usize = 32;
+const ECE_AES128GCM_KEY_INFO: &str = "Content-Encoding: aes128gcm\0";
+const ECE_AES128GCM_NONCE_INFO: &str = "Content-Encoding: nonce\0";
+
+// TODO: When done, remove the aes128gcm prefixes and the EC_ ones.
+// As for now it makes it easier to Ctrl + F into ecec :)
+
+pub struct Aes128GcmEceWebPush;
+impl Aes128GcmEceWebPush {
+ /// Encrypts a Web Push message using the "aes128gcm" scheme. This function
+ /// automatically generates an ephemeral ECDH key pair.
+ pub fn encrypt(
+ remote_pub_key: &dyn RemotePublicKey,
+ auth_secret: &[u8],
+ plaintext: &[u8],
+ params: WebPushParams,
+ ) -> Result<Vec<u8>> {
+ let cryptographer = crypto::holder::get_cryptographer();
+ let local_prv_key = cryptographer.generate_ephemeral_keypair()?;
+ Self::encrypt_with_keys(
+ &*local_prv_key,
+ remote_pub_key,
+ auth_secret,
+ plaintext,
+ params,
+ )
+ }
+
+ /// Encrypts a Web Push message using the "aes128gcm" scheme, with an explicit
+ /// sender key. The sender key can be reused.
+ pub fn encrypt_with_keys(
+ local_prv_key: &dyn LocalKeyPair,
+ remote_pub_key: &dyn RemotePublicKey,
+ auth_secret: &[u8],
+ plaintext: &[u8],
+ params: WebPushParams,
+ ) -> Result<Vec<u8>> {
+ let cryptographer = crypto::holder::get_cryptographer();
+ let salt = match params.salt {
+ Some(salt) => salt,
+ None => {
+ let mut salt = [0u8; ECE_SALT_LENGTH];
+ cryptographer.random_bytes(&mut salt)?;
+ salt.to_vec()
+ }
+ };
+ let mut header = vec![0u8; ECE_AES128GCM_HEADER_LENGTH + ECE_WEBPUSH_PUBLIC_KEY_LENGTH];
+ header[0..ECE_SALT_LENGTH].copy_from_slice(&salt);
+ BigEndian::write_u32(&mut header[ECE_SALT_LENGTH..], params.rs);
+ header[ECE_SALT_LENGTH + 4] = ECE_WEBPUSH_PUBLIC_KEY_LENGTH as u8;
+ let raw_local_pub_key = local_prv_key.pub_as_raw()?;
+ header[ECE_AES128GCM_HEADER_LENGTH
+ ..ECE_AES128GCM_HEADER_LENGTH + ECE_WEBPUSH_PUBLIC_KEY_LENGTH]
+ .copy_from_slice(&raw_local_pub_key);
+ let mut ciphertext = Self::common_encrypt(
+ local_prv_key,
+ remote_pub_key,
+ auth_secret,
+ &salt,
+ params.rs,
+ params.pad_length,
+ plaintext,
+ )?;
+ // TODO: Not efficient and probably allocates more,
+ // we should allocate the buffer upfront if possible.
+ header.append(&mut ciphertext);
+ Ok(header)
+ }
+
+ /// Decrypts a Web Push message encrypted using the "aes128gcm" scheme.
+ pub fn decrypt(
+ local_prv_key: &dyn LocalKeyPair,
+ auth_secret: &[u8],
+ payload: &[u8],
+ ) -> Result<Vec<u8>> {
+ if payload.len() < ECE_AES128GCM_HEADER_LENGTH {
+ return Err(Error::HeaderTooShort);
+ }
+
+ let key_id_len = payload[ECE_SALT_LENGTH + 4] as usize;
+ if payload.len() < ECE_AES128GCM_HEADER_LENGTH + key_id_len {
+ return Err(Error::HeaderTooShort);
+ }
+
+ let rs = BigEndian::read_u32(&payload[ECE_SALT_LENGTH..]);
+ if rs < ECE_AES128GCM_MIN_RS {
+ return Err(Error::InvalidRecordSize);
+ }
+
+ let salt = &payload[0..ECE_SALT_LENGTH];
+ if key_id_len != ECE_WEBPUSH_PUBLIC_KEY_LENGTH {
+ return Err(Error::InvalidKeyLength);
+ }
+ let key_id_pos = ECE_AES128GCM_HEADER_LENGTH;
+ let key_id = &payload[key_id_pos..key_id_pos + key_id_len];
+
+ let ciphertext_start = ECE_AES128GCM_HEADER_LENGTH + key_id_len;
+ if payload.len() == ciphertext_start {
+ return Err(Error::ZeroCiphertext);
+ }
+ let ciphertext = &payload[ciphertext_start..];
+ let cryptographer = crypto::holder::get_cryptographer();
+ let key = cryptographer.import_public_key(key_id)?;
+ Self::common_decrypt(local_prv_key, &*key, auth_secret, salt, rs, ciphertext)
+ }
+}
+
+impl EceWebPush for Aes128GcmEceWebPush {
+ /// Always returns false because "aes128gcm" uses
+ /// a padding scheme that doesn't need a trailer.
+ fn needs_trailer(_: u32, _: usize) -> bool {
+ false
+ }
+
+ fn pad_size() -> usize {
+ ECE_AES128GCM_PAD_SIZE
+ }
+
+ fn min_block_pad_length(pad_len: usize, max_block_len: usize) -> usize {
+ ece_min_block_pad_length(pad_len, max_block_len)
+ }
+
+ fn pad(plaintext: &[u8], block_pad_len: usize, last_record: bool) -> Result<Vec<u8>> {
+ let mut block = Vec::with_capacity(plaintext.len() + 1 /* delimiter */ + block_pad_len);
+ block.extend_from_slice(plaintext);
+ block.push(if last_record { 2 } else { 1 });
+ let padding = vec![0u8; block_pad_len];
+ block.extend(padding);
+ Ok(block)
+ }
+
+ fn unpad(block: &[u8], last_record: bool) -> Result<&[u8]> {
+ let pos = match block.iter().rposition(|&b| b != 0) {
+ Some(pos) => pos,
+ None => return Err(Error::ZeroCiphertext),
+ };
+ let expected_delim = if last_record { 2 } else { 1 };
+ if block[pos] != expected_delim {
+ return Err(Error::DecryptPadding);
+ }
+ Ok(&block[..pos])
+ }
+
+ /// Derives the "aes128gcm" decryption key and nonce given the receiver private
+ /// key, sender public key, authentication secret, and sender salt.
+ fn derive_key_and_nonce(
+ ece_mode: EceMode,
+ local_prv_key: &dyn LocalKeyPair,
+ remote_pub_key: &dyn RemotePublicKey,
+ auth_secret: &[u8],
+ salt: &[u8],
+ ) -> Result<KeyAndNonce> {
+ let cryptographer = crypto::holder::get_cryptographer();
+ let shared_secret = cryptographer.compute_ecdh_secret(remote_pub_key, local_prv_key)?;
+ let raw_remote_pub_key = remote_pub_key.as_raw()?;
+ let raw_local_pub_key = local_prv_key.pub_as_raw()?;
+
+ // The new "aes128gcm" scheme includes the sender and receiver public keys in
+ // the info string when deriving the Web Push IKM.
+ let ikm_info = match ece_mode {
+ EceMode::ENCRYPT => generate_info(&raw_remote_pub_key, &raw_local_pub_key),
+ EceMode::DECRYPT => generate_info(&raw_local_pub_key, &raw_remote_pub_key),
+ }?;
+ let cryptographer = crypto::holder::get_cryptographer();
+ let ikm = cryptographer.hkdf_sha256(
+ auth_secret,
+ &shared_secret,
+ &ikm_info,
+ ECE_WEBPUSH_IKM_LENGTH,
+ )?;
+ let key = cryptographer.hkdf_sha256(
+ salt,
+ &ikm,
+ ECE_AES128GCM_KEY_INFO.as_bytes(),
+ ECE_AES_KEY_LENGTH,
+ )?;
+ let nonce = cryptographer.hkdf_sha256(
+ salt,
+ &ikm,
+ ECE_AES128GCM_NONCE_INFO.as_bytes(),
+ ECE_NONCE_LENGTH,
+ )?;
+ Ok((key, nonce))
+ }
+}
+
+// The "aes128gcm" IKM info string is "WebPush: info\0", followed by the
+// receiver and sender public keys.
+fn generate_info(
+ raw_recv_pub_key: &[u8],
+ raw_sender_pub_key: &[u8],
+) -> Result<[u8; ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH]> {
+ let mut info = [0u8; ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH];
+ let prefix = ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX.as_bytes();
+ let mut offset = prefix.len();
+ info[0..offset].copy_from_slice(prefix);
+ info[offset..offset + ECE_WEBPUSH_PUBLIC_KEY_LENGTH].copy_from_slice(raw_recv_pub_key);
+ offset += ECE_WEBPUSH_PUBLIC_KEY_LENGTH;
+ info[offset..].copy_from_slice(raw_sender_pub_key);
+ Ok(info)
+}