diff --git a/bitwarden_license/bitwarden-sm/src/projects/create.rs b/bitwarden_license/bitwarden-sm/src/projects/create.rs index a5abdd2a8..c4a73d363 100644 --- a/bitwarden_license/bitwarden-sm/src/projects/create.rs +++ b/bitwarden_license/bitwarden-sm/src/projects/create.rs @@ -1,6 +1,6 @@ use bitwarden_api_api::models::ProjectCreateRequestModel; use bitwarden_core::{key_management::SymmetricKeyId, Client}; -use bitwarden_crypto::Encryptable; +use bitwarden_crypto::PrimitiveEncryptable; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; diff --git a/bitwarden_license/bitwarden-sm/src/projects/update.rs b/bitwarden_license/bitwarden-sm/src/projects/update.rs index 8f89e318b..f7d318234 100644 --- a/bitwarden_license/bitwarden-sm/src/projects/update.rs +++ b/bitwarden_license/bitwarden-sm/src/projects/update.rs @@ -1,6 +1,6 @@ use bitwarden_api_api::models::ProjectUpdateRequestModel; use bitwarden_core::{key_management::SymmetricKeyId, Client}; -use bitwarden_crypto::Encryptable; +use bitwarden_crypto::PrimitiveEncryptable; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; diff --git a/bitwarden_license/bitwarden-sm/src/secrets/create.rs b/bitwarden_license/bitwarden-sm/src/secrets/create.rs index c554c9d60..ab59568ec 100644 --- a/bitwarden_license/bitwarden-sm/src/secrets/create.rs +++ b/bitwarden_license/bitwarden-sm/src/secrets/create.rs @@ -1,6 +1,6 @@ use bitwarden_api_api::models::SecretCreateRequestModel; use bitwarden_core::{key_management::SymmetricKeyId, Client}; -use bitwarden_crypto::Encryptable; +use bitwarden_crypto::PrimitiveEncryptable; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; diff --git a/bitwarden_license/bitwarden-sm/src/secrets/update.rs b/bitwarden_license/bitwarden-sm/src/secrets/update.rs index 0f1004724..f5422c468 100644 --- a/bitwarden_license/bitwarden-sm/src/secrets/update.rs +++ b/bitwarden_license/bitwarden-sm/src/secrets/update.rs @@ -1,6 +1,6 @@ use bitwarden_api_api::models::SecretUpdateRequestModel; use bitwarden_core::{key_management::SymmetricKeyId, Client}; -use bitwarden_crypto::Encryptable; +use bitwarden_crypto::PrimitiveEncryptable; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 5655f2110..521e60c85 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -52,7 +52,7 @@ pub(crate) fn auth_request_decrypt_user_key( private_key: String, user_key: UnsignedSharedKey, ) -> Result { - let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; + let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?.into())?; let key: SymmetricCryptoKey = user_key.decapsulate_key_unsigned(&key)?; Ok(key) } @@ -66,7 +66,7 @@ pub(crate) fn auth_request_decrypt_master_key( ) -> Result { use bitwarden_crypto::MasterKey; - let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; + let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?.into())?; let master_key: SymmetricCryptoKey = master_key.decapsulate_key_unsigned(&key)?; let master_key = MasterKey::try_from(&master_key)?; @@ -106,36 +106,11 @@ pub(crate) fn approve_auth_request( )?) } -#[test] -fn test_auth_request() { - let request = new_auth_request("test@bitwarden.com").unwrap(); - - let secret = vec![ - 111, 32, 97, 169, 4, 241, 174, 74, 239, 206, 113, 86, 174, 68, 216, 238, 52, 85, 156, 27, - 134, 149, 54, 55, 91, 147, 45, 130, 131, 237, 51, 31, 191, 106, 155, 14, 160, 82, 47, 40, - 96, 31, 114, 127, 212, 187, 167, 110, 205, 116, 198, 243, 218, 72, 137, 53, 248, 43, 255, - 67, 35, 61, 245, 93, - ]; - - let private_key = - AsymmetricCryptoKey::from_der(&STANDARD.decode(&request.private_key).unwrap()).unwrap(); - - let encrypted = UnsignedSharedKey::encapsulate_key_unsigned( - &SymmetricCryptoKey::try_from(secret.clone()).unwrap(), - &private_key.to_public_key(), - ) - .unwrap(); - - let decrypted = auth_request_decrypt_user_key(request.private_key, encrypted).unwrap(); - - assert_eq!(decrypted.to_encoded(), secret); -} - #[cfg(test)] mod tests { use std::num::NonZeroU32; - use bitwarden_crypto::{Kdf, MasterKey}; + use bitwarden_crypto::{BitwardenLegacyKeyBytes, Kdf, MasterKey, SpkiPublicKeyBytes}; use super::*; use crate::key_management::{ @@ -143,6 +118,33 @@ mod tests { SymmetricKeyId, }; + #[test] + fn test_auth_request() { + let request = new_auth_request("test@bitwarden.com").unwrap(); + + let secret = vec![ + 111, 32, 97, 169, 4, 241, 174, 74, 239, 206, 113, 86, 174, 68, 216, 238, 52, 85, 156, + 27, 134, 149, 54, 55, 91, 147, 45, 130, 131, 237, 51, 31, 191, 106, 155, 14, 160, 82, + 47, 40, 96, 31, 114, 127, 212, 187, 167, 110, 205, 116, 198, 243, 218, 72, 137, 53, + 248, 43, 255, 67, 35, 61, 245, 93, + ]; + + let private_key = + AsymmetricCryptoKey::from_der(&STANDARD.decode(&request.private_key).unwrap().into()) + .unwrap(); + + let secret = BitwardenLegacyKeyBytes::from(secret); + let encrypted = UnsignedSharedKey::encapsulate_key_unsigned( + &SymmetricCryptoKey::try_from(&secret).unwrap(), + &private_key.to_public_key(), + ) + .unwrap(); + + let decrypted = auth_request_decrypt_user_key(request.private_key, encrypted).unwrap(); + + assert_eq!(decrypted.to_encoded().to_vec(), secret.to_vec()); + } + #[test] fn test_approve() { let client = Client::new(None); @@ -166,8 +168,9 @@ mod tests { let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyLRDUwXB4BfQ507D4meFPmwn5zwy3IqTPJO4plrrhnclWahXa240BzyFW9gHgYu+Jrgms5xBfRTBMcEsqqNm7+JpB6C1B6yvnik0DpJgWQw1rwvy4SUYidpR/AWbQi47n/hvnmzI/sQxGddVfvWu1iTKOlf5blbKYAXnUE5DZBGnrWfacNXwRRdtP06tFB0LwDgw+91CeLSJ9py6dm1qX5JIxoO8StJOQl65goLCdrTWlox+0Jh4xFUfCkb+s3px+OhSCzJbvG/hlrSRcUz5GnwlCEyF3v5lfUtV96MJD+78d8pmH6CfFAp2wxKRAbGdk+JccJYO6y6oIXd3Fm7twIDAQAB"; // Verify fingerprint - let pbkey = STANDARD.decode(public_key).unwrap(); - let fingerprint = fingerprint("test@bitwarden.com", &pbkey).unwrap(); + let pubkey = STANDARD.decode(public_key).unwrap(); + let pubkey = SpkiPublicKeyBytes::from(pubkey.clone()); + let fingerprint = fingerprint("test@bitwarden.com", &pubkey).unwrap(); assert_eq!(fingerprint, "childless-unfair-prowler-dropbox-designate"); approve_auth_request(&client, public_key.to_owned()).unwrap(); @@ -181,7 +184,7 @@ mod tests { let dec = auth_request_decrypt_user_key(private_key.to_owned(), enc_user_key).unwrap(); assert_eq!( - &dec.to_encoded(), + &dec.to_encoded().to_vec(), &[ 201, 37, 234, 213, 21, 75, 40, 70, 149, 213, 234, 16, 19, 251, 162, 245, 161, 74, 34, 245, 211, 151, 211, 192, 95, 10, 117, 50, 88, 223, 23, 157 @@ -200,7 +203,7 @@ mod tests { .unwrap(); assert_eq!( - &dec.to_encoded(), + &dec.to_encoded().to_vec(), &[ 109, 128, 172, 147, 206, 123, 134, 95, 16, 36, 155, 113, 201, 18, 186, 230, 216, 212, 173, 188, 74, 11, 134, 131, 137, 242, 105, 178, 105, 126, 52, 139, 248, 91, diff --git a/crates/bitwarden-core/src/auth/login/access_token.rs b/crates/bitwarden-core/src/auth/login/access_token.rs index 1563bc10b..773e8ade8 100644 --- a/crates/bitwarden-core/src/auth/login/access_token.rs +++ b/crates/bitwarden-core/src/auth/login/access_token.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use base64::{engine::general_purpose::STANDARD, Engine}; -use bitwarden_crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}; +use bitwarden_crypto::{BitwardenLegacyKeyBytes, EncString, KeyDecryptable, SymmetricCryptoKey}; use chrono::Utc; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -67,7 +67,8 @@ pub(crate) async fn login_access_token( let payload: Payload = serde_json::from_slice(&decrypted_payload)?; let encryption_key = STANDARD.decode(&payload.encryption_key)?; - let encryption_key = SymmetricCryptoKey::try_from(encryption_key)?; + let encryption_key = BitwardenLegacyKeyBytes::from(encryption_key); + let encryption_key = SymmetricCryptoKey::try_from(&encryption_key)?; let access_token_obj: JwtToken = r.access_token.parse()?; diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index ab8d84cd4..3610f0054 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -27,6 +27,9 @@ pub enum EncryptionSettingsError { #[error("Invalid private key")] InvalidPrivateKey, + #[error("Invalid signing key")] + InvalidSigningKey, + #[error(transparent)] MissingPrivateKey(#[from] MissingPrivateKeyError), @@ -49,19 +52,16 @@ impl EncryptionSettings { signing_key: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { - use bitwarden_crypto::{ - AsymmetricCryptoKey, CoseSerializable, CryptoError, KeyDecryptable, SigningKey, - }; + use bitwarden_crypto::{AsymmetricCryptoKey, CoseSerializable, KeyDecryptable, SigningKey}; use log::warn; use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}; let private_key = { let dec: Vec = private_key.decrypt_with_key(&user_key)?; - // FIXME: [PM-11690] - Temporarily ignore invalid private keys until we have a recovery // process in place. - AsymmetricCryptoKey::from_der(&dec) + AsymmetricCryptoKey::from_der(&dec.into()) .map_err(|_| { warn!("Invalid private key"); }) @@ -74,8 +74,10 @@ impl EncryptionSettings { }; let signing_key = signing_key .map(|key| { + use bitwarden_crypto::CryptoError; + let dec: Vec = key.decrypt_with_key(&user_key)?; - SigningKey::from_cose(dec.as_slice()).map_err(Into::::into) + SigningKey::from_cose(&dec.into()).map_err(Into::::into) }) .transpose()?; diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 3a1cd66a4..a76ea4294 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -8,9 +8,9 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Encryptable, Kdf, - KeyDecryptable, KeyEncryptable, MasterKey, SignatureAlgorithm, SignedPublicKey, SigningKey, - SymmetricCryptoKey, UnsignedSharedKey, UserKey, + AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, + KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, SignatureAlgorithm, + SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; use bitwarden_error::bitwarden_error; use schemars::JsonSchema; @@ -536,6 +536,7 @@ pub(super) fn verify_asymmetric_keys( .decrypt_with_key(user_key) .map_err(VerifyError::DecryptFailed)?; + let decrypted_private_key = Pkcs8PrivateKeyBytes::from(decrypted_private_key); let private_key = AsymmetricCryptoKey::from_der(&decrypted_private_key) .map_err(VerifyError::ParseFailed)?; @@ -605,8 +606,6 @@ pub fn make_user_signing_keys_for_enrollment( Ok(MakeUserSigningKeysResponse { verifying_key: STANDARD.encode(signature_keypair.to_verifying_key().to_cose()), - // This needs to be changed to use the correct COSE content format before rolling out to - // users: https://bitwarden.atlassian.net/browse/PM-22189 signing_key: signature_keypair .to_cose() .encrypt(&mut ctx, SymmetricKeyId::User)?, @@ -845,8 +844,9 @@ mod tests { let encrypted = enroll_admin_password_reset(&client, public_key.to_owned()).unwrap(); let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLtEUdxfcLxDj84yaGFsVF5hZ8Hjlb08NMQDy1RnBma06I3ZESshLYzVz4r/gegMn9OOltfV/Yxlyvida8oW6qdlfJ7AVz6Oa8pV7BiL40C7b76+oqraQpyYw2HChANB1AhXL9SqWngKmLZwjA7qiCrmcc0kZHeOb4KnKtp9iVvPVs+8veFvKgYO4ba2AAOHKFdR0W55/agXfAy+fWUAkC8mc9ikyJdQWaPV6OZvC2XFkOseBQm9Rynudh3BQpoWiL6w620efe7t5k+02/EyOFJL9f/XEEjM/+Yo0t3LAfkuhHGeKiRST59Xc9hTEmyJTeVXROtz+0fjqOp3xkaObAgMBAAECggEACs4xhnO0HaZhh1/iH7zORMIRXKeyxP2LQiTR8xwN5JJ9wRWmGAR9VasS7EZFTDidIGVME2u/h4s5EqXnhxfO+0gGksVvgNXJ/qw87E8K2216g6ZNo6vSGA7H1GH2voWwejJ4/k/cJug6dz2S402rRAKh2Wong1arYHSkVlQp3diiMa5FHAOSE+Cy09O2ZsaF9IXQYUtlW6AVXFrBEPYH2kvkaPXchh8VETMijo6tbvoKLnUHe+wTaDMls7hy8exjtVyI59r3DNzjy1lNGaGb5QSnFMXR+eHhPZc844Wv02MxC15zKABADrl58gpJyjTl6XpDdHCYGsmGpVGH3X9TQQKBgQDz/9beFjzq59ve6rGwn+EtnQfSsyYT+jr7GN8lNEXb3YOFXBgPhfFIcHRh2R00Vm9w2ApfAx2cd8xm2I6HuvQ1Os7g26LWazvuWY0Qzb+KaCLQTEGH1RnTq6CCG+BTRq/a3J8M4t38GV5TWlzv8wr9U4dl6FR4efjb65HXs1GQ4QKBgQC7/uHfrOTEHrLeIeqEuSl0vWNqEotFKdKLV6xpOvNuxDGbgW4/r/zaxDqt0YBOXmRbQYSEhmO3oy9J6XfE1SUln0gbavZeW0HESCAmUIC88bDnspUwS9RxauqT5aF8ODKN/bNCWCnBM1xyonPOs1oT1nyparJVdQoG//Y7vkB3+wKBgBqLqPq8fKAp3XfhHLfUjREDVoiLyQa/YI9U42IOz9LdxKNLo6p8rgVthpvmnRDGnpUuS+KOWjhdqDVANjF6G3t3DG7WNl8Rh5Gk2H4NhFswfSkgQrjebFLlBy9gjQVCWXt8KSmjvPbiY6q52Aaa8IUjA0YJAregvXxfopxO+/7BAoGARicvEtDp7WWnSc1OPoj6N14VIxgYcI7SyrzE0d/1x3ffKzB5e7qomNpxKzvqrVP8DzG7ydh8jaKPmv1MfF8tpYRy3AhmN3/GYwCnPqT75YYrhcrWcVdax5gmQVqHkFtIQkRSCIftzPLlpMGKha/YBV8c1fvC4LD0NPh/Ynv0gtECgYEAyOZg95/kte0jpgUEgwuMrzkhY/AaUJULFuR5MkyvReEbtSBQwV5tx60+T95PHNiFooWWVXiLMsAgyI2IbkxVR1Pzdri3gWK5CTfqb7kLuaj/B7SGvBa2Sxo478KS5K8tBBBWkITqo+wLC0mn3uZi1dyMWO1zopTA+KtEGF2dtGQ="; - let private_key = - AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key).unwrap()).unwrap(); + let private_key = STANDARD.decode(private_key).unwrap(); + let private_key = Pkcs8PrivateKeyBytes::from(private_key); + let private_key = AsymmetricCryptoKey::from_der(&private_key).unwrap(); let decrypted: SymmetricCryptoKey = encrypted.decapsulate_key_unsigned(&private_key).unwrap(); @@ -938,11 +938,7 @@ mod tests { fn test_verify_asymmetric_keys_parse_failed() { let (user_key, key_pair) = setup_asymmetric_keys_test(); - let invalid_private_key = "bad_key" - .to_string() - .into_bytes() - .encrypt_with_key(&user_key.0) - .unwrap(); + let invalid_private_key = "bad_key".to_string().encrypt_with_key(&user_key.0).unwrap(); let request = VerifyAsymmetricKeysRequest { user_key: user_key.0.to_base64(), diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index 4602b5a2a..79bbf124a 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -6,7 +6,9 @@ //! - [KeyIds] is a helper type that combines both symmetric and asymmetric key identifiers. This is //! usually used in the type bounds of [KeyStore], //! [KeyStoreContext](bitwarden_crypto::KeyStoreContext), -//! [Encryptable](bitwarden_crypto::Encryptable) and [Decryptable](bitwarden_crypto::Encryptable). +//! [PrimitiveEncryptable](bitwarden_crypto::PrimitiveEncryptable), +//! [CompositeEncryptable](bitwarden_crypto::CompositeEncryptable), and +//! [Decryptable](bitwarden_crypto::Decryptable). use bitwarden_crypto::{key_ids, KeyStore, SymmetricCryptoKey}; #[cfg(feature = "internal")] diff --git a/crates/bitwarden-core/src/platform/generate_fingerprint.rs b/crates/bitwarden-core/src/platform/generate_fingerprint.rs index 311039d74..e6ff4cbf8 100644 --- a/crates/bitwarden-core/src/platform/generate_fingerprint.rs +++ b/crates/bitwarden-core/src/platform/generate_fingerprint.rs @@ -42,8 +42,7 @@ pub enum FingerprintError { pub(crate) fn generate_fingerprint(input: &FingerprintRequest) -> Result { let key = STANDARD.decode(&input.public_key)?; - - Ok(fingerprint(&input.fingerprint_material, &key)?) + Ok(fingerprint(&input.fingerprint_material, &key.into())?) } /// Errors that can occur when computing a fingerprint. diff --git a/crates/bitwarden-crypto/examples/signature.rs b/crates/bitwarden-crypto/examples/signature.rs index 8524e2943..23ab52b84 100644 --- a/crates/bitwarden-crypto/examples/signature.rs +++ b/crates/bitwarden-crypto/examples/signature.rs @@ -1,7 +1,7 @@ //! This example demonstrates how to create signatures and countersignatures for a message, and how //! to verify them. -use bitwarden_crypto::{CoseSerializable, SigningNamespace}; +use bitwarden_crypto::{CoseSerializable, CoseSign1Bytes, SigningNamespace}; use serde::{Deserialize, Serialize}; const EXAMPLE_NAMESPACE: &SigningNamespace = &SigningNamespace::SignedPublicKey; @@ -38,15 +38,16 @@ fn main() { .expect("Failed to sign message"); // Alice sends the signed object to Bob - mock_server.upload("signature", signature.to_cose()); + mock_server.upload("signature", signature.to_cose().to_vec()); mock_server.upload("serialized_message", serialized_message.as_bytes().to_vec()); // Bob retrieves the signed object from the server - let retrieved_signature = bitwarden_crypto::Signature::from_cose( + let retrieved_signature = bitwarden_crypto::Signature::from_cose(&CoseSign1Bytes::from( mock_server .download("signature") - .expect("Failed to download signature"), - ) + .expect("Failed to download signature") + .clone(), + )) .expect("Failed to deserialize signature"); let retrieved_serialized_message = bitwarden_crypto::SerializedMessage::from_bytes( mock_server @@ -76,7 +77,7 @@ fn main() { ) .expect("Failed to counter sign message"); // Bob sends the counter signature to Charlie - mock_server.upload("bobs_signature", bobs_signature.to_cose()); + mock_server.upload("bobs_signature", bobs_signature.to_cose().to_vec()); // Charlie retrieves the signatures, and the message let retrieved_serialized_message = bitwarden_crypto::SerializedMessage::from_bytes( @@ -88,17 +89,19 @@ fn main() { .content_type() .expect("Failed to get content type from signature"), ); - let retrieved_alice_signature = bitwarden_crypto::Signature::from_cose( + let retrieved_alice_signature = bitwarden_crypto::Signature::from_cose(&CoseSign1Bytes::from( mock_server .download("signature") - .expect("Failed to download Alice's signature"), - ) + .expect("Failed to download Alice's signature") + .clone(), + )) .expect("Failed to deserialize Alice's signature"); - let retrieved_bobs_signature = bitwarden_crypto::Signature::from_cose( + let retrieved_bobs_signature = bitwarden_crypto::Signature::from_cose(&CoseSign1Bytes::from( mock_server .download("bobs_signature") - .expect("Failed to download Bob's signature"), - ) + .expect("Failed to download Bob's signature") + .clone(), + )) .expect("Failed to deserialize Bob's signature"); // Charlie verifies Alice's signature diff --git a/crates/bitwarden-crypto/examples/signed_object.rs b/crates/bitwarden-crypto/examples/signed_object.rs index c49b8ef1b..e34d06a8e 100644 --- a/crates/bitwarden-crypto/examples/signed_object.rs +++ b/crates/bitwarden-crypto/examples/signed_object.rs @@ -1,6 +1,6 @@ //! This example demonstrates how to sign and verify structs. -use bitwarden_crypto::{CoseSerializable, SignedObject, SigningNamespace}; +use bitwarden_crypto::{CoseSerializable, CoseSign1Bytes, SignedObject, SigningNamespace}; use serde::{Deserialize, Serialize}; const EXAMPLE_NAMESPACE: &SigningNamespace = &SigningNamespace::SignedPublicKey; @@ -34,14 +34,15 @@ fn main() { .expect("Failed to sign message"); // Alice sends the signed object to Bob - mock_server.upload("signed_object", signed_object.to_cose()); + mock_server.upload("signed_object", signed_object.to_cose().to_vec()); // Bob retrieves the signed object from the server - let retrieved_signed_object = SignedObject::from_cose( + let retrieved_signed_object = SignedObject::from_cose(&CoseSign1Bytes::from( mock_server .download("signed_object") - .expect("Failed to download signed object"), - ) + .expect("Failed to download signed object") + .clone(), + )) .expect("Failed to deserialize signed object"); // Bob verifies the signed object using Alice's verifying key let verified_message: MessageToBob = retrieved_signed_object diff --git a/crates/bitwarden-crypto/src/content_format.rs b/crates/bitwarden-crypto/src/content_format.rs new file mode 100644 index 000000000..1f44dc65c --- /dev/null +++ b/crates/bitwarden-crypto/src/content_format.rs @@ -0,0 +1,222 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + traits::PrimitiveEncryptableWithContentType, CryptoError, EncString, KeyEncryptable, + KeyEncryptableWithContentType, KeyIds, KeyStoreContext, PrimitiveEncryptable, + SymmetricCryptoKey, +}; + +/// The content format describes the format of the contained bytes. Message encryption always +/// happens on the byte level, and this allows determining what format the contained data has. For +/// instance, an `EncString` in most cases contains UTF-8 encoded text. In some cases it may contain +/// a Pkcs8 private key, or a COSE key. Specifically, for COSE keys, this allows distinguishing +/// between the old symmetric key format, represented as `ContentFormat::OctetStream`, and the new +/// COSE key format, represented as `ContentFormat::CoseKey`. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum ContentFormat { + /// UTF-8 encoded text + Utf8, + /// Pkcs8 private key DER + Pkcs8PrivateKey, + /// SPKI public key DER + SPKIPublicKeyDer, + /// COSE serialized CoseKey + CoseKey, + /// CoseSign1 message + CoseSign1, + /// Bitwarden Legacy Key + /// There are three permissible byte values here: + /// - `[u8; 32]` - AES-CBC (no hmac) key. This is to be removed and banned. + /// - `[u8; 64]` - AES-CBC with HMAC key. This is the v1 userkey key type + /// - `[u8; >64]` - COSE key. Padded to be larger than 64 bytes. + BitwardenLegacyKey, + /// Stream of bytes + OctetStream, +} + +mod private { + /// This trait is used to seal the `ConstContentFormat` trait, preventing external + /// implementations. + pub trait Sealed {} +} + +/// This trait is used to instantiate different typed byte vectors with a specific content format, +/// using `SerializedBytes`. This allows for compile-time guarantees about the content format +/// of the serialized bytes. The exception here is the escape hatch using e.g. `from(Vec)`, +/// which can still be mis-used, but has to be misused explicitly. +pub trait ConstContentFormat: private::Sealed { + /// Returns the content format as a `ContentFormat` enum. + #[allow(private_interfaces)] + fn content_format() -> ContentFormat; +} + +/// A serialized byte array with a specific content format. This is used to represent data that has +/// a specific format, such as UTF-8 encoded text, raw bytes, or COSE keys. The content +/// format is used to determine how the bytes should be interpreted when encrypting or decrypting +/// the data. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Bytes { + inner: Vec, + _marker: std::marker::PhantomData, +} + +impl From> for Bytes { + fn from(inner: Vec) -> Self { + Self { + inner, + _marker: std::marker::PhantomData, + } + } +} + +impl From<&[u8]> for Bytes { + fn from(inner: &[u8]) -> Self { + Self::from(inner.to_vec()) + } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + &self.inner + } +} + +impl Bytes { + /// Returns the serialized bytes as a `Vec`. + pub fn to_vec(&self) -> Vec { + self.inner.clone() + } +} + +/// Content format for UTF-8 encoded text. Used for most text messages. +#[derive(PartialEq, Eq, Clone, Debug)] +pub(crate) struct Utf8ContentFormat; +impl private::Sealed for Utf8ContentFormat {} +impl ConstContentFormat for Utf8ContentFormat { + fn content_format() -> ContentFormat { + ContentFormat::Utf8 + } +} +/// Utf8Bytes is a type alias for Bytes with `Utf8ContentFormat`, which is used for any textual +/// data. +pub(crate) type Utf8Bytes = Bytes; + +/// Content format for raw bytes. Used for attachments and send seed keys. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct OctetStreamContentFormat; +impl private::Sealed for OctetStreamContentFormat {} +impl ConstContentFormat for OctetStreamContentFormat { + #[allow(private_interfaces)] + fn content_format() -> ContentFormat { + ContentFormat::OctetStream + } +} +/// OctetStreamBytes is a type alias for Bytes with `OctetStreamContentFormat`. This should be used +/// for e.g. attachments and other data without an explicit content format. +pub type OctetStreamBytes = Bytes; + +/// Content format for PKCS8 private keys in DER format. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct Pkcs8PrivateKeyDerContentFormat; +impl private::Sealed for Pkcs8PrivateKeyDerContentFormat {} +impl ConstContentFormat for Pkcs8PrivateKeyDerContentFormat { + #[allow(private_interfaces)] + fn content_format() -> ContentFormat { + ContentFormat::Pkcs8PrivateKey + } +} +/// Pkcs8PrivateKeyBytes is a type alias for Bytes with `Pkcs8PrivateKeyDerContentFormat`. This is +/// used for PKCS8 private keys in DER format. +pub type Pkcs8PrivateKeyBytes = Bytes; + +/// Content format for SPKI public keys in DER format. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct SpkiPublicKeyDerContentFormat; +impl private::Sealed for SpkiPublicKeyDerContentFormat {} +impl ConstContentFormat for SpkiPublicKeyDerContentFormat { + #[allow(private_interfaces)] + fn content_format() -> ContentFormat { + ContentFormat::SPKIPublicKeyDer + } +} +/// SpkiPublicKeyBytes is a type alias for Bytes with `SpkiPublicKeyDerContentFormat`. This is used +/// for SPKI public keys in DER format. +pub type SpkiPublicKeyBytes = Bytes; + +/// A marker trait for COSE content formats. +pub trait CoseContentFormat {} + +/// Content format for COSE keys. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct CoseKeyContentFormat; +impl private::Sealed for CoseKeyContentFormat {} +impl ConstContentFormat for CoseKeyContentFormat { + #[allow(private_interfaces)] + fn content_format() -> ContentFormat { + ContentFormat::CoseKey + } +} +impl CoseContentFormat for CoseKeyContentFormat {} +/// CoseKeyBytes is a type alias for Bytes with `CoseKeyContentFormat`. This is used for serialized +/// CoseKey objects. +pub type CoseKeyBytes = Bytes; + +/// A legacy content format for Bitwarden keys. See `ContentFormat::BitwardenLegacyKey` +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct BitwardenLegacyKeyContentFormat; +impl private::Sealed for BitwardenLegacyKeyContentFormat {} +impl ConstContentFormat for BitwardenLegacyKeyContentFormat { + #[allow(private_interfaces)] + fn content_format() -> ContentFormat { + ContentFormat::BitwardenLegacyKey + } +} +/// BitwardenLegacyKeyBytes is a type alias for Bytes with `BitwardenLegacyKeyContentFormat`. This +/// is used for the legacy format for symmetric keys. A description of the format is available in +/// the `ContentFormat::BitwardenLegacyKey` documentation. +pub type BitwardenLegacyKeyBytes = Bytes; + +/// Content format for COSE Sign1 messages. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct CoseSign1ContentFormat; +impl private::Sealed for CoseSign1ContentFormat {} +impl ConstContentFormat for CoseSign1ContentFormat { + #[allow(private_interfaces)] + fn content_format() -> ContentFormat { + ContentFormat::CoseSign1 + } +} +impl CoseContentFormat for CoseSign1ContentFormat {} +/// CoseSign1Bytes is a type alias for Bytes with `CoseSign1ContentFormat`. This is used for +/// serialized COSE Sign1 messages. +pub type CoseSign1Bytes = Bytes; + +impl PrimitiveEncryptable + for Bytes +{ + fn encrypt( + &self, + ctx: &mut KeyStoreContext, + key: Ids::Symmetric, + ) -> Result { + self.inner.encrypt(ctx, key, T::content_format()) + } +} + +impl KeyEncryptable for &Bytes { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { + self.as_ref().encrypt_with_key(key, T::content_format()) + } +} + +impl From for Bytes { + fn from(val: String) -> Self { + Bytes::from(val.into_bytes()) + } +} + +impl From<&str> for Bytes { + fn from(val: &str) -> Self { + Bytes::from(val.as_bytes().to_vec()) + } +} diff --git a/crates/bitwarden-crypto/src/cose.rs b/crates/bitwarden-crypto/src/cose.rs index 04cb676e3..b2a39ab56 100644 --- a/crates/bitwarden-crypto/src/cose.rs +++ b/crates/bitwarden-crypto/src/cose.rs @@ -3,19 +3,30 @@ //! unless there is a a clear benefit, such as a clear cryptographic benefit, which MUST //! be documented publicly. -use coset::{iana, CborSerializable, Label}; +use coset::{ + iana::{self, CoapContentFormat}, + CborSerializable, ContentType, Label, +}; use generic_array::GenericArray; use typenum::U32; use crate::{ + content_format::{Bytes, ConstContentFormat, CoseContentFormat}, error::{EncStringParseError, EncodingError}, - xchacha20, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key, + xchacha20, ContentFormat, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key, }; /// XChaCha20 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; + +// 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. +const CONTENT_TYPE_PADDED_UTF8: &str = "application/x.bitwarden.utf8-padded"; +const CONTENT_TYPE_BITWARDEN_LEGACY_KEY: &str = "application/x.bitwarden.legacy-key"; +const CONTENT_TYPE_SPKI_PUBLIC_KEY: &str = "application/x.bitwarden.spki-public-key"; // Labels // @@ -26,19 +37,26 @@ pub(crate) const SIGNING_NAMESPACE: i64 = -80000; pub(crate) fn encrypt_xchacha20_poly1305( plaintext: &[u8], key: &crate::XChaCha20Poly1305Key, + content_format: ContentFormat, ) -> Result, CryptoError> { - let mut protected_header = coset::HeaderBuilder::new() - .key_id(key.key_id.to_vec()) - .build(); + 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| { + .create_ciphertext(&plaintext, &[], |data, aad| { let ciphertext = crate::xchacha20::encrypt_xchacha20_poly1305(&(*key.enc_key).into(), data, aad); nonce = ciphertext.nonce(); @@ -56,17 +74,23 @@ pub(crate) fn encrypt_xchacha20_poly1305( pub(crate) fn decrypt_xchacha20_poly1305( cose_encrypt0_message: &[u8], key: &crate::XChaCha20Poly1305Key, -) -> Result, CryptoError> { +) -> Result<(Vec, 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); } @@ -82,7 +106,14 @@ pub(crate) fn decrypt_xchacha20_poly1305( aad, ) })?; - Ok(decrypted_message) + + 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); @@ -123,12 +154,70 @@ impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey { } } +impl From for coset::HeaderBuilder { + fn from(format: ContentFormat) -> Self { + let header_builder = coset::HeaderBuilder::new(); + + match format { + ContentFormat::Utf8 => { + header_builder.content_type(CONTENT_TYPE_PADDED_UTF8.to_string()) + } + ContentFormat::Pkcs8PrivateKey => { + header_builder.content_format(CoapContentFormat::Pkcs8) + } + ContentFormat::SPKIPublicKeyDer => { + header_builder.content_type(CONTENT_TYPE_SPKI_PUBLIC_KEY.to_string()) + } + ContentFormat::CoseSign1 => header_builder.content_format(CoapContentFormat::CoseSign1), + ContentFormat::CoseKey => header_builder.content_format(CoapContentFormat::CoseKey), + ContentFormat::BitwardenLegacyKey => { + header_builder.content_type(CONTENT_TYPE_BITWARDEN_LEGACY_KEY.to_string()) + } + ContentFormat::OctetStream => { + header_builder.content_format(CoapContentFormat::OctetStream) + } + } + } +} + +impl TryFrom<&coset::Header> for ContentFormat { + type Error = CryptoError; + + fn try_from(header: &coset::Header) -> Result { + match header.content_type.as_ref() { + Some(ContentType::Text(format)) if format == CONTENT_TYPE_PADDED_UTF8 => { + Ok(ContentFormat::Utf8) + } + Some(ContentType::Text(format)) if format == CONTENT_TYPE_BITWARDEN_LEGACY_KEY => { + Ok(ContentFormat::BitwardenLegacyKey) + } + Some(ContentType::Text(format)) if format == CONTENT_TYPE_SPKI_PUBLIC_KEY => { + Ok(ContentFormat::SPKIPublicKeyDer) + } + Some(ContentType::Assigned(CoapContentFormat::Pkcs8)) => { + Ok(ContentFormat::Pkcs8PrivateKey) + } + Some(ContentType::Assigned(CoapContentFormat::CoseKey)) => Ok(ContentFormat::CoseKey), + Some(ContentType::Assigned(CoapContentFormat::OctetStream)) => { + Ok(ContentFormat::OctetStream) + } + _ => Err(CryptoError::EncString( + EncStringParseError::CoseMissingContentType, + )), + } + } +} + +fn should_pad_content(format: &ContentFormat) -> bool { + matches!(format, ContentFormat::Utf8) +} + /// Trait for structs that are serializable to COSE objects. -pub trait CoseSerializable { +pub trait CoseSerializable { /// Serializes the struct to COSE serialization - fn to_cose(&self) -> Vec; + fn to_cose(&self) -> Bytes; /// Deserializes a serialized COSE object to a struct - fn from_cose(bytes: &[u8]) -> Result + fn from_cose(bytes: &Bytes) -> Result where Self: Sized; } @@ -144,15 +233,62 @@ mod test { ]; const TEST_VECTOR_PLAINTEXT: &[u8] = b"Message test vector"; const TEST_VECTOR_COSE_ENCRYPT0: &[u8] = &[ - 131, 88, 25, 162, 1, 58, 0, 1, 17, 111, 4, 80, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, - 13, 14, 15, 161, 5, 88, 24, 39, 48, 159, 48, 215, 77, 21, 100, 241, 209, 216, 65, 99, 221, - 83, 63, 118, 204, 200, 175, 126, 202, 53, 33, 88, 35, 218, 136, 132, 223, 131, 246, 169, - 120, 134, 49, 56, 173, 169, 133, 232, 109, 248, 101, 59, 226, 90, 97, 210, 181, 76, 68, - 158, 159, 94, 65, 67, 23, 112, 253, 83, + 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() { + 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 { @@ -160,9 +296,9 @@ mod test { }; let plaintext = b"Hello, world!"; - let encrypted = encrypt_xchacha20_poly1305(plaintext, key).unwrap(); + let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::CoseKey).unwrap(); let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap(); - assert_eq!(decrypted, plaintext); + assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::CoseKey)); } #[test] @@ -172,7 +308,10 @@ mod test { 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); + assert_eq!( + decrypted, + (TEST_VECTOR_PLAINTEXT.to_vec(), ContentFormat::OctetStream) + ); } #[test] diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs index 4ce4b0252..1050bb3b0 100644 --- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -10,8 +10,8 @@ use crate::{ error::{CryptoError, EncStringParseError, Result}, rsa::encrypt_rsa2048_oaep_sha1, util::FromStrVisitor, - AsymmetricCryptoKey, AsymmetricPublicCryptoKey, RawPrivateKey, RawPublicKey, - SymmetricCryptoKey, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, BitwardenLegacyKeyBytes, RawPrivateKey, + RawPublicKey, SymmetricCryptoKey, }; // This module is a workaround to avoid deprecated warnings that come from the ZeroizeOnDrop // macro expansion @@ -169,7 +169,7 @@ impl UnsignedSharedKey { Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 { data: encrypt_rsa2048_oaep_sha1( rsa_public_key, - &encapsulated_key.to_encoded(), + encapsulated_key.to_encoded().as_ref(), )?, }) } @@ -200,7 +200,7 @@ impl UnsignedSharedKey { match decapsulation_key.inner() { RawPrivateKey::RsaOaepSha1(rsa_private_key) => { use UnsignedSharedKey::*; - let mut key_data = match self { + let key_data = match self { Rsa2048_OaepSha256_B64 { data } => { rsa_private_key.decrypt(Oaep::new::(), data) } @@ -217,7 +217,7 @@ impl UnsignedSharedKey { } } .map_err(|_| CryptoError::KeyDecrypt)?; - SymmetricCryptoKey::try_from(key_data.as_mut_slice()) + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_data)) } } } diff --git a/crates/bitwarden-crypto/src/enc_string/symmetric.rs b/crates/bitwarden-crypto/src/enc_string/symmetric.rs index 010fb1a9e..c6b9ca13c 100644 --- a/crates/bitwarden-crypto/src/enc_string/symmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/symmetric.rs @@ -8,7 +8,8 @@ use super::{check_length, from_b64, from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result, UnsupportedOperation}, util::FromStrVisitor, - Aes256CbcHmacKey, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, XChaCha20Poly1305Key, + Aes256CbcHmacKey, ContentFormat, KeyDecryptable, KeyEncryptable, KeyEncryptableWithContentType, + SymmetricCryptoKey, Utf8Bytes, XChaCha20Poly1305Key, }; #[cfg(feature = "wasm")] @@ -262,8 +263,9 @@ impl EncString { pub(crate) fn encrypt_xchacha20_poly1305( data_dec: &[u8], key: &XChaCha20Poly1305Key, + content_format: ContentFormat, ) -> Result { - let data = crate::cose::encrypt_xchacha20_poly1305(data_dec, key)?; + let data = crate::cose::encrypt_xchacha20_poly1305(data_dec, key, content_format)?; Ok(EncString::Cose_Encrypt0_B64 { data }) } @@ -277,12 +279,16 @@ impl EncString { } } -impl KeyEncryptable for &[u8] { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptableWithContentType for &[u8] { + fn encrypt_with_key( + self, + key: &SymmetricCryptoKey, + content_format: ContentFormat, + ) -> Result { match key { SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(self, key), SymmetricCryptoKey::XChaCha20Poly1305Key(inner_key) => { - EncString::encrypt_xchacha20_poly1305(self, inner_key) + EncString::encrypt_xchacha20_poly1305(self, inner_key, content_format) } SymmetricCryptoKey::Aes256CbcKey(_) => Err(CryptoError::OperationNotSupported( UnsupportedOperation::EncryptionNotImplementedForKey, @@ -305,7 +311,7 @@ impl KeyDecryptable> for EncString { EncString::Cose_Encrypt0_B64 { data }, SymmetricCryptoKey::XChaCha20Poly1305Key(key), ) => { - let decrypted_message = + let (decrypted_message, _) = crate::cose::decrypt_xchacha20_poly1305(data.as_slice(), key)?; Ok(decrypted_message) } @@ -316,13 +322,13 @@ impl KeyDecryptable> for EncString { impl KeyEncryptable for String { fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { - self.as_bytes().encrypt_with_key(key) + Utf8Bytes::from(self).encrypt_with_key(key) } } impl KeyEncryptable for &str { fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { - self.as_bytes().encrypt_with_key(key) + Utf8Bytes::from(self).encrypt_with_key(key) } } @@ -375,7 +381,7 @@ mod tests { let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test")); let test_string = "encrypted_test_string"; - let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap(); + let cipher = test_string.to_string().encrypt_with_key(&key).unwrap(); let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap(); assert_eq!(decrypted_str, test_string); @@ -385,8 +391,8 @@ mod tests { fn test_enc_string_ref_roundtrip() { let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test")); - let test_string = "encrypted_test_string"; - let cipher = test_string.encrypt_with_key(&key).unwrap(); + let test_string: &'static str = "encrypted_test_string"; + let cipher = test_string.to_string().encrypt_with_key(&key).unwrap(); let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap(); assert_eq!(decrypted_str, test_string); diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index b411009c0..544f95ce8 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -61,6 +61,9 @@ pub enum CryptoError { #[error("Invalid nonce length")] InvalidNonceLength, + #[error("Invalid padding")] + InvalidPadding, + #[error("Signature error, {0}")] SignatureError(#[from] SignatureError), @@ -90,6 +93,8 @@ pub enum EncStringParseError { InvalidCoseEncoding(coset::CoseError), #[error("Algorithm missing in COSE header")] CoseMissingAlgorithm, + #[error("Content type missing in COSE header")] + CoseMissingContentType, } #[derive(Debug, Error)] diff --git a/crates/bitwarden-crypto/src/fingerprint.rs b/crates/bitwarden-crypto/src/fingerprint.rs index 2e7200344..22307e977 100644 --- a/crates/bitwarden-crypto/src/fingerprint.rs +++ b/crates/bitwarden-crypto/src/fingerprint.rs @@ -8,16 +8,16 @@ use num_bigint::BigUint; use num_traits::cast::ToPrimitive; use thiserror::Error; -use crate::{error::Result, wordlist::EFF_LONG_WORD_LIST, CryptoError}; +use crate::{error::Result, wordlist::EFF_LONG_WORD_LIST, CryptoError, SpkiPublicKeyBytes}; /// Computes a fingerprint of the given `fingerprint_material` using the given `public_key`. /// /// This is commonly used for account fingerprints. With the following arguments: /// - `fingerprint_material`: user's id. /// - `public_key`: user's public key. -pub fn fingerprint(fingerprint_material: &str, public_key: &[u8]) -> Result { - let hkdf = - hkdf::Hkdf::::from_prk(public_key).map_err(|_| CryptoError::InvalidKeyLen)?; +pub fn fingerprint(fingerprint_material: &str, public_key: &SpkiPublicKeyBytes) -> Result { + let hkdf = hkdf::Hkdf::::from_prk(public_key.as_ref()) + .map_err(|_| CryptoError::InvalidKeyLen)?; let mut user_fingerprint = [0u8; 32]; hkdf.expand(fingerprint_material.as_bytes(), &mut user_fingerprint) @@ -64,6 +64,7 @@ pub enum FingerprintError { #[cfg(test)] mod tests { use super::fingerprint; + use crate::SpkiPublicKeyBytes; #[test] fn test_fingerprint() { @@ -86,10 +87,10 @@ mod tests { 197, 3, 219, 56, 77, 109, 47, 72, 251, 131, 36, 240, 96, 169, 31, 82, 93, 166, 242, 3, 33, 213, 2, 3, 1, 0, 1, ]; - + let key = SpkiPublicKeyBytes::from(key); assert_eq!( "turban-deftly-anime-chatroom-unselfish", - fingerprint(user_id, key).unwrap() + fingerprint(user_id, &key).unwrap() ); } } diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index 8ef598e33..14a400992 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -4,7 +4,10 @@ use rsa::{pkcs8::DecodePublicKey, RsaPrivateKey, RsaPublicKey}; use serde_repr::{Deserialize_repr, Serialize_repr}; use super::key_encryptable::CryptoKey; -use crate::error::{CryptoError, Result}; +use crate::{ + error::{CryptoError, Result}, + Pkcs8PrivateKeyBytes, SpkiPublicKeyBytes, +}; /// Algorithm / public key encryption scheme used for encryption/decryption. #[derive(Serialize_repr, Deserialize_repr)] @@ -41,14 +44,15 @@ impl AsymmetricPublicCryptoKey { } /// Makes a SubjectPublicKeyInfo DER serialized version of the public key. - pub fn to_der(&self) -> Result> { + pub fn to_der(&self) -> Result { use rsa::pkcs8::EncodePublicKey; match &self.inner { RawPublicKey::RsaOaepSha1(public_key) => Ok(public_key .to_public_key_der() .map_err(|_| CryptoError::InvalidKey)? .as_bytes() - .to_owned()), + .to_owned() + .into()), } } } @@ -110,17 +114,17 @@ impl AsymmetricCryptoKey { } #[allow(missing_docs)] - pub fn from_der(der: &[u8]) -> Result { + pub fn from_der(der: &Pkcs8PrivateKeyBytes) -> Result { use rsa::pkcs8::DecodePrivateKey; Ok(Self { inner: RawPrivateKey::RsaOaepSha1(Box::pin( - RsaPrivateKey::from_pkcs8_der(der).map_err(|_| CryptoError::InvalidKey)?, + RsaPrivateKey::from_pkcs8_der(der.as_ref()).map_err(|_| CryptoError::InvalidKey)?, )), }) } #[allow(missing_docs)] - pub fn to_der(&self) -> Result> { + pub fn to_der(&self) -> Result { match &self.inner { RawPrivateKey::RsaOaepSha1(private_key) => { use rsa::pkcs8::EncodePrivateKey; @@ -128,7 +132,8 @@ impl AsymmetricCryptoKey { .to_pkcs8_der() .map_err(|_| CryptoError::InvalidKey)? .as_bytes() - .to_owned()) + .to_owned() + .into()) } } } @@ -160,7 +165,9 @@ mod tests { use base64::{engine::general_purpose::STANDARD, Engine}; use crate::{ - AsymmetricCryptoKey, AsymmetricPublicCryptoKey, SymmetricCryptoKey, UnsignedSharedKey, + content_format::{Bytes, Pkcs8PrivateKeyDerContentFormat}, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, Pkcs8PrivateKeyBytes, SymmetricCryptoKey, + UnsignedSharedKey, }; #[test] @@ -198,12 +205,15 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= // Load the two different formats and check they are the same key let pem_key = AsymmetricCryptoKey::from_pem(pem_key_str).unwrap(); - let der_key = AsymmetricCryptoKey::from_der(&der_key_vec).unwrap(); + let der_key = AsymmetricCryptoKey::from_der( + &Bytes::::from(der_key_vec.clone()), + ) + .unwrap(); assert_eq!(pem_key.to_der().unwrap(), der_key.to_der().unwrap()); // Check that the keys can be converted back to DER - assert_eq!(der_key.to_der().unwrap(), der_key_vec); - assert_eq!(pem_key.to_der().unwrap(), der_key_vec); + assert_eq!(der_key.to_der().unwrap().to_vec(), der_key_vec); + assert_eq!(pem_key.to_der().unwrap().to_vec(), der_key_vec); } #[test] @@ -251,6 +261,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= )) .unwrap(); + let private_key = Pkcs8PrivateKeyBytes::from(private_key); let private_key = AsymmetricCryptoKey::from_der(&private_key).unwrap(); let public_key = AsymmetricPublicCryptoKey::from_der(&public_key).unwrap(); diff --git a/crates/bitwarden-crypto/src/keys/device_key.rs b/crates/bitwarden-crypto/src/keys/device_key.rs index 6f083e718..b7ba7a9c0 100644 --- a/crates/bitwarden-crypto/src/keys/device_key.rs +++ b/crates/bitwarden-crypto/src/keys/device_key.rs @@ -1,7 +1,7 @@ use super::{AsymmetricCryptoKey, PublicKeyEncryptionAlgorithm}; use crate::{ - error::Result, CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, - UnsignedSharedKey, + error::Result, CryptoError, EncString, KeyDecryptable, KeyEncryptable, Pkcs8PrivateKeyBytes, + SymmetricCryptoKey, UnsignedSharedKey, }; /// Device Key @@ -65,6 +65,7 @@ impl DeviceKey { protected_user_key: UnsignedSharedKey, ) -> Result { let device_private_key: Vec = protected_device_private_key.decrypt_with_key(&self.0)?; + let device_private_key = Pkcs8PrivateKeyBytes::from(device_private_key); let device_private_key = AsymmetricCryptoKey::from_der(&device_private_key)?; let user_key: SymmetricCryptoKey = @@ -88,7 +89,7 @@ impl TryFrom for DeviceKey { #[cfg(test)] mod tests { use super::*; - use crate::derive_symmetric_key; + use crate::{derive_symmetric_key, BitwardenLegacyKeyBytes}; #[test] fn test_trust_device() { @@ -111,21 +112,24 @@ mod tests { #[test] fn test_decrypt_user_key() { // Example keys from desktop app - let user_key: &mut [u8] = &mut [ + let user_key: &[u8] = &[ 109, 128, 172, 147, 206, 123, 134, 95, 16, 36, 155, 113, 201, 18, 186, 230, 216, 212, 173, 188, 74, 11, 134, 131, 137, 242, 105, 178, 105, 126, 52, 139, 248, 91, 215, 21, 128, 91, 226, 222, 165, 67, 251, 34, 83, 81, 77, 147, 225, 76, 13, 41, 102, 45, 183, 218, 106, 89, 254, 208, 251, 101, 130, 10, ]; - let user_key = SymmetricCryptoKey::try_from(user_key).unwrap(); + let user_key = + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(user_key)).unwrap(); - let key_data: &mut [u8] = &mut [ + let key_data: &[u8] = &[ 114, 235, 60, 115, 172, 156, 203, 145, 195, 130, 215, 250, 88, 146, 215, 230, 12, 109, 245, 222, 54, 217, 255, 211, 221, 105, 230, 236, 65, 52, 209, 133, 76, 208, 113, 254, 194, 216, 156, 19, 230, 62, 32, 93, 87, 7, 144, 156, 117, 142, 250, 32, 182, 118, 187, 8, 247, 7, 203, 201, 65, 147, 206, 247, ]; - let device_key = DeviceKey(key_data.try_into().unwrap()); + let device_key = DeviceKey( + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_data)).unwrap(), + ); let protected_user_key: UnsignedSharedKey = "4.f+VbbacRhO2q4MOUSdt1AIjQ2FuLAvg4aDxJMXAh3VxvbmUADj8Ct/R7XEpPUqApmbRS566jS0eRVy8Sk08ogoCdj1IFN9VsIky2i2X1WHK1fUnr3UBmXE3tl2NPBbx56U+h73S2jNTSyet2W18Jg2q7/w8KIhR3J41QrG9aGoOTN93to3hb5W4z6rdrSI0e7GkizbwcIA0NH7Z1JyAhrjPm9+tjRjg060YbEbGaWTAOkZWfgbLjr8bY455DteO2xxG139cOx7EBo66N+YhjsLi0ozkeUyPQkoWBdKMcQllS7jCfB4fDyJA05ALTbk74syKkvqFxqwmQbg+aVn+dcw==".parse().unwrap(); diff --git a/crates/bitwarden-crypto/src/keys/key_encryptable.rs b/crates/bitwarden-crypto/src/keys/key_encryptable.rs index 00416d6a5..c3516f656 100644 --- a/crates/bitwarden-crypto/src/keys/key_encryptable.rs +++ b/crates/bitwarden-crypto/src/keys/key_encryptable.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, hash::Hash, sync::Arc}; use rayon::prelude::*; use uuid::Uuid; -use crate::{error::Result, CryptoError, SymmetricCryptoKey}; +use crate::{error::Result, ContentFormat, CryptoError, SymmetricCryptoKey}; #[allow(missing_docs)] pub trait KeyContainer: Send + Sync { @@ -19,11 +19,26 @@ impl KeyContainer for Arc { #[allow(missing_docs)] pub trait CryptoKey {} -#[allow(missing_docs)] +/// An encryption operation that takes the input value and encrypts it into the output value +/// using a key reference. Implementing this requires a content type to be specified in +/// the implementation. pub trait KeyEncryptable { + /// Encrypts a value using the provided key reference. fn encrypt_with_key(self, key: &Key) -> Result; } +/// An encryption operation that takes the input value and encrypts it into the output value +/// using a key reference, with an externally provided content type. +/// +/// In contrast to `KeyEncryptable`, this trait allows the caller to specify the content format. +/// Because of this, it is not exposed outside of the crate, because outside callers should +/// not make a choice about the content format. Where possible, the content format is +/// ensured at compile time by the type system, not at runtime by the caller passing +/// in a parameter. +pub(crate) trait KeyEncryptableWithContentType { + fn encrypt_with_key(self, key: &Key, content_format: ContentFormat) -> Result; +} + #[allow(missing_docs)] pub trait KeyDecryptable { fn decrypt_with_key(&self, key: &Key) -> Result; diff --git a/crates/bitwarden-crypto/src/keys/master_key.rs b/crates/bitwarden-crypto/src/keys/master_key.rs index 2c15806e5..653d34b7f 100644 --- a/crates/bitwarden-crypto/src/keys/master_key.rs +++ b/crates/bitwarden-crypto/src/keys/master_key.rs @@ -4,7 +4,7 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use generic_array::GenericArray; use rand::Rng; use typenum::U32; -use zeroize::{Zeroize, Zeroizing}; +use zeroize::Zeroize; use super::{ kdf::{Kdf, KdfDerivedKeyMaterial}, @@ -12,7 +12,8 @@ use super::{ }; use crate::{ util::{self}, - CryptoError, EncString, KeyDecryptable, Result, SymmetricCryptoKey, UserKey, + BitwardenLegacyKeyBytes, CryptoError, EncString, KeyDecryptable, Result, SymmetricCryptoKey, + UserKey, }; #[allow(missing_docs)] @@ -129,8 +130,8 @@ pub(super) fn encrypt_user_key( user_key: &SymmetricCryptoKey, ) -> Result { let stretched_master_key = stretch_key(master_key)?; - let user_key_bytes = Zeroizing::new(user_key.to_encoded()); - EncString::encrypt_aes256_hmac(&user_key_bytes, &stretched_master_key) + let user_key_bytes = user_key.to_encoded(); + EncString::encrypt_aes256_hmac(user_key_bytes.as_ref(), &stretched_master_key) } /// Helper function to decrypt a user key with a master or pin key or key-connector-key. @@ -138,7 +139,7 @@ pub(super) fn decrypt_user_key( key: &Pin>>, user_key: EncString, ) -> Result { - let mut dec: Vec = match user_key { + let dec: Vec = match user_key { // Legacy. user_keys were encrypted using `Aes256Cbc_B64` a long time ago. We've since // moved to using `Aes256Cbc_HmacSha256_B64`. However, we still need to support // decrypting these old keys. @@ -159,7 +160,7 @@ pub(super) fn decrypt_user_key( } }; - SymmetricCryptoKey::try_from(dec.as_mut_slice()) + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(dec)) } /// Generate a new random user key and encrypt it with the master key. diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index 4327d20c0..c5dd18bb6 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -1,4 +1,5 @@ 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}; @@ -30,4 +31,4 @@ pub use kdf::{ default_pbkdf2_iterations, Kdf, }; pub(crate) use key_id::{KeyId, KEY_ID_SIZE}; -mod utils; +pub(crate) mod utils; diff --git a/crates/bitwarden-crypto/src/keys/pin_key.rs b/crates/bitwarden-crypto/src/keys/pin_key.rs index e069a4c62..a1503a5ad 100644 --- a/crates/bitwarden-crypto/src/keys/pin_key.rs +++ b/crates/bitwarden-crypto/src/keys/pin_key.rs @@ -1,6 +1,6 @@ use super::{ kdf::{Kdf, KdfDerivedKeyMaterial}, - master_key::{decrypt_user_key, encrypt_user_key}, + master_key::decrypt_user_key, utils::stretch_key, }; use crate::{ @@ -20,7 +20,7 @@ impl PinKey { /// Encrypt the users user key pub fn encrypt_user_key(&self, user_key: &SymmetricCryptoKey) -> Result { - encrypt_user_key(&self.0 .0, user_key) + user_key.encrypt_with_key(self) } /// Decrypt the users user key @@ -31,15 +31,19 @@ impl PinKey { impl CryptoKey for PinKey {} -impl KeyEncryptable for &[u8] { +impl KeyEncryptable for &SymmetricCryptoKey { fn encrypt_with_key(self, key: &PinKey) -> Result { let stretched_key = SymmetricCryptoKey::Aes256CbcHmacKey(stretch_key(&key.0 .0)?); - self.encrypt_with_key(&stretched_key) + // The (stretched) pin key is currently always an AES-256-CBC-HMAC key, and wraps a + // bitwarden legacy encoded symmetric key + self.to_encoded().encrypt_with_key(&stretched_key) } } impl KeyEncryptable for String { fn encrypt_with_key(self, key: &PinKey) -> Result { - self.as_bytes().encrypt_with_key(key) + self.encrypt_with_key(&SymmetricCryptoKey::Aes256CbcHmacKey(stretch_key( + &key.0 .0, + )?)) } } diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index ad2597b95..25407e219 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -11,9 +11,9 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use super::AsymmetricPublicCryptoKey; use crate::{ - cose::CoseSerializable, error::EncodingError, util::FromStrVisitor, CryptoError, - PublicKeyEncryptionAlgorithm, RawPublicKey, SignedObject, SigningKey, SigningNamespace, - VerifyingKey, + cose::CoseSerializable, error::EncodingError, util::FromStrVisitor, CoseSign1Bytes, + CryptoError, PublicKeyEncryptionAlgorithm, RawPublicKey, SignedObject, SigningKey, + SigningNamespace, VerifyingKey, }; #[cfg(feature = "wasm")] @@ -57,7 +57,7 @@ impl SignedPublicKeyMessage { RawPublicKey::RsaOaepSha1(_) => Ok(SignedPublicKeyMessage { algorithm: PublicKeyEncryptionAlgorithm::RsaOaepSha1, content_format: PublicKeyFormat::Spki, - public_key: ByteBuf::from(public_key.to_der()?), + public_key: ByteBuf::from(public_key.to_der()?.as_ref()), }), } } @@ -77,22 +77,24 @@ impl SignedPublicKeyMessage { #[derive(Clone, Debug)] pub struct SignedPublicKey(pub(crate) SignedObject); -impl From for Vec { +impl From for CoseSign1Bytes { fn from(val: SignedPublicKey) -> Self { val.0.to_cose() } } -impl TryFrom> for SignedPublicKey { +impl TryFrom for SignedPublicKey { type Error = EncodingError; - fn try_from(bytes: Vec) -> Result { - Ok(SignedPublicKey(SignedObject::from_cose(&bytes)?)) + fn try_from(bytes: CoseSign1Bytes) -> Result { + Ok(SignedPublicKey(SignedObject::from_cose( + &CoseSign1Bytes::from(bytes), + )?)) } } impl From for String { fn from(val: SignedPublicKey) -> Self { - let bytes: Vec = val.into(); + let bytes: CoseSign1Bytes = val.into(); STANDARD.encode(&bytes) } } @@ -126,7 +128,7 @@ impl FromStr for SignedPublicKey { let bytes = STANDARD .decode(s) .map_err(|_| EncodingError::InvalidCborSerialization)?; - Self::try_from(bytes) + Self::try_from(CoseSign1Bytes::from(bytes)) } } diff --git a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs index b70419aac..de2b54a8a 100644 --- a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs @@ -1,4 +1,4 @@ -use std::{cmp::max, pin::Pin}; +use std::pin::Pin; use base64::{engine::general_purpose::STANDARD, Engine}; use coset::{iana::KeyOperation, CborSerializable, RegisteredLabelWithPrivate}; @@ -18,7 +18,7 @@ use super::{ key_encryptable::CryptoKey, key_id::{KeyId, KEY_ID_SIZE}, }; -use crate::{cose, CryptoError}; +use crate::{cose, BitwardenLegacyKeyBytes, ContentFormat, CoseKeyBytes, CryptoError}; /// [Aes256CbcKey] is a symmetric encryption key, consisting of one 256-bit key, /// used to decrypt legacy type 0 enc strings. The data is not authenticated @@ -145,13 +145,17 @@ impl SymmetricCryptoKey { /// This can be used for storage and transmission in the old byte array format. /// When the wrapping key is a COSE key, and the wrapped key is a COSE key, then this should /// not use the byte representation but instead use the COSE key representation. - pub fn to_encoded(&self) -> Vec { - let mut encoded_key = self.to_encoded_raw(); - match self { - Self::Aes256CbcKey(_) | Self::Aes256CbcHmacKey(_) => encoded_key, - Self::XChaCha20Poly1305Key(_) => { + pub fn to_encoded(&self) -> BitwardenLegacyKeyBytes { + let encoded_key = self.to_encoded_raw(); + match encoded_key { + EncodedSymmetricKey::BitwardenLegacyKey(_) => { + let encoded_key: Vec = encoded_key.into(); + BitwardenLegacyKeyBytes::from(encoded_key) + } + EncodedSymmetricKey::CoseKey(_) => { + let mut encoded_key: Vec = encoded_key.into(); pad_key(&mut encoded_key, Self::AES256_CBC_HMAC_KEY_LEN + 1); - encoded_key + BitwardenLegacyKeyBytes::from(encoded_key) } } } @@ -182,14 +186,16 @@ impl SymmetricCryptoKey { /// format hints an the key type, or can be represented as a byte array, if padded to be /// larger than the byte array representation of the other key types using the /// aforementioned [SymmetricCryptoKey::to_encoded] function. - pub(crate) fn to_encoded_raw(&self) -> Vec { + pub(crate) fn to_encoded_raw(&self) -> EncodedSymmetricKey { match self { - Self::Aes256CbcKey(key) => key.enc_key.to_vec(), + Self::Aes256CbcKey(key) => { + EncodedSymmetricKey::BitwardenLegacyKey(key.enc_key.to_vec().into()) + } Self::Aes256CbcHmacKey(key) => { let mut buf = Vec::with_capacity(64); buf.extend_from_slice(&key.enc_key); buf.extend_from_slice(&key.mac_key); - buf + EncodedSymmetricKey::BitwardenLegacyKey(buf.into()) } Self::XChaCha20Poly1305Key(key) => { let builder = coset::CoseKeyBuilder::new_symmetric_key(key.enc_key.to_vec()); @@ -203,13 +209,23 @@ impl SymmetricCryptoKey { cose_key.alg = Some(RegisteredLabelWithPrivate::PrivateUse( cose::XCHACHA20_POLY1305, )); - cose_key - .to_vec() - .expect("cose key serialization should not fail") + EncodedSymmetricKey::CoseKey( + cose_key + .to_vec() + .expect("cose key serialization should not fail") + .into(), + ) } } } + pub(crate) fn try_from_cose(serialized_key: &[u8]) -> Result { + let cose_key = + coset::CoseKey::from_slice(serialized_key).map_err(|_| CryptoError::InvalidKey)?; + let key = SymmetricCryptoKey::try_from(&cose_key)?; + Ok(key) + } + #[allow(missing_docs)] pub fn to_base64(&self) -> String { STANDARD.encode(self.to_encoded()) @@ -245,62 +261,70 @@ impl TryFrom for SymmetricCryptoKey { type Error = CryptoError; fn try_from(value: String) -> Result { - let b = STANDARD + let bytes = STANDARD .decode(value) .map_err(|_| CryptoError::InvalidKey)?; - Self::try_from(b) + Self::try_from(&BitwardenLegacyKeyBytes::from(bytes)) } } -impl TryFrom> for SymmetricCryptoKey { +impl TryFrom<&BitwardenLegacyKeyBytes> for SymmetricCryptoKey { type Error = CryptoError; - fn try_from(mut value: Vec) -> Result { - Self::try_from(value.as_mut_slice()) - } -} + fn try_from(value: &BitwardenLegacyKeyBytes) -> Result { + let slice = value.as_ref(); -impl TryFrom<&mut [u8]> for SymmetricCryptoKey { - type Error = CryptoError; - - /// Note: This function takes the byte slice by mutable reference and will zero out all - /// the data in it. This is to prevent the key from being left in memory. - fn try_from(value: &mut [u8]) -> Result { // Raw byte serialized keys are either 32, 64, or more bytes long. If they are 32/64, they // are the raw serializations of the AES256-CBC, and AES256-CBC-HMAC keys. If they // are longer, they are COSE keys. The COSE keys are padded to the minimum length of // 65 bytes, when serialized to raw byte arrays. - let result = if value.len() == Self::AES256_CBC_HMAC_KEY_LEN { - let mut enc_key = Box::pin(GenericArray::::default()); - let mut mac_key = Box::pin(GenericArray::::default()); - - enc_key.copy_from_slice(&value[..32]); - mac_key.copy_from_slice(&value[32..]); - - Ok(Self::Aes256CbcHmacKey(Aes256CbcHmacKey { - enc_key, - mac_key, - })) - } else if value.len() == Self::AES256_CBC_KEY_LEN { - let mut enc_key = Box::pin(GenericArray::::default()); - - enc_key.copy_from_slice(&value[..Self::AES256_CBC_KEY_LEN]); - - Ok(Self::Aes256CbcKey(Aes256CbcKey { enc_key })) - } else if value.len() > Self::AES256_CBC_HMAC_KEY_LEN { - let unpadded_value = unpad_key(value)?; - let cose_key = - coset::CoseKey::from_slice(unpadded_value).map_err(|_| CryptoError::InvalidKey)?; - SymmetricCryptoKey::try_from(&cose_key) + let result = if slice.len() == Self::AES256_CBC_HMAC_KEY_LEN + || slice.len() == Self::AES256_CBC_KEY_LEN + { + Self::try_from(EncodedSymmetricKey::BitwardenLegacyKey(value.clone())) + } else if slice.len() > Self::AES256_CBC_HMAC_KEY_LEN { + let unpadded_value = unpad_key(slice)?; + Ok(Self::try_from_cose(unpadded_value)?) } else { Err(CryptoError::InvalidKeyLen) }; - value.zeroize(); result } } +impl TryFrom for SymmetricCryptoKey { + type Error = CryptoError; + + fn try_from(value: EncodedSymmetricKey) -> Result { + match value { + EncodedSymmetricKey::BitwardenLegacyKey(key) + if key.as_ref().len() == Self::AES256_CBC_KEY_LEN => + { + let mut enc_key = Box::pin(GenericArray::::default()); + enc_key.copy_from_slice(&key.as_ref()[..Self::AES256_CBC_KEY_LEN]); + Ok(Self::Aes256CbcKey(Aes256CbcKey { enc_key })) + } + EncodedSymmetricKey::BitwardenLegacyKey(key) + if key.as_ref().len() == Self::AES256_CBC_HMAC_KEY_LEN => + { + let mut enc_key = Box::pin(GenericArray::::default()); + enc_key.copy_from_slice(&key.as_ref()[..32]); + + let mut mac_key = Box::pin(GenericArray::::default()); + mac_key.copy_from_slice(&key.as_ref()[32..]); + + Ok(Self::Aes256CbcHmacKey(Aes256CbcHmacKey { + enc_key, + mac_key, + })) + } + EncodedSymmetricKey::CoseKey(key) => Self::try_from_cose(key.as_ref()), + _ => Err(CryptoError::InvalidKey), + } + } +} + impl CryptoKey for SymmetricCryptoKey {} // We manually implement these to make sure we don't print any sensitive data @@ -350,10 +374,7 @@ impl std::fmt::Debug for XChaCha20Poly1305Key { /// size of the byte array. The previous key types [SymmetricCryptoKey::Aes256CbcHmacKey] and /// [SymmetricCryptoKey::Aes256CbcKey] are 64 and 32 bytes long respectively. fn pad_key(key_bytes: &mut Vec, min_length: usize) { - // at least 1 byte of padding is required - let pad_bytes = min_length.saturating_sub(key_bytes.len()).max(1); - let padded_length = max(min_length, key_bytes.len() + 1); - key_bytes.resize(padded_length, pad_bytes as u8); + crate::keys::utils::pad_bytes(key_bytes, min_length); } /// Unpad a key that is padded using the PKCS7-like padding defined by [pad_key]. @@ -367,11 +388,32 @@ fn pad_key(key_bytes: &mut Vec, min_length: usize) { /// size of the byte array the previous key types [SymmetricCryptoKey::Aes256CbcHmacKey] and /// [SymmetricCryptoKey::Aes256CbcKey] are 64 and 32 bytes long respectively. fn unpad_key(key_bytes: &[u8]) -> Result<&[u8], CryptoError> { - let pad_len = *key_bytes.last().ok_or(CryptoError::InvalidKey)? as usize; - if pad_len >= key_bytes.len() { - return Err(CryptoError::InvalidKey); + crate::keys::utils::unpad_bytes(key_bytes).map_err(|_| CryptoError::InvalidKey) +} + +/// Encoded representation of [SymmetricCryptoKey] +pub enum EncodedSymmetricKey { + /// An Aes256-CBC-HMAC key, or a Aes256-CBC key + BitwardenLegacyKey(BitwardenLegacyKeyBytes), + /// A symmetric key encoded as a COSE key + CoseKey(CoseKeyBytes), +} +impl From for Vec { + fn from(val: EncodedSymmetricKey) -> Self { + match val { + EncodedSymmetricKey::BitwardenLegacyKey(key) => key.to_vec(), + EncodedSymmetricKey::CoseKey(key) => key.to_vec(), + } + } +} +impl EncodedSymmetricKey { + #[allow(private_interfaces)] + pub fn content_format(&self) -> ContentFormat { + match self { + EncodedSymmetricKey::BitwardenLegacyKey(_) => ContentFormat::BitwardenLegacyKey, + EncodedSymmetricKey::CoseKey(_) => ContentFormat::CoseKey, + } } - Ok(key_bytes[..key_bytes.len() - pad_len].as_ref()) } /// Test only helper for deriving a symmetric key. @@ -394,7 +436,7 @@ mod tests { use super::{derive_symmetric_key, SymmetricCryptoKey}; use crate::{ keys::symmetric_crypto_key::{pad_key, unpad_key}, - Aes256CbcHmacKey, Aes256CbcKey, XChaCha20Poly1305Key, + Aes256CbcHmacKey, Aes256CbcKey, BitwardenLegacyKeyBytes, XChaCha20Poly1305Key, }; #[test] @@ -413,14 +455,15 @@ mod tests { fn test_encode_decode_old_symmetric_crypto_key() { let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); let encoded = key.to_encoded(); - let decoded = SymmetricCryptoKey::try_from(encoded).unwrap(); + let decoded = SymmetricCryptoKey::try_from(&encoded).unwrap(); assert_eq!(key, decoded); } #[test] fn test_decode_new_symmetric_crypto_key() { - let key_b64 = STANDARD.decode("pQEEAlDib+JxbqMBlcd3KTUesbufAzoAARFvBIQDBAUGIFggt79surJXmqhPhYuuqi9ZyPfieebmtw2OsmN5SDrb4yUB").unwrap(); - let key = SymmetricCryptoKey::try_from(key_b64).unwrap(); + let key = STANDARD.decode("pQEEAlDib+JxbqMBlcd3KTUesbufAzoAARFvBIQDBAUGIFggt79surJXmqhPhYuuqi9ZyPfieebmtw2OsmN5SDrb4yUB").unwrap(); + let key = BitwardenLegacyKeyBytes::from(key); + let key = SymmetricCryptoKey::try_from(&key).unwrap(); match key { SymmetricCryptoKey::XChaCha20Poly1305Key(_) => (), _ => panic!("Invalid key type"), @@ -431,7 +474,7 @@ mod tests { fn test_encode_xchacha20_poly1305_key() { let key = SymmetricCryptoKey::make_xchacha20_poly1305_key(); let encoded = key.to_encoded(); - let decoded = SymmetricCryptoKey::try_from(encoded).unwrap(); + let decoded = SymmetricCryptoKey::try_from(&encoded).unwrap(); assert_eq!(key, decoded); } @@ -483,8 +526,10 @@ mod tests { #[test] fn test_eq_aes_cbc() { - let key1 = SymmetricCryptoKey::try_from(vec![1u8; 32]).unwrap(); - let key2 = SymmetricCryptoKey::try_from(vec![2u8; 32]).unwrap(); + let key1 = + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(vec![1u8; 32])).unwrap(); + let key2 = + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(vec![2u8; 32])).unwrap(); assert_ne!(key1, key2); let key3 = SymmetricCryptoKey::try_from(key1.to_base64()).unwrap(); assert_eq!(key1, key3); diff --git a/crates/bitwarden-crypto/src/keys/utils.rs b/crates/bitwarden-crypto/src/keys/utils.rs index 21edd60af..e81bf61d1 100644 --- a/crates/bitwarden-crypto/src/keys/utils.rs +++ b/crates/bitwarden-crypto/src/keys/utils.rs @@ -1,10 +1,10 @@ -use std::pin::Pin; +use std::{cmp::max, pin::Pin}; use generic_array::GenericArray; use typenum::U32; use super::Aes256CbcHmacKey; -use crate::{util::hkdf_expand, Result}; +use crate::{util::hkdf_expand, CryptoError, Result}; /// Stretch the given key using HKDF. /// This can be either a kdf-derived key (PIN/Master password) or @@ -16,6 +16,27 @@ pub(super) fn stretch_key(key: &Pin>>) -> Result, min_length: usize) { + // at least 1 byte of padding is required + let pad_bytes = min_length.saturating_sub(bytes.len()).max(1); + let padded_length = max(min_length, bytes.len() + 1); + bytes.resize(padded_length, pad_bytes as u8); +} + +/// Unpads bytes that is padded using the PKCS7-like padding defined by [pad_bytes]. +/// The last N bytes of the padded bytes all have the value N. Minimum of 1 padding byte. +/// For example, padded to size 4, the value 0,0 becomes 0,0,2,2. +pub(crate) fn unpad_bytes(padded_bytes: &[u8]) -> Result<&[u8], CryptoError> { + let pad_len = *padded_bytes.last().ok_or(CryptoError::InvalidPadding)? as usize; + if pad_len >= padded_bytes.len() { + return Err(CryptoError::InvalidPadding); + } + Ok(padded_bytes[..(padded_bytes.len() - pad_len)].as_ref()) +} + #[cfg(test)] mod tests { use super::*; @@ -46,4 +67,17 @@ mod tests { stretched.mac_key.as_slice() ); } + + #[test] + fn test_pad_bytes_roundtrip() { + let original_bytes = vec![1u8; 10]; + let mut cloned_bytes = original_bytes.clone(); + let mut encoded_bytes = vec![1u8; 12]; + encoded_bytes[10] = 2; + encoded_bytes[11] = 2; + pad_bytes(&mut cloned_bytes, 12); + assert_eq!(encoded_bytes, cloned_bytes); + let unpadded_bytes = unpad_bytes(&cloned_bytes).unwrap(); + assert_eq!(original_bytes, unpadded_bytes); + } } diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index 6db9df997..60428d8a4 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -13,6 +13,8 @@ static ALLOC: ZeroizingAllocator = ZeroizingAllocator(std::alloc::System); mod aes; +mod content_format; +pub use content_format::*; mod enc_string; pub use enc_string::{EncString, UnsignedSharedKey}; mod error; @@ -36,7 +38,9 @@ mod signing; pub use signing::*; mod traits; mod xchacha20; -pub use traits::{Decryptable, Encryptable, IdentifyKey, KeyId, KeyIds}; +pub use traits::{ + CompositeEncryptable, Decryptable, IdentifyKey, KeyId, KeyIds, PrimitiveEncryptable, +}; pub use zeroizing_alloc::ZeroAlloc as ZeroizingAllocator; #[cfg(feature = "uniffi")] diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs index 47bbbe6be..1c2b95bc3 100644 --- a/crates/bitwarden-crypto/src/signing/signature.rs +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -7,9 +7,10 @@ use super::{ VerifyingKey, }; use crate::{ + content_format::CoseSign1ContentFormat, cose::{CoseSerializable, SIGNING_NAMESPACE}, error::{EncodingError, SignatureError}, - CryptoError, + CoseSign1Bytes, CryptoError, }; /// A signature cryptographically attests to a (namespace, data) pair. The namespace is included in @@ -172,25 +173,26 @@ impl SigningKey { } } -impl CoseSerializable for Signature { - fn from_cose(bytes: &[u8]) -> Result { - let cose_sign1 = - CoseSign1::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?; +impl CoseSerializable for Signature { + fn from_cose(bytes: &CoseSign1Bytes) -> Result { + let cose_sign1 = CoseSign1::from_slice(bytes.as_ref()) + .map_err(|_| EncodingError::InvalidCoseEncoding)?; Ok(Signature(cose_sign1)) } - fn to_cose(&self) -> Vec { + fn to_cose(&self) -> CoseSign1Bytes { self.0 .clone() .to_vec() .expect("Signature is always serializable") + .into() } } #[cfg(test)] mod tests { use super::*; - use crate::SignatureAlgorithm; + use crate::{CoseKeyBytes, SignatureAlgorithm}; const VERIFYING_KEY: &[u8] = &[ 166, 1, 1, 2, 80, 55, 131, 40, 191, 230, 137, 76, 182, 184, 139, 94, 152, 45, 63, 13, 71, @@ -212,7 +214,7 @@ mod tests { #[test] fn test_cose_roundtrip_encode_signature() { - let signature = Signature::from_cose(SIGNATURE).unwrap(); + let signature = Signature::from_cose(&CoseSign1Bytes::from(SIGNATURE)).unwrap(); let cose_bytes = signature.to_cose(); let decoded_signature = Signature::from_cose(&cose_bytes).unwrap(); assert_eq!(signature.inner(), decoded_signature.inner()); @@ -220,8 +222,8 @@ mod tests { #[test] fn test_verify_testvector() { - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); - let signature = Signature::from_cose(SIGNATURE).unwrap(); + let verifying_key = VerifyingKey::from_cose(&CoseKeyBytes::from(VERIFYING_KEY)).unwrap(); + let signature = Signature::from_cose(&CoseSign1Bytes::from(SIGNATURE)).unwrap(); let serialized_message = SerializedMessage::from_bytes(SERIALIZED_MESSAGE.to_vec(), CoapContentFormat::Cbor); diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index 289e52a23..94134d290 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -7,9 +7,10 @@ use super::{ verifying_key::VerifyingKey, SigningNamespace, }; use crate::{ + content_format::CoseSign1ContentFormat, cose::{CoseSerializable, SIGNING_NAMESPACE}, error::{EncodingError, SignatureError}, - CryptoError, + CoseSign1Bytes, CryptoError, }; /// A signed object is a message containing a payload and signature that attests the payload's @@ -149,18 +150,20 @@ impl SigningKey { } } -impl CoseSerializable for SignedObject { - fn from_cose(bytes: &[u8]) -> Result { +impl CoseSerializable for SignedObject { + fn from_cose(bytes: &CoseSign1Bytes) -> Result { Ok(SignedObject( - CoseSign1::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?, + CoseSign1::from_slice(bytes.as_ref()) + .map_err(|_| EncodingError::InvalidCoseEncoding)?, )) } - fn to_cose(&self) -> Vec { + fn to_cose(&self) -> CoseSign1Bytes { self.0 .clone() .to_vec() .expect("SignedObject is always serializable") + .into() } } @@ -169,8 +172,8 @@ mod tests { use serde::{Deserialize, Serialize}; use crate::{ - CoseSerializable, CryptoError, SignatureAlgorithm, SignedObject, SigningKey, - SigningNamespace, VerifyingKey, + CoseKeyBytes, CoseSerializable, CoseSign1Bytes, CryptoError, SignatureAlgorithm, + SignedObject, SigningKey, SigningNamespace, VerifyingKey, }; const VERIFYING_KEY: &[u8] = &[ @@ -196,13 +199,14 @@ mod tests { #[test] fn test_roundtrip_cose() { - let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap(); + let signed_object = + SignedObject::from_cose(&::from(SIGNED_OBJECT)).unwrap(); assert_eq!( signed_object.content_type().unwrap(), coset::iana::CoapContentFormat::Cbor ); let cose_bytes = signed_object.to_cose(); - assert_eq!(cose_bytes, SIGNED_OBJECT); + assert_eq!(cose_bytes, CoseSign1Bytes::from(SIGNED_OBJECT)); } #[test] @@ -210,8 +214,9 @@ mod tests { let test_message = TestMessage { field1: "Test message".to_string(), }; - let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap(); - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let signed_object = + SignedObject::from_cose(&::from(SIGNED_OBJECT)).unwrap(); + let verifying_key = VerifyingKey::from_cose(&::from(VERIFYING_KEY)).unwrap(); let namespace = SigningNamespace::ExampleNamespace; let payload: TestMessage = signed_object .verify_and_unwrap(&verifying_key, &namespace) diff --git a/crates/bitwarden-crypto/src/signing/signing_key.rs b/crates/bitwarden-crypto/src/signing/signing_key.rs index c8567ad34..74658001b 100644 --- a/crates/bitwarden-crypto/src/signing/signing_key.rs +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -13,10 +13,11 @@ use super::{ SignatureAlgorithm, }; use crate::{ + content_format::CoseKeyContentFormat, cose::CoseSerializable, error::{EncodingError, Result}, keys::KeyId, - CryptoKey, + CoseKeyBytes, CryptoKey, }; /// A `SigningKey` without the key id. This enum contains a variant for each supported signature @@ -86,9 +87,9 @@ impl SigningKey { } } -impl CoseSerializable for SigningKey { +impl CoseSerializable for SigningKey { /// Serializes the signing key to a COSE-formatted byte array. - fn to_cose(&self) -> Vec { + fn to_cose(&self) -> CoseKeyBytes { match &self.inner { RawSigningKey::Ed25519(key) => { coset::CoseKeyBuilder::new_okp_key() @@ -107,14 +108,15 @@ impl CoseSerializable for SigningKey { .build() .to_vec() .expect("Signing key is always serializable") + .into() } } } /// Deserializes a COSE-formatted byte array into a signing key. - fn from_cose(bytes: &[u8]) -> Result { + fn from_cose(bytes: &CoseKeyBytes) -> Result { let cose_key = - CoseKey::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?; + CoseKey::from_slice(bytes.as_ref()).map_err(|_| EncodingError::InvalidCoseEncoding)?; match (&cose_key.alg, &cose_key.kty) { ( diff --git a/crates/bitwarden-crypto/src/signing/verifying_key.rs b/crates/bitwarden-crypto/src/signing/verifying_key.rs index 372fa0cd7..12fa0eebf 100644 --- a/crates/bitwarden-crypto/src/signing/verifying_key.rs +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -11,10 +11,11 @@ use coset::{ use super::{ed25519_verifying_key, key_id, SignatureAlgorithm}; use crate::{ + content_format::CoseKeyContentFormat, cose::CoseSerializable, error::{EncodingError, SignatureError}, keys::KeyId, - CryptoError, + CoseKeyBytes, CryptoError, }; /// A `VerifyingKey` without the key id. This enum contains a variant for each supported signature @@ -57,8 +58,8 @@ impl VerifyingKey { } } -impl CoseSerializable for VerifyingKey { - fn to_cose(&self) -> Vec { +impl CoseSerializable for VerifyingKey { + fn to_cose(&self) -> CoseKeyBytes { match &self.inner { RawVerifyingKey::Ed25519(key) => coset::CoseKeyBuilder::new_okp_key() .key_id((&self.id).into()) @@ -78,16 +79,17 @@ impl CoseSerializable for VerifyingKey { .add_key_op(KeyOperation::Verify) .build() .to_vec() - .expect("Verifying key is always serializable"), + .expect("Verifying key is always serializable") + .into(), } } - fn from_cose(bytes: &[u8]) -> Result + fn from_cose(bytes: &CoseKeyBytes) -> Result where Self: Sized, { - let cose_key = - coset::CoseKey::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?; + let cose_key = coset::CoseKey::from_slice(bytes.as_ref()) + .map_err(|_| EncodingError::InvalidCoseEncoding)?; let algorithm = cose_key .alg @@ -127,7 +129,7 @@ mod tests { #[test] fn test_cose_roundtrip_encode_verifying() { - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let verifying_key = VerifyingKey::from_cose(&CoseKeyBytes::from(VERIFYING_KEY)).unwrap(); let cose = verifying_key.to_cose(); let parsed_key = VerifyingKey::from_cose(&cose).unwrap(); @@ -136,7 +138,7 @@ mod tests { #[test] fn test_testvector() { - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let verifying_key = VerifyingKey::from_cose(&CoseKeyBytes::from(VERIFYING_KEY)).unwrap(); assert_eq!(verifying_key.algorithm(), SignatureAlgorithm::Ed25519); verifying_key @@ -146,7 +148,7 @@ mod tests { #[test] fn test_invalid_testvector() { - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let verifying_key = VerifyingKey::from_cose(&CoseKeyBytes::from(VERIFYING_KEY)).unwrap(); assert_eq!(verifying_key.algorithm(), SignatureAlgorithm::Ed25519); // This should fail, as the signed object is not valid for the given verifying key. diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index 7647f2aef..9675cfb5e 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -9,15 +9,17 @@ use zeroize::Zeroizing; use super::KeyStoreInner; use crate::{ derive_shareable_key, error::UnsupportedOperation, signing, store::backend::StoreBackend, - AsymmetricCryptoKey, CryptoError, EncString, KeyId, KeyIds, Result, Signature, - SignatureAlgorithm, SignedObject, SignedPublicKey, SignedPublicKeyMessage, SigningKey, - SymmetricCryptoKey, UnsignedSharedKey, + AsymmetricCryptoKey, BitwardenLegacyKeyBytes, ContentFormat, CryptoError, EncString, KeyId, + KeyIds, Result, Signature, SignatureAlgorithm, SignedObject, SignedPublicKey, + SignedPublicKeyMessage, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, }; /// The context of a crypto operation using [super::KeyStore] /// /// This will usually be accessed from an implementation of [crate::Decryptable] or -/// [crate::Encryptable], but can also be obtained through [super::KeyStore::context] +/// [crate::CompositeEncryptable], [crate::PrimitiveEncryptable], +/// but can also be obtained +/// through [super::KeyStore::context] /// /// This context contains access to the user keys stored in the [super::KeyStore] (sometimes /// referred to as `global keys`) and it also contains it's own individual secure backend for key @@ -59,8 +61,8 @@ use crate::{ /// /// const LOCAL_KEY: SymmKeyId = SymmKeyId::Local("local_key_id"); /// -/// impl Encryptable for Data { -/// fn encrypt(&self, ctx: &mut KeyStoreContext, key: SymmKeyId) -> Result { +/// impl CompositeEncryptable for Data { +/// fn encrypt_composite(&self, ctx: &mut KeyStoreContext, key: SymmKeyId) -> Result { /// let local_key_id = ctx.unwrap_symmetric_key(key, LOCAL_KEY, &self.key)?; /// self.name.encrypt(ctx, local_key_id) /// } @@ -139,25 +141,50 @@ impl KeyStoreContext<'_, Ids> { /// /// # Arguments /// - /// * `encryption_key` - The key id used to decrypt the `encrypted_key`. It must already exist - /// in the context + /// * `wrapping_key` - The key id used to decrypt the `wrapped_key`. It must already exist in + /// the context /// * `new_key_id` - The key id where the decrypted key will be stored. If it already exists, it /// will be overwritten - /// * `encrypted_key` - The key to decrypt + /// * `wrapped_key` - The key to decrypt pub fn unwrap_symmetric_key( &mut self, - encryption_key: Ids::Symmetric, + wrapping_key: Ids::Symmetric, new_key_id: Ids::Symmetric, - encrypted_key: &EncString, + wrapped_key: &EncString, ) -> Result { - let mut new_key_material = - self.decrypt_data_with_symmetric_key(encryption_key, encrypted_key)?; + let wrapping_key = self.get_symmetric_key(wrapping_key)?; + + let key = match (wrapped_key, wrapping_key) { + (EncString::Aes256Cbc_B64 { iv, data }, SymmetricCryptoKey::Aes256CbcKey(key)) => { + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from( + crate::aes::decrypt_aes256(iv, data.clone(), &key.enc_key)?, + ))? + } + ( + EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data }, + SymmetricCryptoKey::Aes256CbcHmacKey(key), + ) => SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from( + crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), &key.mac_key, &key.enc_key)?, + ))?, + ( + EncString::Cose_Encrypt0_B64 { data }, + SymmetricCryptoKey::XChaCha20Poly1305Key(key), + ) => { + let (content_bytes, content_format) = + crate::cose::decrypt_xchacha20_poly1305(data, key)?; + match content_format { + ContentFormat::BitwardenLegacyKey => { + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(content_bytes))? + } + ContentFormat::CoseKey => SymmetricCryptoKey::try_from_cose(&content_bytes)?, + _ => return Err(CryptoError::InvalidKey), + } + } + _ => return Err(CryptoError::InvalidKey), + }; #[allow(deprecated)] - self.set_symmetric_key( - new_key_id, - SymmetricCryptoKey::try_from(new_key_material.as_mut_slice())?, - )?; + self.set_symmetric_key(new_key_id, key)?; // Returning the new key identifier for convenience Ok(new_key_id) @@ -186,11 +213,27 @@ impl KeyStoreContext<'_, Ids> { // or `Aes256CbcKey`, or by specifying the content format to be CoseKey, in case the // wrapped key is a `XChaCha20Poly1305Key`. match (wrapping_key_instance, key_to_wrap_instance) { - (Aes256CbcHmacKey(_), Aes256CbcHmacKey(_) | Aes256CbcKey(_)) => self - .encrypt_data_with_symmetric_key( + ( + Aes256CbcHmacKey(_), + Aes256CbcHmacKey(_) | Aes256CbcKey(_) | XChaCha20Poly1305Key(_), + ) => self.encrypt_data_with_symmetric_key( + wrapping_key, + key_to_wrap_instance + .to_encoded() + .as_ref() + .to_vec() + .as_slice(), + ContentFormat::BitwardenLegacyKey, + ), + (XChaCha20Poly1305Key(_), _) => { + let encoded = key_to_wrap_instance.to_encoded_raw(); + let content_format = encoded.content_format(); + self.encrypt_data_with_symmetric_key( wrapping_key, - key_to_wrap_instance.to_encoded().as_slice(), - ), + Into::>::into(encoded).as_slice(), + content_format, + ) + } _ => Err(CryptoError::OperationNotSupported( UnsupportedOperation::EncryptionNotImplementedForKey, )), @@ -415,6 +458,13 @@ impl KeyStoreContext<'_, Ids> { EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data }, SymmetricCryptoKey::Aes256CbcHmacKey(key), ) => crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), &key.mac_key, &key.enc_key), + ( + EncString::Cose_Encrypt0_B64 { data }, + SymmetricCryptoKey::XChaCha20Poly1305Key(key), + ) => { + let (data, _) = crate::cose::decrypt_xchacha20_poly1305(data, key)?; + Ok(data) + } _ => Err(CryptoError::InvalidKey), } } @@ -423,6 +473,7 @@ impl KeyStoreContext<'_, Ids> { &self, key: Ids::Symmetric, data: &[u8], + content_format: ContentFormat, ) -> Result { let key = self.get_symmetric_key(key)?; match key { @@ -431,7 +482,7 @@ impl KeyStoreContext<'_, Ids> { )), SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(data, key), SymmetricCryptoKey::XChaCha20Poly1305Key(key) => { - EncString::encrypt_xchacha20_poly1305(data, key) + EncString::encrypt_xchacha20_poly1305(data, key, content_format) } } } @@ -469,10 +520,13 @@ mod tests { use serde::{Deserialize, Serialize}; use crate::{ - store::{tests::DataView, KeyStore}, + store::{ + tests::{Data, DataView}, + KeyStore, + }, traits::tests::{TestIds, TestSigningKey, TestSymmKey}, - CryptoError, Decryptable, Encryptable, SignatureAlgorithm, SigningKey, SigningNamespace, - SymmetricCryptoKey, + CompositeEncryptable, CryptoError, Decryptable, SignatureAlgorithm, SigningKey, + SigningNamespace, SymmetricCryptoKey, }; #[test] @@ -505,7 +559,9 @@ mod tests { // Encrypt some data with the key let data = DataView("Hello, World!".to_string(), key_a0_id); - let _encrypted = data.encrypt(&mut store.context(), key_a0_id).unwrap(); + let _encrypted: Data = data + .encrypt_composite(&mut store.context(), key_a0_id) + .unwrap(); } #[test] @@ -543,7 +599,7 @@ mod tests { // with one and decrypt with the other let data = DataView("Hello, World!".to_string(), key_2_id); - let encrypted = data.encrypt(&mut ctx, key_2_id).unwrap(); + let encrypted = data.encrypt_composite(&mut ctx, key_2_id).unwrap(); let decrypted1 = encrypted.decrypt(&mut ctx, key_2_id).unwrap(); let decrypted2 = encrypted.decrypt(&mut ctx, new_key_id).unwrap(); @@ -552,6 +608,64 @@ mod tests { assert_eq!(decrypted1.0, decrypted2.0); } + #[test] + fn test_wrap_unwrap() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + + // Aes256 CBC HMAC keys + let key_aes_1_id = TestSymmKey::A(1); + let key_aes_1 = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + ctx.set_symmetric_key(key_aes_1_id, key_aes_1.clone()) + .unwrap(); + let key_aes_2_id = TestSymmKey::A(2); + let key_aes_2 = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + ctx.set_symmetric_key(key_aes_2_id, key_aes_2.clone()) + .unwrap(); + + // XChaCha20 Poly1305 keys + let key_xchacha_3_id = TestSymmKey::A(3); + let key_xchacha_3 = SymmetricCryptoKey::make_xchacha20_poly1305_key(); + ctx.set_symmetric_key(key_xchacha_3_id, key_xchacha_3.clone()) + .unwrap(); + let key_xchacha_4_id = TestSymmKey::A(4); + let key_xchacha_4 = SymmetricCryptoKey::make_xchacha20_poly1305_key(); + ctx.set_symmetric_key(key_xchacha_4_id, key_xchacha_4.clone()) + .unwrap(); + + // Wrap and unwrap the keys + let wrapped_key_1_2 = ctx.wrap_symmetric_key(key_aes_1_id, key_aes_2_id).unwrap(); + let wrapped_key_1_3 = ctx + .wrap_symmetric_key(key_aes_1_id, key_xchacha_3_id) + .unwrap(); + let wrapped_key_3_1 = ctx + .wrap_symmetric_key(key_xchacha_3_id, key_aes_1_id) + .unwrap(); + let wrapped_key_3_4 = ctx + .wrap_symmetric_key(key_xchacha_3_id, key_xchacha_4_id) + .unwrap(); + + // Unwrap the keys + let unwrapped_key_2 = ctx + .unwrap_symmetric_key(key_aes_1_id, key_aes_2_id, &wrapped_key_1_2) + .unwrap(); + let unwrapped_key_3 = ctx + .unwrap_symmetric_key(key_aes_1_id, key_xchacha_3_id, &wrapped_key_1_3) + .unwrap(); + let unwrapped_key_1 = ctx + .unwrap_symmetric_key(key_xchacha_3_id, key_aes_1_id, &wrapped_key_3_1) + .unwrap(); + let unwrapped_key_4 = ctx + .unwrap_symmetric_key(key_xchacha_3_id, key_xchacha_4_id, &wrapped_key_3_4) + .unwrap(); + + // Assert that the unwrapped keys are the same as the original keys + assert_eq!(unwrapped_key_2, key_aes_2_id); + assert_eq!(unwrapped_key_3, key_xchacha_3_id); + assert_eq!(unwrapped_key_1, key_aes_1_id); + assert_eq!(unwrapped_key_4, key_xchacha_4_id); + } + #[test] fn test_signing() { let store: KeyStore = KeyStore::default(); diff --git a/crates/bitwarden-crypto/src/store/mod.rs b/crates/bitwarden-crypto/src/store/mod.rs index ff133a9c4..765762bac 100644 --- a/crates/bitwarden-crypto/src/store/mod.rs +++ b/crates/bitwarden-crypto/src/store/mod.rs @@ -26,7 +26,7 @@ use std::sync::{Arc, RwLock}; use rayon::prelude::*; -use crate::{Decryptable, Encryptable, IdentifyKey, KeyId, KeyIds}; +use crate::{CompositeEncryptable, Decryptable, IdentifyKey, KeyId, KeyIds}; mod backend; mod context; @@ -78,8 +78,8 @@ pub use context::KeyStoreContext; /// SymmKeyId::User /// } /// } -/// impl Encryptable for Data { -/// fn encrypt(&self, ctx: &mut KeyStoreContext, key: SymmKeyId) -> Result { +/// impl CompositeEncryptable for Data { +/// fn encrypt_composite(&self, ctx: &mut KeyStoreContext, key: SymmKeyId) -> Result { /// self.0.encrypt(ctx, key) /// } /// } @@ -136,7 +136,7 @@ impl KeyStore { /// context-local store will be cleared when the context is dropped. /// /// If you are only looking to encrypt or decrypt items, you should implement - /// [Encryptable]/[Decryptable] and use the [KeyStore::encrypt], [KeyStore::decrypt], + /// [CompositeEncryptable]/[Decryptable] and use the [KeyStore::encrypt], [KeyStore::decrypt], /// [KeyStore::encrypt_list] and [KeyStore::decrypt_list] methods instead. /// /// The current implementation of context only clears the keys automatically when the context is @@ -150,11 +150,11 @@ impl KeyStore { /// future to also not be [Send]. /// /// Some other possible use cases for this API and alternative recommendations are: - /// - Decrypting or encrypting multiple [Decryptable] or [Encryptable] items while sharing any - /// local keys. This is not recommended as it can lead to fragile and flaky + /// - Decrypting or encrypting multiple [Decryptable] or [CompositeEncryptable] items while + /// sharing any local keys. This is not recommended as it can lead to fragile and flaky /// decryption/encryption operations. We recommend any local keys to be used only in the - /// context of a single [Encryptable] or [Decryptable] implementation. In the future we might - /// enforce this. + /// context of a single [CompositeEncryptable] or [Decryptable] implementation. In the future + /// we might enforce this. /// - Obtaining the key material directly. We strongly recommend against doing this as it can /// lead to key material being leaked, but we need to support it for backwards compatibility. /// If you want to access the key material to encrypt it or derive a new key from it, we @@ -218,12 +218,16 @@ impl KeyStore { /// already be present in the store, otherwise this will return an error. /// This method is not parallelized, and is meant for single item encryption. /// If you need to encrypt multiple items, use `encrypt_list` instead. - pub fn encrypt + IdentifyKey, Output>( + pub fn encrypt< + Key: KeyId, + Data: CompositeEncryptable + IdentifyKey, + Output, + >( &self, data: Data, ) -> Result { let key = data.key_identifier(); - data.encrypt(&mut self.context(), key) + data.encrypt_composite(&mut self.context(), key) } /// Decrypt a list of items using this key store. The keys returned by @@ -266,7 +270,7 @@ impl KeyStore { /// single item encryption. pub fn encrypt_list< Key: KeyId, - Data: Encryptable + IdentifyKey + Send + Sync, + Data: CompositeEncryptable + IdentifyKey + Send + Sync, Output: Send + Sync, >( &self, @@ -281,7 +285,7 @@ impl KeyStore { for item in chunk { let key = item.key_identifier(); - result.push(item.encrypt(&mut ctx, key)); + result.push(item.encrypt_composite(&mut ctx, key)); ctx.clear_local(); } @@ -315,7 +319,7 @@ pub(crate) mod tests { use crate::{ store::{KeyStore, KeyStoreContext}, traits::tests::{TestIds, TestSymmKey}, - EncString, SymmetricCryptoKey, + EncString, PrimitiveEncryptable, SymmetricCryptoKey, }; pub struct DataView(pub String, pub TestSymmKey); @@ -333,8 +337,8 @@ pub(crate) mod tests { } } - impl crate::Encryptable for DataView { - fn encrypt( + impl crate::CompositeEncryptable for DataView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: TestSymmKey, diff --git a/crates/bitwarden-crypto/src/traits/encryptable.rs b/crates/bitwarden-crypto/src/traits/encryptable.rs index 7f339ca35..654a1fa90 100644 --- a/crates/bitwarden-crypto/src/traits/encryptable.rs +++ b/crates/bitwarden-crypto/src/traits/encryptable.rs @@ -1,83 +1,176 @@ -use crate::{store::KeyStoreContext, CryptoError, EncString, KeyId, KeyIds}; +//! This module defines traits for encrypting data. There are three categories here. +//! +//! Some (legacy) encryptables are made up of many small individually encrypted items. For instance, +//! a cipher is currently made up of many small `EncString`s and some further json objects that +//! themselves contain `EncString`s. The use of this is generally discouraged for new designs. +//! Still, this is generally the only trait that should be implemented outside of the crypto crate. +//! +//! Encrypting data directly, a content type must be provided, since an encrypted byte array alone +//! is not enough to tell the decryption code how to interpret the decrypted bytes. For this, there +//! are two traits, `PrimitiveEncryptable` and `PrimitiveEncryptableWithContentType`. The former +//! assumes that the implementation provides content format when encrypting, based on the type +//! of struct that is being encrypted. The latter allows the caller to specify the content format +//! at runtime, which is only allowed within the crypto crate. +//! +//! `PrimitiveEncryptable` is implemented for `crate::content_format::Bytes` types, where `C` is +//! a type that implements the `ConstContentFormat` trait. This allows for compile-time type +//! checking of the content format, and the risk of using the wrong content format is limited to +//! converting untyped bytes into a `Bytes` -/// An encryption operation that takes the input value and encrypts it into the output value. -/// Implementations should generally consist of calling [Encryptable::encrypt] for all the fields of -/// the type. -pub trait Encryptable { - #[allow(missing_docs)] +use crate::{store::KeyStoreContext, ContentFormat, CryptoError, EncString, KeyId, KeyIds}; + +/// An encryption operation that takes the input value and encrypts the fields on it recursively. +/// Implementations should generally consist of calling [PrimitiveEncryptable::encrypt] for all the +/// fields of the type. Sometimes, it is necessary to call +/// [CompositeEncryptable::encrypt_composite], if the object is not a flat struct. +pub trait CompositeEncryptable { + /// For a struct made up of many small encstrings, such as a cipher, this takes the struct + /// and recursively encrypts all the fields / sub-structs. + fn encrypt_composite( + &self, + ctx: &mut KeyStoreContext, + key: Key, + ) -> Result; +} + +impl, Output> + CompositeEncryptable> for Option +{ + fn encrypt_composite( + &self, + ctx: &mut KeyStoreContext, + key: Key, + ) -> Result, CryptoError> { + self.as_ref() + .map(|value| value.encrypt_composite(ctx, key)) + .transpose() + } +} + +impl, Output> + CompositeEncryptable> for Vec +{ + fn encrypt_composite( + &self, + ctx: &mut KeyStoreContext, + key: Key, + ) -> Result, CryptoError> { + self.iter() + .map(|value| value.encrypt_composite(ctx, key)) + .collect() + } +} + +/// An encryption operation that takes the input value - a primitive such as `String` and encrypts +/// it into the output value. The implementation decides the content format. +pub trait PrimitiveEncryptable { + /// Encrypts a primitive without requiring an externally provided content type fn encrypt(&self, ctx: &mut KeyStoreContext, key: Key) -> Result; } -impl Encryptable for &[u8] { +impl, Output> + PrimitiveEncryptable> for Option +{ + fn encrypt( + &self, + ctx: &mut KeyStoreContext, + key: Key, + ) -> Result, CryptoError> { + self.as_ref() + .map(|value| value.encrypt(ctx, key)) + .transpose() + } +} + +impl PrimitiveEncryptable for &str { fn encrypt( &self, ctx: &mut KeyStoreContext, key: Ids::Symmetric, ) -> Result { - ctx.encrypt_data_with_symmetric_key(key, self) + self.as_bytes().encrypt(ctx, key, ContentFormat::Utf8) } } -impl Encryptable for Vec { +impl PrimitiveEncryptable for String { fn encrypt( &self, ctx: &mut KeyStoreContext, key: Ids::Symmetric, ) -> Result { - ctx.encrypt_data_with_symmetric_key(key, self) + self.as_bytes().encrypt(ctx, key, ContentFormat::Utf8) } } -impl Encryptable for &str { +/// An encryption operation that takes the input value - a primitive such as `Vec` - and +/// encrypts it into the output value. The caller must specify the content format. +pub(crate) trait PrimitiveEncryptableWithContentType { + /// Encrypts a primitive, given an externally provided content type + fn encrypt( + &self, + ctx: &mut KeyStoreContext, + key: Key, + content_format: ContentFormat, + ) -> Result; +} + +impl PrimitiveEncryptableWithContentType for &[u8] { fn encrypt( &self, ctx: &mut KeyStoreContext, key: Ids::Symmetric, + content_format: ContentFormat, ) -> Result { - self.as_bytes().encrypt(ctx, key) + ctx.encrypt_data_with_symmetric_key(key, self, content_format) } } -impl Encryptable for String { +impl PrimitiveEncryptableWithContentType for Vec { fn encrypt( &self, ctx: &mut KeyStoreContext, key: Ids::Symmetric, + content_format: ContentFormat, ) -> Result { - self.as_bytes().encrypt(ctx, key) + ctx.encrypt_data_with_symmetric_key(key, self, content_format) } } -impl, Output> - Encryptable> for Option +impl, Output> + PrimitiveEncryptableWithContentType> for Option { fn encrypt( &self, ctx: &mut KeyStoreContext, key: Key, + content_format: crate::ContentFormat, ) -> Result, CryptoError> { self.as_ref() - .map(|value| value.encrypt(ctx, key)) + .map(|value| value.encrypt(ctx, key, content_format)) .transpose() } } -impl, Output> - Encryptable> for Vec +impl, Output> + PrimitiveEncryptableWithContentType> for Vec { fn encrypt( &self, ctx: &mut KeyStoreContext, key: Key, + content_format: ContentFormat, ) -> Result, CryptoError> { - self.iter().map(|value| value.encrypt(ctx, key)).collect() + self.iter() + .map(|value| value.encrypt(ctx, key, content_format)) + .collect() } } #[cfg(test)] mod tests { use crate::{ - traits::tests::*, AsymmetricCryptoKey, Decryptable, Encryptable, KeyStore, + traits::{encryptable::PrimitiveEncryptableWithContentType, tests::*}, + AsymmetricCryptoKey, ContentFormat, Decryptable, KeyStore, PrimitiveEncryptable, PublicKeyEncryptionAlgorithm, SymmetricCryptoKey, }; @@ -110,8 +203,12 @@ mod tests { let vec_data = vec![1, 2, 3, 4, 5]; let slice_data: &[u8] = &vec_data; - let vec_encrypted = vec_data.encrypt(&mut ctx, key).unwrap(); - let slice_encrypted = slice_data.encrypt(&mut ctx, key).unwrap(); + let vec_encrypted = vec_data + .encrypt(&mut ctx, key, ContentFormat::OctetStream) + .unwrap(); + let slice_encrypted = slice_data + .encrypt(&mut ctx, key, ContentFormat::OctetStream) + .unwrap(); let vec_decrypted: Vec = vec_encrypted.decrypt(&mut ctx, key).unwrap(); let slice_decrypted: Vec = slice_encrypted.decrypt(&mut ctx, key).unwrap(); diff --git a/crates/bitwarden-crypto/src/traits/mod.rs b/crates/bitwarden-crypto/src/traits/mod.rs index 96782d195..4b9bab2b6 100644 --- a/crates/bitwarden-crypto/src/traits/mod.rs +++ b/crates/bitwarden-crypto/src/traits/mod.rs @@ -1,5 +1,6 @@ mod encryptable; -pub use encryptable::Encryptable; +pub(crate) use encryptable::PrimitiveEncryptableWithContentType; +pub use encryptable::{CompositeEncryptable, PrimitiveEncryptable}; mod decryptable; pub use decryptable::Decryptable; diff --git a/crates/bitwarden-exporters/src/export.rs b/crates/bitwarden-exporters/src/export.rs index 234e1b3ca..475916b79 100644 --- a/crates/bitwarden-exporters/src/export.rs +++ b/crates/bitwarden-exporters/src/export.rs @@ -1,5 +1,5 @@ use bitwarden_core::{key_management::KeyIds, Client}; -use bitwarden_crypto::{Encryptable, IdentifyKey, KeyStoreContext}; +use bitwarden_crypto::{CompositeEncryptable, IdentifyKey, KeyStoreContext}; use bitwarden_vault::{Cipher, CipherView, Collection, Folder, FolderView}; use crate::{ @@ -80,7 +80,7 @@ fn encrypt_import( view.set_new_fido2_credentials(ctx, passkeys)?; } - let new_cipher = view.encrypt(ctx, view.key_identifier())?; + let new_cipher = view.encrypt_composite(ctx, view.key_identifier())?; Ok(new_cipher) } diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index 8ecf5deeb..dc3af947d 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -8,8 +8,8 @@ use bitwarden_core::{ require, }; use bitwarden_crypto::{ - generate_random_bytes, CryptoError, Decryptable, EncString, Encryptable, IdentifyKey, - KeyStoreContext, + generate_random_bytes, CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, + KeyStoreContext, OctetStreamBytes, PrimitiveEncryptable, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -193,8 +193,8 @@ impl Decryptable for SendText { } } -impl Encryptable for SendTextView { - fn encrypt( +impl CompositeEncryptable for SendTextView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, @@ -221,8 +221,8 @@ impl Decryptable for SendFile { } } -impl Encryptable for SendFileView { - fn encrypt( +impl CompositeEncryptable for SendFileView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, @@ -301,8 +301,8 @@ impl Decryptable for Send { } } -impl Encryptable for SendView { - fn encrypt( +impl CompositeEncryptable for SendView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, @@ -331,15 +331,15 @@ impl Encryptable for SendView { name: self.name.encrypt(ctx, send_key)?, notes: self.notes.encrypt(ctx, send_key)?, - key: k.encrypt(ctx, key)?, + key: OctetStreamBytes::from(k.clone()).encrypt(ctx, key)?, password: self.new_password.as_ref().map(|password| { let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); STANDARD.encode(password) }), r#type: self.r#type, - file: self.file.encrypt(ctx, send_key)?, - text: self.text.encrypt(ctx, send_key)?, + file: self.file.encrypt_composite(ctx, send_key)?, + text: self.text.encrypt_composite(ctx, send_key)?, max_access_count: self.max_access_count, access_count: self.access_count, diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index 64a45929b..f703f47f4 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -1,7 +1,9 @@ use std::path::Path; use bitwarden_core::Client; -use bitwarden_crypto::{Decryptable, EncString, Encryptable, IdentifyKey}; +use bitwarden_crypto::{ + Decryptable, EncString, IdentifyKey, OctetStreamBytes, PrimitiveEncryptable, +}; use thiserror::Error; use crate::{Send, SendListView, SendView}; @@ -127,7 +129,7 @@ impl SendClient { let key = Send::get_key(&mut ctx, &send.key, send.key_identifier())?; - let encrypted = buffer.encrypt(&mut ctx, key)?; + let encrypted = OctetStreamBytes::from(buffer).encrypt(&mut ctx, key)?; Ok(encrypted.to_buffer()?) } } diff --git a/crates/bitwarden-vault/src/cipher/attachment.rs b/crates/bitwarden-vault/src/cipher/attachment.rs index e37b6ad02..213be72df 100644 --- a/crates/bitwarden-vault/src/cipher/attachment.rs +++ b/crates/bitwarden-vault/src/cipher/attachment.rs @@ -1,6 +1,7 @@ use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; use bitwarden_crypto::{ - CryptoError, Decryptable, EncString, Encryptable, IdentifyKey, KeyStoreContext, + CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext, + OctetStreamBytes, PrimitiveEncryptable, }; use serde::{Deserialize, Serialize}; #[cfg(feature = "wasm")] @@ -78,8 +79,10 @@ impl IdentifyKey for AttachmentFile { } } -impl Encryptable for AttachmentFileView<'_> { - fn encrypt( +impl CompositeEncryptable + for AttachmentFileView<'_> +{ + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, @@ -91,7 +94,8 @@ impl Encryptable for Attachment // Because this is a new attachment, we have to generate a key for it, encrypt the contents // with it, and then encrypt the key with the cipher key let attachment_key = ctx.generate_symmetric_key(ATTACHMENT_KEY)?; - let encrypted_contents = self.contents.encrypt(ctx, attachment_key)?; + let encrypted_contents = + OctetStreamBytes::from(self.contents).encrypt(ctx, attachment_key)?; attachment.key = Some(ctx.wrap_symmetric_key(ciphers_key, attachment_key)?); let contents = encrypted_contents.to_buffer()?; @@ -101,7 +105,7 @@ impl Encryptable for Attachment attachment.size_name = Some(size_name(contents.len())); Ok(AttachmentEncryptResult { - attachment: attachment.encrypt(ctx, ciphers_key)?, + attachment: attachment.encrypt_composite(ctx, ciphers_key)?, contents, }) } @@ -137,8 +141,8 @@ impl Decryptable> for AttachmentFile { } } -impl Encryptable for AttachmentView { - fn encrypt( +impl CompositeEncryptable for AttachmentView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, diff --git a/crates/bitwarden-vault/src/cipher/card.rs b/crates/bitwarden-vault/src/cipher/card.rs index c51a287c7..07c6090b4 100644 --- a/crates/bitwarden-vault/src/cipher/card.rs +++ b/crates/bitwarden-vault/src/cipher/card.rs @@ -1,6 +1,9 @@ use bitwarden_api_api::models::CipherCardModel; use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; -use bitwarden_crypto::{CryptoError, Decryptable, EncString, Encryptable, KeyStoreContext}; +use bitwarden_crypto::{ + CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext, + PrimitiveEncryptable, +}; use serde::{Deserialize, Serialize}; #[cfg(feature = "wasm")] use tsify_next::Tsify; @@ -63,8 +66,8 @@ pub enum CardBrand { Other, } -impl Encryptable for CardView { - fn encrypt( +impl CompositeEncryptable for CardView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 4c9c1bdb1..9d7032557 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -4,7 +4,8 @@ use bitwarden_core::{ require, MissingFieldError, VaultLockedError, }; use bitwarden_crypto::{ - CryptoError, Decryptable, EncString, Encryptable, IdentifyKey, KeyStoreContext, + CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext, + PrimitiveEncryptable, }; use bitwarden_error::bitwarden_error; use chrono::{DateTime, Utc}; @@ -270,8 +271,8 @@ impl CipherListView { } } -impl Encryptable for CipherView { - fn encrypt( +impl CompositeEncryptable for CipherView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, @@ -294,20 +295,26 @@ impl Encryptable for CipherView { name: cipher_view.name.encrypt(ctx, ciphers_key)?, notes: cipher_view.notes.encrypt(ctx, ciphers_key)?, r#type: cipher_view.r#type, - login: cipher_view.login.encrypt(ctx, ciphers_key)?, - identity: cipher_view.identity.encrypt(ctx, ciphers_key)?, - card: cipher_view.card.encrypt(ctx, ciphers_key)?, - secure_note: cipher_view.secure_note.encrypt(ctx, ciphers_key)?, - ssh_key: cipher_view.ssh_key.encrypt(ctx, ciphers_key)?, + login: cipher_view.login.encrypt_composite(ctx, ciphers_key)?, + identity: cipher_view.identity.encrypt_composite(ctx, ciphers_key)?, + card: cipher_view.card.encrypt_composite(ctx, ciphers_key)?, + secure_note: cipher_view + .secure_note + .encrypt_composite(ctx, ciphers_key)?, + ssh_key: cipher_view.ssh_key.encrypt_composite(ctx, ciphers_key)?, favorite: cipher_view.favorite, reprompt: cipher_view.reprompt, organization_use_totp: cipher_view.organization_use_totp, edit: cipher_view.edit, view_password: cipher_view.view_password, - local_data: cipher_view.local_data.encrypt(ctx, ciphers_key)?, - attachments: cipher_view.attachments.encrypt(ctx, ciphers_key)?, - fields: cipher_view.fields.encrypt(ctx, ciphers_key)?, - password_history: cipher_view.password_history.encrypt(ctx, ciphers_key)?, + local_data: cipher_view.local_data.encrypt_composite(ctx, ciphers_key)?, + attachments: cipher_view + .attachments + .encrypt_composite(ctx, ciphers_key)?, + fields: cipher_view.fields.encrypt_composite(ctx, ciphers_key)?, + password_history: cipher_view + .password_history + .encrypt_composite(ctx, ciphers_key)?, creation_date: cipher_view.creation_date, deleted_date: cipher_view.deleted_date, revision_date: cipher_view.revision_date, @@ -465,8 +472,9 @@ impl CipherView { if let Some(attachments) = &mut self.attachments { for attachment in attachments { if let Some(attachment_key) = &mut attachment.key { - let dec_attachment_key: Vec = attachment_key.decrypt(ctx, old_key)?; - *attachment_key = dec_attachment_key.encrypt(ctx, new_key)?; + let tmp_attachment_key_id = SymmetricKeyId::Local("attachment_key"); + ctx.unwrap_symmetric_key(old_key, tmp_attachment_key_id, attachment_key)?; + *attachment_key = ctx.wrap_symmetric_key(new_key, tmp_attachment_key_id)?; } } } @@ -500,7 +508,7 @@ impl CipherView { if let Some(fido2_credentials) = &mut login.fido2_credentials { let dec_fido2_credentials: Vec = fido2_credentials.decrypt(ctx, old_key)?; - *fido2_credentials = dec_fido2_credentials.encrypt(ctx, new_key)?; + *fido2_credentials = dec_fido2_credentials.encrypt_composite(ctx, new_key)?; } } Ok(()) @@ -522,8 +530,9 @@ impl CipherView { // If the cipher has a key, we need to re-encrypt it with the new organization key if let Some(cipher_key) = &mut self.key { - let dec_cipher_key: Vec = cipher_key.decrypt(ctx, old_key)?; - *cipher_key = dec_cipher_key.encrypt(ctx, new_key)?; + let tmp_cipher_key_id = SymmetricKeyId::Local("cipher_key"); + ctx.unwrap_symmetric_key(old_key, tmp_cipher_key_id, cipher_key)?; + *cipher_key = ctx.wrap_symmetric_key(new_key, tmp_cipher_key_id)?; } else { // If the cipher does not have a key, we need to reencrypt all attachment keys self.reencrypt_attachment_keys(ctx, old_key, new_key)?; @@ -544,7 +553,8 @@ impl CipherView { let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; - require!(self.login.as_mut()).fido2_credentials = Some(creds.encrypt(ctx, ciphers_key)?); + require!(self.login.as_mut()).fido2_credentials = + Some(creds.encrypt_composite(ctx, ciphers_key)?); Ok(()) } @@ -944,11 +954,14 @@ mod tests { .unwrap(); // Make sure that the cipher key is decryptable - let _: Vec = original_cipher - .key - .unwrap() - .decrypt(&mut key_store.context(), SymmetricKeyId::User) - .unwrap(); + let wrapped_key = original_cipher.key.unwrap(); + let mut ctx = key_store.context(); + ctx.unwrap_symmetric_key( + SymmetricKeyId::User, + SymmetricKeyId::Local("test_cipher_key"), + &wrapped_key, + ) + .unwrap(); } #[test] @@ -1090,12 +1103,20 @@ mod tests { // Check that the attachment key has been re-encrypted with the org key, // and the value matches with the original attachment key let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap(); - let new_attachment_key_dec: Vec<_> = new_attachment_key - .decrypt(&mut key_store.context(), org_key) + let mut ctx = key_store.context(); + let new_attachment_key_id = ctx + .unwrap_symmetric_key( + org_key, + SymmetricKeyId::Local("test_attachment_key"), + &new_attachment_key, + ) + .unwrap(); + #[allow(deprecated)] + let new_attachment_key_dec = ctx + .dangerous_get_symmetric_key(new_attachment_key_id) .unwrap(); - let new_attachment_key_dec: SymmetricCryptoKey = new_attachment_key_dec.try_into().unwrap(); - assert_eq!(new_attachment_key_dec, attachment_key_val); + assert_eq!(*new_attachment_key_dec, attachment_key_val); let cred2: Fido2CredentialFullView = cipher .login @@ -1152,19 +1173,20 @@ mod tests { cipher.move_to_organization(&mut ctx, org).unwrap(); // Check that the cipher key has been re-encrypted with the org key, - let new_cipher_key_dec: Vec<_> = cipher - .key - .clone() - .unwrap() - .decrypt(&mut ctx, org_key) + let wrapped_new_cipher_key = cipher.key.clone().unwrap(); + let new_cipher_key_dec = ctx + .unwrap_symmetric_key( + org_key, + SymmetricKeyId::Local("test_cipher_key"), + &wrapped_new_cipher_key, + ) .unwrap(); - - let new_cipher_key_dec: SymmetricCryptoKey = new_cipher_key_dec.try_into().unwrap(); - + #[allow(deprecated)] + let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap(); #[allow(deprecated)] let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap(); - assert_eq!(new_cipher_key_dec, *cipher_key_val); + assert_eq!(new_cipher_key_dec, cipher_key_val); // Check that the attachment key hasn't changed assert_eq!( diff --git a/crates/bitwarden-vault/src/cipher/field.rs b/crates/bitwarden-vault/src/cipher/field.rs index fd77c1e16..d6c988415 100644 --- a/crates/bitwarden-vault/src/cipher/field.rs +++ b/crates/bitwarden-vault/src/cipher/field.rs @@ -3,7 +3,10 @@ use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, require, }; -use bitwarden_crypto::{CryptoError, Decryptable, EncString, Encryptable, KeyStoreContext}; +use bitwarden_crypto::{ + CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext, + PrimitiveEncryptable, +}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; #[cfg(feature = "wasm")] @@ -50,8 +53,8 @@ pub struct FieldView { pub linked_id: Option, } -impl Encryptable for FieldView { - fn encrypt( +impl CompositeEncryptable for FieldView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, diff --git a/crates/bitwarden-vault/src/cipher/identity.rs b/crates/bitwarden-vault/src/cipher/identity.rs index 2319e7557..de8461593 100644 --- a/crates/bitwarden-vault/src/cipher/identity.rs +++ b/crates/bitwarden-vault/src/cipher/identity.rs @@ -1,6 +1,9 @@ use bitwarden_api_api::models::CipherIdentityModel; use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; -use bitwarden_crypto::{CryptoError, Decryptable, EncString, Encryptable, KeyStoreContext}; +use bitwarden_crypto::{ + CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext, + PrimitiveEncryptable, +}; use serde::{Deserialize, Serialize}; #[cfg(feature = "wasm")] use tsify_next::Tsify; @@ -59,8 +62,8 @@ pub struct IdentityView { pub license_number: Option, } -impl Encryptable for IdentityView { - fn encrypt( +impl CompositeEncryptable for IdentityView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, diff --git a/crates/bitwarden-vault/src/cipher/local_data.rs b/crates/bitwarden-vault/src/cipher/local_data.rs index 63e822c37..504e0803d 100644 --- a/crates/bitwarden-vault/src/cipher/local_data.rs +++ b/crates/bitwarden-vault/src/cipher/local_data.rs @@ -1,5 +1,5 @@ use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; -use bitwarden_crypto::{CryptoError, Decryptable, Encryptable, KeyStoreContext}; +use bitwarden_crypto::{CompositeEncryptable, CryptoError, Decryptable, KeyStoreContext}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; #[cfg(feature = "wasm")] @@ -23,8 +23,8 @@ pub struct LocalDataView { last_launched: Option>, } -impl Encryptable for LocalDataView { - fn encrypt( +impl CompositeEncryptable for LocalDataView { + fn encrypt_composite( &self, _ctx: &mut KeyStoreContext, _key: SymmetricKeyId, diff --git a/crates/bitwarden-vault/src/cipher/login.rs b/crates/bitwarden-vault/src/cipher/login.rs index fe04ea5c6..96e7ab27a 100644 --- a/crates/bitwarden-vault/src/cipher/login.rs +++ b/crates/bitwarden-vault/src/cipher/login.rs @@ -4,7 +4,10 @@ use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, require, }; -use bitwarden_crypto::{CryptoError, Decryptable, EncString, Encryptable, KeyStoreContext}; +use bitwarden_crypto::{ + CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext, + PrimitiveEncryptable, +}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -197,8 +200,8 @@ impl From for Fido2CredentialNewView { } } -impl Encryptable for Fido2CredentialFullView { - fn encrypt( +impl CompositeEncryptable for Fido2CredentialFullView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, @@ -322,8 +325,8 @@ pub struct LoginListView { pub uris: Option>, } -impl Encryptable for LoginUriView { - fn encrypt( +impl CompositeEncryptable for LoginUriView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, @@ -336,8 +339,8 @@ impl Encryptable for LoginUriView { } } -impl Encryptable for LoginView { - fn encrypt( +impl CompositeEncryptable for LoginView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, @@ -346,7 +349,7 @@ impl Encryptable for LoginView { username: self.username.encrypt(ctx, key)?, password: self.password.encrypt(ctx, key)?, password_revision_date: self.password_revision_date, - uris: self.uris.encrypt(ctx, key)?, + uris: self.uris.encrypt_composite(ctx, key)?, totp: self.totp.encrypt(ctx, key)?, autofill_on_page_load: self.autofill_on_page_load, fido2_credentials: self.fido2_credentials.clone(), @@ -406,8 +409,8 @@ impl Decryptable for Login { } } -impl Encryptable for Fido2CredentialView { - fn encrypt( +impl CompositeEncryptable for Fido2CredentialView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, diff --git a/crates/bitwarden-vault/src/cipher/secure_note.rs b/crates/bitwarden-vault/src/cipher/secure_note.rs index 0abbbe007..79472b3c5 100644 --- a/crates/bitwarden-vault/src/cipher/secure_note.rs +++ b/crates/bitwarden-vault/src/cipher/secure_note.rs @@ -3,7 +3,7 @@ use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, require, }; -use bitwarden_crypto::{CryptoError, Decryptable, Encryptable, KeyStoreContext}; +use bitwarden_crypto::{CompositeEncryptable, CryptoError, Decryptable, KeyStoreContext}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; #[cfg(feature = "wasm")] @@ -42,8 +42,8 @@ pub struct SecureNoteView { pub r#type: SecureNoteType, } -impl Encryptable for SecureNoteView { - fn encrypt( +impl CompositeEncryptable for SecureNoteView { + fn encrypt_composite( &self, _ctx: &mut KeyStoreContext, _key: SymmetricKeyId, diff --git a/crates/bitwarden-vault/src/cipher/ssh_key.rs b/crates/bitwarden-vault/src/cipher/ssh_key.rs index 3e2cd659c..854abb38d 100644 --- a/crates/bitwarden-vault/src/cipher/ssh_key.rs +++ b/crates/bitwarden-vault/src/cipher/ssh_key.rs @@ -1,5 +1,8 @@ use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; -use bitwarden_crypto::{CryptoError, Decryptable, EncString, Encryptable, KeyStoreContext}; +use bitwarden_crypto::{ + CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext, + PrimitiveEncryptable, +}; use serde::{Deserialize, Serialize}; #[cfg(feature = "wasm")] use tsify_next::Tsify; @@ -34,8 +37,8 @@ pub struct SshKeyView { pub fingerprint: String, } -impl Encryptable for SshKeyView { - fn encrypt( +impl CompositeEncryptable for SshKeyView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, diff --git a/crates/bitwarden-vault/src/folder.rs b/crates/bitwarden-vault/src/folder.rs index 18083a5f5..8168b97aa 100644 --- a/crates/bitwarden-vault/src/folder.rs +++ b/crates/bitwarden-vault/src/folder.rs @@ -4,7 +4,8 @@ use bitwarden_core::{ require, }; use bitwarden_crypto::{ - CryptoError, Decryptable, EncString, Encryptable, IdentifyKey, KeyStoreContext, + CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext, + PrimitiveEncryptable, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -47,8 +48,8 @@ impl IdentifyKey for FolderView { } } -impl Encryptable for FolderView { - fn encrypt( +impl CompositeEncryptable for FolderView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, diff --git a/crates/bitwarden-vault/src/password_history.rs b/crates/bitwarden-vault/src/password_history.rs index f0d63577a..14fe469bf 100644 --- a/crates/bitwarden-vault/src/password_history.rs +++ b/crates/bitwarden-vault/src/password_history.rs @@ -1,7 +1,8 @@ use bitwarden_api_api::models::CipherPasswordHistoryModel; use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; use bitwarden_crypto::{ - CryptoError, Decryptable, EncString, Encryptable, IdentifyKey, KeyStoreContext, + CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext, + PrimitiveEncryptable, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -41,8 +42,8 @@ impl IdentifyKey for PasswordHistoryView { } } -impl Encryptable for PasswordHistoryView { - fn encrypt( +impl CompositeEncryptable for PasswordHistoryView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 371fa04d8..13ab4da05 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -2,10 +2,11 @@ use std::str::FromStr; use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; use bitwarden_crypto::{ - AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CoseSerializable, CryptoError, Decryptable, - EncString, Encryptable, Kdf, KeyDecryptable, KeyEncryptable, KeyStore, MasterKey, - SignatureAlgorithm, SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, - VerifyingKey, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, BitwardenLegacyKeyBytes, CoseKeyBytes, + CoseSerializable, CoseSign1Bytes, CryptoError, Decryptable, EncString, Kdf, KeyDecryptable, + KeyEncryptable, KeyStore, MasterKey, OctetStreamBytes, Pkcs8PrivateKeyBytes, + PrimitiveEncryptable, SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, + SymmetricCryptoKey, UnsignedSharedKey, VerifyingKey, }; use wasm_bindgen::prelude::*; @@ -29,6 +30,7 @@ impl PureCrypto { enc_string: String, key: Vec, ) -> Result { + let key = &BitwardenLegacyKeyBytes::from(key); EncString::from_str(&enc_string)?.decrypt_with_key(&SymmetricCryptoKey::try_from(key)?) } @@ -36,6 +38,7 @@ impl PureCrypto { enc_string: String, key: Vec, ) -> Result, CryptoError> { + let key = &BitwardenLegacyKeyBytes::from(key); EncString::from_str(&enc_string)?.decrypt_with_key(&SymmetricCryptoKey::try_from(key)?) } @@ -52,17 +55,21 @@ impl PureCrypto { enc_bytes: Vec, key: Vec, ) -> Result, CryptoError> { + let key = &BitwardenLegacyKeyBytes::from(key); EncString::from_buffer(&enc_bytes)?.decrypt_with_key(&SymmetricCryptoKey::try_from(key)?) } pub fn symmetric_encrypt_string(plain: String, key: Vec) -> Result { + let key = &BitwardenLegacyKeyBytes::from(key); plain .encrypt_with_key(&SymmetricCryptoKey::try_from(key)?) .map(|enc| enc.to_string()) } + /// DEPRECATED: Only used by send keys pub fn symmetric_encrypt_bytes(plain: Vec, key: Vec) -> Result { - plain + let key = &BitwardenLegacyKeyBytes::from(key); + OctetStreamBytes::from(plain) .encrypt_with_key(&SymmetricCryptoKey::try_from(key)?) .map(|enc| enc.to_string()) } @@ -71,7 +78,8 @@ impl PureCrypto { plain: Vec, key: Vec, ) -> Result, CryptoError> { - plain + let key = &BitwardenLegacyKeyBytes::from(key); + OctetStreamBytes::from(plain) .encrypt_with_key(&SymmetricCryptoKey::try_from(key)?)? .to_buffer() } @@ -87,7 +95,7 @@ impl PureCrypto { let result = master_key .decrypt_user_key(encrypted_user_key) .map_err(|_| CryptoError::InvalidKey)?; - Ok(result.to_encoded()) + Ok(result.to_encoded().to_vec()) } pub fn encrypt_user_key_with_master_password( @@ -97,17 +105,22 @@ impl PureCrypto { kdf: Kdf, ) -> Result { let master_key = MasterKey::derive(master_password.as_str(), email.as_str(), &kdf)?; + let user_key = &BitwardenLegacyKeyBytes::from(user_key); let user_key = SymmetricCryptoKey::try_from(user_key)?; let result = master_key.encrypt_user_key(&user_key)?; Ok(result.to_string()) } pub fn make_user_key_aes256_cbc_hmac() -> Vec { - SymmetricCryptoKey::make_aes256_cbc_hmac_key().to_encoded() + SymmetricCryptoKey::make_aes256_cbc_hmac_key() + .to_encoded() + .to_vec() } pub fn make_user_key_xchacha20_poly1305() -> Vec { - SymmetricCryptoKey::make_xchacha20_poly1305_key().to_encoded() + SymmetricCryptoKey::make_xchacha20_poly1305_key() + .to_encoded() + .to_vec() } /// Wraps (encrypts) a symmetric key using a symmetric wrapping key, returning the wrapped key @@ -118,16 +131,14 @@ impl PureCrypto { ) -> Result { let tmp_store: KeyStore = KeyStore::default(); let mut context = tmp_store.context(); + let wrapping_key = + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(wrapping_key))?; #[allow(deprecated)] - context.set_symmetric_key( - SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, - )?; + context.set_symmetric_key(SymmetricKeyId::Local("wrapping_key"), wrapping_key)?; + let key_to_be_wrapped = + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_to_be_wrapped))?; #[allow(deprecated)] - context.set_symmetric_key( - SymmetricKeyId::Local("key_to_wrap"), - SymmetricCryptoKey::try_from(key_to_be_wrapped)?, - )?; + context.set_symmetric_key(SymmetricKeyId::Local("key_to_wrap"), key_to_be_wrapped)?; // Note: The order of arguments is different here, and should probably be refactored Ok(context .wrap_symmetric_key( @@ -145,11 +156,10 @@ impl PureCrypto { ) -> Result, CryptoError> { let tmp_store: KeyStore = KeyStore::default(); let mut context = tmp_store.context(); + let wrapping_key = + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(wrapping_key))?; #[allow(deprecated)] - context.set_symmetric_key( - SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, - )?; + context.set_symmetric_key(SymmetricKeyId::Local("wrapping_key"), wrapping_key)?; // Note: The order of arguments is different here, and should probably be refactored context.unwrap_symmetric_key( SymmetricKeyId::Local("wrapping_key"), @@ -158,7 +168,7 @@ impl PureCrypto { )?; #[allow(deprecated)] let key = context.dangerous_get_symmetric_key(SymmetricKeyId::Local("wrapped_key"))?; - Ok(key.to_encoded()) + Ok(key.to_encoded().to_vec()) } /// Wraps (encrypts) an SPKI DER encoded encapsulation (public) key using a symmetric wrapping @@ -175,10 +185,9 @@ impl PureCrypto { #[allow(deprecated)] context.set_symmetric_key( SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(wrapping_key))?, )?; - // Note: The order of arguments is different here, and should probably be refactored - Ok(encapsulation_key + Ok(SpkiPublicKeyBytes::from(encapsulation_key) .encrypt(&mut context, SymmetricKeyId::Local("wrapping_key"))? .to_string()) } @@ -194,9 +203,8 @@ impl PureCrypto { #[allow(deprecated)] context.set_symmetric_key( SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(wrapping_key))?, )?; - // Note: The order of arguments is different here, and should probably be refactored EncString::from_str(wrapped_key.as_str())? .decrypt(&mut context, SymmetricKeyId::Local("wrapping_key")) } @@ -212,10 +220,9 @@ impl PureCrypto { #[allow(deprecated)] context.set_symmetric_key( SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(wrapping_key))?, )?; - // Note: The order of arguments is different here, and should probably be refactored - Ok(decapsulation_key + Ok(Pkcs8PrivateKeyBytes::from(decapsulation_key) .encrypt(&mut context, SymmetricKeyId::Local("wrapping_key"))? .to_string()) } @@ -231,9 +238,8 @@ impl PureCrypto { #[allow(deprecated)] context.set_symmetric_key( SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(wrapping_key))?, )?; - // Note: The order of arguments is different here, and should probably be refactored EncString::from_str(wrapped_key.as_str())? .decrypt(&mut context, SymmetricKeyId::Local("wrapping_key")) } @@ -247,7 +253,7 @@ impl PureCrypto { ) -> Result { let encapsulation_key = AsymmetricPublicCryptoKey::from_der(encapsulation_key.as_slice())?; Ok(UnsignedSharedKey::encapsulate_key_unsigned( - &SymmetricCryptoKey::try_from(shared_key)?, + &SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(shared_key))?, &encapsulation_key, )? .to_string()) @@ -262,9 +268,10 @@ impl PureCrypto { ) -> Result, CryptoError> { Ok(UnsignedSharedKey::from_str(encapsulated_key.as_str())? .decapsulate_key_unsigned(&AsymmetricCryptoKey::from_der( - decapsulation_key.as_slice(), + &Pkcs8PrivateKeyBytes::from(decapsulation_key), )?)? - .to_encoded()) + .to_encoded() + .to_vec()) } /// Given a wrapped signing key and the symmetric key it is wrapped with, this returns @@ -274,16 +281,16 @@ impl PureCrypto { wrapping_key: Vec, ) -> Result, CryptoError> { let bytes = Self::symmetric_decrypt_bytes(signing_key, wrapping_key)?; - let signing_key = SigningKey::from_cose(&bytes)?; + let signing_key = SigningKey::from_cose(&CoseKeyBytes::from(bytes))?; let verifying_key = signing_key.to_verifying_key(); - Ok(verifying_key.to_cose()) + Ok(verifying_key.to_cose().to_vec()) } /// Returns the algorithm used for the given verifying key. pub fn key_algorithm_for_verifying_key( verifying_key: Vec, ) -> Result { - let verifying_key = VerifyingKey::from_cose(verifying_key.as_slice())?; + let verifying_key = VerifyingKey::from_cose(&CoseKeyBytes::from(verifying_key))?; let algorithm = verifying_key.algorithm(); Ok(algorithm) } @@ -296,11 +303,12 @@ impl PureCrypto { signed_public_key: Vec, verifying_key: Vec, ) -> Result, CryptoError> { - let signed_public_key = SignedPublicKey::try_from(signed_public_key)?; - let verifying_key = VerifyingKey::from_cose(verifying_key.as_slice())?; + let signed_public_key = SignedPublicKey::try_from(CoseSign1Bytes::from(signed_public_key))?; + let verifying_key = VerifyingKey::from_cose(&CoseKeyBytes::from(verifying_key))?; signed_public_key .verify_and_unwrap(&verifying_key) .map(|public_key| public_key.to_der())? + .map(|pk| pk.to_vec()) } } @@ -526,7 +534,12 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= #[test] fn test_wrap_encapsulation_key() { let decapsulation_key = AsymmetricCryptoKey::from_pem(PEM_KEY).unwrap(); - let encapsulation_key = decapsulation_key.to_public_key().to_der().unwrap(); + let encapsulation_key = decapsulation_key + .to_public_key() + .to_der() + .unwrap() + .as_ref() + .to_vec(); let wrapping_key = PureCrypto::make_user_key_aes256_cbc_hmac(); let wrapped_key = PureCrypto::wrap_encapsulation_key(encapsulation_key.clone(), wrapping_key.clone()) @@ -541,13 +554,13 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= let decapsulation_key = AsymmetricCryptoKey::from_pem(PEM_KEY).unwrap(); let wrapping_key = PureCrypto::make_user_key_aes256_cbc_hmac(); let wrapped_key = PureCrypto::wrap_decapsulation_key( - decapsulation_key.to_der().unwrap(), + decapsulation_key.to_der().unwrap().to_vec(), wrapping_key.clone(), ) .unwrap(); let unwrapped_key = PureCrypto::unwrap_decapsulation_key(wrapped_key, wrapping_key).unwrap(); - assert_eq!(decapsulation_key.to_der().unwrap(), unwrapped_key); + assert_eq!(decapsulation_key.to_der().unwrap().to_vec(), unwrapped_key); } #[test] @@ -555,12 +568,14 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= let shared_key = PureCrypto::make_user_key_aes256_cbc_hmac(); let decapsulation_key = AsymmetricCryptoKey::from_pem(PEM_KEY).unwrap(); let encapsulation_key = decapsulation_key.to_public_key().to_der().unwrap(); - let encapsulated_key = - PureCrypto::encapsulate_key_unsigned(shared_key.clone(), encapsulation_key.clone()) - .unwrap(); + let encapsulated_key = PureCrypto::encapsulate_key_unsigned( + shared_key.clone(), + encapsulation_key.clone().to_vec(), + ) + .unwrap(); let unwrapped_key = PureCrypto::decapsulate_key_unsigned( encapsulated_key, - decapsulation_key.to_der().unwrap(), + decapsulation_key.to_der().unwrap().to_vec(), ) .unwrap(); assert_eq!(shared_key, unwrapped_key); @@ -568,9 +583,10 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= #[test] fn test_key_algorithm_for_verifying_key() { - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let verifying_key = + VerifyingKey::from_cose(&CoseKeyBytes::from(VERIFYING_KEY.to_vec())).unwrap(); let algorithm = - PureCrypto::key_algorithm_for_verifying_key(verifying_key.to_cose()).unwrap(); + PureCrypto::key_algorithm_for_verifying_key(verifying_key.to_cose().to_vec()).unwrap(); assert_eq!(algorithm, SignatureAlgorithm::Ed25519); } @@ -581,14 +597,15 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= SIGNING_KEY_WRAPPING_KEY.to_vec(), ) .unwrap(); - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let verifying_key = + VerifyingKey::from_cose(&CoseKeyBytes::from(VERIFYING_KEY.to_vec())).unwrap(); let verifying_key_derived = PureCrypto::verifying_key_for_signing_key( wrapped_signing_key.to_string(), SIGNING_KEY_WRAPPING_KEY.to_vec(), ) .unwrap(); let verifying_key_derived = - VerifyingKey::from_cose(verifying_key_derived.as_slice()).unwrap(); + VerifyingKey::from_cose(&CoseKeyBytes::from(verifying_key_derived)).unwrap(); assert_eq!(verifying_key.to_cose(), verifying_key_derived.to_cose()); }