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