Skip to content

[PM-23022] Re-sort bitwarden-crypto crate #344

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
5 changes: 2 additions & 3 deletions crates/bitwarden-crypto/src/content_format.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use serde::{Deserialize, Serialize};

use crate::{
traits::PrimitiveEncryptableWithContentType, CryptoError, EncString, KeyEncryptable,
KeyEncryptableWithContentType, KeyIds, KeyStoreContext, PrimitiveEncryptable,
SymmetricCryptoKey,
CryptoError, EncString, KeyEncryptable, KeyEncryptableWithContentType, KeyIds, KeyStoreContext,
PrimitiveEncryptable, PrimitiveEncryptableWithContentType, SymmetricCryptoKey,
};

/// The content format describes the format of the contained bytes. Message encryption always
Expand Down
262 changes: 4 additions & 258 deletions crates/bitwarden-crypto/src/cose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,20 @@

use coset::{
iana::{self, CoapContentFormat},
CborSerializable, ContentType, Label,
ContentType, Label,
};
use generic_array::GenericArray;
use typenum::U32;

use crate::{
content_format::{Bytes, ConstContentFormat, CoseContentFormat},
error::{EncStringParseError, EncodingError},
xchacha20, ContentFormat, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key,
ContentFormat, CryptoError,
};

/// XChaCha20 <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03> is used over ChaCha20
/// to be able to randomly generate nonces, and to not have to worry about key wearout. Since
/// the draft was never published as an RFC, we use a private-use value for the algorithm.
pub(crate) const XCHACHA20_POLY1305: i64 = -70000;
const XCHACHA20_TEXT_PAD_BLOCK_SIZE: usize = 32;
pub(crate) const XCHACHA20_TEXT_PAD_BLOCK_SIZE: usize = 32;

// Note: These are in the "unregistered" tree: https://datatracker.ietf.org/doc/html/rfc6838#section-3.4
// These are only used within Bitwarden, and not meant for exchange with other systems.
Expand All @@ -33,126 +31,7 @@ const CONTENT_TYPE_SPKI_PUBLIC_KEY: &str = "application/x.bitwarden.spki-public-
/// The label used for the namespace ensuring strong domain separation when using signatures.
pub(crate) const SIGNING_NAMESPACE: i64 = -80000;

/// Encrypts a plaintext message using XChaCha20Poly1305 and returns a COSE Encrypt0 message
pub(crate) fn encrypt_xchacha20_poly1305(
plaintext: &[u8],
key: &crate::XChaCha20Poly1305Key,
content_format: ContentFormat,
) -> Result<Vec<u8>, CryptoError> {
let mut plaintext = plaintext.to_vec();

let header_builder: coset::HeaderBuilder = content_format.into();
let mut protected_header = header_builder.key_id(key.key_id.to_vec()).build();
// This should be adjusted to use the builder pattern once implemented in coset.
// The related coset upstream issue is:
// https://github.com/google/coset/issues/105
protected_header.alg = Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305));

if should_pad_content(&content_format) {
// Pad the data to a block size in order to hide plaintext length
crate::keys::utils::pad_bytes(&mut plaintext, XCHACHA20_TEXT_PAD_BLOCK_SIZE);
}

let mut nonce = [0u8; xchacha20::NONCE_SIZE];
let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
.protected(protected_header)
.create_ciphertext(&plaintext, &[], |data, aad| {
let ciphertext =
crate::xchacha20::encrypt_xchacha20_poly1305(&(*key.enc_key).into(), data, aad);
nonce = ciphertext.nonce();
ciphertext.encrypted_bytes().to_vec()
})
.unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
.build();

cose_encrypt0
.to_vec()
.map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))
}

/// Decrypts a COSE Encrypt0 message, using a XChaCha20Poly1305 key
pub(crate) fn decrypt_xchacha20_poly1305(
cose_encrypt0_message: &[u8],
key: &crate::XChaCha20Poly1305Key,
) -> Result<(Vec<u8>, ContentFormat), CryptoError> {
let msg = coset::CoseEncrypt0::from_slice(cose_encrypt0_message)
.map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))?;

