Skip to content

Commit 16369db

Browse files
committed
Implement data envelope
1 parent 824c1cf commit 16369db

File tree

12 files changed

+714
-24
lines changed

12 files changed

+714
-24
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//! This example demonstrates how to seal a piece of data.
2+
//!
3+
//! If there is a struct that should be kept secret, in can be sealed with a `DataEnvelope`. This
4+
//! will automatically create a content-encryption-key. This is useful because the key is stored
5+
//! separately. Rotating the encrypting key now only requires re-uploading the
6+
//! content-encryption-key instead of the entire data. Further, server-side tampering (swapping of
7+
//! individual fields encrypted by the same key) is prevented.
8+
//!
9+
//! In general, if a struct of data should be protected, the `DataEnvelope` should be used.
10+
11+
use bitwarden_crypto::{key_ids, safe::SealableData};
12+
use serde::{Deserialize, Serialize};
13+
14+
#[derive(Serialize, Deserialize)]
15+
struct MyItem {
16+
a: u64,
17+
b: String,
18+
}
19+
impl SealableData for MyItem {}
20+
21+
fn main() {
22+
let store = bitwarden_crypto::KeyStore::<ExampleIds>::default();
23+
let mut ctx: bitwarden_crypto::KeyStoreContext<'_, ExampleIds> = store.context_mut();
24+
let mut disk = MockDisk::new();
25+
26+
let my_item = MyItem {
27+
a: 42,
28+
b: "Hello, World!".to_string(),
29+
};
30+
// Seal the item into an encrypted blob, and store the content-encryption-key in the context.
31+
let sealed_item = bitwarden_crypto::safe::DataEnvelope::seal(
32+
my_item,
33+
&bitwarden_crypto::safe::DataEnvelopeNamespace::VaultItem,
34+
ExampleSymmetricKey::ItemKey,
35+
&mut ctx,
36+
)
37+
.expect("Sealing should work");
38+
39+
// Store the sealed item on disk
40+
disk.save("sealed_item", (&sealed_item).into());
41+
let sealed_item = disk
42+
.load("sealed_item")
43+
.expect("Failed to load sealed item")
44+
.clone();
45+
let sealed_item: bitwarden_crypto::safe::DataEnvelope =
46+
bitwarden_crypto::safe::DataEnvelope::from(sealed_item);
47+
48+
let my_item: MyItem = sealed_item
49+
.unseal(
50+
&bitwarden_crypto::safe::DataEnvelopeNamespace::VaultItem,
51+
ExampleSymmetricKey::ItemKey,
52+
&mut ctx,
53+
)
54+
.expect("Unsealing should work");
55+
assert!(my_item.a == 42);
56+
assert!(my_item.b == "Hello, World!");
57+
}
58+
59+
pub(crate) struct MockDisk {
60+
map: std::collections::HashMap<String, Vec<u8>>,
61+
}
62+
63+
impl MockDisk {
64+
pub(crate) fn new() -> Self {
65+
MockDisk {
66+
map: std::collections::HashMap::new(),
67+
}
68+
}
69+
70+
pub(crate) fn save(&mut self, key: &str, value: Vec<u8>) {
71+
self.map.insert(key.to_string(), value);
72+
}
73+
74+
pub(crate) fn load(&self, key: &str) -> Option<&Vec<u8>> {
75+
self.map.get(key)
76+
}
77+
}
78+
79+
key_ids! {
80+
#[symmetric]
81+
pub enum ExampleSymmetricKey {
82+
#[local]
83+
ItemKey
84+
}
85+
86+
#[asymmetric]
87+
pub enum ExampleAsymmetricKey {
88+
Key(u8),
89+
}
90+
91+
#[signing]
92+
pub enum ExampleSigningKey {
93+
Key(u8),
94+
}
95+
pub ExampleIds => ExampleSymmetricKey, ExampleAsymmetricKey, ExampleSigningKey;
96+
}

