Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions crates/bitwarden-crypto/examples/protect_key_with_secret.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! This example demonstrates how to securely protect keys with a high-entropy secret using the
//! [SecretProtectedKeyEnvelope].
//!
//! Unlike the [bitwarden_crypto::safe::PasswordProtectedKeyEnvelope], which is meant for
//! low-entropy secrets (PIN, password) and uses a slow, memory-hard KDF, this envelope is meant for
//! high-entropy secrets of arbitrary length (a random URL-fragment secret, a derived key, random
//! bytes) and uses a cheap KDF.

use bitwarden_crypto::{
KeyStore, KeyStoreContext, SymmetricKeyAlgorithm, key_slot_ids,
safe::{
HighEntropySecret, SecretProtectedKeyEnvelope, SecretProtectedKeyEnvelopeError,
SecretProtectedKeyEnvelopeNamespace,
},
};

fn main() {
let key_store = KeyStore::<ExampleIds>::default();
let mut ctx: KeyStoreContext<'_, ExampleIds> = key_store.context_mut();
let mut disk = MockDisk::new();

// Alice wants to protect a key with a high-entropy secret.
// For example to:
// - Protect a send with a random URL fragment secret
// - Protect a key with another (derived) key
// For this, the `SecretProtectedKeyEnvelope` is used.
// (For low-entropy secrets such as a PIN, use the `PasswordProtectedKeyEnvelope` instead.)

// Alice has some data protected with a symmetric key. She wants the symmetric key protected
// with a high-entropy secret (here, 16 random bytes).
let data_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
let secret = HighEntropySecret::make(16).expect("16 bytes is a valid size");

// Seal the key with the secret.
// The KDF salt is chosen for you, and does not need to be separately tracked or synced.
// Next, store this protected key envelope on disk.
let envelope = SecretProtectedKeyEnvelope::seal(
data_key,
&secret,
SecretProtectedKeyEnvelopeNamespace::ExampleUse,
&ctx,
)
.expect("Sealing should work");
disk.save("data_key_envelope", (&envelope).into());

// Wipe the context to simulate new session
ctx.clear_local();

// Load the envelope from disk and unseal it with the secret, and store it in the context.
let deserialized: SecretProtectedKeyEnvelope = SecretProtectedKeyEnvelope::try_from(
disk.load("data_key_envelope")
.expect("Loading from disk should work"),
)
.expect("Deserializing envelope should work");
let _unsealed_data_key = deserialized
.unseal(
&secret,
SecretProtectedKeyEnvelopeNamespace::ExampleUse,
&mut ctx,
)
.expect("Unsealing should work");

// Alice wants to rotate the secret. Re-sealing will update the secret and salt.
let new_secret = HighEntropySecret::make(16).expect("16 bytes is a valid size");
let envelope = envelope
.reseal(
&secret,
&new_secret,
SecretProtectedKeyEnvelopeNamespace::ExampleUse,
)
.expect("The secret should be valid");
disk.save("data_key_envelope", (&envelope).into());

// Alice wants to change the protected key. This requires creating a new envelope
let data_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
let envelope = SecretProtectedKeyEnvelope::seal(
data_key,
&new_secret,
SecretProtectedKeyEnvelopeNamespace::ExampleUse,
&ctx,
)
.expect("Sealing should work");
disk.save("data_key_envelope", (&envelope).into());

// Alice tries a secret but it is wrong
let wrong_secret = HighEntropySecret::make(16).expect("16 bytes is a valid size");
assert!(matches!(
envelope.unseal(
&wrong_secret,
SecretProtectedKeyEnvelopeNamespace::ExampleUse,
&mut ctx
),
Err(SecretProtectedKeyEnvelopeError::WrongSecret)
));
}

pub(crate) struct MockDisk {
map: std::collections::HashMap<String, Vec<u8>>,
}

