diff --git a/Cargo.lock b/Cargo.lock index ad1d28d3..ee4be047 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -658,7 +658,10 @@ dependencies = [ "console_log", "js-sys", "log", + "serde", "serde_json", + "thiserror 1.0.69", + "tsify-next", "wasm-bindgen", "wasm-bindgen-futures", ] diff --git a/crates/bitwarden-crypto/src/enc_string/symmetric.rs b/crates/bitwarden-crypto/src/enc_string/symmetric.rs index 69711f74..576f29ea 100644 --- a/crates/bitwarden-crypto/src/enc_string/symmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/symmetric.rs @@ -1,4 +1,4 @@ -use std::{fmt::Display, str::FromStr}; +use std::{collections::HashMap, fmt::Display, str::FromStr}; use aes::cipher::typenum::U32; use base64::{engine::general_purpose::STANDARD, Engine}; @@ -8,7 +8,7 @@ use serde::Deserialize; use super::{check_length, from_b64, from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result}, - KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey, + DecryptedWithAdditionalData, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey, }; #[cfg(feature = "wasm")] @@ -47,9 +47,10 @@ export type EncString = string; /// /// Where: /// - `[type]`: is a digit number representing the variant. -/// - `[iv]`: (optional) is the initialization vector used for encryption. +/// - `[iv]`: is the initialization vector used for encryption. /// - `[data]`: is the encrypted data. -/// - `[mac]`: (optional) is the MAC used to validate the integrity of the data. +/// - `[mac]`: (only present on types with message authentication) is the MAC used to validate the +/// integrity of the data. #[derive(Clone, zeroize::ZeroizeOnDrop, PartialEq)] #[allow(unused, non_camel_case_types)] pub enum EncString { @@ -241,6 +242,42 @@ impl KeyEncryptable for &[u8] { impl KeyDecryptable> for EncString { fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result> { + let dec: DecryptedWithAdditionalData = self.decrypt_with_key(key)?; + Ok(dec.clear_bytes().to_vec()) + } +} + +impl KeyEncryptable for String { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { + self.as_bytes().encrypt_with_key(key) + } +} + +impl KeyEncryptable for &str { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { + self.as_bytes().encrypt_with_key(key) + } +} + +impl KeyDecryptable for EncString { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { + let dec: Vec = self.decrypt_with_key(key)?; + String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String) + } +} + +impl KeyDecryptable for EncString { + /// Decrypt an [EncString] using a [SymmetricCryptoKey] while preserving additional data for the + /// context of decryption. + /// + /// -- Additional Data by [EncString] variant -- + /// - [EncString::AesCbc256_B64]: + /// - "type": "0" Note: this is unauthenticated data + /// - [EncString::AesCbc128_HmacSha256_B64]: + /// - "type": "1" Note: this is unauthenticated data + /// - [EncString::AesCbc256_HmacSha256_B64]: + /// - "type": "2" Note: this is unauthenticated data + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { match self { EncString::AesCbc256_B64 { iv, data } => { if key.mac_key.is_some() { @@ -248,7 +285,10 @@ impl KeyDecryptable> for EncString { } let dec = crate::aes::decrypt_aes256(iv, data.clone(), &key.key)?; - Ok(dec) + Ok(DecryptedWithAdditionalData::new( + dec, + HashMap::from([("type".to_string(), "0".to_string())]), + )) } EncString::AesCbc128_HmacSha256_B64 { iv, mac, data } => { // TODO: SymmetricCryptoKey is designed to handle 32 byte keys only, but this @@ -258,37 +298,24 @@ impl KeyDecryptable> for EncString { let enc_key = key.key[0..16].into(); let mac_key = key.key[16..32].into(); let dec = crate::aes::decrypt_aes128_hmac(iv, mac, data.clone(), mac_key, enc_key)?; - Ok(dec) + Ok(DecryptedWithAdditionalData::new( + dec, + HashMap::from([("type".to_string(), "1".to_string())]), + )) } EncString::AesCbc256_HmacSha256_B64 { iv, mac, data } => { let mac_key = key.mac_key.as_ref().ok_or(CryptoError::InvalidMac)?; let dec = crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), mac_key, &key.key)?; - Ok(dec) + Ok(DecryptedWithAdditionalData::new( + dec, + HashMap::from([("type".to_string(), "2".to_string())]), + )) } } } } -impl KeyEncryptable for String { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { - self.as_bytes().encrypt_with_key(key) - } -} - -impl KeyEncryptable for &str { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { - self.as_bytes().encrypt_with_key(key) - } -} - -impl KeyDecryptable for EncString { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { - let dec: Vec = self.decrypt_with_key(key)?; - String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String) - } -} - /// Usually we wouldn't want to expose EncStrings in the API or the schemas. /// But during the transition phase we will expose endpoints using the EncString type. impl schemars::JsonSchema for EncString { diff --git a/crates/bitwarden-crypto/src/keys/key_encryptable.rs b/crates/bitwarden-crypto/src/keys/key_encryptable.rs index 044be9fc..e413562b 100644 --- a/crates/bitwarden-crypto/src/keys/key_encryptable.rs +++ b/crates/bitwarden-crypto/src/keys/key_encryptable.rs @@ -35,6 +35,37 @@ pub trait KeyDecryptable { fn decrypt_with_key(&self, key: &Key) -> Result; } +pub struct DecryptedWithAdditionalData { + clear_text: Vec, + additional_data: HashMap, +} + +impl DecryptedWithAdditionalData { + pub fn new(clear_text: Vec, additional_data: HashMap) -> Self { + Self { + clear_text, + additional_data, + } + } + + pub fn clear_bytes(&self) -> &[u8] { + &self.clear_text + } + + pub fn clear_text_utf8(&self) -> Result { + String::from_utf8(self.clear_text.clone()).map_err(|_| CryptoError::InvalidUtf8String) + } + + /// Additional data on the context of the decryption of the clear text. + /// Note that not all of this data is authenticated for all [crate::EncString] variants. + /// + /// See [KeyDecryptable<_,DecryptedWithAdditionalData>::decrypt_with_key] implementation for + /// more information. + pub fn additional_data(&self) -> &HashMap { + &self.additional_data + } +} + impl, Key: CryptoKey, Output> KeyEncryptable> for Option { diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index ac173296..62e1ff74 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -1,5 +1,7 @@ mod key_encryptable; -pub use key_encryptable::{CryptoKey, KeyContainer, KeyDecryptable, KeyEncryptable, LocateKey}; +pub use key_encryptable::{ + CryptoKey, DecryptedWithAdditionalData, KeyContainer, KeyDecryptable, KeyEncryptable, LocateKey, +}; mod master_key; pub use master_key::{ default_argon2_iterations, default_argon2_memory, default_argon2_parallelism, diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index 291305f0..2e94e75c 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -24,7 +24,10 @@ console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } js-sys = "0.3.68" log = "0.4.20" +serde.workspace = true serde_json = ">=1.0.96, <2.0" +thiserror = { workspace = true } +tsify-next.workspace = true # When upgrading wasm-bindgen, make sure to update the version in the workflows! wasm-bindgen = { version = "=0.2.99", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.41" diff --git a/crates/bitwarden-wasm-internal/src/client.rs b/crates/bitwarden-wasm-internal/src/client.rs index 4049be56..9eee0788 100644 --- a/crates/bitwarden-wasm-internal/src/client.rs +++ b/crates/bitwarden-wasm-internal/src/client.rs @@ -6,7 +6,14 @@ use bitwarden_error::prelude::*; use log::{set_max_level, Level}; use wasm_bindgen::prelude::*; -use crate::{vault::ClientVault, ClientCrypto}; +use crate::{ + crypto::{ + pure_crypto::{self, DecryptedBytes, DecryptedString, EncryptOptions}, + PureCryptoError, + }, + vault::ClientVault, + ClientCrypto, +}; #[wasm_bindgen] pub enum LogLevel { @@ -27,6 +34,60 @@ fn convert_level(level: LogLevel) -> Level { } } +#[wasm_bindgen] +pub struct BitwardenPure; + +#[wasm_bindgen] +impl BitwardenPure { + pub fn version() -> String { + Self::setup_once(); + env!("SDK_VERSION").to_owned() + } + + pub fn echo(msg: String) -> String { + Self::setup_once(); + msg + } + + pub fn throw(msg: String) -> Result<(), TestError> { + Self::setup_once(); + Err(TestError(msg)) + } + + pub fn symmetric_decrypt( + enc_string: String, + key_b64: String, + ) -> Result { + Self::setup_once(); + pure_crypto::symmetric_decrypt(enc_string, key_b64) + } + + pub fn symmetric_decrypt_to_bytes( + enc_string: String, + key_b64: String, + ) -> Result { + Self::setup_once(); + pure_crypto::symmetric_decrypt_to_bytes(enc_string, key_b64) + } + + pub fn symmetric_encrypt( + plain: String, + key_b64: String, + encrypt_options: EncryptOptions, + ) -> Result { + Self::setup_once(); + pure_crypto::symmetric_encrypt(plain, key_b64, encrypt_options) + } + + fn setup_once() { + console_error_panic_hook::set_once(); + let log_level = convert_level(LogLevel::Info); + if let Err(_e) = console_log::init_with_level(log_level) { + set_max_level(log_level.to_level_filter()) + } + } +} + // Rc<...> is to avoid needing to take ownership of the Client during our async run_command // function https://github.com/rustwasm/wasm-bindgen/issues/2195#issuecomment-799588401 #[wasm_bindgen] diff --git a/crates/bitwarden-wasm-internal/src/crypto.rs b/crates/bitwarden-wasm-internal/src/crypto.rs index 1f9a3b48..0a181ebe 100644 --- a/crates/bitwarden-wasm-internal/src/crypto.rs +++ b/crates/bitwarden-wasm-internal/src/crypto.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::{rc::Rc, str::FromStr}; use bitwarden_core::{ client::encryption_settings::EncryptionSettingsError, @@ -8,8 +8,104 @@ use bitwarden_core::{ }, Client, }; +use bitwarden_crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}; +use bitwarden_error::prelude::*; +use thiserror::Error; use wasm_bindgen::prelude::*; +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum PureCryptoError { + #[error("Cryptography error, {0}")] + Crypto(#[from] bitwarden_crypto::CryptoError), +} + +pub mod pure_crypto { + use std::collections::HashMap; + + use bitwarden_crypto::DecryptedWithAdditionalData; + use serde::{Deserialize, Serialize}; + + use super::*; + + #[derive(tsify_next::Tsify, Serialize, Deserialize)] + #[tsify(into_wasm_abi, from_wasm_abi)] + pub struct DecryptedString { + pub clear_text: String, + pub additional_data: HashMap, + } + + #[derive(tsify_next::Tsify, Serialize, Deserialize)] + #[tsify(into_wasm_abi, from_wasm_abi)] + pub struct DecryptedBytes { + pub clear_bytes: Vec, + pub additional_data: HashMap, + } + + #[derive(tsify_next::Tsify, Serialize, Deserialize)] + #[tsify(into_wasm_abi, from_wasm_abi)] + pub struct EncryptOptions { + pub additional_data: Option>, + } + + impl TryFrom for DecryptedString { + type Error = PureCryptoError; + fn try_from(decrypted_bytes: DecryptedBytes) -> Result { + Ok(Self { + clear_text: String::from_utf8(decrypted_bytes.clear_bytes) + .map_err(|_| bitwarden_crypto::CryptoError::InvalidUtf8String)?, + additional_data: decrypted_bytes.additional_data, + }) + } + } + + impl From for DecryptedBytes { + fn from(decrypted: DecryptedWithAdditionalData) -> Self { + Self { + clear_bytes: decrypted.clear_bytes().to_vec(), + additional_data: decrypted.additional_data().clone(), + } + } + } + + pub fn symmetric_decrypt( + enc_string: String, + key_b64: String, + ) -> Result { + let dec = symmetric_decrypt_to_bytes(enc_string, key_b64)?; + Ok(dec.try_into()?) + } + + pub fn symmetric_decrypt_to_bytes( + enc_string: String, + key_b64: String, + ) -> Result { + let enc_string = EncString::from_str(&enc_string)?; + let key = SymmetricCryptoKey::try_from(key_b64)?; + + let dec: DecryptedWithAdditionalData = enc_string.decrypt_with_key(&key)?; + Ok(dec.into()) + } + + pub fn symmetric_encrypt( + plain: String, + key_b64: String, + encrypt_options: EncryptOptions, + ) -> Result { + if encrypt_options + .additional_data + .is_some_and(|additional_data| additional_data.keys().len() > 0) + { + panic!("Additional data is not supported yet"); + } + + let key = SymmetricCryptoKey::try_from(key_b64)?; + + let encrypted: EncString = plain.encrypt_with_key(&key)?; + Ok(encrypted.to_string()) + } +} + #[wasm_bindgen] pub struct ClientCrypto(Rc);