crates/bitwarden-crypto/src/content_format.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub(crate) enum ContentFormat {
2424
CoseKey,
2525
/// CoseSign1 message
2626
CoseSign1,
27+
/// CoseEncrypt0 message
28+
CoseEncrypt0,
2729
/// Bitwarden Legacy Key
2830
/// There are three permissible byte values here:
2931
/// - `[u8; 32]` - AES-CBC (no hmac) key. This is to be removed and banned.
@@ -32,6 +34,8 @@ pub(crate) enum ContentFormat {
3234
BitwardenLegacyKey,
3335
/// Stream of bytes
3436
OctetStream,
37+
/// Cbor serialized data
38+
Cbor,
3539
}
3640

3741
mod private {
@@ -210,6 +214,34 @@ impl FromB64ContentFormat for CoseSign1ContentFormat {}
210214
/// serialized COSE Sign1 messages.
211215
pub type CoseSign1Bytes = Bytes<CoseSign1ContentFormat>;
212216

217+
/// CBOR serialized data
218+
#[derive(PartialEq, Eq, Clone, Debug)]
219+
pub struct CborContentFormat;
220+
impl private::Sealed for CborContentFormat {}
221+
impl ConstContentFormat for CborContentFormat {
222+
#[allow(private_interfaces)]
223+
fn content_format() -> ContentFormat {
224+
ContentFormat::Cbor
225+
}
226+
}
227+
/// CborBytes is a type alias for Bytes with `CborContentFormat`. This is used for CBOR serialized
228+
/// data.
229+
pub type CborBytes = Bytes<CborContentFormat>;
230+
231+
/// Content format for COSE Encrypt0 messages.
232+
#[derive(PartialEq, Eq, Clone, Debug)]
233+
pub struct CoseEncrypt0ContentFormat;
234+
impl private::Sealed for CoseEncrypt0ContentFormat {}
235+
impl ConstContentFormat for CoseEncrypt0ContentFormat {
236+
#[allow(private_interfaces)]
237+
fn content_format() -> ContentFormat {
238+
ContentFormat::CoseEncrypt0
239+
}
240+
}
241+
/// CoseEncrypt0Bytes is a type alias for Bytes with `CoseEncrypt0ContentFormat`. This is used for
242+
/// serialized COSE Encrypt0 messages.
243+
pub type CoseEncrypt0Bytes = Bytes<CoseEncrypt0ContentFormat>;
244+
213245
impl<Ids: KeyIds, T: ConstContentFormat> PrimitiveEncryptable<Ids, Ids::Symmetric, EncString>
214246
for Bytes<T>
215247
{

crates/bitwarden-crypto/src/cose.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use thiserror::Error;
1212
use typenum::U32;
1313

1414
use crate::{
15-
ContentFormat, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key,
15+
ContentFormat, CoseEncrypt0Bytes, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key,
1616
content_format::{Bytes, ConstContentFormat, CoseContentFormat},
1717
error::{EncStringParseError, EncodingError},
1818
xchacha20,
@@ -40,13 +40,15 @@ const CONTENT_TYPE_SPKI_PUBLIC_KEY: &str = "application/x.bitwarden.spki-public-
4040
//
4141
/// The label used for the namespace ensuring strong domain separation when using signatures.
4242
pub(crate) const SIGNING_NAMESPACE: i64 = -80000;
43+
/// The label used for the namespace ensuring strong domain separation when using data envelopes.
44+
pub(crate) const DATA_ENVELOPE_NAMESPACE: i64 = -80001;
4345

4446
/// Encrypts a plaintext message using XChaCha20Poly1305 and returns a COSE Encrypt0 message
4547
pub(crate) fn encrypt_xchacha20_poly1305(
4648
plaintext: &[u8],
4749
key: &crate::XChaCha20Poly1305Key,
4850
content_format: ContentFormat,
49-
) -> Result<Vec<u8>, CryptoError> {
51+
) -> Result<CoseEncrypt0Bytes, CryptoError> {
5052
let mut plaintext = plaintext.to_vec();
5153

5254
let header_builder: coset::HeaderBuilder = content_format.into();
@@ -78,14 +80,15 @@ pub(crate) fn encrypt_xchacha20_poly1305(
7880
cose_encrypt0
7981
.to_vec()
8082
.map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))
83+
.map(CoseEncrypt0Bytes::from)
8184
}
8285

8386
/// Decrypts a COSE Encrypt0 message, using a XChaCha20Poly1305 key
8487
pub(crate) fn decrypt_xchacha20_poly1305(
85-
cose_encrypt0_message: &[u8],
88+
cose_encrypt0_message: &CoseEncrypt0Bytes,
8689
key: &crate::XChaCha20Poly1305Key,
8790
) -> Result<(Vec<u8>, ContentFormat), CryptoError> {
88-
let msg = coset::CoseEncrypt0::from_slice(cose_encrypt0_message)
91+
let msg = coset::CoseEncrypt0::from_slice(cose_encrypt0_message.as_ref())
8992
.map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))?;
9093

9194
let Some(ref alg) = msg.protected.header.alg else {
@@ -180,12 +183,16 @@ impl From<ContentFormat> for coset::HeaderBuilder {
180183
}
181184
ContentFormat::CoseSign1 => header_builder.content_format(CoapContentFormat::CoseSign1),
182185
ContentFormat::CoseKey => header_builder.content_format(CoapContentFormat::CoseKey),
186+
ContentFormat::CoseEncrypt0 => {
187+
header_builder.content_format(CoapContentFormat::CoseEncrypt0)
188+
}
183189
ContentFormat::BitwardenLegacyKey => {
184190
header_builder.content_type(CONTENT_TYPE_BITWARDEN_LEGACY_KEY.to_string())
185191
}
186192
ContentFormat::OctetStream => {
187193
header_builder.content_format(CoapContentFormat::OctetStream)
188194
}
195+
ContentFormat::Cbor => header_builder.content_format(CoapContentFormat::Cbor),
189196
}
190197
}
191198
}
@@ -211,6 +218,7 @@ impl TryFrom<&coset::Header> for ContentFormat {
211218
Some(ContentType::Assigned(CoapContentFormat::OctetStream)) => {
212219
Ok(ContentFormat::OctetStream)
213220
}
221+
Some(ContentType::Assigned(CoapContentFormat::Cbor)) => Ok(ContentFormat::Cbor),
214222
_ => Err(CryptoError::EncString(
215223
EncStringParseError::CoseMissingContentType,
216224
)),
@@ -363,7 +371,9 @@ mod test {
363371
key_id: KEY_ID,
364372
enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
365373
};
366-
let decrypted = decrypt_xchacha20_poly1305(TEST_VECTOR_COSE_ENCRYPT0, &key).unwrap();
374+
let decrypted =
375+
decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key)
376+
.unwrap();
367377
assert_eq!(
368378
decrypted,
369379
(TEST_VECTOR_PLAINTEXT.to_vec(), ContentFormat::OctetStream)
@@ -377,7 +387,7 @@ mod test {
377387
enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
378388
};
379389
assert!(matches!(
380-
decrypt_xchacha20_poly1305(TEST_VECTOR_COSE_ENCRYPT0, &key),
390+
decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key),
381391
Err(CryptoError::WrongCoseKeyId)
382392
));
383393
}
@@ -394,7 +404,7 @@ mod test {
394404
.create_ciphertext(&[], &[], |_, _| Vec::new())
395405
.unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
396406
.build();
397-
let serialized_message = cose_encrypt0.to_vec().unwrap();
407+
let serialized_message = CoseEncrypt0Bytes::from(cose_encrypt0.to_vec().unwrap());
398408

399409
let key = XChaCha20Poly1305Key {
400410
key_id: KEY_ID,

crates/bitwarden-crypto/src/enc_string/symmetric.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use serde::Deserialize;
66

77
use super::{check_length, from_b64, from_b64_vec, split_enc_string};
88
use crate::{
9-
Aes256CbcHmacKey, ContentFormat, KeyDecryptable, KeyEncryptable, KeyEncryptableWithContentType,
10-
SymmetricCryptoKey, Utf8Bytes, XChaCha20Poly1305Key,
9+
Aes256CbcHmacKey, ContentFormat, CoseEncrypt0Bytes, KeyDecryptable, KeyEncryptable,
10+
KeyEncryptableWithContentType, SymmetricCryptoKey, Utf8Bytes, XChaCha20Poly1305Key,
1111
error::{CryptoError, EncStringParseError, Result, UnsupportedOperationError},
1212
};
1313

@@ -269,7 +269,9 @@ impl EncString {
269269
content_format: ContentFormat,
270270
) -> Result<EncString> {
271271
let data = crate::cose::encrypt_xchacha20_poly1305(data_dec, key, content_format)?;
272-
Ok(EncString::Cose_Encrypt0_B64 { data })
272+
Ok(EncString::Cose_Encrypt0_B64 {
273+
data: data.to_vec(),
274+
})
273275
}
274276

275277
/// The numerical representation of the encryption type of the [EncString].
@@ -314,8 +316,10 @@ impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
314316
EncString::Cose_Encrypt0_B64 { data },
315317
SymmetricCryptoKey::XChaCha20Poly1305Key(key),
316318
) => {
317-
let (decrypted_message, _) =
318-
crate::cose::decrypt_xchacha20_poly1305(data.as_slice(), key)?;
319+
let (decrypted_message, _) = crate::cose::decrypt_xchacha20_poly1305(
320+
&CoseEncrypt0Bytes::from(data.as_slice()),
321+
key,
322+
)?;
319323
Ok(decrypted_message)
320324
}
321325
_ => Err(CryptoError::WrongKeyType),

crates/bitwarden-crypto/src/error.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use bitwarden_error::bitwarden_error;
55
use thiserror::Error;
66
use uuid::Uuid;
77

8-
use crate::fingerprint::FingerprintError;
8+
use crate::{fingerprint::FingerprintError, safe::DataEnvelopeError};
99

1010
#[allow(missing_docs)]
1111
#[bitwarden_error(flat)]
@@ -68,6 +68,9 @@ pub enum CryptoError {
6868
#[error("Signature error, {0}")]
6969
Signature(#[from] SignatureError),
7070

71+
#[error("DataEnvelope error, {0}")]
72+
DataEnvelopeError(#[from] DataEnvelopeError),
73+
7174
#[error("Encoding error, {0}")]
7275
Encoding(#[from] EncodingError),
7376
}

crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ pub struct XChaCha20Poly1305Key {
7373
pub(crate) enc_key: Pin<Box<GenericArray<u8, U32>>>,
7474
}
7575

76+
impl XChaCha20Poly1305Key {
77+
/// Creates a new XChaCha20Poly1305Key with a securely sampled cryptographic key and key id.
78+
pub fn make() -> Self {
79+
let mut rng = rand::thread_rng();
80+
let mut enc_key = Box::pin(GenericArray::<u8, U32>::default());
81+
rng.fill(enc_key.as_mut_slice());
82+
let mut key_id = [0u8; KEY_ID_SIZE];
83+
rng.fill(&mut key_id);
84+
Self { enc_key, key_id }
85+
}
86+
}
87+
7688
impl ConstantTimeEq for XChaCha20Poly1305Key {
7789
fn ct_eq(&self, other: &Self) -> Choice {
7890
self.enc_key.ct_eq(&other.enc_key) & self.key_id.ct_eq(&other.key_id)

0 commit comments

Comments
 (0)