impl MockDisk {
pub(crate) fn new() -> Self {
MockDisk {
map: std::collections::HashMap::new(),
}
}

pub(crate) fn save(&mut self, key: &str, value: Vec<u8>) {
self.map.insert(key.to_string(), value);
}

pub(crate) fn load(&self, key: &str) -> Option<&Vec<u8>> {
self.map.get(key)
}
}

key_slot_ids! {
#[symmetric]
pub enum ExampleSymmetricKey {
#[local]
DataKey(LocalId)
}

#[private]
pub enum ExamplePrivateKey {
Key(u8),
#[local]
Local(LocalId)
}

#[signing]
pub enum ExampleSigningKey {
Key(u8),
#[local]
Local(LocalId)
}

pub ExampleIds => ExampleSymmetricKey, ExamplePrivateKey, ExampleSigningKey;
}
3 changes: 2 additions & 1 deletion crates/bitwarden-crypto/src/cose/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub(crate) enum SafeObjectNamespace {
//Reserved:
//PrivateKeyEnvelope = 4,
//SigningKeyEnvelope = 5,
//SecretProtectedKeyEnvelope = 6,
SecretProtectedKeyEnvelope = 6,
}

impl TryFrom<i128> for SafeObjectNamespace {
Expand All @@ -84,6 +84,7 @@ impl TryFrom<i128> for SafeObjectNamespace {
1 => Ok(SafeObjectNamespace::PasswordProtectedKeyEnvelope),
2 => Ok(SafeObjectNamespace::DataEnvelope),
3 => Ok(SafeObjectNamespace::SymmetricKeyEnvelope),
6 => Ok(SafeObjectNamespace::SecretProtectedKeyEnvelope),
_ => Err(()),
}
}
Expand Down
27 changes: 24 additions & 3 deletions crates/bitwarden-crypto/src/cose/symmetric.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
//! COSE symmetric encryption β€” the middle layer of the three-layer stack:
//! - Lowest: Hazmat primitive (`crate::hazmat::symmetric_encryption`)
//! Content encryption of COSE `CoseEncrypt`/`CoseEncrypt0` messages by the hazmat symmetric
//! ciphers.
//!
//! This is the middle layer of the three-layer symmetric-encryption stack:
//! - Lowest: hazmat primitive ([`crate::hazmat::symmetric_encryption`])
//! - Mid: COSE framing (this module)
//! - High: Consumer (`crate::safe`, `EncString`)
//! - High: consumer ([`crate::safe`], [`EncString`](crate::EncString))
//!
//! The [`CoseEncryptCipher`] trait adds `encrypt_cose`/`decrypt_cose` (multi-recipient
//! [`CoseEncrypt`]) and `encrypt_cose0`/`decrypt_cose0` (single-recipient [`CoseEncrypt0`]) to an
//! [`Aead`] cipher: the caller supplies the content-encryption key (CEK) - typically derived via a
//! KDF or held in the key store - and the protected headers, while the cipher owns the
//! symmetric-encryption details. The cipher declares its COSE content-encryption algorithm in the
//! protected header (so it is authenticated as associated data) and a fresh nonce is generated per
//! message and stored in the unprotected `iv` header.
//!
//! Two ciphers are implemented, and the message shape (single- vs multi-recipient) is orthogonal to
//! the cipher choice:
//! - AES-256-GCM, used by the
//! [`SecretProtectedKeyEnvelope`](crate::safe::SecretProtectedKeyEnvelope) over [`CoseEncrypt`].
//! AES-GCM is sound here because the CEK is locally derived and unique per message, so there is
//! no nonce-reuse problem. See [`crate::hazmat::symmetric_encryption::aes_gcm`] for the caveats.
//! - XChaCha20-Poly1305, used by the [`SymmetricKeyEnvelope`](crate::safe::SymmetricKeyEnvelope)
//! over [`CoseEncrypt0`]. It uses a private-use COSE algorithm identifier (see
//! [`XCHACHA20_POLY1305`]).

