Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Use-sdk-for-encrypt-decrypt #110

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 53 additions & 26 deletions crates/bitwarden-crypto/src/enc_string/symmetric.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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")]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -241,14 +242,53 @@ impl KeyEncryptable<SymmetricCryptoKey, EncString> for &[u8] {

impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
let dec: DecryptedWithAdditionalData = self.decrypt_with_key(key)?;
Ok(dec.clear_bytes().to_vec())
}
}

impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
self.as_bytes().encrypt_with_key(key)
}
}

impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
self.as_bytes().encrypt_with_key(key)
}
}

impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
let dec: Vec<u8> = self.decrypt_with_key(key)?;
String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
}
}

impl KeyDecryptable<SymmetricCryptoKey, DecryptedWithAdditionalData> 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<DecryptedWithAdditionalData> {
match self {
EncString::AesCbc256_B64 { iv, data } => {
if key.mac_key.is_some() {
return Err(CryptoError::MacNotProvided);
}

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
Expand All @@ -258,37 +298,24 @@ impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> 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<SymmetricCryptoKey, EncString> for String {
fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
self.as_bytes().encrypt_with_key(key)
}
}

impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
self.as_bytes().encrypt_with_key(key)
}
}

impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
let dec: Vec<u8> = 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 {
Expand Down
31 changes: 31 additions & 0 deletions crates/bitwarden-crypto/src/keys/key_encryptable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,37 @@ pub trait KeyDecryptable<Key: CryptoKey, Output> {
fn decrypt_with_key(&self, key: &Key) -> Result<Output>;
}

pub struct DecryptedWithAdditionalData {
clear_text: Vec<u8>,
additional_data: HashMap<String, String>,
}

impl DecryptedWithAdditionalData {
pub fn new(clear_text: Vec<u8>, additional_data: HashMap<String, String>) -> Self {
Self {
clear_text,
additional_data,
}
}

pub fn clear_bytes(&self) -> &[u8] {
&self.clear_text
}

pub fn clear_text_utf8(&self) -> Result<String> {
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<String, String> {
&self.additional_data
}
}

impl<T: KeyEncryptable<Key, Output>, Key: CryptoKey, Output> KeyEncryptable<Key, Option<Output>>
for Option<T>
{
Expand Down
4 changes: 3 additions & 1 deletion crates/bitwarden-crypto/src/keys/mod.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
3 changes: 3 additions & 0 deletions crates/bitwarden-wasm-internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
63 changes: 62 additions & 1 deletion crates/bitwarden-wasm-internal/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<DecryptedString, PureCryptoError> {
Self::setup_once();
pure_crypto::symmetric_decrypt(enc_string, key_b64)
}

pub fn symmetric_decrypt_to_bytes(
enc_string: String,
key_b64: String,
) -> Result<DecryptedBytes, PureCryptoError> {
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<String, PureCryptoError> {
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]
Expand Down
98 changes: 97 additions & 1 deletion crates/bitwarden-wasm-internal/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::rc::Rc;
use std::{rc::Rc, str::FromStr};

use bitwarden_core::{
client::encryption_settings::EncryptionSettingsError,
Expand All @@ -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<String, String>,
}

#[derive(tsify_next::Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct DecryptedBytes {
pub clear_bytes: Vec<u8>,
pub additional_data: HashMap<String, String>,
}

#[derive(tsify_next::Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct EncryptOptions {
pub additional_data: Option<HashMap<String, String>>,
}

impl TryFrom<DecryptedBytes> for DecryptedString {
type Error = PureCryptoError;
fn try_from(decrypted_bytes: DecryptedBytes) -> Result<Self, PureCryptoError> {
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<DecryptedWithAdditionalData> 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<DecryptedString, PureCryptoError> {
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<DecryptedBytes, PureCryptoError> {
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<String, PureCryptoError> {
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<Client>);

Expand Down
Loading