let Some(ref alg) = msg.protected.header.alg else {
return Err(CryptoError::EncString(
EncStringParseError::CoseMissingAlgorithm,
));
};

if *alg != coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) {
return Err(CryptoError::WrongKeyType);
}

let content_format = ContentFormat::try_from(&msg.protected.header)
.map_err(|_| CryptoError::EncString(EncStringParseError::CoseMissingContentType))?;

if key.key_id != *msg.protected.header.key_id {
return Err(CryptoError::WrongCoseKeyId);
}

let decrypted_message = msg.decrypt(&[], |data, aad| {
let nonce = msg.unprotected.iv.as_slice();
crate::xchacha20::decrypt_xchacha20_poly1305(
nonce
.try_into()
.map_err(|_| CryptoError::InvalidNonceLength)?,
&(*key.enc_key).into(),
data,
aad,
)
})?;

if should_pad_content(&content_format) {
// Unpad the data to get the original plaintext
let data = crate::keys::utils::unpad_bytes(&decrypted_message)?;
return Ok((data.to_vec(), content_format));
}

Ok((decrypted_message, content_format))
}

const SYMMETRIC_KEY: Label = Label::Int(iana::SymmetricKeyParameter::K as i64);

impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey {
type Error = CryptoError;

fn try_from(cose_key: &coset::CoseKey) -> Result<Self, Self::Error> {
let key_bytes = cose_key
.params
.iter()
.find_map(|(label, value)| match (label, value) {
(&SYMMETRIC_KEY, ciborium::Value::Bytes(bytes)) => Some(bytes),
_ => None,
})
.ok_or(CryptoError::InvalidKey)?;
let alg = cose_key.alg.as_ref().ok_or(CryptoError::InvalidKey)?;

match alg {
coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
// Ensure the length is correct since `GenericArray::clone_from_slice` panics if it
// receives the wrong length.
if key_bytes.len() != xchacha20::KEY_SIZE {
return Err(CryptoError::InvalidKey);
}
let enc_key = Box::pin(GenericArray::<u8, U32>::clone_from_slice(key_bytes));
let key_id = cose_key
.key_id
.as_slice()
.try_into()
.map_err(|_| CryptoError::InvalidKey)?;
Ok(SymmetricCryptoKey::XChaCha20Poly1305Key(
XChaCha20Poly1305Key { enc_key, key_id },
))
}
_ => Err(CryptoError::InvalidKey),
}
}
}
pub(crate) const SYMMETRIC_KEY: Label = Label::Int(iana::SymmetricKeyParameter::K as i64);

impl From<ContentFormat> for coset::HeaderBuilder {
fn from(format: ContentFormat) -> Self {
Expand Down Expand Up @@ -208,10 +87,6 @@ impl TryFrom<&coset::Header> for ContentFormat {
}
}

fn should_pad_content(format: &ContentFormat) -> bool {
matches!(format, ContentFormat::Utf8)
}

/// Trait for structs that are serializable to COSE objects.
pub trait CoseSerializable<T: CoseContentFormat + ConstContentFormat> {
/// Serializes the struct to COSE serialization
Expand All @@ -221,132 +96,3 @@ pub trait CoseSerializable<T: CoseContentFormat + ConstContentFormat> {
where
Self: Sized;
}
#[cfg(test)]
mod test {
use super::*;

const KEY_ID: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
const KEY_DATA: [u8; 32] = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
0x1e, 0x1f,
];
const TEST_VECTOR_PLAINTEXT: &[u8] = b"Message test vector";
const TEST_VECTOR_COSE_ENCRYPT0: &[u8] = &[
131, 88, 28, 163, 1, 58, 0, 1, 17, 111, 3, 24, 42, 4, 80, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 161, 5, 88, 24, 78, 20, 28, 157, 180, 246, 131, 220, 82, 104, 72, 73,
75, 43, 69, 139, 216, 167, 145, 220, 67, 168, 144, 173, 88, 35, 127, 234, 194, 83, 189,
172, 65, 29, 156, 73, 98, 87, 231, 87, 129, 15, 235, 127, 125, 97, 211, 51, 212, 211, 2,
13, 36, 123, 53, 12, 31, 191, 40, 13, 175,
];

#[test]
fn test_encrypt_decrypt_roundtrip_octetstream() {
let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
SymmetricCryptoKey::make_xchacha20_poly1305_key()
else {
panic!("Failed to create XChaCha20Poly1305Key");
};

let plaintext = b"Hello, world!";
let encrypted =
encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::OctetStream).unwrap();
let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::OctetStream));
}

