summaryrefslogtreecommitdiffstats
path: root/third_party/rust/jwcrypto/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/rust/jwcrypto/src
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/jwcrypto/src')
-rw-r--r--third_party/rust/jwcrypto/src/ec.rs198
-rw-r--r--third_party/rust/jwcrypto/src/error.rs25
-rw-r--r--third_party/rust/jwcrypto/src/lib.rs243
3 files changed, 466 insertions, 0 deletions
diff --git a/third_party/rust/jwcrypto/src/ec.rs b/third_party/rust/jwcrypto/src/ec.rs
new file mode 100644
index 0000000000..acea12cbbc
--- /dev/null
+++ b/third_party/rust/jwcrypto/src/ec.rs
@@ -0,0 +1,198 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::{
+ error::{JwCryptoError, Result},
+ Algorithm, CompactJwe, DecryptionParameters, EncryptionAlgorithm, EncryptionParameters,
+ JweHeader, Jwk, JwkKeyParameters,
+};
+use rc_crypto::{
+ aead,
+ agreement::{self, EphemeralKeyPair, InputKeyMaterial, UnparsedPublicKey},
+ digest, rand,
+};
+use serde_derive::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+pub struct ECKeysParameters {
+ pub crv: String,
+ pub x: String,
+ pub y: String,
+}
+
+pub(crate) fn encrypt_to_jwe(
+ data: &[u8],
+ encryption_params: EncryptionParameters,
+) -> Result<CompactJwe> {
+ let EncryptionParameters::ECDH_ES { enc, peer_jwk } = encryption_params;
+ let local_key_pair = EphemeralKeyPair::generate(&agreement::ECDH_P256)?;
+ let local_public_key = extract_pub_key_jwk(&local_key_pair)?;
+ let JwkKeyParameters::EC(ref ec_key_params) = peer_jwk.key_parameters;
+ let protected_header = JweHeader {
+ kid: peer_jwk.kid.clone(),
+ alg: Algorithm::ECDH_ES,
+ enc,
+ epk: Some(local_public_key),
+ apu: None,
+ apv: None,
+ };
+
+ let secret = derive_shared_secret(&protected_header, local_key_pair, &ec_key_params)?;
+
+ let encryption_algorithm = match protected_header.enc {
+ EncryptionAlgorithm::A256GCM => &aead::AES_256_GCM,
+ };
+ let sealing_key = aead::SealingKey::new(encryption_algorithm, &secret.as_ref())?;
+ let additional_data = serde_json::to_string(&protected_header)?;
+ let additional_data =
+ base64::encode_config(additional_data.as_bytes(), base64::URL_SAFE_NO_PAD);
+ let additional_data = additional_data.as_bytes();
+ let aad = aead::Aad::from(additional_data);
+ let mut iv: Vec<u8> = vec![0; 12];
+ rand::fill(&mut iv)?;
+ let nonce = aead::Nonce::try_assume_unique_for_key(encryption_algorithm, &iv)?;
+ let mut encrypted = aead::seal(&sealing_key, nonce, aad, data)?;
+
+ let tag_idx = encrypted.len() - encryption_algorithm.tag_len();
+ let auth_tag = encrypted.split_off(tag_idx);
+ let ciphertext = encrypted;
+
+ Ok(CompactJwe::new(
+ Some(protected_header),
+ None,
+ Some(iv),
+ ciphertext,
+ Some(auth_tag),
+ )?)
+}
+
+pub(crate) fn decrypt_jwe(
+ jwe: &CompactJwe,
+ decryption_params: DecryptionParameters,
+) -> Result<String> {
+ let DecryptionParameters::ECDH_ES { local_key_pair } = decryption_params;
+
+ let protected_header = jwe
+ .protected_header()?
+ .ok_or_else(|| JwCryptoError::IllegalState("protected_header must be present."))?;
+ if protected_header.alg != Algorithm::ECDH_ES {
+ return Err(JwCryptoError::IllegalState("alg mismatch."));
+ }
+
+ // Part 1: Reconstruct the secret.
+ let peer_jwk = protected_header
+ .epk
+ .as_ref()
+ .ok_or_else(|| JwCryptoError::IllegalState("epk not present"))?;
+ let JwkKeyParameters::EC(ref ec_key_params) = peer_jwk.key_parameters;
+ let secret = derive_shared_secret(&protected_header, local_key_pair, &ec_key_params)?;
+
+ // Part 2: decrypt the payload
+ if jwe.encrypted_key()?.is_some() {
+ return Err(JwCryptoError::IllegalState(
+ "The Encrypted Key must be empty.",
+ ));
+ }
+ let encryption_algorithm = match protected_header.enc {
+ EncryptionAlgorithm::A256GCM => &aead::AES_256_GCM,
+ };
+ let auth_tag = jwe
+ .auth_tag()?
+ .ok_or_else(|| JwCryptoError::IllegalState("auth_tag must be present."))?;
+ if auth_tag.len() != encryption_algorithm.tag_len() {
+ return Err(JwCryptoError::IllegalState(
+ "The auth tag must be 16 bytes long.",
+ ));
+ }
+ let iv = jwe
+ .iv()?
+ .ok_or_else(|| JwCryptoError::IllegalState("iv must be present."))?;
+ let opening_key = aead::OpeningKey::new(&encryption_algorithm, &secret.as_ref())?;
+ let ciphertext_and_tag: Vec<u8> = [jwe.ciphertext()?, auth_tag].concat();
+ let nonce = aead::Nonce::try_assume_unique_for_key(&encryption_algorithm, &iv)?;
+ let aad = aead::Aad::from(jwe.protected_header_raw().as_bytes());
+ let plaintext = aead::open(&opening_key, nonce, aad, &ciphertext_and_tag)?;
+ Ok(String::from_utf8(plaintext.to_vec())?)
+}
+
+fn derive_shared_secret(
+ protected_header: &JweHeader,
+ local_key_pair: EphemeralKeyPair,
+ peer_key: &ECKeysParameters,
+) -> Result<digest::Digest> {
+ let (private_key, _) = local_key_pair.split();
+ let peer_public_key_raw_bytes = public_key_from_ec_params(peer_key)?;
+ let peer_public_key = UnparsedPublicKey::new(&agreement::ECDH_P256, &peer_public_key_raw_bytes);
+ // Note: We don't support key-wrapping, but if we did `algorithm_id` would be `alg` instead.
+ let algorithm_id = protected_header.enc.algorithm_id();
+ let ikm = private_key.agree(&peer_public_key)?;
+ let apu = protected_header.apu.as_deref().unwrap_or_default();
+ let apv = protected_header.apv.as_deref().unwrap_or_default();
+ get_secret_from_ikm(ikm, &apu, &apv, &algorithm_id)
+}
+
+fn public_key_from_ec_params(jwk: &ECKeysParameters) -> Result<Vec<u8>> {
+ let x = base64::decode_config(&jwk.x, base64::URL_SAFE_NO_PAD)?;
+ let y = base64::decode_config(&jwk.y, base64::URL_SAFE_NO_PAD)?;
+ if jwk.crv != "P-256" {
+ return Err(JwCryptoError::PartialImplementation(
+ "Only P-256 curves are supported.",
+ ));
+ }
+ if x.len() != (256 / 8) {
+ return Err(JwCryptoError::IllegalState("X must be 32 bytes long."));
+ }
+ if y.len() != (256 / 8) {
+ return Err(JwCryptoError::IllegalState("Y must be 32 bytes long."));
+ }
+ let mut peer_pub_key: Vec<u8> = vec![0x04];
+ peer_pub_key.extend_from_slice(&x);
+ peer_pub_key.extend_from_slice(&y);
+ Ok(peer_pub_key)
+}
+
+fn get_secret_from_ikm(
+ ikm: InputKeyMaterial,
+ apu: &str,
+ apv: &str,
+ alg: &str,
+) -> Result<digest::Digest> {
+ let secret = ikm.derive(|z| {
+ let mut buf: Vec<u8> = vec![];
+ // ConcatKDF (1 iteration since keyLen <= hashLen).
+ // See rfc7518 section 4.6 for reference.
+ buf.extend_from_slice(&1u32.to_be_bytes());
+ buf.extend_from_slice(&z);
+ // otherinfo
+ buf.extend_from_slice(&(alg.len() as u32).to_be_bytes());
+ buf.extend_from_slice(alg.as_bytes());
+ buf.extend_from_slice(&(apu.len() as u32).to_be_bytes());
+ buf.extend_from_slice(apu.as_bytes());
+ buf.extend_from_slice(&(apv.len() as u32).to_be_bytes());
+ buf.extend_from_slice(apv.as_bytes());
+ buf.extend_from_slice(&256u32.to_be_bytes());
+ digest::digest(&digest::SHA256, &buf)
+ })?;
+ Ok(secret)
+}
+
+pub fn extract_pub_key_jwk(key_pair: &EphemeralKeyPair) -> Result<Jwk> {
+ let pub_key_bytes = key_pair.public_key().to_bytes()?;
+ // Uncompressed form (see SECG SEC1 section 2.3.3).
+ // First byte is 4, then 32 bytes for x, and 32 bytes for y.
+ assert_eq!(pub_key_bytes.len(), 1 + 32 + 32);
+ assert_eq!(pub_key_bytes[0], 0x04);
+ let x = Vec::from(&pub_key_bytes[1..33]);
+ let x = base64::encode_config(&x, base64::URL_SAFE_NO_PAD);
+ let y = Vec::from(&pub_key_bytes[33..]);
+ let y = base64::encode_config(&y, base64::URL_SAFE_NO_PAD);
+ Ok(Jwk {
+ kid: None,
+ key_parameters: JwkKeyParameters::EC(ECKeysParameters {
+ crv: "P-256".to_owned(),
+ x,
+ y,
+ }),
+ })
+}
diff --git a/third_party/rust/jwcrypto/src/error.rs b/third_party/rust/jwcrypto/src/error.rs
new file mode 100644
index 0000000000..cc12067996
--- /dev/null
+++ b/third_party/rust/jwcrypto/src/error.rs
@@ -0,0 +1,25 @@
+/* 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 thiserror::Error;
+
+pub(crate) type Result<T> = std::result::Result<T, JwCryptoError>;
+
+#[derive(Error, Debug)]
+pub enum JwCryptoError {
+ #[error("Deserialization error")]
+ DeserializationError,
+ #[error("Illegal state error: {0}")]
+ IllegalState(&'static str),
+ #[error("Partial implementation error: {0}")]
+ PartialImplementation(&'static str),
+ #[error("Base64 decode error: {0}")]
+ Base64Decode(#[from] base64::DecodeError),
+ #[error("Crypto error: {0}")]
+ CryptoError(#[from] rc_crypto::Error),
+ #[error("JSON error: {0}")]
+ JsonError(#[from] serde_json::Error),
+ #[error("UTF8 decode error: {0}")]
+ UTF8DecodeError(#[from] std::string::FromUtf8Error),
+}
diff --git a/third_party/rust/jwcrypto/src/lib.rs b/third_party/rust/jwcrypto/src/lib.rs
new file mode 100644
index 0000000000..aa3bb086d5
--- /dev/null
+++ b/third_party/rust/jwcrypto/src/lib.rs
@@ -0,0 +1,243 @@
+/* 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/. */
+
+//! Theorically, everything done in this crate could and should be done in a JWT library.
+//! However, none of the existing rust JWT libraries can handle ECDH-ES encryption, and API choices
+//! made by their authors make it difficult to add this feature.
+//! In the past, we chose cjose to do that job, but it added three C dependencies to build and link
+//! against: jansson, openssl and cjose itself.
+
+pub use error::JwCryptoError;
+use error::Result;
+use rc_crypto::agreement::EphemeralKeyPair;
+use serde_derive::{Deserialize, Serialize};
+use std::str::FromStr;
+
+pub mod ec;
+mod error;
+
+pub enum EncryptionParameters<'a> {
+ // ECDH-ES in Direct Key Agreement mode.
+ #[allow(non_camel_case_types)]
+ ECDH_ES {
+ enc: EncryptionAlgorithm,
+ peer_jwk: &'a Jwk,
+ },
+}
+
+pub enum DecryptionParameters {
+ // ECDH-ES in Direct Key Agreement mode.
+ #[allow(non_camel_case_types)]
+ ECDH_ES { local_key_pair: EphemeralKeyPair },
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+enum Algorithm {
+ #[serde(rename = "ECDH-ES")]
+ #[allow(non_camel_case_types)]
+ ECDH_ES,
+}
+#[derive(Serialize, Deserialize, Debug)]
+pub enum EncryptionAlgorithm {
+ A256GCM,
+}
+
+impl EncryptionAlgorithm {
+ fn algorithm_id(&self) -> &'static str {
+ match self {
+ Self::A256GCM => "A256GCM",
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct JweHeader {
+ alg: Algorithm,
+ enc: EncryptionAlgorithm,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ kid: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ epk: Option<Jwk>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ apu: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ apv: Option<String>,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct Jwk {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub kid: Option<String>,
+ #[serde(flatten)]
+ pub key_parameters: JwkKeyParameters,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(tag = "kty")]
+pub enum JwkKeyParameters {
+ EC(ec::ECKeysParameters),
+}
+
+#[derive(Debug)]
+pub struct CompactJwe {
+ jwe_segments: Vec<String>,
+}
+
+impl CompactJwe {
+ // A builder pattern would be nicer, but this will do for now.
+ fn new(
+ protected_header: Option<JweHeader>,
+ encrypted_key: Option<Vec<u8>>,
+ iv: Option<Vec<u8>>,
+ ciphertext: Vec<u8>,
+ auth_tag: Option<Vec<u8>>,
+ ) -> Result<Self> {
+ let protected_header = protected_header
+ .as_ref()
+ .map(|h| serde_json::to_string(&h))
+ .transpose()?
+ .map(|h| base64::encode_config(&h, base64::URL_SAFE_NO_PAD))
+ .unwrap_or_default();
+ let encrypted_key = encrypted_key
+ .as_ref()
+ .map(|k| base64::encode_config(&k, base64::URL_SAFE_NO_PAD))
+ .unwrap_or_default();
+ let iv = iv
+ .as_ref()
+ .map(|iv| base64::encode_config(&iv, base64::URL_SAFE_NO_PAD))
+ .unwrap_or_default();
+ let ciphertext = base64::encode_config(&ciphertext, base64::URL_SAFE_NO_PAD);
+ let auth_tag = auth_tag
+ .as_ref()
+ .map(|t| base64::encode_config(&t, base64::URL_SAFE_NO_PAD))
+ .unwrap_or_default();
+ let jwe_segments = vec![protected_header, encrypted_key, iv, ciphertext, auth_tag];
+ Ok(Self { jwe_segments })
+ }
+
+ fn protected_header(&self) -> Result<Option<JweHeader>> {
+ Ok(self
+ .try_deserialize_base64_segment(0)?
+ .map(|s| serde_json::from_slice(&s))
+ .transpose()?)
+ }
+
+ fn protected_header_raw(&self) -> &str {
+ &self.jwe_segments[0]
+ }
+
+ fn encrypted_key(&self) -> Result<Option<Vec<u8>>> {
+ self.try_deserialize_base64_segment(1)
+ }
+
+ fn iv(&self) -> Result<Option<Vec<u8>>> {
+ self.try_deserialize_base64_segment(2)
+ }
+
+ fn ciphertext(&self) -> Result<Vec<u8>> {
+ Ok(self
+ .try_deserialize_base64_segment(3)?
+ .ok_or_else(|| JwCryptoError::IllegalState("Ciphertext is empty"))?)
+ }
+
+ fn auth_tag(&self) -> Result<Option<Vec<u8>>> {
+ self.try_deserialize_base64_segment(4)
+ }
+
+ fn try_deserialize_base64_segment(&self, index: usize) -> Result<Option<Vec<u8>>> {
+ Ok(match self.jwe_segments[index].is_empty() {
+ true => None,
+ false => Some(base64::decode_config(
+ &self.jwe_segments[index],
+ base64::URL_SAFE_NO_PAD,
+ )?),
+ })
+ }
+}
+
+impl FromStr for CompactJwe {
+ type Err = JwCryptoError;
+ fn from_str(str: &str) -> Result<Self> {
+ let jwe_segments: Vec<String> = str.split('.').map(|s| s.to_owned()).collect();
+ if jwe_segments.len() != 5 {
+ return Err(JwCryptoError::DeserializationError);
+ }
+ Ok(Self { jwe_segments })
+ }
+}
+
+impl ToString for CompactJwe {
+ fn to_string(&self) -> String {
+ assert!(self.jwe_segments.len() == 5);
+ self.jwe_segments.join(".")
+ }
+}
+
+/// Encrypt and serialize data in the JWE compact form.
+pub fn encrypt_to_jwe(data: &[u8], encryption_params: EncryptionParameters) -> Result<String> {
+ let jwe = match encryption_params {
+ EncryptionParameters::ECDH_ES { .. } => ec::encrypt_to_jwe(data, encryption_params)?,
+ };
+ Ok(jwe.to_string())
+}
+
+/// Deserialize and decrypt data in the JWE compact form.
+pub fn decrypt_jwe(jwe: &str, decryption_params: DecryptionParameters) -> Result<String> {
+ let jwe = jwe.parse()?;
+ Ok(match decryption_params {
+ DecryptionParameters::ECDH_ES { .. } => ec::decrypt_jwe(&jwe, decryption_params)?,
+ })
+}
+
+#[test]
+fn test_encrypt_decrypt_jwe_ecdh_es() {
+ use rc_crypto::agreement;
+ let key_pair = EphemeralKeyPair::generate(&agreement::ECDH_P256).unwrap();
+ let jwk = ec::extract_pub_key_jwk(&key_pair).unwrap();
+ let data = b"The big brown fox jumped over... What?";
+ let encrypted = encrypt_to_jwe(
+ data,
+ EncryptionParameters::ECDH_ES {
+ enc: EncryptionAlgorithm::A256GCM,
+ peer_jwk: &jwk,
+ },
+ )
+ .unwrap();
+ let decrypted = decrypt_jwe(
+ &encrypted,
+ DecryptionParameters::ECDH_ES {
+ local_key_pair: key_pair,
+ },
+ )
+ .unwrap();
+ assert_eq!(decrypted, std::str::from_utf8(data).unwrap());
+}
+
+#[test]
+fn test_compact_jwe_roundtrip() {
+ let mut iv = [0u8; 16];
+ rc_crypto::rand::fill(&mut iv).unwrap();
+ let mut ciphertext = [0u8; 243];
+ rc_crypto::rand::fill(&mut ciphertext).unwrap();
+ let mut auth_tag = [0u8; 16];
+ rc_crypto::rand::fill(&mut auth_tag).unwrap();
+ let jwe = CompactJwe::new(
+ Some(JweHeader {
+ alg: Algorithm::ECDH_ES,
+ enc: EncryptionAlgorithm::A256GCM,
+ kid: None,
+ epk: None,
+ apu: None,
+ apv: None,
+ }),
+ None,
+ Some(iv.to_vec()),
+ ciphertext.to_vec(),
+ Some(auth_tag.to_vec()),
+ )
+ .unwrap();
+ let compacted = jwe.to_string();
+ let jwe2: CompactJwe = compacted.parse().unwrap();
+ assert_eq!(jwe.jwe_segments, jwe2.jwe_segments);
+}