1use 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
27pub(crate) const XCHACHA20_POLY1305: i64 = -70000;
34pub(crate) const XAES_256_GCM: i64 = -70010;
40pub(crate) const AES_256_CBC_HMAC_SHA256_AEAD: i64 = -70011;
46pub(crate) const ALG_ARGON2ID13: i64 = -71000;
47pub(crate) const ALG_PBKDF2_SHA256: i64 = -71010;
50
51pub(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;
58pub(crate) const CONTAINED_KEY_ID: i64 = -71005;
61pub(crate) const PBKDF2_ITERATIONS: i64 = -71011;
64pub(crate) const PBKDF2_SALT: i64 = -71012;
65
66const 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
73pub(crate) const SIGNING_NAMESPACE: i64 = -80000;
75
76pub(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 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
123pub(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
276pub trait CoseSerializable<T: CoseContentFormat + ConstContentFormat> {
278 fn to_cose(&self) -> Bytes<T>;
280 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
331pub(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}