#[test]
fn test_encrypt_decrypt_roundtrip_utf8() {
let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
SymmetricCryptoKey::make_xchacha20_poly1305_key()
else {
panic!("Failed to create XChaCha20Poly1305Key");
};

let plaintext = b"Hello, world!";
let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Utf8).unwrap();
let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::Utf8));
}

#[test]
fn test_encrypt_decrypt_roundtrip_pkcs8() {
let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
SymmetricCryptoKey::make_xchacha20_poly1305_key()
else {
panic!("Failed to create XChaCha20Poly1305Key");
};

let plaintext = b"Hello, world!";
let encrypted =
encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Pkcs8PrivateKey).unwrap();
let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
assert_eq!(
decrypted,
(plaintext.to_vec(), ContentFormat::Pkcs8PrivateKey)
);
}

#[test]
fn test_encrypt_decrypt_roundtrip_cosekey() {
let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
SymmetricCryptoKey::make_xchacha20_poly1305_key()
else {
panic!("Failed to create XChaCha20Poly1305Key");
};

let plaintext = b"Hello, world!";
let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::CoseKey).unwrap();
let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::CoseKey));
}

#[test]
fn test_decrypt_test_vector() {
let key = XChaCha20Poly1305Key {
key_id: KEY_ID,
enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
};
let decrypted = decrypt_xchacha20_poly1305(TEST_VECTOR_COSE_ENCRYPT0, &key).unwrap();
assert_eq!(
decrypted,
(TEST_VECTOR_PLAINTEXT.to_vec(), ContentFormat::OctetStream)
);
}

#[test]
fn test_fail_wrong_key_id() {
let key = XChaCha20Poly1305Key {
key_id: [1; 16], // Different key ID
enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
};
assert!(matches!(
decrypt_xchacha20_poly1305(TEST_VECTOR_COSE_ENCRYPT0, &key),
Err(CryptoError::WrongCoseKeyId)
));
}

#[test]
fn test_fail_wrong_algorithm() {
let protected_header = coset::HeaderBuilder::new()
.algorithm(iana::Algorithm::A256GCM)
.key_id(KEY_ID.to_vec())
.build();
let nonce = [0u8; 16];
let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
.protected(protected_header)
.create_ciphertext(&[], &[], |_, _| Vec::new())
.unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
.build();
let serialized_message = cose_encrypt0.to_vec().unwrap();

let key = XChaCha20Poly1305Key {
key_id: KEY_ID,
enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
};
assert!(matches!(
decrypt_xchacha20_poly1305(&serialized_message, &key),
Err(CryptoError::WrongKeyType)
));
}
}
5 changes: 2 additions & 3 deletions crates/bitwarden-crypto/src/keys/device_key.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use super::{AsymmetricCryptoKey, PublicKeyEncryptionAlgorithm};
use crate::{
error::Result, CryptoError, EncString, KeyDecryptable, KeyEncryptable, Pkcs8PrivateKeyBytes,
SymmetricCryptoKey, UnsignedSharedKey,
error::Result, AsymmetricCryptoKey, CryptoError, EncString, KeyDecryptable, KeyEncryptable,
Pkcs8PrivateKeyBytes, PublicKeyEncryptionAlgorithm, SymmetricCryptoKey, UnsignedSharedKey,
};

