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 ALG_ARGON2ID13: i64 = -71000;
41pub(crate) const ALG_PBKDF2_SHA256: i64 = -71010;
44
45pub(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;
52pub(crate) const CONTAINED_KEY_ID: i64 = -71005;
55pub(crate) const PBKDF2_ITERATIONS: i64 = -71011;
58pub(crate) const PBKDF2_SALT: i64 = -71012;
59
60const 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
67pub(crate) const SIGNING_NAMESPACE: i64 = -80000;
69
70pub(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 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
117pub(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
270pub trait CoseSerializable<T: CoseContentFormat + ConstContentFormat> {
272 fn to_cose(&self) -> Bytes<T>;
274 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
325pub(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}