Skip to main content

bitwarden_crypto/cose/
mod.rs

1//! This file contains private-use constants for COSE encoded key types and algorithms.
2//! Standardized values from <https://www.iana.org/assignments/cose/cose.xhtml> should always be preferred
3//! unless there is a a clear benefit, such as a clear cryptographic benefit, which MUST
4//! be documented publicly.
5
6use std::fmt::Debug;
7
8pub(crate) mod symmetric;
9mod thumbprint;
10use coset::{
11    ContentType, Header, Label,
12    iana::{self, CoapContentFormat, KeyOperation},
13};
14use hybrid_array::Array;
15use thiserror::Error;
16pub(crate) use thumbprint::thumbprint_from_required_params;
17pub use thumbprint::{CoseKeyThumbprint, CoseKeyThumbprintExt};
18use typenum::U32;
19
20use crate::{
21    Aes256GcmKey, ContentFormat, CryptoError, SymmetricCryptoKey, XAes256GcmKey,
22    XChaCha20Poly1305Key,
23    content_format::{Bytes, ConstContentFormat, CoseContentFormat},
24    error::{EncStringParseError, EncodingError},
25};
26
27// Custom COSE algorithm values
28// NOTE: Any algorithm value below -65536 is reserved for private use in the IANA allocations and
29// can be used freely.
30/// XChaCha20 <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03> is used over ChaCha20
31/// to be able to randomly generate nonces, and to not have to worry about key wearout. Since
32/// the draft was never published as an RFC, we use a private-use value for the algorithm.
33pub(crate) const XCHACHA20_POLY1305: i64 = -70000;
34/// XAES-256-GCM (<https://c2sp.org/XAES-256-GCM>) extended-nonce AEAD. Given
35/// an input key and 192-bit nonce, a counter-based KDF is instantiated with
36/// CMAC-AES256, the input key, and the first 96 bits of the input nonce. The
37/// derived key and last 96 bits of the input nonce are then used to encrypt
38/// with AES-256-GCM.
39pub(crate) const XAES_256_GCM: i64 = -70010;
40/// AES-256-CBC-HMAC-SHA256 used as an Encrypt-then-MAC AEAD. No IANA-registered COSE algorithm
41/// describes this construction: the MAC is taken over the IV, length-prefixed ciphertext, and
42/// associated data, so that the associated data required by COSE is authenticated. See
43/// [`Aes256CbcHmacSha256Aead`](crate::hazmat::symmetric_encryption::Aes256CbcHmacSha256Aead) for
44/// the exact MAC input. A private-use value is therefore allocated.
45pub(crate) const AES_256_CBC_HMAC_SHA256_AEAD: i64 = -70011;
46pub(crate) const ALG_ARGON2ID13: i64 = -71000;
47/// PBKDF2-HMAC-SHA256 KDF algorithm discriminant, used by the password protected key envelope in
48/// the FIPS cipher suite. PBKDF2 is FIPS-approved, unlike Argon2id ([`ALG_ARGON2ID13`]).
49pub(crate) const ALG_PBKDF2_SHA256: i64 = -71010;
50
51// Custom labels for COSE headers
52// NOTE: Any label below -65536 is reserved for private use in the IANA allocations and can be used
53// freely.
54pub(crate) const ARGON2_SALT: i64 = -71001;
55pub(crate) const ARGON2_ITERATIONS: i64 = -71002;
56pub(crate) const ARGON2_MEMORY: i64 = -71003;
57pub(crate) const ARGON2_PARALLELISM: i64 = -71004;
58/// Indicates for any object containing a key (wrapped key, password protected key envelope) which
59/// key ID that contained key has
60pub(crate) const CONTAINED_KEY_ID: i64 = -71005;
61/// PBKDF2 iterations and salt, used by the password protected key envelope in the FIPS cipher
62/// suite.
63pub(crate) const PBKDF2_ITERATIONS: i64 = -71011;
64pub(crate) const PBKDF2_SALT: i64 = -71012;
65
66// Note: These are in the "unregistered" tree: https://datatracker.ietf.org/doc/html/rfc6838#section-3.4
67// These are only used within Bitwarden, and not meant for exchange with other systems.
68const CONTENT_TYPE_PADDED_UTF8: &str = "application/x.bitwarden.utf8-padded";
69pub(crate) const CONTENT_TYPE_PADDED_CBOR: &str = "application/x.bitwarden.cbor-padded";
70const CONTENT_TYPE_BITWARDEN_LEGACY_KEY: &str = "application/x.bitwarden.legacy-key";
71const CONTENT_TYPE_SPKI_PUBLIC_KEY: &str = "application/x.bitwarden.spki-public-key";
72
73/// The label used for the namespace ensuring strong domain separation when using signatures.
74pub(crate) const SIGNING_NAMESPACE: i64 = -80000;
75
76// Domain separation / Namespaces
77//
78// Cryptographic objects are strongly domain separated so that items can only be decrypted
79// in the correct context, making cryptographic analysis significantly easier and preventing
80// misuse of cryptographic objects. For this, there is a partitioning at two layers. First,
81// the object types are partitioned into e.g. EncString, DataEnvelope, Signature, KeyEnvelope, and
82// so on. Second, within each of these types, each of these spans their own namespace for usages.
83// For instance, a DataEnvelope may describe that the contained item is only valid as a vault item,
84// or as account settings.
85
86/// MUST be placed in the protected header of cose objects
87pub(crate) const SAFE_OBJECT_NAMESPACE: i64 = -80002;
88
89#[allow(clippy::enum_variant_names)]
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub(crate) enum SafeObjectNamespace {
92    PasswordProtectedKeyEnvelope = 1,
93    DataEnvelope = 2,
94    SymmetricKeyEnvelope = 3,
95    //Reserved:
96    //PrivateKeyEnvelope = 4,
97    //SigningKeyEnvelope = 5,
98    SecretProtectedKeyEnvelope = 6,
99}
100
101impl TryFrom<i128> for SafeObjectNamespace {
102    type Error = ();
103
104    fn try_from(value: i128) -> Result<Self, Self::Error> {
105        match value {
106            1 => Ok(SafeObjectNamespace::PasswordProtectedKeyEnvelope),
107            2 => Ok(SafeObjectNamespace::DataEnvelope),
108            3 => Ok(SafeObjectNamespace::SymmetricKeyEnvelope),
109            6 => Ok(SafeObjectNamespace::SecretProtectedKeyEnvelope),
110            _ => Err(()),
111        }
112    }
113}
114
115impl From<SafeObjectNamespace> for i128 {
116    fn from(namespace: SafeObjectNamespace) -> Self {
117        namespace as i128
118    }
119}
120
121pub(crate) trait ContentNamespace: TryFrom<i128> + Into<i128> + PartialEq + Debug {}
122
123/// Each type of object has it's own namespace for strong domain separation to eliminate
124/// attacks which attempt to confuse object types. For signatures, this refers to signature
125/// namespaces, for data envelopes to data envelope namespaces and so on.
126pub(crate) const SAFE_CONTENT_NAMESPACE: i64 = -80001;
127
128const SYMMETRIC_KEY: Label = Label::Int(iana::SymmetricKeyParameter::K as i64);
129
130impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey {
131    type Error = CryptoError;
132
133    #[bitwarden_logging::instrument(err)]
134    fn try_from(cose_key: &coset::CoseKey) -> Result<Self, Self::Error> {
135        let key_bytes = cose_key
136            .params
137            .iter()
138            .find_map(|(label, value)| match (label, value) {
139                (&SYMMETRIC_KEY, ciborium::Value::Bytes(bytes)) => Some(bytes),
140                _ => None,
141            })
142            .ok_or(CryptoError::InvalidKey)?;
143        let alg = cose_key.alg.as_ref().ok_or(CryptoError::InvalidKey)?;
144        let key_opts = cose_key
145            .key_ops
146            .iter()
147            .map(|op| match op {
148                coset::RegisteredLabel::Assigned(iana::KeyOperation::Encrypt) => {
149                    Ok(KeyOperation::Encrypt)
150                }
151                coset::RegisteredLabel::Assigned(iana::KeyOperation::Decrypt) => {
152                    Ok(KeyOperation::Decrypt)
153                }
154                coset::RegisteredLabel::Assigned(iana::KeyOperation::WrapKey) => {
155                    Ok(KeyOperation::WrapKey)
156                }
157                coset::RegisteredLabel::Assigned(iana::KeyOperation::UnwrapKey) => {
158                    Ok(KeyOperation::UnwrapKey)
159                }
160                _ => Err(CryptoError::InvalidKey),
161            })
162            .collect::<Result<Vec<KeyOperation>, CryptoError>>()?;
163
164        match alg {
165            coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
166                let enc_key = Box::pin(
167                    Array::<u8, U32>::try_from(key_bytes).map_err(|_| CryptoError::InvalidKey)?,
168                );
169                let key_id = cose_key
170                    .key_id
171                    .as_slice()
172                    .try_into()
173                    .map_err(|_| CryptoError::InvalidKey)?;
174                Ok(SymmetricCryptoKey::XChaCha20Poly1305Key(
175                    XChaCha20Poly1305Key {
176                        enc_key,
177                        key_id,
178                        supported_operations: key_opts,
179                    },
180                ))
181            }
182            coset::Algorithm::PrivateUse(XAES_256_GCM) => {
183                let enc_key = Box::pin(
184                    Array::<u8, U32>::try_from(key_bytes).map_err(|_| CryptoError::InvalidKey)?,
185                );
186                let key_id = cose_key
187                    .key_id
188                    .as_slice()
189                    .try_into()
190                    .map_err(|_| CryptoError::InvalidKey)?;
191                Ok(SymmetricCryptoKey::XAes256GcmKey(XAes256GcmKey {
192                    enc_key,
193                    key_id,
194                    supported_operations: key_opts,
195                }))
196            }
197            coset::Algorithm::Assigned(iana::Algorithm::A256GCM) => {
198                let enc_key = Box::pin(
199                    Array::<u8, U32>::try_from(key_bytes).map_err(|_| CryptoError::InvalidKey)?,
200                );
201                let key_id = cose_key
202                    .key_id
203                    .as_slice()
204                    .try_into()
205                    .map_err(|_| CryptoError::InvalidKey)?;
206                Ok(SymmetricCryptoKey::Aes256GcmKey(Aes256GcmKey {
207                    enc_key,
208                    key_id,
209                    supported_operations: key_opts,
210                }))
211            }
212            _ => Err(CryptoError::InvalidKey),
213        }
214    }
215}
216
217impl From<ContentFormat> for coset::HeaderBuilder {
218    fn from(format: ContentFormat) -> Self {
219        let header_builder = coset::HeaderBuilder::new();
220
221        match format {
222            ContentFormat::Utf8 => {
223                header_builder.content_type(CONTENT_TYPE_PADDED_UTF8.to_string())
224            }
225            ContentFormat::Pkcs8PrivateKey => {
226                header_builder.content_format(CoapContentFormat::Pkcs8)
227            }
228            ContentFormat::SPKIPublicKeyDer => {
229                header_builder.content_type(CONTENT_TYPE_SPKI_PUBLIC_KEY.to_string())
230            }
231            ContentFormat::CoseSign1 => header_builder.content_format(CoapContentFormat::CoseSign1),
232            ContentFormat::CoseKey => header_builder.content_format(CoapContentFormat::CoseKey),
233            ContentFormat::CoseEncrypt0 => {
234                header_builder.content_format(CoapContentFormat::CoseEncrypt0)
235            }
236            ContentFormat::BitwardenLegacyKey => {
237                header_builder.content_type(CONTENT_TYPE_BITWARDEN_LEGACY_KEY.to_string())
238            }
239            ContentFormat::OctetStream => {
240                header_builder.content_format(CoapContentFormat::OctetStream)
241            }
242            ContentFormat::Cbor => header_builder.content_format(CoapContentFormat::Cbor),
243        }
244    }
245}
246
247impl TryFrom<&coset::Header> for ContentFormat {
248    type Error = CryptoError;
249
250    fn try_from(header: &coset::Header) -> Result<Self, Self::Error> {
251        match header.content_type.as_ref() {
252            Some(ContentType::Text(format)) if format == CONTENT_TYPE_PADDED_UTF8 => {
253                Ok(ContentFormat::Utf8)
254            }
255            Some(ContentType::Text(format)) if format == CONTENT_TYPE_BITWARDEN_LEGACY_KEY => {
256                Ok(ContentFormat::BitwardenLegacyKey)
257            }
258            Some(ContentType::Text(format)) if format == CONTENT_TYPE_SPKI_PUBLIC_KEY => {
259                Ok(ContentFormat::SPKIPublicKeyDer)
260            }
261            Some(ContentType::Assigned(CoapContentFormat::Pkcs8)) => {
262                Ok(ContentFormat::Pkcs8PrivateKey)
263            }
264            Some(ContentType::Assigned(CoapContentFormat::CoseKey)) => Ok(ContentFormat::CoseKey),
265            Some(ContentType::Assigned(CoapContentFormat::OctetStream)) => {
266                Ok(ContentFormat::OctetStream)
267            }
268            Some(ContentType::Assigned(CoapContentFormat::Cbor)) => Ok(ContentFormat::Cbor),
269            _ => Err(CryptoError::EncString(
270                EncStringParseError::CoseMissingContentType,
271            )),
272        }
273    }
274}
275
276/// Trait for structs that are serializable to COSE objects.
277pub trait CoseSerializable<T: CoseContentFormat + ConstContentFormat> {
278    /// Serializes the struct to COSE serialization
279    fn to_cose(&self) -> Bytes<T>;
280    /// Deserializes a serialized COSE object to a struct
281    fn from_cose(bytes: &Bytes<T>) -> Result<Self, EncodingError>
282    where
283        Self: Sized;
284}
285
286pub(crate) fn extract_integer(
287    header: &Header,
288    target_label: i64,
289    value_name: &str,
290) -> Result<i128, CoseExtractError> {
291    header
292        .rest
293        .iter()
294        .find_map(|(label, value)| match (label, value) {
295            (Label::Int(label_value), ciborium::Value::Integer(int_value))
296                if *label_value == target_label =>
297            {
298                Some(*int_value)
299            }
300            _ => None,
301        })
302        .map(Into::into)
303        .ok_or_else(|| CoseExtractError::MissingValue(value_name.to_string()))
304}
305
306pub(crate) fn extract_bytes(
307    header: &Header,
308    target_label: i64,
309    value_name: &str,
310) -> Result<Vec<u8>, CoseExtractError> {
311    header
312        .rest
313        .iter()
314        .find_map(|(label, value)| match (label, value) {
315            (Label::Int(label_value), ciborium::Value::Bytes(byte_value))
316                if *label_value == target_label =>
317            {
318                Some(byte_value.clone())
319            }
320            _ => None,
321        })
322        .ok_or(CoseExtractError::MissingValue(value_name.to_string()))
323}
324
325#[derive(Debug, Error)]
326pub(crate) enum CoseExtractError {
327    #[error("Missing value {0}")]
328    MissingValue(String),
329}
330
331/// Helper function to convert a COSE KeyOperation to a debug string
332pub(crate) fn debug_key_operation(key_operation: KeyOperation) -> &'static str {
333    match key_operation {
334        KeyOperation::Sign => "Sign",
335        KeyOperation::Verify => "Verify",
336        KeyOperation::Encrypt => "Encrypt",
337        KeyOperation::Decrypt => "Decrypt",
338        KeyOperation::WrapKey => "WrapKey",
339        KeyOperation::UnwrapKey => "UnwrapKey",
340        KeyOperation::DeriveKey => "DeriveKey",
341        KeyOperation::DeriveBits => "DeriveBits",
342        _ => "Unknown",
343    }
344}