/// Device Key
Expand Down
10 changes: 5 additions & 5 deletions crates/bitwarden-crypto/src/keys/master_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use super::{
};
use crate::{
util::{self},
BitwardenLegacyKeyBytes, CryptoError, EncString, KeyDecryptable, Result, SymmetricCryptoKey,
UserKey,
Aes256CbcKey, BitwardenLegacyKeyBytes, CryptoError, EncString, KeyDecryptable, Result,
SymmetricCryptoKey, UserKey,
};

#[allow(missing_docs)]
Expand Down Expand Up @@ -144,7 +144,7 @@ pub(super) fn decrypt_user_key(
// moved to using `Aes256Cbc_HmacSha256_B64`. However, we still need to support
// decrypting these old keys.
EncString::Aes256Cbc_B64 { .. } => {
let legacy_key = SymmetricCryptoKey::Aes256CbcKey(super::Aes256CbcKey {
let legacy_key = SymmetricCryptoKey::Aes256CbcKey(Aes256CbcKey {
enc_key: Box::pin(GenericArray::clone_from_slice(key)),
});
user_key.decrypt_with_key(&legacy_key)?
Expand Down Expand Up @@ -186,8 +186,8 @@ mod tests {

use super::{make_user_key, HashPurpose, Kdf, MasterKey};
use crate::{
keys::{master_key::KdfDerivedKeyMaterial, symmetric_crypto_key::derive_symmetric_key},
EncString, SymmetricCryptoKey,
derive_symmetric_key, keys::master_key::KdfDerivedKeyMaterial, EncString,
SymmetricCryptoKey,
};

#[test]
Expand Down
16 changes: 0 additions & 16 deletions crates/bitwarden-crypto/src/keys/mod.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,7 @@
mod key_encryptable;
pub(crate) use key_encryptable::KeyEncryptableWithContentType;
pub use key_encryptable::{CryptoKey, KeyContainer, KeyDecryptable, KeyEncryptable};
mod master_key;
pub use master_key::{HashPurpose, MasterKey};
mod shareable_key;
pub use shareable_key::derive_shareable_key;
mod symmetric_crypto_key;
#[cfg(test)]
pub use symmetric_crypto_key::derive_symmetric_key;
pub use symmetric_crypto_key::{
Aes256CbcHmacKey, Aes256CbcKey, SymmetricCryptoKey, XChaCha20Poly1305Key,
};
mod asymmetric_crypto_key;
pub use asymmetric_crypto_key::{
AsymmetricCryptoKey, AsymmetricPublicCryptoKey, PublicKeyEncryptionAlgorithm,
};
pub(crate) use asymmetric_crypto_key::{RawPrivateKey, RawPublicKey};
mod signed_public_key;
pub use signed_public_key::{SignedPublicKey, SignedPublicKeyMessage};
mod user_key;
pub use user_key::UserKey;
mod device_key;
Expand Down
4 changes: 1 addition & 3 deletions crates/bitwarden-crypto/src/keys/pin_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ use super::{
master_key::decrypt_user_key,
utils::stretch_key,
};
use crate::{
keys::key_encryptable::CryptoKey, EncString, KeyEncryptable, Result, SymmetricCryptoKey,
};
use crate::{CryptoKey, EncString, KeyEncryptable, Result, SymmetricCryptoKey};

/// Pin Key.
///
Expand Down
6 changes: 4 additions & 2 deletions crates/bitwarden-crypto/src/keys/shareable_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ use hmac::Mac;
use typenum::{U32, U64};
use zeroize::{Zeroize, Zeroizing};

use super::Aes256CbcHmacKey;
use crate::util::{hkdf_expand, PbkdfSha256Hmac};
use crate::{
util::{hkdf_expand, PbkdfSha256Hmac},
Aes256CbcHmacKey,
};

/// Derive a shareable key using hkdf from secret and name.
///
Expand Down
Loading
Loading