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..03a2a9965 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,13 @@ 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::{ + BitwardenLegacyKeyContentFormat, Bytes, Kdf, MasterKey, SpkiPublicKeyDerContentFormat, + }; use super::*; use crate::key_management::{ @@ -143,6 +120,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 = Bytes::::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); @@ -160,14 +164,15 @@ mod tests { let private_key ="2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap(); client .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key, None) + .initialize_user_crypto_master_key(master_key, user_key, private_key, None, None) .unwrap(); 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 = Bytes::::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 +186,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 +205,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, @@ -227,7 +232,13 @@ mod tests { existing_device .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key.clone(), None) + .initialize_user_crypto_master_key( + master_key, + user_key, + private_key.clone(), + None, + None, + ) .unwrap(); // Initialize a new device which will request to be logged in @@ -246,6 +257,7 @@ mod tests { email: email.to_owned(), private_key, signing_key: None, + security_state: None, method: InitUserCryptoMethod::AuthRequest { request_private_key: auth_req.private_key, method: AuthRequestMethod::UserKey { diff --git a/crates/bitwarden-core/src/auth/login/access_token.rs b/crates/bitwarden-core/src/auth/login/access_token.rs index 1563bc10b..95a89463b 100644 --- a/crates/bitwarden-core/src/auth/login/access_token.rs +++ b/crates/bitwarden-core/src/auth/login/access_token.rs @@ -1,7 +1,9 @@ use std::path::{Path, PathBuf}; use base64::{engine::general_purpose::STANDARD, Engine}; -use bitwarden_crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}; +use bitwarden_crypto::{ + BitwardenLegacyKeyContentFormat, Bytes, EncString, KeyDecryptable, SymmetricCryptoKey, +}; use chrono::Utc; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -67,7 +69,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 = Bytes::::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/auth/login/api_key.rs b/crates/bitwarden-core/src/auth/login/api_key.rs index 782a6df34..c6d4bae73 100644 --- a/crates/bitwarden-core/src/auth/login/api_key.rs +++ b/crates/bitwarden-core/src/auth/login/api_key.rs @@ -56,6 +56,7 @@ pub(crate) async fn login_api_key( user_key, private_key, None, + None, )?; } diff --git a/crates/bitwarden-core/src/auth/login/auth_request.rs b/crates/bitwarden-core/src/auth/login/auth_request.rs index 363086fb1..52d20b6a7 100644 --- a/crates/bitwarden-core/src/auth/login/auth_request.rs +++ b/crates/bitwarden-core/src/auth/login/auth_request.rs @@ -121,6 +121,7 @@ pub(crate) async fn complete_auth_request( email: auth_req.email, private_key: require!(r.private_key).parse()?, signing_key: None, + security_state: None, method: InitUserCryptoMethod::AuthRequest { request_private_key: auth_req.private_key, method, diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index 76656d91b..450564c2e 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -55,6 +55,7 @@ pub(crate) async fn login_password( user_key, private_key, None, + None, )?; } diff --git a/crates/bitwarden-core/src/auth/password/validate.rs b/crates/bitwarden-core/src/auth/password/validate.rs index 229c7fde7..c10906fa4 100644 --- a/crates/bitwarden-core/src/auth/password/validate.rs +++ b/crates/bitwarden-core/src/auth/password/validate.rs @@ -145,6 +145,7 @@ mod tests { user_key.parse().unwrap(), private_key, None, + None, ) .unwrap(); @@ -193,6 +194,7 @@ mod tests { user_key.parse().unwrap(), private_key, None, + None, ) .unwrap(); diff --git a/crates/bitwarden-core/src/auth/pin.rs b/crates/bitwarden-core/src/auth/pin.rs index c337f9327..91ae64c9b 100644 --- a/crates/bitwarden-core/src/auth/pin.rs +++ b/crates/bitwarden-core/src/auth/pin.rs @@ -80,6 +80,7 @@ mod tests { user_key.parse().unwrap(), private_key, None, + None, ) .unwrap(); diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index 570a28fd2..9dc712871 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -43,6 +43,7 @@ pub(super) fn make_register_tde_keys( // Note: Signing keys are not supported on registration yet. This needs to be changed as // soon as registration is supported. None, + None, )?; Ok(RegisterTdeKeyResponse { diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index ab8d84cd4..841d2bf51 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,3 +1,4 @@ +use bitwarden_crypto::{security_state::SignedSecurityState, CryptoError}; #[cfg(feature = "internal")] use bitwarden_crypto::{EncString, UnsignedSharedKey}; #[cfg(any(feature = "internal", feature = "secrets"))] @@ -27,6 +28,12 @@ pub enum EncryptionSettingsError { #[error("Invalid private key")] InvalidPrivateKey, + #[error("Invalid signing key")] + InvalidSigningKey, + + #[error("Invalid security state")] + InvalidSecurityState, + #[error(transparent)] MissingPrivateKey(#[from] MissingPrivateKeyError), @@ -47,50 +54,89 @@ impl EncryptionSettings { user_key: SymmetricCryptoKey, private_key: EncString, signing_key: Option, + security_state: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { - use bitwarden_crypto::{ - AsymmetricCryptoKey, CoseSerializable, CryptoError, KeyDecryptable, SigningKey, - }; + use bitwarden_crypto::{AsymmetricCryptoKey, KeyDecryptable}; 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) - .map_err(|_| { - warn!("Invalid private key"); - }) - .ok() - - // Some( - // AsymmetricCryptoKey::from_der(&dec) - // .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?, - // ) - }; - let signing_key = signing_key - .map(|key| { - let dec: Vec = key.decrypt_with_key(&user_key)?; - SigningKey::from_cose(dec.as_slice()).map_err(Into::::into) - }) - .transpose()?; - - // FIXME: [PM-18098] When this is part of crypto we won't need to use deprecated methods - #[allow(deprecated)] - { - let mut ctx = store.context_mut(); - ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; - if let Some(private_key) = private_key { + use crate::key_management::{AsymmetricKeyId, SymmetricKeyId}; + + // This is an all-or-nothing check. The server cannot pretend a signing key or security + // state to be missing, because they are *always* present when the user key is an + // XChaCha20Poly1305Key. Thus, the server or network cannot lie about the presence of these, + // because otherwise the entire user account will fail to decrypt. + let is_v2_user = matches!(user_key, SymmetricCryptoKey::XChaCha20Poly1305Key(_)); + if is_v2_user { + // For v2 users, we mandate the signing key and security state to be present + // The private key must also be valid. + + use bitwarden_crypto::{ + security_state::SecurityState, Bytes, CoseSerializable, SigningKey, + }; + + // Both of these are required for v2 users + let signing_key = signing_key.ok_or(EncryptionSettingsError::Crypto( + CryptoError::SecurityDowngrade("Signing key is required for v2 users".to_string()), + ))?; + let security_state = security_state.ok_or(EncryptionSettingsError::Crypto( + CryptoError::SecurityDowngrade( + "Security state is required for v2 users".to_string(), + ), + ))?; + + // Everything MUST decrypt. + let signing_key: Vec = signing_key.decrypt_with_key(&user_key)?; + let signing_key = Bytes::from(signing_key); + let signing_key = SigningKey::from_cose(&signing_key) + .map_err(|_| EncryptionSettingsError::InvalidSigningKey)?; + let private_key: Vec = private_key.decrypt_with_key(&user_key)?; + let private_key = Bytes::from(private_key); + let private_key = AsymmetricCryptoKey::from_der(&private_key) + .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?; + let _security_state: SecurityState = security_state + .verify_and_unwrap(&signing_key.to_verifying_key()) + .map_err(|_| EncryptionSettingsError::InvalidSecurityState)?; + + #[allow(deprecated)] + { + use crate::key_management::SigningKeyId; + + let mut ctx = store.context_mut(); + ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; - } - - if let Some(signing_key) = signing_key { ctx.set_signing_key(SigningKeyId::UserSigningKey, signing_key)?; } + } else { + let private_key = { + use bitwarden_crypto::Bytes; + + let dec: Vec = private_key.decrypt_with_key(&user_key)?; + let dec = Bytes::from(dec); + + // FIXME: [PM-11690] - Temporarily ignore invalid private keys until we have a + // recovery process in place. + AsymmetricCryptoKey::from_der(&dec) + .map_err(|_| { + warn!("Invalid private key"); + }) + .ok() + + // Some( + // AsymmetricCryptoKey::from_der(&dec) + // .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?, + // ) + }; + + // FIXME: [PM-18098] When this is part of crypto we won't need to use deprecated methods + #[allow(deprecated)] + { + let mut ctx = store.context_mut(); + ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; + if let Some(private_key) = private_key { + ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; + } + } } Ok(()) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 8749a4d32..fd6007ad4 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -4,7 +4,9 @@ use bitwarden_crypto::KeyStore; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::SymmetricCryptoKey; #[cfg(feature = "internal")] -use bitwarden_crypto::{EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey}; +use bitwarden_crypto::{ + security_state::SignedSecurityState, EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey, +}; use chrono::Utc; use uuid::Uuid; @@ -199,9 +201,16 @@ impl InternalClient { user_key: EncString, private_key: EncString, signing_key: Option, + security_state: Option, ) -> Result<(), EncryptionSettingsError> { let user_key = master_key.decrypt_user_key(user_key)?; - EncryptionSettings::new_decrypted_key(user_key, private_key, signing_key, &self.key_store)?; + EncryptionSettings::new_decrypted_key( + user_key, + private_key, + signing_key, + security_state, + &self.key_store, + )?; Ok(()) } @@ -212,8 +221,15 @@ impl InternalClient { user_key: SymmetricCryptoKey, private_key: EncString, signing_key: Option, + security_state: Option, ) -> Result<(), EncryptionSettingsError> { - EncryptionSettings::new_decrypted_key(user_key, private_key, signing_key, &self.key_store)?; + EncryptionSettings::new_decrypted_key( + user_key, + private_key, + signing_key, + security_state, + &self.key_store, + )?; Ok(()) } @@ -225,9 +241,15 @@ impl InternalClient { pin_protected_user_key: EncString, private_key: EncString, signing_key: Option, + security_state: Option, ) -> Result<(), EncryptionSettingsError> { let decrypted_user_key = pin_key.decrypt_user_key(pin_protected_user_key)?; - self.initialize_user_crypto_decrypted_key(decrypted_user_key, private_key, signing_key) + self.initialize_user_crypto_decrypted_key( + decrypted_user_key, + private_key, + signing_key, + security_state, + ) } #[cfg(feature = "secrets")] diff --git a/crates/bitwarden-core/src/client/test_accounts.rs b/crates/bitwarden-core/src/client/test_accounts.rs index 48a5cdba9..07e96cecd 100644 --- a/crates/bitwarden-core/src/client/test_accounts.rs +++ b/crates/bitwarden-core/src/client/test_accounts.rs @@ -126,6 +126,7 @@ pub fn test_bitwarden_com_account() -> TestAccount { private_key: "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse::().unwrap().to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".to_owned(), @@ -185,6 +186,7 @@ pub fn test_legacy_user_key_account() -> TestAccount { email: "legacy@bitwarden.com".to_owned(), private_key: "2.leBIE5u0aQUeXi++JzAnrA==|P8x+hs00RJx7epw+49qVtBhLJxE/JTL5dEHg6kq5pbZLdUY8ZvWK49v0EqgHbv1r298N9+msoO9hmdSIVIAZyycemYDSoc1rX4S1KpS/ZMA/Vd3VLFb+o13Ts62GFQ5ygHKgQZfzjU6jO5P/B/0igzFoxyJDomhW5NBC1P9+e/5qNRZN8loKvAaWc/7XtpRayPQqWx+AgYc2ntb1GF5hRVrW4M47bG5ZKllbJWtQKg2sXIy2lDBbKLRFWF4RFzNVcXQGMoPdWLY0f3uTwUH01dyGmFFMbOvfBEuYqmZyPdd93ve8zuFOEqkj46Ulpq2CVG8NvZARTwsdKl6XB0wGuHFoTsDJT2SJGl67pBBKsVRGxy059QW+9hAIB+emIV0T/7+0rvdeSXZ4AbG+oXGEXFTkHefwJKfeT0MBTAjYKr7ZRLgqvf7n39+nCEJU4l22kp8FmjcWIU7AgNipdGHC+UT2yfOcYlvgBgWDcMXcbVDMyus9105RgcW6PHozUj7yjbohI/A3XWmAFufP6BSnmEFCKoik78X/ry09xwiH2rN4KVXe/k9LpRNB2QBGIVsfgCrkxjeE8r0nA59Rvwrhny1z5BkvMW/N1KrGuafg/IYgegx72gJNuZPZlFu1Vs7HxySHmzYvm3DPV7bzCaAxxNtvZmQquNIEnsDQfjJO76iL1JCtDqNJVzGLHTMTr7S5hkOcydcH3kfKwZdA1ULVd2qu0SwOUEP/ECjU/cS5INy6WPYzNMAe/g2DISpQjNwBb5K17PIiGOR7/Q/A6E8pVnkHiAXuUFr9aLOYN9BWSu5Z+BPHH65na2FDmssix5WV09I2sUBfvdNCjkrUGdYgo8E+vOTn35x9GJHF45uhmgC1yAn/+/RSpORlrSVJ7NNP11dn3htUpSsIy/b7ituAu8Ry5mhicFU8CXJL4NeMlXThUt8P++wxs4wMkBvJ8J9NJAVKbAOA2o+GOdjbh6Ww3IRegkurWh4oL/dFSx0LpaXJuw6HFT/LzticPlSwHtUP11hZ81seMsXmkSZd8IugRFfwpPl7N6PVRWDOKxLf4gPqcnJ11TvfasXy1uolV2vZCPbrbbVzQMPdVwL/OzwfhqsIgQZI8rsDMK5D2EX8MaT8MDfGcsYcVTL9PmuZYLpOUnnHX0A1opAAa9iPw3d+eWB/GAyLvKPnMTUqVNos8HcCktXckCshihA8QuBJOwg3m0j2LPSZ5Jvf8gbXauBmt9I4IlJq0xfpgquYY1WNnO8IcWE4N9W+ASvOr9gnduA6CkDeAlyMUFmdpkeCjGMcsV741bTCPApSQlL3/TOT1cjK3iejWpz0OaVHXyg02hW2fNkOfYfr81GvnLvlHxIg4Prw89gKuWU+kQk82lFQo6QQpqbCbJC2FleurD8tYoSY0srhuioVInffvTxw2NMF7FQEqUcsK9AMKSEiDqzBi35Um/fiE3JL4XZBFw8Xzl7X3ab5nlg8X+xD5uSZY+oxD3sDVXjLaQ5JUoys+MCm0FkUj85l0zT6rvM4QLhU1RDK1U51T9HJhh8hsFJsqL4abRzwEWG7PSi859zN4UsgyuQfmBJv/n7QAFCbrJhVBlGB1TKLZRzvgmKoxTYTG3cJFkjetLcUTwrwC9naxAQRfF4=|ufHf73IzJ707dx44w4fjkuD7tDa50OwmmkxcypAT9uQ=".parse::().unwrap().to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".to_owned(), user_key: "0.8UClLa8IPE1iZT7chy5wzQ==|6PVfHnVk5S3XqEtQemnM5yb4JodxmPkkWzmDRdfyHtjORmvxqlLX40tBJZ+CKxQWmS8tpEB5w39rbgHg/gqs0haGdZG4cPbywsgGzxZ7uNI=".parse().unwrap(), diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 3a1cd66a4..556403e50 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -8,8 +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, + security_state::SignedSecurityState, AsymmetricCryptoKey, Bytes, CoseSerializable, CryptoError, + EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, Pkcs8PrivateKeyDerContentFormat, + PrimitiveEncryptable, RotateUserKeysResponse, SignatureAlgorithm, SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; use bitwarden_error::bitwarden_error; @@ -53,6 +54,8 @@ pub struct InitUserCryptoRequest { pub private_key: EncString, /// The user's signing key pub signing_key: Option, + /// The user's security state + pub security_state: Option, /// The initialization method to use pub method: InitUserCryptoMethod, } @@ -149,6 +152,7 @@ pub(super) async fn initialize_user_crypto( user_key, req.private_key, req.signing_key, + req.security_state, )?; } InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => { @@ -157,6 +161,7 @@ pub(super) async fn initialize_user_crypto( user_key, req.private_key, req.signing_key, + req.security_state, )?; } InitUserCryptoMethod::Pin { @@ -169,6 +174,7 @@ pub(super) async fn initialize_user_crypto( pin_protected_user_key, req.private_key, req.signing_key, + req.security_state, )?; } InitUserCryptoMethod::AuthRequest { @@ -192,6 +198,7 @@ pub(super) async fn initialize_user_crypto( user_key, req.private_key, req.signing_key, + req.security_state, )?; } InitUserCryptoMethod::DeviceKey { @@ -207,6 +214,7 @@ pub(super) async fn initialize_user_crypto( user_key, req.private_key, req.signing_key, + req.security_state, )?; } InitUserCryptoMethod::KeyConnector { @@ -223,6 +231,7 @@ pub(super) async fn initialize_user_crypto( user_key, req.private_key, req.signing_key, + req.security_state, )?; } } @@ -536,6 +545,8 @@ pub(super) fn verify_asymmetric_keys( .decrypt_with_key(user_key) .map_err(VerifyError::DecryptFailed)?; + let decrypted_private_key: Bytes = + Bytes::from(decrypted_private_key); let private_key = AsymmetricCryptoKey::from_der(&decrypted_private_key) .map_err(VerifyError::ParseFailed)?; @@ -570,50 +581,125 @@ pub(super) fn verify_asymmetric_keys( }) } -/// A new signing key pair along with the signed public key +/// Response for the `make_keys_for_user_crypto_v2`, containing a set of keys for a user #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] -pub struct MakeUserSigningKeysResponse { - /// Base64 encoded verifying key - verifying_key: String, - /// Signing key, encrypted with the user's symmetric key - signing_key: EncString, +pub struct EnrollUserCryptoV2Response { + /// User key + user_key: String, + + /// Wrapped private key + private_key: EncString, + /// Public key + public_key: String, /// The user's public key, signed by the signing key signed_public_key: SignedPublicKey, + + /// Signing key, encrypted with the user's symmetric key + signing_key: EncString, + /// Base64 encoded verifying key + verifying_key: String, + + /// The user's signed security state + security_state: SignedSecurityState, + /// The security state's version + security_version: u64, } -/// Makes a new set of signing keys for a user, which should only be done during -/// once. This also signs the public key with the signing key -/// and returns the signed public key. -pub fn make_user_signing_keys_for_enrollment( +/// Initializes the user's cryptographic state for v2 users. +/// If the client already contains a v1 user, then this user's private-key will be +/// re-used. +pub fn make_keys_for_user_crypto_v2( client: &Client, -) -> Result { +) -> Result { let key_store = client.internal.get_key_store(); let mut ctx = key_store.context(); - // Make new keypair and sign the public key with it - let signature_keypair = SigningKey::make(SignatureAlgorithm::Ed25519); - let temporary_signature_keypair_id = SigningKeyId::Local("temporary_key_for_rotation"); + let temporary_user_key_id = SymmetricKeyId::Local("temporary_user_key"); + let temporary_signing_key_id = SigningKeyId::Local("temporary_signing_key"); + let temporary_private_key_id = AsymmetricKeyId::Local("temporary_private_key"); + + // If the user already has a private key, use it. Otherwise, create a temporary one. + let private_key_id = if ctx.has_asymmetric_key(AsymmetricKeyId::UserPrivateKey) { + AsymmetricKeyId::UserPrivateKey + } else { + ctx.make_asymmetric_key(temporary_private_key_id)? + }; + + // New user key + let user_key = SymmetricCryptoKey::make_xchacha20_poly1305_key(); #[allow(deprecated)] - ctx.set_signing_key(temporary_signature_keypair_id, signature_keypair.clone())?; - let signed_public_key = ctx.make_signed_public_key( - AsymmetricKeyId::UserPrivateKey, - temporary_signature_keypair_id, + ctx.set_symmetric_key(temporary_user_key_id, user_key.clone())?; + + // New signing key + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + #[allow(deprecated)] + ctx.set_signing_key(temporary_signing_key_id, signing_key.clone())?; + + // Sign existing public key + let signed_public_key = ctx.make_signed_public_key(private_key_id, temporary_signing_key_id)?; + #[allow(deprecated)] + let public_key = ctx + .dangerous_get_asymmetric_key(private_key_id)? + .to_public_key(); + + // Initialize security state for the user + let security_state = bitwarden_crypto::security_state::SecurityState::initialize_for_user( + client + .internal + .get_user_id() + .ok_or(CryptoError::UninitializedError)?, + ); + let signed_security_state = bitwarden_crypto::security_state::sign( + &security_state, + temporary_signing_key_id, + &mut ctx, )?; - Ok(MakeUserSigningKeysResponse { - verifying_key: STANDARD.encode(signature_keypair.to_verifying_key().to_cose()), + Ok(EnrollUserCryptoV2Response { + user_key: user_key.to_base64(), + + #[allow(deprecated)] + private_key: ctx + .dangerous_get_asymmetric_key(private_key_id)? + .to_der()? + .encrypt(&mut ctx, temporary_user_key_id)?, + public_key: STANDARD.encode(public_key.to_der()?), + signed_public_key, + // 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 + signing_key: signing_key .to_cose() - .encrypt(&mut ctx, SymmetricKeyId::User)?, - signed_public_key, + .encrypt(&mut ctx, temporary_user_key_id)?, + verifying_key: STANDARD.encode(signing_key.to_verifying_key().to_cose()), + + security_state: signed_security_state, + security_version: security_state.version(), }) } +/// Gets a set of new wrapped account keys for a user, given a new user key. +/// +/// In the current implementation, it just re-encrypts any existing keys. This function expects a +/// user to be a v2 user; that is, they have a signing +pub fn get_v2_rotated_account_keys( + client: &Client, + user_key: String, +) -> Result { + let key_store = client.internal.get_key_store(); + let ctx = key_store.context(); + + bitwarden_crypto::get_v2_rotated_account_keys( + SymmetricCryptoKey::try_from(user_key)?, + AsymmetricKeyId::UserPrivateKey, + SigningKeyId::UserSigningKey, + &ctx, + ) +} + #[cfg(test)] mod tests { use std::num::NonZeroU32; @@ -641,6 +727,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".into(), user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".parse().unwrap(), @@ -662,6 +749,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: "123412341234".into(), user_key: new_password_response.new_key, @@ -721,6 +809,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".into(), user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".parse().unwrap(), @@ -744,6 +833,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Pin { pin: "1234".into(), pin_protected_user_key: pin_key.pin_protected_user_key, @@ -788,6 +878,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Pin { pin: "1234".into(), pin_protected_user_key, @@ -818,6 +909,68 @@ mod tests { assert_eq!(client_key, client3_key); } + #[tokio::test] + async fn test_user_crypto_v2() { + let client = Client::new(None); + + let priv_key: EncString = "2.kmLY8NJVuiKBFJtNd/ZFpA==|qOodlRXER+9ogCe3yOibRHmUcSNvjSKhdDuztLlucs10jLiNoVVVAc+9KfNErLSpx5wmUF1hBOJM8zwVPjgQTrmnNf/wuDpwiaCxNYb/0v4FygPy7ccAHK94xP1lfqq7U9+tv+/yiZSwgcT+xF0wFpoxQeNdNRFzPTuD9o4134n8bzacD9DV/WjcrXfRjbBCzzuUGj1e78+A7BWN7/5IWLz87KWk8G7O/W4+8PtEzlwkru6Wd1xO19GYU18oArCWCNoegSmcGn7w7NDEXlwD403oY8Oa7ylnbqGE28PVJx+HLPNIdSC6YKXeIOMnVs7Mctd/wXC93zGxAWD6ooTCzHSPVV50zKJmWIG2cVVUS7j35H3rGDtUHLI+ASXMEux9REZB8CdVOZMzp2wYeiOpggebJy6MKOZqPT1R3X0fqF2dHtRFPXrNsVr1Qt6bS9qTyO4ag1/BCvXF3P1uJEsI812BFAne3cYHy5bIOxuozPfipJrTb5WH35bxhElqwT3y/o/6JWOGg3HLDun31YmiZ2HScAsUAcEkA4hhoTNnqy4O2s3yVbCcR7jF7NLsbQc0MDTbnjxTdI4VnqUIn8s2c9hIJy/j80pmO9Bjxp+LQ9a2hUkfHgFhgHxZUVaeGVth8zG2kkgGdrp5VHhxMVFfvB26Ka6q6qE/UcS2lONSv+4T8niVRJz57qwctj8MNOkA3PTEfe/DP/LKMefke31YfT0xogHsLhDkx+mS8FCc01HReTjKLktk/Jh9mXwC5oKwueWWwlxI935ecn+3I2kAuOfMsgPLkoEBlwgiREC1pM7VVX1x8WmzIQVQTHd4iwnX96QewYckGRfNYWz/zwvWnjWlfcg8kRSe+68EHOGeRtC5r27fWLqRc0HNcjwpgHkI/b6czerCe8+07TWql4keJxJxhBYj3iOH7r9ZS8ck51XnOb8tGL1isimAJXodYGzakwktqHAD7MZhS+P02O+6jrg7d+yPC2ZCuS/3TOplYOCHQIhnZtR87PXTUwr83zfOwAwCyv6KP84JUQ45+DItrXLap7nOVZKQ5QxYIlbThAO6eima6Zu5XHfqGPMNWv0bLf5+vAjIa5np5DJrSwz9no/hj6CUh0iyI+SJq4RGI60lKtypMvF6MR3nHLEHOycRUQbZIyTHWl4QQLdHzuwN9lv10ouTEvNr6sFflAX2yb6w3hlCo7oBytH3rJekjb3IIOzBpeTPIejxzVlh0N9OT5MZdh4sNKYHUoWJ8mnfjdM+L4j5Q2Kgk/XiGDgEebkUxiEOQUdVpePF5uSCE+TPav/9FIRGXGiFn6NJMaU7aBsDTFBLloffFLYDpd8/bTwoSvifkj7buwLYM+h/qcnfdy5FWau1cKav+Blq/ZC0qBpo658RTC8ZtseAFDgXoQZuksM10hpP9bzD04Bx30xTGX81QbaSTNwSEEVrOtIhbDrj9OI43KH4O6zLzK+t30QxAv5zjk10RZ4+5SAdYndIlld9Y62opCfPDzRy3ubdve4ZEchpIKWTQvIxq3T5ogOhGaWBVYnkMtM2GVqvWV//46gET5SH/MdcwhACUcZ9kCpMnWH9CyyUwYvTT3UlNyV+DlS27LMPvaw7tx7qa+GfNCoCBd8S4esZpQYK/WReiS8=|pc7qpD42wxyXemdNPuwxbh8iIaryrBPu8f/DGwYdHTw=".parse().unwrap(); + let encrypted_userkey: EncString = "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".parse().unwrap(); + + initialize_user_crypto( + &client, + InitUserCryptoRequest { + user_id: Some(uuid::Uuid::new_v4()), + kdf_params: Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }, + email: "test@bitwarden.com".into(), + private_key: priv_key, + signing_key: None, + security_state: None, + method: InitUserCryptoMethod::Password { + password: "asdfasdfasdf".into(), + user_key: encrypted_userkey.clone(), + }, + }, + ) + .await + .unwrap(); + + let master_key = MasterKey::derive( + "asdfasdfasdf", + "test@bitwarden.com", + &Kdf::PBKDF2 { + iterations: NonZeroU32::new(100_000).unwrap(), + }, + ) + .unwrap(); + let enrollment_response = make_keys_for_user_crypto_v2(&client).unwrap(); + let encrypted_userkey_v2 = master_key + .encrypt_user_key(&SymmetricCryptoKey::try_from(enrollment_response.user_key).unwrap()) + .unwrap(); + + let client2 = Client::new(None); + initialize_user_crypto( + &client2, + InitUserCryptoRequest { + user_id: Some(uuid::Uuid::new_v4()), + kdf_params: Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }, + email: "test@bitwarden.com".into(), + private_key: enrollment_response.private_key, + signing_key: Some(enrollment_response.signing_key), + security_state: Some(enrollment_response.security_state), + method: InitUserCryptoMethod::Password { + password: "asdfasdfasdf".into(), + user_key: encrypted_userkey_v2, + }, + }, + ) + .await + .unwrap(); + } + #[test] fn test_enroll_admin_password_reset() { use base64::{engine::general_purpose::STANDARD, Engine}; @@ -837,7 +990,7 @@ mod tests { let private_key ="2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap(); client .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key, None) + .initialize_user_crypto_master_key(master_key, user_key, private_key, None, None) .unwrap(); let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsy7RFHcX3C8Q4/OMmhhbFReYWfB45W9PDTEA8tUZwZmtOiN2RErIS2M1c+K/4HoDJ/TjpbX1f2MZcr4nWvKFuqnZXyewFc+jmvKVewYi+NAu2++vqKq2kKcmMNhwoQDQdQIVy/Uqlp4Cpi2cIwO6ogq5nHNJGR3jm+CpyrafYlbz1bPvL3hbyoGDuG2tgADhyhXUdFuef2oF3wMvn1lAJAvJnPYpMiXUFmj1ejmbwtlxZDrHgUJvUcp7nYdwUKaFoi+sOttHn3u7eZPtNvxMjhSS/X/1xBIzP/mKNLdywH5LoRxniokUk+fV3PYUxJsiU3lV0Trc/tH46jqd8ZGjmwIDAQAB"; @@ -845,8 +998,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 = Bytes::::from(private_key); + let private_key = AsymmetricCryptoKey::from_der(&private_key).unwrap(); let decrypted: SymmetricCryptoKey = encrypted.decapsulate_key_unsigned(&private_key).unwrap(); @@ -938,11 +1092,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/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index 03390939a..c4cb50385 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -1,14 +1,13 @@ -use bitwarden_crypto::CryptoError; +use bitwarden_crypto::{CryptoError, RotateUserKeysResponse}; #[cfg(feature = "internal")] use bitwarden_crypto::{EncString, UnsignedSharedKey}; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use super::crypto::{ - derive_key_connector, make_key_pair, make_user_signing_keys_for_enrollment, - verify_asymmetric_keys, DeriveKeyConnectorError, DeriveKeyConnectorRequest, - EnrollAdminPasswordResetError, MakeKeyPairResponse, MakeUserSigningKeysResponse, - VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse, + derive_key_connector, make_key_pair, verify_asymmetric_keys, DeriveKeyConnectorError, + DeriveKeyConnectorRequest, EnrollAdminPasswordResetError, EnrollUserCryptoV2Response, + MakeKeyPairResponse, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse, }; #[cfg(feature = "internal")] use crate::key_management::crypto::{ @@ -18,7 +17,10 @@ use crate::key_management::crypto::{ }; use crate::{ client::encryption_settings::EncryptionSettingsError, - key_management::crypto::CryptoClientError, Client, + key_management::crypto::{ + get_v2_rotated_account_keys, make_keys_for_user_crypto_v2, CryptoClientError, + }, + Client, }; /// A client for the crypto operations. @@ -64,10 +66,16 @@ impl CryptoClient { } /// Makes a new signing key pair and signs the public key for the user - pub fn make_user_signing_keys_for_enrollment( + pub fn make_keys_for_user_crypto_v2(&self) -> Result { + make_keys_for_user_crypto_v2(&self.client) + } + + /// Creates a rotated set of account keys for the current state + pub fn get_v2_rotated_account_keys( &self, - ) -> Result { - make_user_signing_keys_for_enrollment(&self.client) + user_key: String, + ) -> Result { + get_v2_rotated_account_keys(&self.client, user_key) } } diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index db89b378b..4f4660a4b 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}; pub mod crypto; diff --git a/crates/bitwarden-core/src/platform/generate_fingerprint.rs b/crates/bitwarden-core/src/platform/generate_fingerprint.rs index 311039d74..96b1850c6 100644 --- a/crates/bitwarden-core/src/platform/generate_fingerprint.rs +++ b/crates/bitwarden-core/src/platform/generate_fingerprint.rs @@ -3,7 +3,7 @@ //! This module contains the logic for generating fingerprints. use base64::{engine::general_purpose::STANDARD, Engine}; -use bitwarden_crypto::fingerprint; +use bitwarden_crypto::{fingerprint, Bytes, SpkiPublicKeyDerContentFormat}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -42,7 +42,7 @@ pub enum FingerprintError { pub(crate) fn generate_fingerprint(input: &FingerprintRequest) -> Result { let key = STANDARD.decode(&input.public_key)?; - + let key = Bytes::::from(key); Ok(fingerprint(&input.fingerprint_material, &key)?) } @@ -108,6 +108,7 @@ mod tests { user_key.parse().unwrap(), private_key.parse().unwrap(), None, + None, ) .unwrap(); diff --git a/crates/bitwarden-core/tests/register.rs b/crates/bitwarden-core/tests/register.rs index 2b93f8227..af4abd94a 100644 --- a/crates/bitwarden-core/tests/register.rs +++ b/crates/bitwarden-core/tests/register.rs @@ -34,6 +34,7 @@ async fn test_register_initialize_crypto() { email: email.to_owned(), private_key: register_response.keys.private, signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: password.to_owned(), user_key: register_response.encrypted_user_key, diff --git a/crates/bitwarden-crypto/README.md b/crates/bitwarden-crypto/README.md index c568f475c..5dc24ef2e 100644 --- a/crates/bitwarden-crypto/README.md +++ b/crates/bitwarden-crypto/README.md @@ -13,7 +13,7 @@ secure. ## Example: ```rust -use bitwarden_crypto::{SymmetricCryptoKey, KeyEncryptable, KeyDecryptable, CryptoError}; +use bitwarden_crypto::{SymmetricCryptoKey, KeyEncryptable, KeyDecryptable, CryptoError, ContentFormat}; async fn example() -> Result<(), CryptoError> { let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); diff --git a/crates/bitwarden-crypto/examples/signature.rs b/crates/bitwarden-crypto/examples/signature.rs index 8524e2943..71f6fb958 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::{Bytes, CoseSerializable, CoseSign1ContentFormat, SigningNamespace}; use serde::{Deserialize, Serialize}; const EXAMPLE_NAMESPACE: &SigningNamespace = &SigningNamespace::SignedPublicKey; @@ -38,16 +38,18 @@ 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( - mock_server - .download("signature") - .expect("Failed to download signature"), - ) - .expect("Failed to deserialize signature"); + let retrieved_signature = + bitwarden_crypto::Signature::from_cose(&Bytes::::from( + mock_server + .download("signature") + .expect("Failed to download signature") + .clone(), + )) + .expect("Failed to deserialize signature"); let retrieved_serialized_message = bitwarden_crypto::SerializedMessage::from_bytes( mock_server .download("serialized_message") @@ -76,7 +78,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,18 +90,22 @@ fn main() { .content_type() .expect("Failed to get content type from signature"), ); - let retrieved_alice_signature = bitwarden_crypto::Signature::from_cose( - mock_server - .download("signature") - .expect("Failed to download Alice's signature"), - ) - .expect("Failed to deserialize Alice's signature"); - let retrieved_bobs_signature = bitwarden_crypto::Signature::from_cose( - mock_server - .download("bobs_signature") - .expect("Failed to download Bob's signature"), - ) - .expect("Failed to deserialize Bob's signature"); + let retrieved_alice_signature = + bitwarden_crypto::Signature::from_cose(&Bytes::::from( + mock_server + .download("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(&Bytes::::from( + mock_server + .download("bobs_signature") + .expect("Failed to download Bob's signature") + .clone(), + )) + .expect("Failed to deserialize Bob's signature"); // Charlie verifies Alice's signature if !retrieved_alice_signature.verify( diff --git a/crates/bitwarden-crypto/examples/signed_object.rs b/crates/bitwarden-crypto/examples/signed_object.rs index c49b8ef1b..0223338b2 100644 --- a/crates/bitwarden-crypto/examples/signed_object.rs +++ b/crates/bitwarden-crypto/examples/signed_object.rs @@ -1,6 +1,8 @@ //! This example demonstrates how to sign and verify structs. -use bitwarden_crypto::{CoseSerializable, SignedObject, SigningNamespace}; +use bitwarden_crypto::{ + Bytes, CoseSerializable, CoseSign1ContentFormat, SignedObject, SigningNamespace, +}; use serde::{Deserialize, Serialize}; const EXAMPLE_NAMESPACE: &SigningNamespace = &SigningNamespace::SignedPublicKey; @@ -34,14 +36,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(&Bytes::::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..4d2df5ea8 --- /dev/null +++ b/crates/bitwarden-crypto/src/content_format.rs @@ -0,0 +1,186 @@ +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use tsify_next::Tsify; + +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(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub 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, + /// CBOR serialized data + Cbor, +} + +/// 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 { + /// Returns the content format as a `ContentFormat` enum. + 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 ConstContentFormat for Utf8ContentFormat { + fn content_format() -> ContentFormat { + ContentFormat::Utf8 + } +} + +/// Content format for raw bytes. Used for attachments and send seed keys. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct OctetStreamContentFormat; +impl ConstContentFormat for OctetStreamContentFormat { + fn content_format() -> ContentFormat { + ContentFormat::OctetStream + } +} + +/// Content format for PKCS8 private keys in DER format. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct Pkcs8PrivateKeyDerContentFormat; +impl ConstContentFormat for Pkcs8PrivateKeyDerContentFormat { + fn content_format() -> ContentFormat { + ContentFormat::Pkcs8PrivateKey + } +} + +/// Content format for SPKI public keys in DER format. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct SpkiPublicKeyDerContentFormat; +impl ConstContentFormat for SpkiPublicKeyDerContentFormat { + fn content_format() -> ContentFormat { + ContentFormat::SPKIPublicKeyDer + } +} + +/// Content format for COSE keys. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct CoseKeyContentFormat; +impl ConstContentFormat for CoseKeyContentFormat { + fn content_format() -> ContentFormat { + ContentFormat::CoseKey + } +} +impl CoseContentFormat for CoseKeyContentFormat {} + +/// A legacy content format for Bitwarden keys. See `ContentFormat::BitwardenLegacyKey` +#[allow(unused)] +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct BitwardenLegacyKeyContentFormat; +impl ConstContentFormat for BitwardenLegacyKeyContentFormat { + fn content_format() -> ContentFormat { + ContentFormat::BitwardenLegacyKey + } +} + +/// Content format for COSE Sign1 messages. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct CoseSign1ContentFormat; +impl ConstContentFormat for CoseSign1ContentFormat { + fn content_format() -> ContentFormat { + ContentFormat::CoseSign1 + } +} +impl CoseContentFormat for CoseSign1ContentFormat {} + +/// A marker trait for COSE content formats. +pub trait CoseContentFormat {} + +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..682146ab4 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,71 @@ 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) + } + ContentFormat::Cbor => header_builder.content_format(CoapContentFormat::Cbor), + } + } +} + +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 +234,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 +297,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 +309,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..90f2880e0 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, BitwardenLegacyKeyContentFormat, Bytes, + 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,9 @@ impl UnsignedSharedKey { } } .map_err(|_| CryptoError::KeyDecrypt)?; - SymmetricCryptoKey::try_from(key_data.as_mut_slice()) + SymmetricCryptoKey::try_from(&Bytes::::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..e0e8ca17e 100644 --- a/crates/bitwarden-crypto/src/enc_string/symmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/symmetric.rs @@ -6,9 +6,11 @@ use serde::Deserialize; use super::{check_length, from_b64, from_b64_vec, split_enc_string}; use crate::{ + content_format::{Bytes, Utf8ContentFormat}, error::{CryptoError, EncStringParseError, Result, UnsupportedOperation}, util::FromStrVisitor, - Aes256CbcHmacKey, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, XChaCha20Poly1305Key, + Aes256CbcHmacKey, ContentFormat, KeyDecryptable, KeyEncryptable, KeyEncryptableWithContentType, + SymmetricCryptoKey, XChaCha20Poly1305Key, }; #[cfg(feature = "wasm")] @@ -262,8 +264,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 +280,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 +312,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 +323,13 @@ impl KeyDecryptable> for EncString { impl KeyEncryptable for String { fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { - self.as_bytes().encrypt_with_key(key) + Into::>::into(self).encrypt_with_key(key) } } impl KeyEncryptable for &str { fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { - self.as_bytes().encrypt_with_key(key) + Into::>::into(self).encrypt_with_key(key) } } @@ -375,7 +382,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 +392,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..3eb6e0708 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -61,11 +61,20 @@ pub enum CryptoError { #[error("Invalid nonce length")] InvalidNonceLength, + #[error("Invalid padding")] + InvalidPadding, + #[error("Signature error, {0}")] SignatureError(#[from] SignatureError), #[error("Encoding error, {0}")] EncodingError(#[from] EncodingError), + + #[error("Uninitialized error")] + UninitializedError, + + #[error("Attempted security downgrade {0}")] + SecurityDowngrade(String), } #[derive(Debug, Error)] @@ -90,6 +99,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..0ac977f4a 100644 --- a/crates/bitwarden-crypto/src/fingerprint.rs +++ b/crates/bitwarden-crypto/src/fingerprint.rs @@ -8,16 +8,22 @@ use num_bigint::BigUint; use num_traits::cast::ToPrimitive; use thiserror::Error; -use crate::{error::Result, wordlist::EFF_LONG_WORD_LIST, CryptoError}; +use crate::{ + content_format::SpkiPublicKeyDerContentFormat, error::Result, wordlist::EFF_LONG_WORD_LIST, + Bytes, CryptoError, +}; /// 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: &Bytes, +) -> 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 +70,7 @@ pub enum FingerprintError { #[cfg(test)] mod tests { use super::fingerprint; + use crate::{Bytes, SpkiPublicKeyDerContentFormat}; #[test] fn test_fingerprint() { @@ -86,10 +93,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: Bytes = Bytes::from(key.to_vec()); assert_eq!( "turban-deftly-anime-chatroom-unselfish", - fingerprint(user_id, key).unwrap() + fingerprint(user_id, &key).unwrap() ); } } diff --git a/crates/bitwarden-crypto/src/key_rotation/mod.rs b/crates/bitwarden-crypto/src/key_rotation/mod.rs new file mode 100644 index 000000000..93c04537a --- /dev/null +++ b/crates/bitwarden-crypto/src/key_rotation/mod.rs @@ -0,0 +1,49 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use tsify_next::Tsify; + +use crate::{ + CoseSerializable, CryptoError, EncString, KeyEncryptable, KeyStoreContext, SymmetricCryptoKey, +}; + +/// Rotated set of account keys +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct RotateUserKeysResponse { + /// The verifying key + verifying_key: String, + /// Signing key, encrypted with a symmetric key (user key, org key) + signing_key: EncString, + /// The user's public key, signed by the signing key + signed_public_key: String, + // The user's public key, without signature + public_key: String, + // The user's private key, encrypted with the user key + private_key: EncString, +} + +/// Re-encrypts the user's keys with the provided symmetric key for a v2 user. +pub fn get_v2_rotated_account_keys( + new_user_key: SymmetricCryptoKey, + current_user_private_key_id: Ids::Asymmetric, + current_user_signing_key_id: Ids::Signing, + ctx: &KeyStoreContext, +) -> Result { + let signing_key = ctx.get_signing_key(current_user_signing_key_id)?; + let private_key = ctx.get_asymmetric_key(current_user_private_key_id)?; + let signed_public_key: Vec = ctx + .make_signed_public_key(current_user_private_key_id, current_user_signing_key_id)? + .into(); + + Ok(RotateUserKeysResponse { + verifying_key: STANDARD.encode(signing_key.to_verifying_key().to_cose()), + signing_key: signing_key.to_cose().encrypt_with_key(&new_user_key)?, + signed_public_key: STANDARD.encode(&signed_public_key), + public_key: STANDARD.encode(private_key.to_public_key().to_der()?), + private_key: private_key.to_der()?.encrypt_with_key(&new_user_key)?, + }) +} diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index 8ef598e33..666451489 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::{ + content_format::{Bytes, Pkcs8PrivateKeyDerContentFormat, SpkiPublicKeyDerContentFormat}, + error::{CryptoError, Result}, +}; /// 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: &Bytes) -> 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,6 +165,7 @@ mod tests { use base64::{engine::general_purpose::STANDARD, Engine}; use crate::{ + content_format::{Bytes, Pkcs8PrivateKeyDerContentFormat}, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, SymmetricCryptoKey, UnsignedSharedKey, }; @@ -198,12 +204,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 +260,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= )) .unwrap(); + let private_key = Bytes::::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..1998dfa3a 100644 --- a/crates/bitwarden-crypto/src/keys/device_key.rs +++ b/crates/bitwarden-crypto/src/keys/device_key.rs @@ -1,7 +1,8 @@ use super::{AsymmetricCryptoKey, PublicKeyEncryptionAlgorithm}; use crate::{ - error::Result, CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, - UnsignedSharedKey, + content_format::{Bytes, Pkcs8PrivateKeyDerContentFormat}, + error::Result, + CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, UnsignedSharedKey, }; /// Device Key @@ -65,6 +66,8 @@ 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: Bytes = + Bytes::from(device_private_key); let device_private_key = AsymmetricCryptoKey::from_der(&device_private_key)?; let user_key: SymmetricCryptoKey = @@ -88,7 +91,7 @@ impl TryFrom for DeviceKey { #[cfg(test)] mod tests { use super::*; - use crate::derive_symmetric_key; + use crate::{derive_symmetric_key, BitwardenLegacyKeyContentFormat}; #[test] fn test_trust_device() { @@ -111,21 +114,26 @@ 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(&Bytes::::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(&Bytes::::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..190be2a9d 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 { @@ -24,6 +24,11 @@ pub trait KeyEncryptable { fn encrypt_with_key(self, key: &Key) -> Result; } +#[allow(missing_docs)] +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..3e380a9a2 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, + BitwardenLegacyKeyContentFormat, Bytes, 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(&Bytes::::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..247971f1e 100644 --- a/crates/bitwarden-crypto/src/keys/pin_key.rs +++ b/crates/bitwarden-crypto/src/keys/pin_key.rs @@ -4,7 +4,8 @@ use super::{ utils::stretch_key, }; use crate::{ - keys::key_encryptable::CryptoKey, EncString, KeyEncryptable, Result, SymmetricCryptoKey, + keys::key_encryptable::CryptoKey, ContentFormat, EncString, KeyEncryptable, + KeyEncryptableWithContentType, Result, SymmetricCryptoKey, }; /// Pin Key. @@ -31,15 +32,15 @@ impl PinKey { impl CryptoKey for PinKey {} -impl KeyEncryptable for &[u8] { - fn encrypt_with_key(self, key: &PinKey) -> Result { +impl KeyEncryptableWithContentType for &[u8] { + fn encrypt_with_key(self, key: &PinKey, content_format: ContentFormat) -> Result { let stretched_key = SymmetricCryptoKey::Aes256CbcHmacKey(stretch_key(&key.0 .0)?); - self.encrypt_with_key(&stretched_key) + self.encrypt_with_key(&stretched_key, content_format) } } impl KeyEncryptable for String { fn encrypt_with_key(self, key: &PinKey) -> Result { - self.as_bytes().encrypt_with_key(key) + self.as_bytes().encrypt_with_key(key, ContentFormat::Utf8) } } diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index ad2597b95..d46898996 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -11,9 +11,12 @@ 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, + content_format::{Bytes, CoseSign1ContentFormat}, + cose::CoseSerializable, + error::EncodingError, + util::FromStrVisitor, + CryptoError, PublicKeyEncryptionAlgorithm, RawPublicKey, SignedObject, SigningKey, + SigningNamespace, VerifyingKey, }; #[cfg(feature = "wasm")] @@ -57,7 +60,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()), }), } } @@ -79,14 +82,18 @@ pub struct SignedPublicKey(pub(crate) SignedObject); impl From for Vec { fn from(val: SignedPublicKey) -> Self { - val.0.to_cose() + val.0.to_cose().to_vec() } } impl TryFrom> for SignedPublicKey { type Error = EncodingError; fn try_from(bytes: Vec) -> Result { - Ok(SignedPublicKey(SignedObject::from_cose(&bytes)?)) + Ok(SignedPublicKey(SignedObject::from_cose(&Bytes::< + CoseSign1ContentFormat, + >::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..bc60f3997 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,9 @@ use super::{ key_encryptable::CryptoKey, key_id::{KeyId, KEY_ID_SIZE}, }; -use crate::{cose, CryptoError}; +use crate::{ + cose, BitwardenLegacyKeyContentFormat, Bytes, ContentFormat, CoseKeyContentFormat, 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 +147,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) -> Bytes { + let encoded_key = self.to_encoded_raw(); + match encoded_key { + EncodedSymmetricKey::LegacyNonCoseKey(_) => { + let encoded_key: Vec = encoded_key.into(); + Bytes::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 + Bytes::from(encoded_key) } } } @@ -182,14 +188,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::LegacyNonCoseKey(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::LegacyNonCoseKey(buf.into()) } Self::XChaCha20Poly1305Key(key) => { let builder = coset::CoseKeyBuilder::new_symmetric_key(key.enc_key.to_vec()); @@ -203,13 +211,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 +263,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(&Bytes::::from(bytes)) } } -impl TryFrom> for SymmetricCryptoKey { +impl TryFrom<&Bytes> for SymmetricCryptoKey { type Error = CryptoError; - fn try_from(mut value: Vec) -> Result { - Self::try_from(value.as_mut_slice()) - } -} - -impl TryFrom<&mut [u8]> for SymmetricCryptoKey { - type Error = CryptoError; + fn try_from(value: &Bytes) -> Result { + let slice = value.as_ref(); - /// 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::LegacyNonCoseKey(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::LegacyNonCoseKey(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::LegacyNonCoseKey(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 +376,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 +390,31 @@ 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) +} + +/// An enum to represent the different encodings of symmetric crypto keys. +pub enum EncodedSymmetricKey { + /// An Aes256-CBC-HMAC key, or a Aes256-CBC key + LegacyNonCoseKey(Bytes), + /// A symmetric key encoded as a COSE key + CoseKey(Bytes), +} +impl From for Vec { + fn from(val: EncodedSymmetricKey) -> Self { + match val { + EncodedSymmetricKey::LegacyNonCoseKey(key) => key.to_vec(), + EncodedSymmetricKey::CoseKey(key) => key.to_vec(), + } + } +} +impl EncodedSymmetricKey { + pub fn content_format(&self) -> ContentFormat { + match self { + EncodedSymmetricKey::LegacyNonCoseKey(_) => 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 +437,8 @@ mod tests { use super::{derive_symmetric_key, SymmetricCryptoKey}; use crate::{ keys::symmetric_crypto_key::{pad_key, unpad_key}, - Aes256CbcHmacKey, Aes256CbcKey, XChaCha20Poly1305Key, + Aes256CbcHmacKey, Aes256CbcKey, BitwardenLegacyKeyContentFormat, Bytes, + XChaCha20Poly1305Key, }; #[test] @@ -413,14 +457,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 = Bytes::::from(key); + let key = SymmetricCryptoKey::try_from(&key).unwrap(); match key { SymmetricCryptoKey::XChaCha20Poly1305Key(_) => (), _ => panic!("Invalid key type"), @@ -431,7 +476,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 +528,18 @@ 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(&Bytes::::from(vec![ + 1u8; + 32 + ])) + .unwrap(); + let key2 = + SymmetricCryptoKey::try_from(&Bytes::::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..757fc78ba 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; @@ -20,6 +22,8 @@ pub use error::CryptoError; pub(crate) use error::Result; mod fingerprint; pub use fingerprint::fingerprint; +mod key_rotation; +pub use key_rotation::*; mod keys; pub use keys::*; mod rsa; @@ -32,11 +36,16 @@ mod store; pub use store::{KeyStore, KeyStoreContext}; mod cose; pub use cose::CoseSerializable; +/// The `SecurityState` module provides functionality to cryptographically attest to which features +/// are allowed to be used for a user. +pub mod security_state; 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/security_state.rs b/crates/bitwarden-crypto/src/security_state.rs new file mode 100644 index 000000000..efabbc190 --- /dev/null +++ b/crates/bitwarden-crypto/src/security_state.rs @@ -0,0 +1,195 @@ +//! Security state is a signed object that attests to a user's (or later an organization's) security +//! state. The security goal is to prevent downgrades of specific features within the user's account +//! by the server / a networked attacker with TLS introspection access. +//! +//! A security state contains a security version. Based on this version, features can be disabled. +//! Since the server cannot sign a security state, it can no longer downgrade the feature, because +//! it cannot produce an arbitrary valid signed security state. +//! +//! Note: A long-term compromised server can record the security state of a user, and then replay +//! this specific state, or the entire account to downgrade users to previous states. This can be +//! prevented per logged in session by the client, and for bootstrapping a client by +//! using an extended login-with-device protocol. +//! +//! To utilize the security state to disable a feature the following steps are taken: +//! 1. Assume: Feature with format version A is insecure, and cannot be changed by simple mutation +//! 2. A new, safe format version B is introduced, and an upgrade path created +//! 3. The upgrade path is made mandatory +//! 4. After upgrades are run, the sdk validates that all items are in format version B, and the +//! security state can be updated to contain the security version N+1 +//! 5. The client, given a security state with security version N+1 will reject all items that are +//! in format version A. + +use std::str::FromStr; + +use base64::{engine::general_purpose::STANDARD, Engine}; +use serde::{Deserialize, Serialize}; +use serde_bytes::ByteBuf; + +use crate::{ + cose::CoseSerializable, error::EncodingError, util::FromStrVisitor, CryptoError, KeyIds, + KeyStoreContext, SignedObject, SigningKey, SigningNamespace, VerifyingKey, +}; + +#[cfg(feature = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] +const TS_CUSTOM_TYPES: &'static str = r#" +export type SignedSecurityState = string; +"#; + +/// The security state is a signed object attesting to the security state of a user. +/// +/// It contains a version, which can only ever increment. Based on the version, old formats and +/// features are blocked. This prevents a server from downgrading a user's account features, because +/// only the user can create this signed object. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SecurityState { + /// The entity ID is a permanent, unchangeable, unique identifier for the object this security + /// state applies to. For users, this is the user ID, which never changes. + entity_id: ByteBuf, + /// The version of the security state gates feature availability. It can only ever be + /// incremented. Components can use it to gate format support of specific formats (like + /// item url hashes). + version: u64, +} + +impl SecurityState { + /// Initialize a new `SecurityState` for the given user ID, to the lowest version possible. + /// The user needs to be a v2 encryption user. + pub fn initialize_for_user(user_id: uuid::Uuid) -> Self { + SecurityState { + entity_id: user_id.as_bytes().to_vec().into(), + version: 2, + } + } + + fn sign(&self, signing_key: &SigningKey) -> Result { + Ok(SignedSecurityState( + signing_key.sign(self, &SigningNamespace::SecurityState)?, + )) + } + + /// Returns the version of the security state + pub fn version(&self) -> u64 { + self.version + } +} + +/// Signs the `SecurityState` with the provided signing key ID from the context. +pub fn sign( + security_state: &SecurityState, + signing_key_id: Ids::Signing, + ctx: &mut KeyStoreContext, +) -> Result { + let signing_key = ctx.get_signing_key(signing_key_id)?; + security_state.sign(signing_key) +} + +impl SignedSecurityState { + /// Verifies the signature of the `SignedSecurityState` using the provided `VerifyingKey`. + pub fn verify_and_unwrap( + self, + verifying_key: &VerifyingKey, + ) -> Result { + self.0 + .verify_and_unwrap(verifying_key, &SigningNamespace::SecurityState) + } +} + +/// A signed and serialized `SecurityState` object. +#[derive(Clone, Debug)] +pub struct SignedSecurityState(pub(crate) SignedObject); + +impl From for Vec { + fn from(val: SignedSecurityState) -> Self { + val.0.to_cose().to_vec() + } +} + +impl TryFrom> for SignedSecurityState { + type Error = EncodingError; + fn try_from(bytes: Vec) -> Result { + use crate::content_format::{Bytes, CoseSign1ContentFormat}; + let bytes = Bytes::::from(bytes); + Ok(SignedSecurityState(SignedObject::from_cose(&bytes)?)) + } +} + +impl From for String { + fn from(val: SignedSecurityState) -> Self { + let bytes: Vec = val.into(); + STANDARD.encode(&bytes) + } +} + +impl FromStr for SignedSecurityState { + type Err = EncodingError; + + fn from_str(s: &str) -> Result { + let bytes = STANDARD + .decode(s) + .map_err(|_| EncodingError::InvalidCborSerialization)?; + Self::try_from(bytes) + } +} + +impl<'de> Deserialize<'de> for SignedSecurityState { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(FromStrVisitor::new()) + } +} + +impl serde::Serialize for SignedSecurityState { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let b64_serialized_signed_public_key: String = self.clone().into(); + serializer.serialize_str(&b64_serialized_signed_public_key) + } +} + +impl schemars::JsonSchema for SignedSecurityState { + fn schema_name() -> String { + "SecurityState".to_string() + } + + fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + generator.subschema_for::() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::SignatureAlgorithm; + + #[test] + fn test_security_state_signing() { + let user_id = uuid::Uuid::new_v4(); + let security_state = SecurityState::initialize_for_user(user_id); + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + let signed_security_state = security_state.sign(&signing_key).unwrap(); + + let verifying_key = signing_key.to_verifying_key(); + let verified_security_state = signed_security_state + .verify_and_unwrap(&verifying_key) + .unwrap(); + + assert_eq!( + uuid::Uuid::from_bytes( + verified_security_state + .entity_id + .as_ref() + .try_into() + .unwrap() + ), + user_id + ); + assert_eq!(verified_security_state.version(), 2); + } +} diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs index fd48bd2b9..7f5b0c2bb 100644 --- a/crates/bitwarden-crypto/src/signing/namespace.rs +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -12,6 +12,9 @@ pub enum SigningNamespace { /// The namespace for /// [`SignedPublicKey`](crate::keys::SignedPublicKey). SignedPublicKey = 1, + /// The namespace for + /// [`SignedSecurityState`](crate::security_state::SignedSecurityState). + SecurityState = 2, /// This namespace is only used in tests #[cfg(test)] ExampleNamespace = -1, @@ -33,6 +36,7 @@ impl TryFrom for SigningNamespace { fn try_from(value: i64) -> Result { match value { 1 => Ok(SigningNamespace::SignedPublicKey), + 2 => Ok(SigningNamespace::SecurityState), #[cfg(test)] -1 => Ok(SigningNamespace::ExampleNamespace), #[cfg(test)] diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs index 47bbbe6be..ca2635805 100644 --- a/crates/bitwarden-crypto/src/signing/signature.rs +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -7,6 +7,7 @@ use super::{ VerifyingKey, }; use crate::{ + content_format::{Bytes, CoseSign1ContentFormat}, cose::{CoseSerializable, SIGNING_NAMESPACE}, error::{EncodingError, SignatureError}, CryptoError, @@ -172,25 +173,29 @@ 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: &Bytes) -> 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) -> Bytes { self.0 .clone() .to_vec() .expect("Signature is always serializable") + .into() } } #[cfg(test)] mod tests { use super::*; - use crate::SignatureAlgorithm; + use crate::{ + content_format::{Bytes, CoseKeyContentFormat}, + 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 +217,8 @@ mod tests { #[test] fn test_cose_roundtrip_encode_signature() { - let signature = Signature::from_cose(SIGNATURE).unwrap(); + let signature = + Signature::from_cose(&Bytes::::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 +226,10 @@ 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(&Bytes::::from(VERIFYING_KEY)).unwrap(); + let signature = + Signature::from_cose(&Bytes::::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..b50366286 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -7,6 +7,7 @@ use super::{ verifying_key::VerifyingKey, SigningNamespace, }; use crate::{ + content_format::{Bytes, CoseSign1ContentFormat}, cose::{CoseSerializable, SIGNING_NAMESPACE}, error::{EncodingError, SignatureError}, CryptoError, @@ -149,18 +150,20 @@ impl SigningKey { } } -impl CoseSerializable for SignedObject { - fn from_cose(bytes: &[u8]) -> Result { +impl CoseSerializable for SignedObject { + fn from_cose(bytes: &Bytes) -> 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) -> Bytes { self.0 .clone() .to_vec() .expect("SignedObject is always serializable") + .into() } } @@ -169,6 +172,7 @@ mod tests { use serde::{Deserialize, Serialize}; use crate::{ + content_format::{Bytes, CoseKeyContentFormat, CoseSign1ContentFormat}, CoseSerializable, CryptoError, SignatureAlgorithm, SignedObject, SigningKey, SigningNamespace, VerifyingKey, }; @@ -196,13 +200,18 @@ mod tests { #[test] fn test_roundtrip_cose() { - let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap(); + let signed_object = + SignedObject::from_cose(&Into::>::into(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, + Bytes::::from(SIGNED_OBJECT) + ); } #[test] @@ -210,8 +219,12 @@ 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(&Into::>::into(SIGNED_OBJECT)) + .unwrap(); + let verifying_key = + VerifyingKey::from_cose(&Into::>::into(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..12644b3f9 100644 --- a/crates/bitwarden-crypto/src/signing/signing_key.rs +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -13,6 +13,7 @@ use super::{ SignatureAlgorithm, }; use crate::{ + content_format::{Bytes, CoseKeyContentFormat}, cose::CoseSerializable, error::{EncodingError, Result}, keys::KeyId, @@ -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) -> Bytes { 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: &Bytes) -> 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..507a1d0c7 100644 --- a/crates/bitwarden-crypto/src/signing/verifying_key.rs +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -11,6 +11,7 @@ use coset::{ use super::{ed25519_verifying_key, key_id, SignatureAlgorithm}; use crate::{ + content_format::{Bytes, CoseKeyContentFormat}, cose::CoseSerializable, error::{EncodingError, SignatureError}, keys::KeyId, @@ -57,8 +58,8 @@ impl VerifyingKey { } } -impl CoseSerializable for VerifyingKey { - fn to_cose(&self) -> Vec { +impl CoseSerializable for VerifyingKey { + fn to_cose(&self) -> Bytes { 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: &Bytes) -> 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,9 @@ mod tests { #[test] fn test_cose_roundtrip_encode_verifying() { - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let verifying_key = + VerifyingKey::from_cose(&Bytes::::from(VERIFYING_KEY.to_vec())) + .unwrap(); let cose = verifying_key.to_cose(); let parsed_key = VerifyingKey::from_cose(&cose).unwrap(); @@ -136,7 +140,9 @@ mod tests { #[test] fn test_testvector() { - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let verifying_key = + VerifyingKey::from_cose(&Bytes::::from(VERIFYING_KEY.to_vec())) + .unwrap(); assert_eq!(verifying_key.algorithm(), SignatureAlgorithm::Ed25519); verifying_key @@ -146,7 +152,9 @@ mod tests { #[test] fn test_invalid_testvector() { - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let verifying_key = + VerifyingKey::from_cose(&Bytes::::from(VERIFYING_KEY.to_vec())) + .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..a9605d50c 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -9,15 +9,18 @@ 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, BitwardenLegacyKeyContentFormat, Bytes, ContentFormat, CryptoError, + EncString, KeyId, KeyIds, PublicKeyEncryptionAlgorithm, 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 +62,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 +142,52 @@ 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(&Bytes::::from( + crate::aes::decrypt_aes256(iv, data.clone(), &key.enc_key)?, + ))? + } + ( + EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data }, + SymmetricCryptoKey::Aes256CbcHmacKey(key), + ) => SymmetricCryptoKey::try_from(&Bytes::::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( + &Bytes::::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 +216,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, )), @@ -265,6 +311,15 @@ impl KeyStoreContext<'_, Ids> { Ok(key_id) } + /// Makes a new asymmetric encryption key using the current default algorithm, and stores it in + /// the context + pub fn make_asymmetric_key(&mut self, key_id: Ids::Asymmetric) -> Result { + let key = AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); + #[allow(deprecated)] + self.set_asymmetric_key(key_id, key)?; + Ok(key_id) + } + /// Generate a new signature key using the current default algorithm, and store it in the /// context pub fn make_signing_key(&mut self, key_id: Ids::Signing) -> Result { @@ -335,7 +390,10 @@ impl KeyStoreContext<'_, Ids> { .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}"))) } - fn get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&AsymmetricCryptoKey> { + pub(crate) fn get_asymmetric_key( + &self, + key_id: Ids::Asymmetric, + ) -> Result<&AsymmetricCryptoKey> { if key_id.is_local() { self.local_asymmetric_keys.get(key_id) } else { @@ -344,7 +402,7 @@ impl KeyStoreContext<'_, Ids> { .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}"))) } - fn get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> { + pub(crate) fn get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> { if key_id.is_local() { self.local_signing_keys.get(key_id) } else { @@ -415,6 +473,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 +488,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 +497,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 +535,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 +574,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 +614,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 +623,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..8a065af48 100644 --- a/crates/bitwarden-crypto/src/traits/encryptable.rs +++ b/crates/bitwarden-crypto/src/traits/encryptable.rs @@ -1,83 +1,171 @@ -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 caller provides the content format when encrypting. The latter is a convenience +//! trait that pre-defines the content format based on the type of data that is encrypted. This is +//! currently done exactly for `String` and `&str`, which are encrypted as UTF-8, -/// 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 +198,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-crypto/src/uniffi_support.rs b/crates/bitwarden-crypto/src/uniffi_support.rs index 8d66a0429..6c9c70885 100644 --- a/crates/bitwarden-crypto/src/uniffi_support.rs +++ b/crates/bitwarden-crypto/src/uniffi_support.rs @@ -1,6 +1,8 @@ use std::{num::NonZeroU32, str::FromStr}; -use crate::{CryptoError, EncString, SignedPublicKey, UnsignedSharedKey}; +use crate::{ + security_state::SignedSecurityState, CryptoError, EncString, SignedPublicKey, UnsignedSharedKey, +}; uniffi::custom_type!(NonZeroU32, u32, { remote, @@ -32,3 +34,12 @@ uniffi::custom_type!(SignedPublicKey, String, { }, lower: |obj| obj.into(), }); + +uniffi::custom_type!(SignedSecurityState, String, { + try_lift: |val| { + val.parse().map_err(|e| { + CryptoError::EncodingError(e).into() + }) + }, + lower: |obj| obj.into(), +}); 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..ec28a46c4 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, Bytes, CompositeEncryptable, CryptoError, Decryptable, EncString, + IdentifyKey, KeyStoreContext, OctetStreamContentFormat, 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: Bytes::::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..d8a9a2936 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::{ + Bytes, Decryptable, EncString, IdentifyKey, OctetStreamContentFormat, 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 = Bytes::::from(buffer).encrypt(&mut ctx, key)?; Ok(encrypted.to_buffer()?) } } diff --git a/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt b/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt index dd759516b..6e61cc7ef 100644 --- a/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt +++ b/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt @@ -255,6 +255,7 @@ class MainActivity : FragmentActivity() { email = EMAIL, privateKey = loginBody.PrivateKey, signingKey = null, + securityState = null, method = InitUserCryptoMethod.Password( password = PASSWORD, userKey = loginBody.Key ) @@ -341,6 +342,7 @@ class MainActivity : FragmentActivity() { email = EMAIL, privateKey = privateKey!!, signingKey = null, + securityState = null, method = InitUserCryptoMethod.DecryptedKey(decryptedUserKey = key) ) ) @@ -380,6 +382,7 @@ class MainActivity : FragmentActivity() { email = EMAIL, privateKey = privateKey!!, signingKey = null, + securityState = null, method = InitUserCryptoMethod.Pin( pinProtectedUserKey = pinProtectedUserKey, pin = PIN ) diff --git a/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift b/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift index 6651785d7..9ddaf6847 100644 --- a/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift +++ b/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift @@ -194,6 +194,7 @@ struct ContentView: View { email: EMAIL, privateKey: loginData.PrivateKey, signingKey: nil, + securityState: nil, method: InitUserCryptoMethod.password( password: PASSWORD, userKey: loginData.Key @@ -253,6 +254,7 @@ struct ContentView: View { email: EMAIL, privateKey: privateKey, signingKey: nil, + securityState: nil, method: InitUserCryptoMethod.decryptedKey( decryptedUserKey: key ) @@ -281,6 +283,7 @@ struct ContentView: View { email: EMAIL, privateKey: privateKey, signingKey: nil, + securityState: nil, method: InitUserCryptoMethod.pin(pin: PIN, pinProtectedUserKey: pinProtectedUserKey) )) } diff --git a/crates/bitwarden-vault/src/cipher/attachment.rs b/crates/bitwarden-vault/src/cipher/attachment.rs index e37b6ad02..b37af2f6c 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, + Bytes, CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext, + OctetStreamContentFormat, 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 = + Bytes::::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 0d4c515dd..5492810cc 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}; @@ -268,8 +269,8 @@ impl CipherListView { } } -impl Encryptable for CipherView { - fn encrypt( +impl CompositeEncryptable for CipherView { + fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, @@ -292,20 +293,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, @@ -425,7 +432,7 @@ impl CipherView { ctx: &mut KeyStoreContext, key: SymmetricKeyId, ) -> Result<(), CryptoError> { - let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; + let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key.take())?; const NEW_KEY: SymmetricKeyId = SymmetricKeyId::Local("new_cipher_key"); @@ -463,8 +470,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)?; } } } @@ -498,7 +506,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(()) @@ -520,8 +528,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)?; @@ -542,7 +551,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(()) } @@ -942,11 +952,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] @@ -1088,12 +1101,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 @@ -1150,19 +1171,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..5e126163f 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, BitwardenLegacyKeyContentFormat, Bytes, + CoseKeyContentFormat, CoseSerializable, CryptoError, Decryptable, EncString, Kdf, + KeyDecryptable, KeyEncryptable, KeyStore, MasterKey, OctetStreamContentFormat, + Pkcs8PrivateKeyDerContentFormat, PrimitiveEncryptable, SignatureAlgorithm, SignedPublicKey, + SigningKey, SpkiPublicKeyDerContentFormat, SymmetricCryptoKey, UnsignedSharedKey, VerifyingKey, }; use wasm_bindgen::prelude::*; @@ -29,6 +30,7 @@ impl PureCrypto { enc_string: String, key: Vec, ) -> Result { + let key = &Bytes::::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 = &Bytes::::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 = &Bytes::::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 = &Bytes::::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 = &Bytes::::from(key); + Bytes::::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 = &Bytes::::from(key); + Bytes::::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 = &Bytes::::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,16 @@ impl PureCrypto { ) -> Result { let tmp_store: KeyStore = KeyStore::default(); let mut context = tmp_store.context(); - #[allow(deprecated)] - context.set_symmetric_key( - SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, + let wrapping_key = SymmetricCryptoKey::try_from( + &Bytes::::from(wrapping_key), )?; #[allow(deprecated)] - context.set_symmetric_key( - SymmetricKeyId::Local("key_to_wrap"), - SymmetricCryptoKey::try_from(key_to_be_wrapped)?, - )?; + context.set_symmetric_key(SymmetricKeyId::Local("wrapping_key"), wrapping_key)?; + let key_to_be_wrapped = SymmetricCryptoKey::try_from(&Bytes::< + BitwardenLegacyKeyContentFormat, + >::from(key_to_be_wrapped))?; + #[allow(deprecated)] + 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 +158,11 @@ impl PureCrypto { ) -> Result, CryptoError> { let tmp_store: KeyStore = KeyStore::default(); let mut context = tmp_store.context(); - #[allow(deprecated)] - context.set_symmetric_key( - SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, + let wrapping_key = SymmetricCryptoKey::try_from( + &Bytes::::from(wrapping_key), )?; + #[allow(deprecated)] + 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 +171,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,12 +188,15 @@ impl PureCrypto { #[allow(deprecated)] context.set_symmetric_key( SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, + SymmetricCryptoKey::try_from(&Bytes::::from( + wrapping_key, + ))?, )?; - // Note: The order of arguments is different here, and should probably be refactored - Ok(encapsulation_key - .encrypt(&mut context, SymmetricKeyId::Local("wrapping_key"))? - .to_string()) + Ok( + Bytes::::from(encapsulation_key) + .encrypt(&mut context, SymmetricKeyId::Local("wrapping_key"))? + .to_string(), + ) } /// Unwraps (decrypts) a wrapped SPKI DER encoded encapsulation (public) key using a symmetric @@ -194,9 +210,10 @@ impl PureCrypto { #[allow(deprecated)] context.set_symmetric_key( SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, + SymmetricCryptoKey::try_from(&Bytes::::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,12 +229,15 @@ impl PureCrypto { #[allow(deprecated)] context.set_symmetric_key( SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, + SymmetricCryptoKey::try_from(&Bytes::::from( + wrapping_key, + ))?, )?; - // Note: The order of arguments is different here, and should probably be refactored - Ok(decapsulation_key - .encrypt(&mut context, SymmetricKeyId::Local("wrapping_key"))? - .to_string()) + Ok( + Bytes::::from(decapsulation_key) + .encrypt(&mut context, SymmetricKeyId::Local("wrapping_key"))? + .to_string(), + ) } /// Unwraps (decrypts) a wrapped PKCS8 DER encoded decapsulation (private) key using a symmetric @@ -231,9 +251,10 @@ impl PureCrypto { #[allow(deprecated)] context.set_symmetric_key( SymmetricKeyId::Local("wrapping_key"), - SymmetricCryptoKey::try_from(wrapping_key)?, + SymmetricCryptoKey::try_from(&Bytes::::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 +268,9 @@ 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(&Bytes::::from( + shared_key, + ))?, &encapsulation_key, )? .to_string()) @@ -261,10 +284,13 @@ impl PureCrypto { decapsulation_key: Vec, ) -> Result, CryptoError> { Ok(UnsignedSharedKey::from_str(encapsulated_key.as_str())? - .decapsulate_key_unsigned(&AsymmetricCryptoKey::from_der( - decapsulation_key.as_slice(), - )?)? - .to_encoded()) + .decapsulate_key_unsigned(&AsymmetricCryptoKey::from_der(&Bytes::< + Pkcs8PrivateKeyDerContentFormat, + >::from( + decapsulation_key + ))?)? + .to_encoded() + .to_vec()) } /// Given a wrapped signing key and the symmetric key it is wrapped with, this returns @@ -274,16 +300,17 @@ 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(&Bytes::::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(&Bytes::::from(verifying_key))?; let algorithm = verifying_key.algorithm(); Ok(algorithm) } @@ -297,10 +324,12 @@ impl PureCrypto { 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 verifying_key = + VerifyingKey::from_cose(&Bytes::::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 +555,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 +575,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 +589,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 +604,11 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= #[test] fn test_key_algorithm_for_verifying_key() { - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let verifying_key = + VerifyingKey::from_cose(&Bytes::::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 +619,17 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= SIGNING_KEY_WRAPPING_KEY.to_vec(), ) .unwrap(); - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let verifying_key = + VerifyingKey::from_cose(&Bytes::::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(&Bytes::::from(verifying_key_derived)) + .unwrap(); assert_eq!(verifying_key.to_cose(), verifying_key_derived.to_cose()); }