use coset::{
Algorithm, CborSerializable, CoseEncrypt, CoseEncrypt0, CoseEncrypt0Builder,
Expand Down
17 changes: 17 additions & 0 deletions crates/bitwarden-crypto/src/safe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ include:
Internally, the module uses a KDF to protect against brute-forcing, but it does not expose this to
the consumer. The consumer only provides a password and key.

## Secret-protected key envelope

Use the secret-protected key envelope to protect a symmetric key with a **high-entropy** secret of
arbitrary length. Examples include:

- protecting a send's key with a random URL-fragment secret
- protecting a key with PRF output
- protecting a key with a key-connector-stored-secret
- protecting a key with a biometric-derived-secret

Because the secret is assumed to be high-entropy and not brute-forceable, this envelope uses a cheap
KDF (HKDF) rather than the slow, memory-hard KDF used by the password-protected key envelope. The
consumer only provides a secret and a key; the salt is stored in the envelope.

Use the [password-protected key envelope](#password-protected-key-envelope) instead when the secret
is low-entropy (a PIN or password).

## Data envelope

Use the data envelope to protect a struct (document) of data. Examples include:
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-crypto/src/safe/high_entropy_secret.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! A high-entropy secret is a wrapper around secret bytes that are guaranteed to be high-entropy,
//! and therefore safe to use as input keying material for a cheap KDF (such as the one used by the
//! `SecretProtectedKeyEnvelope`).
//! [crate::safe::SecretProtectedKeyEnvelope]).
//!
//! Examples of high-entropy secrets are a random URL-fragment secret, a derived key, or random
//! bytes. They are unlike low-entropy secrets such as PINs or passwords, which can be brute-forced
Expand Down
20 changes: 18 additions & 2 deletions crates/bitwarden-crypto/src/safe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ mod password_protected_key_envelope;
pub use password_protected_key_envelope::*;
mod high_entropy_secret;
pub use high_entropy_secret::*;
mod secret_protected_key_envelope;
pub use secret_protected_key_envelope::*;
mod symmetric_key_envelope;
pub use symmetric_key_envelope::*;
mod data_envelope;
Expand Down Expand Up @@ -35,8 +37,9 @@ pub(super) enum DecodeSealedKeyError {
/// content format declared in the envelope's protected `header`.
///
/// Shared by the key envelopes
/// ([`PasswordProtectedKeyEnvelope`], `SecretProtectedKeyEnvelope`, and [`SymmetricKeyEnvelope`]),
/// which all store the wrapped key using the same content-format-tagged encoding.
/// ([`PasswordProtectedKeyEnvelope`], [`SecretProtectedKeyEnvelope`], and
/// [`SymmetricKeyEnvelope`]), which all store the wrapped key using the same content-format-tagged
/// encoding.
pub(super) fn decode_sealed_symmetric_key(
header: &coset::Header,
key_bytes: Vec<u8>,
Expand All @@ -53,6 +56,19 @@ pub(super) fn decode_sealed_symmetric_key(
SymmetricCryptoKey::try_from(encoded_key).map_err(|_| DecodeSealedKeyError::InvalidKey)
}

/// Extract the single recipient from a [`coset::CoseEncrypt`].
///
/// The COSE objects used by this module's envelopes always carry exactly one recipient (holding the
/// KDF parameters). Returns an error if there is not exactly one recipient.
pub(super) fn extract_single_recipient(
cose_encrypt: &coset::CoseEncrypt,
) -> Result<&coset::CoseRecipient, ()> {
match cose_encrypt.recipients.as_slice() {
[recipient] => Ok(recipient),
_ => Err(()),
}
}

/// Extract the contained key ID from a COSE header, if present.
/// Only COSE keys have a key ID; legacy keys do not.
pub(super) fn extract_key_id(header: &coset::Header) -> Result<Option<KeyId>, ()> {
Expand Down
Loading
Loading