Skip to main content

bitwarden_crypto/safe/
mod.rs

1#![doc = include_str!("./README.md")]
2
3mod password_protected_key_envelope;
4pub use password_protected_key_envelope::*;
5mod high_entropy_secret;
6pub use high_entropy_secret::*;
7mod secret_protected_key_envelope;
8pub use secret_protected_key_envelope::*;
9mod symmetric_key_envelope;
10pub use symmetric_key_envelope::*;
11mod data_envelope;
12pub use data_envelope::*;
13mod helpers;
14
15use ciborium::Value;
16
17use crate::{
18    BitwardenLegacyKeyBytes, ContentFormat, CoseKeyBytes, EncodedSymmetricKey, KEY_ID_SIZE,
19    SymmetricCryptoKey,
20    cose::{CONTAINED_KEY_ID, extract_bytes},
21    keys::KeyId,
22    safe::helpers::set_header_value,
23};
24
25/// Failure modes when decoding the raw key bytes recovered from a key envelope back into a
26/// [`SymmetricCryptoKey`]. See [`decode_sealed_symmetric_key`].
27pub(super) enum DecodeSealedKeyError {
28    /// The protected header did not declare a valid content format.
29    InvalidContentFormat,
30    /// The declared content format is not a supported symmetric key encoding.
31    UnsupportedContentFormat,
32    /// The decoded bytes do not form a valid symmetric key.
33    InvalidKey,
34}
35
36/// Decodes the raw key bytes recovered from a key envelope into a [`SymmetricCryptoKey`], using the
37/// content format declared in the envelope's protected `header`.
38///
39/// Shared by the key envelopes
40/// ([`PasswordProtectedKeyEnvelope`], [`SecretProtectedKeyEnvelope`], and
41/// [`SymmetricKeyEnvelope`]), which all store the wrapped key using the same content-format-tagged
42/// encoding.
43pub(super) fn decode_sealed_symmetric_key(
44    header: &coset::Header,
45    key_bytes: Vec<u8>,
46) -> Result<SymmetricCryptoKey, DecodeSealedKeyError> {
47    let encoded_key = match ContentFormat::try_from(header)
48        .map_err(|_| DecodeSealedKeyError::InvalidContentFormat)?
49    {
50        ContentFormat::BitwardenLegacyKey => {
51            EncodedSymmetricKey::BitwardenLegacyKey(BitwardenLegacyKeyBytes::from(key_bytes))
52        }
53        ContentFormat::CoseKey => EncodedSymmetricKey::CoseKey(CoseKeyBytes::from(key_bytes)),
54        _ => return Err(DecodeSealedKeyError::UnsupportedContentFormat),
55    };
56    SymmetricCryptoKey::try_from(encoded_key).map_err(|_| DecodeSealedKeyError::InvalidKey)
57}
58
59/// Extract the single recipient from a [`coset::CoseEncrypt`].
60///
61/// The COSE objects used by this module's envelopes always carry exactly one recipient (holding the
62/// KDF parameters). Returns an error if there is not exactly one recipient.
63pub(super) fn extract_single_recipient(
64    cose_encrypt: &coset::CoseEncrypt,
65) -> Result<&coset::CoseRecipient, ()> {
66    match cose_encrypt.recipients.as_slice() {
67        [recipient] => Ok(recipient),
68        _ => Err(()),
69    }
70}
71
72/// Extract the contained key ID from a COSE header, if present.
73/// Only COSE keys have a key ID; legacy keys do not.
74pub(super) fn extract_key_id(header: &coset::Header) -> Result<Option<KeyId>, ()> {
75    let key_id_bytes = extract_bytes(header, CONTAINED_KEY_ID, "key id");
76
77    if let Ok(bytes) = key_id_bytes {
78        let key_id_array: [u8; KEY_ID_SIZE] = bytes.as_slice().try_into().map_err(|_| ())?;
79        Ok(Some(KeyId::from(key_id_array)))
80    } else {
81        Ok(None)
82    }
83}
84
85/// Set the contained key ID on a COSE header, if present.
86pub(super) fn set_contained_key_id(header: &mut coset::Header, key_id: Option<KeyId>) {
87    if let Some(key_id) = key_id {
88        set_header_value(header, CONTAINED_KEY_ID, Value::from(Vec::from(&key_id)));
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn set_contained_key_id_round_trips_through_extract_key_id() {
98        let key_id = KeyId::from([7u8; KEY_ID_SIZE]);
99        let mut header = coset::HeaderBuilder::new().build();
100
101        set_contained_key_id(&mut header, Some(key_id.clone()));
102
103        assert_eq!(extract_key_id(&header), Ok(Some(key_id)));
104    }
105
106    #[test]
107    fn extract_key_id_returns_none_when_absent() {
108        let header = coset::HeaderBuilder::new().build();
109
110        assert_eq!(extract_key_id(&header), Ok(None));
111    }
112
113    #[test]
114    fn set_contained_key_id_with_none_leaves_header_without_key_id() {
115        let mut header = coset::HeaderBuilder::new().build();
116
117        set_contained_key_id(&mut header, None);
118
119        assert_eq!(extract_key_id(&header), Ok(None));
120    }
121}