diff --git a/.dictionary b/.dictionary index 7fd1a976be..d9b7bd6825 100644 --- a/.dictionary +++ b/.dictionary @@ -115,6 +115,7 @@ integrations io ios janerik +JWE ktlint lang latencies diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b811b950..7e7b954330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * General * BUGFIX: fix `int32` to `ErrorType` mapping. The `InvalidOverflow` had a value mismatch between glean-core and the bindings. This would only be a problem in unit tests. ([#1063](https://github.com/mozilla/glean/pull/1063)) + * Implement a JWE metric type ([#1062](https://github.com/mozilla/glean/pull/1062)). [Full changelog](https://github.com/mozilla/glean/compare/v31.4.0...main) diff --git a/glean-core/ffi/glean.h b/glean-core/ffi/glean.h index 70a59aea42..eb9f9548ba 100644 --- a/glean-core/ffi/glean.h +++ b/glean-core/ffi/glean.h @@ -372,6 +372,8 @@ void glean_destroy_event_metric(uint64_t v); void glean_destroy_glean(void); +void glean_destroy_jwe_metric(uint64_t v); + void glean_destroy_labeled_boolean_metric(uint64_t v); void glean_destroy_labeled_counter_metric(uint64_t v); @@ -440,6 +442,25 @@ uint8_t glean_is_first_run(void); uint8_t glean_is_upload_enabled(void); +void glean_jwe_set(uint64_t metric_id, + FfiStr header, + FfiStr key, + FfiStr init_vector, + FfiStr cipher_text, + FfiStr auth_tag); + +void glean_jwe_set_with_compact_repr(uint64_t metric_id, FfiStr value); + +int32_t glean_jwe_test_get_num_recorded_errors(uint64_t metric_id, + int32_t error_type, + FfiStr storage_name); + +char *glean_jwe_test_get_value(uint64_t metric_id, FfiStr storage_name); + +char *glean_jwe_test_get_value_as_json_string(uint64_t metric_id, FfiStr storage_name); + +uint8_t glean_jwe_test_has_value(uint64_t metric_id, FfiStr storage_name); + /** * Create a new instance of the sub-metric of this labeled metric. */ @@ -524,6 +545,13 @@ uint64_t glean_new_event_metric(FfiStr category, RawStringArray extra_keys, int32_t extra_keys_len); +uint64_t glean_new_jwe_metric(FfiStr category, + FfiStr name, + RawStringArray send_in_pings, + int32_t send_in_pings_len, + Lifetime lifetime, + uint8_t disabled); + /** * Create a new labeled metric. */ diff --git a/glean-core/ffi/src/jwe.rs b/glean-core/ffi/src/jwe.rs new file mode 100644 index 0000000000..b4c93ed7c2 --- /dev/null +++ b/glean-core/ffi/src/jwe.rs @@ -0,0 +1,83 @@ +// 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 https://mozilla.org/MPL/2.0/. + +use std::os::raw::c_char; + +use ffi_support::FfiStr; + +use crate::ffi_string_ext::FallibleToString; +use crate::{define_metric, handlemap_ext::HandleMapExtension, with_glean_value, Lifetime}; + +define_metric!(JweMetric => JWE_METRICS { + new -> glean_new_jwe_metric(), + test_get_num_recorded_errors -> glean_jwe_test_get_num_recorded_errors, + destroy -> glean_destroy_jwe_metric, +}); + +#[no_mangle] +pub extern "C" fn glean_jwe_set_with_compact_repr(metric_id: u64, value: FfiStr) { + with_glean_value(|glean| { + JWE_METRICS.call_with_log(metric_id, |metric| { + let value = value.to_string_fallible()?; + metric.set_with_compact_repr(&glean, value); + Ok(()) + }) + }) +} + +#[no_mangle] +pub extern "C" fn glean_jwe_set( + metric_id: u64, + header: FfiStr, + key: FfiStr, + init_vector: FfiStr, + cipher_text: FfiStr, + auth_tag: FfiStr, +) { + with_glean_value(|glean| { + JWE_METRICS.call_with_log(metric_id, |metric| { + let header = header.to_string_fallible()?; + let key = key.to_string_fallible()?; + let init_vector = init_vector.to_string_fallible()?; + let cipher_text = cipher_text.to_string_fallible()?; + let auth_tag = auth_tag.to_string_fallible()?; + metric.set(&glean, header, key, init_vector, cipher_text, auth_tag); + Ok(()) + }) + }) +} + +#[no_mangle] +pub extern "C" fn glean_jwe_test_has_value(metric_id: u64, storage_name: FfiStr) -> u8 { + with_glean_value(|glean| { + JWE_METRICS.call_infallible(metric_id, |metric| { + metric + .test_get_value(glean, storage_name.as_str()) + .is_some() + }) + }) +} + +#[no_mangle] +pub extern "C" fn glean_jwe_test_get_value(metric_id: u64, storage_name: FfiStr) -> *mut c_char { + with_glean_value(|glean| { + JWE_METRICS.call_infallible(metric_id, |metric| { + metric.test_get_value(glean, storage_name.as_str()).unwrap() + }) + }) +} + +#[no_mangle] +pub extern "C" fn glean_jwe_test_get_value_as_json_string( + metric_id: u64, + storage_name: FfiStr, +) -> *mut c_char { + with_glean_value(|glean| { + JWE_METRICS.call_infallible(metric_id, |metric| { + metric + .test_get_value_as_json_string(glean, storage_name.as_str()) + .unwrap() + }) + }) +} diff --git a/glean-core/ffi/src/lib.rs b/glean-core/ffi/src/lib.rs index f4c1ab017a..cadcd101bf 100644 --- a/glean-core/ffi/src/lib.rs +++ b/glean-core/ffi/src/lib.rs @@ -26,6 +26,7 @@ mod event; mod ffi_string_ext; mod from_raw; mod handlemap_ext; +mod jwe; mod labeled; mod memory_distribution; pub mod ping_type; diff --git a/glean-core/ios/Glean/GleanFfi.h b/glean-core/ios/Glean/GleanFfi.h index 70a59aea42..eb9f9548ba 100644 --- a/glean-core/ios/Glean/GleanFfi.h +++ b/glean-core/ios/Glean/GleanFfi.h @@ -372,6 +372,8 @@ void glean_destroy_event_metric(uint64_t v); void glean_destroy_glean(void); +void glean_destroy_jwe_metric(uint64_t v); + void glean_destroy_labeled_boolean_metric(uint64_t v); void glean_destroy_labeled_counter_metric(uint64_t v); @@ -440,6 +442,25 @@ uint8_t glean_is_first_run(void); uint8_t glean_is_upload_enabled(void); +void glean_jwe_set(uint64_t metric_id, + FfiStr header, + FfiStr key, + FfiStr init_vector, + FfiStr cipher_text, + FfiStr auth_tag); + +void glean_jwe_set_with_compact_repr(uint64_t metric_id, FfiStr value); + +int32_t glean_jwe_test_get_num_recorded_errors(uint64_t metric_id, + int32_t error_type, + FfiStr storage_name); + +char *glean_jwe_test_get_value(uint64_t metric_id, FfiStr storage_name); + +char *glean_jwe_test_get_value_as_json_string(uint64_t metric_id, FfiStr storage_name); + +uint8_t glean_jwe_test_has_value(uint64_t metric_id, FfiStr storage_name); + /** * Create a new instance of the sub-metric of this labeled metric. */ @@ -524,6 +545,13 @@ uint64_t glean_new_event_metric(FfiStr category, RawStringArray extra_keys, int32_t extra_keys_len); +uint64_t glean_new_jwe_metric(FfiStr category, + FfiStr name, + RawStringArray send_in_pings, + int32_t send_in_pings_len, + Lifetime lifetime, + uint8_t disabled); + /** * Create a new labeled metric. */ diff --git a/glean-core/src/lib_unit_tests.rs b/glean-core/src/lib_unit_tests.rs index 7f46f2bdf9..e59ad01b61 100644 --- a/glean-core/src/lib_unit_tests.rs +++ b/glean-core/src/lib_unit_tests.rs @@ -372,6 +372,7 @@ fn correct_order() { Timespan(Duration::new(5, 0), TimeUnit::Second), TimingDistribution(Histogram::functional(2.0, 8.0)), MemoryDistribution(Histogram::functional(2.0, 8.0)), + Jwe("eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ".into()), ]; for metric in all_metrics { @@ -396,6 +397,7 @@ fn correct_order() { Timespan(..) => assert_eq!(10, disc), TimingDistribution(..) => assert_eq!(11, disc), MemoryDistribution(..) => assert_eq!(12, disc), + Jwe(..) => assert_eq!(13, disc), } } } diff --git a/glean-core/src/metrics/jwe.rs b/glean-core/src/metrics/jwe.rs new file mode 100644 index 0000000000..bb6ede4628 --- /dev/null +++ b/glean-core/src/metrics/jwe.rs @@ -0,0 +1,468 @@ +// 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 https://mozilla.org/MPL/2.0/. + +use std::fmt; +use std::str::FromStr; + +use serde::Serialize; + +use crate::error_recording::{record_error, ErrorType}; +use crate::metrics::{Metric, MetricType}; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +const DEFAULT_MAX_CHARS_PER_VARIABLE_SIZE_ELEMENT: usize = 1024; + +/// Verifies if a string is [`BASE64URL`](https://tools.ietf.org/html/rfc4648#section-5) compliant. +/// +/// As such, the string must match the regex: `[a-zA-Z0-9\-\_]*`. +/// +/// > **Note** As described in the [JWS specification](https://tools.ietf.org/html/rfc7515#section-2), +/// > the BASE64URL encoding used by JWE discards any padding, +/// > that is why we can ignore that for this validation. +/// +/// The regex crate isn't used here because it adds to the binary size, +/// and the Glean SDK doesn't use regular expressions anywhere else. +fn validate_base64url_encoding(value: &str) -> bool { + let mut iter = value.chars(); + + loop { + match iter.next() { + // We are done, so the whole expression is valid. + None => return true, + // Valid characters. + Some('_') | Some('-') | Some('a'..='z') | Some('A'..='Z') | Some('0'..='9') => (), + // An invalid character. + Some(_) => return false, + } + } +} + +/// Representation of a [JWE](https://tools.ietf.org/html/rfc7516). +/// +/// **Note** Variable sized elements will be constrained to a length of DEFAULT_MAX_CHARS_PER_VARIABLE_SIZE_ELEMENT, +/// this is a constraint introduced by Glean to prevent abuses and not part of the spec. +#[derive(Serialize)] +struct Jwe { + /// A variable-size JWE protected header. + header: String, + /// A variable-size [encrypted key](https://tools.ietf.org/html/rfc7516#appendix-A.1.3). + /// This can be an empty octet sequence. + key: String, + /// A fixed-size, 96-bit, base64 encoded [JWE Initialization vector](https://tools.ietf.org/html/rfc7516#appendix-A.1.4) (e.g. “48V1_ALb6US04U3b”). + /// If not required by the encryption algorithm, can be an empty octet sequence. + init_vector: String, + /// The variable-size base64 encoded cipher text. + cipher_text: String, + /// A fixed-size, 132-bit, base64 encoded authentication tag. + /// Can be an empty octet sequence. + auth_tag: String, +} + +impl Jwe { + /// Create a new JWE struct. + fn new>( + header: S, + key: S, + init_vector: S, + cipher_text: S, + auth_tag: S, + ) -> Result { + let mut header = header.into(); + header = Self::validate_non_empty("header", header)?; + header = Self::validate_max_size("header", header)?; + header = Self::validate_base64url_encoding("header", header)?; + + let mut key = key.into(); + key = Self::validate_max_size("key", key)?; + key = Self::validate_base64url_encoding("key", key)?; + + let mut init_vector = init_vector.into(); + init_vector = Self::validate_fixed_size_or_empty("init_vector", init_vector, 96)?; + init_vector = Self::validate_base64url_encoding("init_vector", init_vector)?; + + let mut cipher_text = cipher_text.into(); + cipher_text = Self::validate_non_empty("cipher_text", cipher_text)?; + cipher_text = Self::validate_max_size("cipher_text", cipher_text)?; + cipher_text = Self::validate_base64url_encoding("cipher_text", cipher_text)?; + + let mut auth_tag = auth_tag.into(); + auth_tag = Self::validate_fixed_size_or_empty("auth_tag", auth_tag, 128)?; + auth_tag = Self::validate_base64url_encoding("auth_tag", auth_tag)?; + + Ok(Self { + header, + key, + init_vector, + cipher_text, + auth_tag, + }) + } + + fn validate_base64url_encoding( + name: &str, + value: String, + ) -> Result { + if !validate_base64url_encoding(&value) { + return Err(( + ErrorType::InvalidValue, + format!("`{}` element in JWE value is not valid BASE64URL.", name), + )); + } + + Ok(value) + } + + fn validate_non_empty(name: &str, value: String) -> Result { + if value.is_empty() { + return Err(( + ErrorType::InvalidValue, + format!("`{}` element in JWE value must not be empty.", name), + )); + } + + Ok(value) + } + + fn validate_max_size(name: &str, value: String) -> Result { + if value.len() > DEFAULT_MAX_CHARS_PER_VARIABLE_SIZE_ELEMENT { + return Err(( + ErrorType::InvalidOverflow, + format!( + "`{}` element in JWE value must not exceed {} characters.", + name, DEFAULT_MAX_CHARS_PER_VARIABLE_SIZE_ELEMENT + ), + )); + } + + Ok(value) + } + + fn validate_fixed_size_or_empty( + name: &str, + value: String, + size_in_bits: usize, + ) -> Result { + // Each Base64 digit represents exactly 6 bits of data. + // By dividing the size_in_bits by 6 and ceiling the result, + // we get the amount of characters the value should have. + let num_chars = (size_in_bits as f32 / 6f32).ceil() as usize; + if !value.is_empty() && value.len() != num_chars { + return Err(( + ErrorType::InvalidOverflow, + format!( + "`{}` element in JWE value must have exactly {}-bits or be empty.", + name, size_in_bits + ), + )); + } + + Ok(value) + } +} + +/// Trait implementation to convert a JWE [`compact representation`](https://tools.ietf.org/html/rfc7516#appendix-A.2.7) +/// string into a Jwe struct. +impl FromStr for Jwe { + type Err = (ErrorType, String); + + fn from_str(s: &str) -> Result { + let mut elements: Vec<&str> = s.split('.').collect(); + + if elements.len() != 5 { + return Err(( + ErrorType::InvalidValue, + "JWE value is not formatted as expected.".into(), + )); + } + + // Consume the vector extracting each part of the JWE from it. + // + // Safe unwraps, we already defined that the slice has five elements. + let auth_tag = elements.pop().unwrap(); + let cipher_text = elements.pop().unwrap(); + let init_vector = elements.pop().unwrap(); + let key = elements.pop().unwrap(); + let header = elements.pop().unwrap(); + + Self::new(header, key, init_vector, cipher_text, auth_tag) + } +} + +/// Trait implementation to print the Jwe struct as the proper JWE [`compact representation`](https://tools.ietf.org/html/rfc7516#appendix-A.2.7). +impl fmt::Display for Jwe { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}.{}.{}.{}.{}", + self.header, self.key, self.init_vector, self.cipher_text, self.auth_tag + ) + } +} + +/// A JWE metric. +/// +/// This metric will be work as a "transport" for JWE encrypted data. +/// +/// The actual encrypti on is done somewhere else, +/// Glean must only make sure the data is valid JWE. +#[derive(Clone, Debug)] +pub struct JweMetric { + meta: CommonMetricData, +} + +impl MetricType for JweMetric { + fn meta(&self) -> &CommonMetricData { + &self.meta + } + + fn meta_mut(&mut self) -> &mut CommonMetricData { + &mut self.meta + } +} + +impl JweMetric { + /// Create a new JWE metric. + pub fn new(meta: CommonMetricData) -> Self { + Self { meta } + } + + /// Set to the specified JWE value. + /// + /// ## Arguments + /// + /// * `glean` - the Glean instance this metric belongs to. + /// * `value` - the [`compact representation`](https://tools.ietf.org/html/rfc7516#appendix-A.2.7) of a JWE value. + pub fn set_with_compact_repr>(&self, glean: &Glean, value: S) { + if !self.should_record(glean) { + return; + } + + let value = value.into(); + match Jwe::from_str(&value) { + Ok(_) => glean + .storage() + .record(glean, &self.meta, &Metric::Jwe(value)), + Err((error_type, msg)) => record_error(glean, &self.meta, error_type, msg, None), + }; + } + + /// Build a JWE value from its elements and set to it. + /// + /// ## Arguments + /// + /// * `glean` - the Glean instance this metric belongs to. + /// * `header` - the JWE Protected Header element. + /// * `key` - the JWE Encrypted Key element. + /// * `init_vector` - the JWE Initialization Vector element. + /// * `cipher_text` - the JWE Ciphertext element. + /// * `auth_tag` - the JWE Authentication Tag element. + pub fn set>( + &self, + glean: &Glean, + header: S, + key: S, + init_vector: S, + cipher_text: S, + auth_tag: S, + ) { + if !self.should_record(glean) { + return; + } + + match Jwe::new(header, key, init_vector, cipher_text, auth_tag) { + Ok(jwe) => glean + .storage() + .record(glean, &self.meta, &Metric::Jwe(jwe.to_string())), + Err((error_type, msg)) => record_error(glean, &self.meta, error_type, msg, None), + }; + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Get the currently stored value as a string. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { + match StorageManager.snapshot_metric( + glean.storage(), + storage_name, + &self.meta.identifier(glean), + ) { + Some(Metric::Jwe(b)) => Some(b), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Get the currently stored JWE as a JSON String of the serialized value. + /// + /// This doesn't clear the stored value. + pub fn test_get_value_as_json_string( + &self, + glean: &Glean, + storage_name: &str, + ) -> Option { + self.test_get_value(glean, storage_name).map(|snapshot| { + serde_json::to_string( + &Jwe::from_str(&snapshot).expect("Stored JWE metric should be valid JWE value."), + ) + .unwrap() + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + const HEADER: &str = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ"; + const KEY: &str = "OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg"; + const INIT_VECTOR: &str = "48V1_ALb6US04U3b"; + const CIPHER_TEXT: &str = + "5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A"; + const AUTH_TAG: &str = "XFBoMYUZodetZdvTiFvSkQ"; + const JWE: &str = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ"; + + #[test] + fn generates_jwe_from_correct_input() { + let jwe = Jwe::from_str(JWE).unwrap(); + assert_eq!(jwe.header, HEADER); + assert_eq!(jwe.key, KEY); + assert_eq!(jwe.init_vector, INIT_VECTOR); + assert_eq!(jwe.cipher_text, CIPHER_TEXT); + assert_eq!(jwe.auth_tag, AUTH_TAG); + + assert!(Jwe::new(HEADER, KEY, INIT_VECTOR, CIPHER_TEXT, AUTH_TAG).is_ok()); + } + + #[test] + fn jwe_validates_header_value_correctly() { + // When header is empty, correct error is returned + match Jwe::new("", KEY, INIT_VECTOR, CIPHER_TEXT, AUTH_TAG) { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidValue), + } + + // When header is bigger than max size, correct error is returned + let too_long = (0..1025).map(|_| "X").collect::(); + match Jwe::new( + too_long, + KEY.into(), + INIT_VECTOR.into(), + CIPHER_TEXT.into(), + AUTH_TAG.into(), + ) { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidOverflow), + } + + // When header is not valid BASE64URL, correct error is returned + let not64 = "inv@alid value!"; + match Jwe::new(not64, KEY, INIT_VECTOR, CIPHER_TEXT, AUTH_TAG) { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidValue), + } + } + + #[test] + fn jwe_validates_key_value_correctly() { + // When key is empty,JWE is created + assert!(Jwe::new(HEADER, "", INIT_VECTOR, CIPHER_TEXT, AUTH_TAG).is_ok()); + + // When key is bigger than max size, correct error is returned + let too_long = (0..1025).map(|_| "X").collect::(); + match Jwe::new(HEADER, &too_long, INIT_VECTOR, CIPHER_TEXT, AUTH_TAG) { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidOverflow), + } + + // When key is not valid BASE64URL, correct error is returned + let not64 = "inv@alid value!"; + match Jwe::new(HEADER, not64, INIT_VECTOR, CIPHER_TEXT, AUTH_TAG) { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidValue), + } + } + + #[test] + fn jwe_validates_init_vector_value_correctly() { + // When init_vector is empty, JWE is created + assert!(Jwe::new(HEADER, KEY, "", CIPHER_TEXT, AUTH_TAG).is_ok()); + + // When init_vector is not the correct size, correct error is returned + match Jwe::new(HEADER, KEY, "foo", CIPHER_TEXT, AUTH_TAG) { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidOverflow), + } + + // When init_vector is not valid BASE64URL, correct error is returned + let not64 = "inv@alid value!!"; + match Jwe::new(HEADER, KEY, not64, CIPHER_TEXT, AUTH_TAG) { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidValue), + } + } + + #[test] + fn jwe_validates_cipher_text_value_correctly() { + // When cipher_text is empty, correct error is returned + match Jwe::new(HEADER, KEY, INIT_VECTOR, "", AUTH_TAG) { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidValue), + } + + // When cipher_text is bigger than max size, correct error is returned + let too_long = (0..1025).map(|_| "X").collect::(); + match Jwe::new(HEADER, KEY, INIT_VECTOR, &too_long, AUTH_TAG) { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidOverflow), + } + + // When cipher_text is not valid BASE64URL, correct error is returned + let not64 = "inv@alid value!"; + match Jwe::new(HEADER, KEY, INIT_VECTOR, not64, AUTH_TAG) { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidValue), + } + } + + #[test] + fn jwe_validates_auth_tag_value_correctly() { + // When auth_tag is empty, JWE is created + assert!(Jwe::new(HEADER, KEY, INIT_VECTOR, CIPHER_TEXT, "").is_ok()); + + // When auth_tag is not the correct size, correct error is returned + match Jwe::new(HEADER, KEY, INIT_VECTOR, CIPHER_TEXT, "foo") { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidOverflow), + } + + // When auth_tag is not valid BASE64URL, correct error is returned + let not64 = "inv@alid value!!!!!!!!"; + match Jwe::new(HEADER, KEY, INIT_VECTOR, CIPHER_TEXT, not64) { + Ok(_) => panic!("Should not have built JWE successfully."), + Err((error_type, _)) => assert_eq!(error_type, ErrorType::InvalidValue), + } + } + + #[test] + fn tranforms_jwe_struct_to_string_correctly() { + let jwe = Jwe::from_str(JWE).unwrap(); + assert_eq!(jwe.to_string(), JWE); + } + + #[test] + fn validates_base64url_correctly() { + assert!(validate_base64url_encoding( + "0987654321AaBbCcDdEeFfGgHhIiKkLlMmNnOoPpQqRrSsTtUuVvXxWwYyZz-_" + )); + assert!(validate_base64url_encoding("")); + assert!(!validate_base64url_encoding("aa aa")); + assert!(!validate_base64url_encoding("aa.aa")); + assert!(!validate_base64url_encoding("!nv@lid-val*e")); + } +} diff --git a/glean-core/src/metrics/mod.rs b/glean-core/src/metrics/mod.rs index 12bba0a870..49aee1beb3 100644 --- a/glean-core/src/metrics/mod.rs +++ b/glean-core/src/metrics/mod.rs @@ -16,6 +16,7 @@ mod custom_distribution; mod datetime; mod event; mod experiment; +mod jwe; mod labeled; mod memory_distribution; mod memory_unit; @@ -46,6 +47,7 @@ pub use crate::histogram::HistogramType; pub use self::custom_distribution::CustomDistributionMetric; #[cfg(test)] pub(crate) use self::experiment::RecordedExperimentData; +pub use self::jwe::JweMetric; pub use self::labeled::{ combine_base_identifier_and_label, dynamic_label, strip_label, LabeledMetric, }; @@ -113,6 +115,8 @@ pub enum Metric { TimingDistribution(Histogram), /// A memory distribution. See [`MemoryDistributionMetric`](struct.MemoryDistributionMetric.html) for more information. MemoryDistribution(Histogram), + /// A JWE metric. See [`JweMetric`](struct.JweMetric.html) for more information. + Jwe(String), } /// A `MetricType` describes common behavior across all metrics. @@ -153,6 +157,7 @@ impl Metric { Metric::TimingDistribution(_) => "timing_distribution", Metric::Uuid(_) => "uuid", Metric::MemoryDistribution(_) => "memory_distribution", + Metric::Jwe(_) => "jwe", } } @@ -176,6 +181,7 @@ impl Metric { Metric::TimingDistribution(hist) => json!(timing_distribution::snapshot(hist)), Metric::Uuid(s) => json!(s), Metric::MemoryDistribution(hist) => json!(memory_distribution::snapshot(hist)), + Metric::Jwe(s) => json!(s), } } } diff --git a/glean-core/tests/jwe.rs b/glean-core/tests/jwe.rs new file mode 100644 index 0000000000..e79aa56a96 --- /dev/null +++ b/glean-core/tests/jwe.rs @@ -0,0 +1,113 @@ +// 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 https://mozilla.org/MPL/2.0/. + +mod common; +use crate::common::*; + +use serde_json::json; + +use glean_core::metrics::*; +use glean_core::storage::StorageManager; +use glean_core::{CommonMetricData, Lifetime}; + +const HEADER: &str = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ"; +const KEY: &str = "OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg"; +const INIT_VECTOR: &str = "48V1_ALb6US04U3b"; +const CIPHER_TEXT: &str = + "5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A"; +const AUTH_TAG: &str = "XFBoMYUZodetZdvTiFvSkQ"; +const JWE: &str = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ"; + +#[test] +fn jwe_metric_is_generated_and_stored() { + let (glean, _t) = new_glean(None); + + let metric = JweMetric::new(CommonMetricData { + name: "jwe_metric".into(), + category: "local".into(), + send_in_pings: vec!["core".into()], + ..Default::default() + }); + + metric.set_with_compact_repr(&glean, JWE); + let snapshot = StorageManager + .snapshot_as_json(glean.storage(), "core", false) + .unwrap(); + + assert_eq!( + json!({"jwe": {"local.jwe_metric": metric.test_get_value(&glean, "core") }}), + snapshot + ); +} + +#[test] +fn set_properly_sets_the_value_in_all_stores() { + let (glean, _t) = new_glean(None); + let store_names: Vec = vec!["store1".into(), "store2".into()]; + + let metric = JweMetric::new(CommonMetricData { + name: "jwe_metric".into(), + category: "local".into(), + send_in_pings: store_names.clone(), + disabled: false, + lifetime: Lifetime::Ping, + ..Default::default() + }); + + metric.set_with_compact_repr(&glean, JWE); + + // Check that the data was correctly set in each store. + for store_name in store_names { + let snapshot = StorageManager + .snapshot_as_json(glean.storage(), &store_name, false) + .unwrap(); + + assert_eq!( + json!({"jwe": {"local.jwe_metric": metric.test_get_value(&glean, &store_name) }}), + snapshot + ); + } +} + +#[test] +fn get_test_value_returns_the_period_delimited_string() { + let (glean, _t) = new_glean(None); + + let metric = JweMetric::new(CommonMetricData { + name: "jwe_metric".into(), + category: "local".into(), + send_in_pings: vec!["core".into()], + disabled: false, + lifetime: Lifetime::Ping, + ..Default::default() + }); + + metric.set_with_compact_repr(&glean, JWE); + + assert_eq!(metric.test_get_value(&glean, "core").unwrap(), JWE); +} + +#[test] +fn get_test_value_as_json_string_returns_the_expected_repr() { + let (glean, _t) = new_glean(None); + + let metric = JweMetric::new(CommonMetricData { + name: "jwe_metric".into(), + category: "local".into(), + send_in_pings: vec!["core".into()], + disabled: false, + lifetime: Lifetime::Ping, + ..Default::default() + }); + + metric.set_with_compact_repr(&glean, JWE); + + let expected_json = format!("{{\"header\":\"{}\",\"key\":\"{}\",\"init_vector\":\"{}\",\"cipher_text\":\"{}\",\"auth_tag\":\"{}\"}}", HEADER, KEY, INIT_VECTOR, CIPHER_TEXT, AUTH_TAG); + assert_eq!( + metric + .test_get_value_as_json_string(&glean, "core") + .unwrap(), + expected_json + ); +}