Skip to main content

bitwarden_crypto/safe/
helpers.rs

1use std::fmt::DebugStruct;
2
3use ciborium::Value;
4
5use crate::cose::{
6    ContentNamespace, SAFE_CONTENT_NAMESPACE, SAFE_OBJECT_NAMESPACE, SafeObjectNamespace,
7    extract_integer, symmetric::CoseContentEncryptionAlgorithm,
8};
9
10#[derive(Debug)]
11pub(super) enum ExtractionError {
12    MissingNamespace,
13    InvalidNamespace,
14}
15
16pub(super) fn extract_safe_object_namespace(
17    header: &coset::Header,
18) -> Result<SafeObjectNamespace, ExtractionError> {
19    match extract_integer(header, SAFE_OBJECT_NAMESPACE, "safe object namespace") {
20        Ok(value) => value
21            .try_into()
22            .map_err(|_| ExtractionError::InvalidNamespace),
23        Err(_) => Err(ExtractionError::MissingNamespace),
24    }
25}
26
27pub(super) fn extract_safe_content_namespace<T: ContentNamespace>(
28    header: &coset::Header,
29) -> Result<T, ExtractionError> {
30    match extract_integer(header, SAFE_CONTENT_NAMESPACE, "safe content namespace") {
31        Ok(value) => value
32            .try_into()
33            .map_err(|_| ExtractionError::InvalidNamespace),
34        Err(_) => Err(ExtractionError::MissingNamespace),
35    }
36}
37
38pub(super) fn debug_fmt<C: ContentNamespace>(
39    debug_struct: &mut DebugStruct,
40    header: &coset::Header,
41) {
42    if let Ok(object_namespace) = extract_safe_object_namespace(header) {
43        debug_struct.field("object_namespace", &object_namespace);
44    }
45    if let Ok(content_namespace) = extract_safe_content_namespace::<C>(header) {
46        debug_struct.field("content_namespace", &content_namespace);
47    }
48    if let Some(algorithm) = header.alg.as_ref()
49        && let Ok(content_encryption_algorithm) =
50            CoseContentEncryptionAlgorithm::try_from(algorithm)
51    {
52        let label = match content_encryption_algorithm {
53            CoseContentEncryptionAlgorithm::Aes256Gcm => "AES-256-GCM",
54            CoseContentEncryptionAlgorithm::XAes256Gcm => "XAES-256-GCM",
55            CoseContentEncryptionAlgorithm::XChaCha20Poly1305 => "XChaCha20-Poly1305",
56        };
57        debug_struct.field("content_encryption_algorithm", &label);
58    }
59}
60
61pub(super) fn set_header_value(header: &mut coset::Header, label: i64, value: Value) {
62    if let Some((_, existing_value)) =
63        header
64            .rest
65            .iter_mut()
66            .find(|(existing_label, _)| matches!(existing_label, coset::Label::Int(existing) if *existing == label))
67    {
68        *existing_value = value;
69    } else {
70        header.rest.push((coset::Label::Int(label), value));
71    }
72}
73
74pub(super) fn set_safe_namespaces<T: ContentNamespace>(
75    header: &mut coset::Header,
76    object_namespace: SafeObjectNamespace,
77    content_namespace: T,
78) {
79    set_header_value(
80        header,
81        SAFE_OBJECT_NAMESPACE,
82        Value::from(i128::from(object_namespace)),
83    );
84    set_header_value(
85        header,
86        SAFE_CONTENT_NAMESPACE,
87        Value::from(content_namespace.into()),
88    );
89}
90
91/// Validates the provided header contains the expected object and content namespace.
92/// For backward compatibility, missing values are OK, but incorrect values are not.
93/// The validation happens individually for both namespace layers, and either one
94/// missing with the other being present is OK.
95pub(super) fn validate_safe_namespaces<T: ContentNamespace>(
96    header: &coset::Header,
97    expected_object_namespace: SafeObjectNamespace,
98    expected_content_namespace: T,
99) -> Result<(), ExtractionError> {
100    match extract_safe_object_namespace(header) {
101        Ok(ns) if ns == expected_object_namespace => (),
102        // If the namespace is present but doesn't match, return an error immediately.
103        Ok(_) => return Err(ExtractionError::InvalidNamespace),
104        // If the namespace is missing, do not validate for backward compatibility
105        Err(ExtractionError::MissingNamespace) => (),
106        // If the namespace is present but invalid (e.g., not an integer or out of range), return an
107        // error.
108        Err(ExtractionError::InvalidNamespace) => return Err(ExtractionError::InvalidNamespace),
109    }
110
111    match extract_safe_content_namespace::<T>(header) {
112        Ok(ns) if ns == expected_content_namespace => Ok(()),
113        // If the namespace is present but doesn't match, return an error immediately.
114        Ok(_) => Err(ExtractionError::InvalidNamespace),
115        // If the namespace is missing, do not validate for backward compatibility
116        Err(ExtractionError::MissingNamespace) => Ok(()),
117        // If the namespace is present but invalid (e.g., not an integer or out of range), return an
118        // error.
119        Err(ExtractionError::InvalidNamespace) => Err(ExtractionError::InvalidNamespace),
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use ciborium::Value;
126
127    use super::*;
128    use crate::{cose::SAFE_OBJECT_NAMESPACE, safe::DataEnvelopeNamespace};
129
130    fn count_label(header: &coset::Header, label: i64) -> usize {
131        header
132            .rest
133            .iter()
134            .filter(
135                |(existing_label, _)| {
136                    matches!(existing_label, coset::Label::Int(existing) if *existing == label)
137                },
138            )
139            .count()
140    }
141
142    fn extract_safe_namespaces<T: ContentNamespace>(
143        header: &coset::Header,
144    ) -> Result<(SafeObjectNamespace, T), ExtractionError> {
145        let object_namespace = extract_safe_object_namespace(header)?;
146        let content_namespace = extract_safe_content_namespace(header)?;
147
148        Ok((object_namespace, content_namespace))
149    }
150
151    #[test]
152    fn set_safe_namespaces_sets_both_namespace_labels() {
153        let mut header = coset::HeaderBuilder::new().build();
154
155        set_safe_namespaces(
156            &mut header,
157            SafeObjectNamespace::DataEnvelope,
158            DataEnvelopeNamespace::ExampleNamespace,
159        );
160
161        let extracted = extract_safe_namespaces::<DataEnvelopeNamespace>(&header);
162        assert!(matches!(
163            extracted,
164            Ok((
165                SafeObjectNamespace::DataEnvelope,
166                DataEnvelopeNamespace::ExampleNamespace
167            ))
168        ));
169    }
170
171    #[test]
172    fn set_safe_namespaces_overwrites_existing_namespace_values() {
173        let mut header = coset::HeaderBuilder::new()
174            .value(SAFE_OBJECT_NAMESPACE, Value::from(999_i64))
175            .value(SAFE_CONTENT_NAMESPACE, Value::from(999_i64))
176            .build();
177
178        set_safe_namespaces(
179            &mut header,
180            SafeObjectNamespace::DataEnvelope,
181            DataEnvelopeNamespace::ExampleNamespace,
182        );
183
184        assert_eq!(count_label(&header, SAFE_OBJECT_NAMESPACE), 1);
185        assert_eq!(count_label(&header, SAFE_CONTENT_NAMESPACE), 1);
186        assert!(matches!(
187            extract_safe_namespaces::<DataEnvelopeNamespace>(&header),
188            Ok((
189                SafeObjectNamespace::DataEnvelope,
190                DataEnvelopeNamespace::ExampleNamespace
191            ))
192        ));
193    }
194
195    #[test]
196    fn extract_safe_namespaces_fails_when_namespace_missing() {
197        let header = coset::HeaderBuilder::new().build();
198
199        assert!(matches!(
200            extract_safe_namespaces::<DataEnvelopeNamespace>(&header),
201            Err(ExtractionError::MissingNamespace)
202        ));
203    }
204
205    #[test]
206    fn extract_safe_namespaces_fails_when_namespace_invalid() {
207        let header = coset::HeaderBuilder::new()
208            .value(
209                SAFE_OBJECT_NAMESPACE,
210                Value::from(SafeObjectNamespace::DataEnvelope as i64),
211            )
212            .value(SAFE_CONTENT_NAMESPACE, Value::from(999_i64))
213            .build();
214
215        assert!(matches!(
216            extract_safe_namespaces::<DataEnvelopeNamespace>(&header),
217            Err(ExtractionError::InvalidNamespace)
218        ));
219    }
220
221    #[test]
222    fn validate_safe_namespaces_allows_missing_labels_for_backwards_compat() {
223        let header = coset::HeaderBuilder::new().build();
224
225        let result = validate_safe_namespaces(
226            &header,
227            SafeObjectNamespace::DataEnvelope,
228            DataEnvelopeNamespace::ExampleNamespace,
229        );
230        assert!(result.is_ok());
231    }
232
233    #[test]
234    fn validate_safe_namespaces_rejects_namespace_mismatch() {
235        let mut header = coset::HeaderBuilder::new().build();
236        set_safe_namespaces(
237            &mut header,
238            SafeObjectNamespace::DataEnvelope,
239            DataEnvelopeNamespace::ExampleNamespace,
240        );
241
242        let result = validate_safe_namespaces(
243            &header,
244            SafeObjectNamespace::DataEnvelope,
245            DataEnvelopeNamespace::ExampleNamespace2,
246        );
247        assert!(matches!(result, Err(ExtractionError::InvalidNamespace)));
248    }
249}