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

[PM-5791] Change decrypt to return Sensitive #536

Merged
merged 29 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8c869f2
Explore introducing a Decrypted struct
Hinton Jan 19, 2024
74a5b08
Replace with std::mem::take
Hinton Jan 19, 2024
c57bea6
Merge branch 'main' of github.com:bitwarden/sdk into ps/zeroize-decry…
Hinton Jan 25, 2024
d5f2ae5
Document and introduce type alias
Hinton Jan 25, 2024
97ecc2c
Format
Hinton Jan 25, 2024
449466a
Add Decrypted to asymmetric
Hinton Jan 25, 2024
ed4d92b
Use Decrypt everywhere
Hinton Jan 25, 2024
18cf0d6
Use boxed value and fix tests
Hinton Jan 26, 2024
3d247ad
Merge branch 'main' of github.com:bitwarden/sdk into ps/zeroize-decry…
Hinton Jan 26, 2024
b848625
Resolve review feedback
Hinton Jan 29, 2024
d85fc9f
Extract a common sensitive type.
Hinton Jan 29, 2024
d7eedaa
Format
Hinton Jan 29, 2024
cb5f43a
Merge branch 'main' into ps/zeroize-decrypted
dani-garcia Mar 7, 2024
ba14e8c
Update exporters to use Sensitive
dani-garcia Mar 7, 2024
19fecae
Add support for base64 encode/decode for the Sensitive types
dani-garcia Mar 7, 2024
cece11e
Merge branch 'main' into ps/zeroize-decrypted
dani-garcia Mar 11, 2024
ef8ba37
Make base64 memory test required
dani-garcia Mar 11, 2024
621cf72
Merge branch 'main' of github.com:bitwarden/sdk into ps/zeroize-decry…
Hinton Apr 22, 2024
b87bd95
Use ::test in tests
Hinton Apr 22, 2024
37b18ad
Mark send key as sensitive
Hinton Apr 22, 2024
b2c3ff3
Merge branch 'main' of github.com:bitwarden/sdk into ps/zeroize-decry…
Hinton Apr 22, 2024
1f731a8
Remove accidentally duplicated generate_cipher
Hinton Apr 22, 2024
ef08b7c
Convert Send::derive_shareable_key to expect SensitiveVec
Hinton Apr 23, 2024
16ea5c3
Fix compile
Hinton Apr 23, 2024
31a02f8
Refactor sub_title to not expose temporary values
Hinton Apr 23, 2024
456f54f
Merge branch 'main' of github.com:bitwarden/sdk into ps/zeroize-decry…
Hinton Apr 23, 2024
0d40054
Shore up uri checksum
Hinton Apr 23, 2024
da787a2
Remove converter for DecryptedString
Hinton Apr 25, 2024
7baa6f1
Review feedback
Hinton Apr 25, 2024
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
15 changes: 15 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/bitwarden-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ subtle = ">=2.5.0, <3.0"
thiserror = ">=1.0.40, <2.0"
uniffi = { version = "=0.25.2", optional = true }
uuid = { version = ">=1.3.3, <2.0", features = ["serde"] }
zeroize = { version = ">=1.7.0, <2.0", features = ["zeroize_derive"] }

[dev-dependencies]
rand_chacha = "0.3.1"
Expand Down
108 changes: 108 additions & 0 deletions crates/bitwarden-crypto/src/decrypted.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use std::{
borrow::Cow,
fmt::{self, Formatter},
};

use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use zeroize::{Zeroize, ZeroizeOnDrop};

use crate::{CryptoError, CryptoKey, KeyEncryptable};

/// A wrapper for decrypted values.
///
/// Implements `Zeroize` and `ZeroizeOnDrop` to ensure that the value is zeroized on drop. Please be
/// careful if cloning or copying the inner value using `expose` since any copy will not be
/// zeroized.
#[derive(Zeroize, ZeroizeOnDrop, PartialEq, Clone)]
pub struct Decrypted<V: Zeroize> {
value: V,
}

impl<V: Zeroize> Decrypted<V> {
pub fn new(value: V) -> Self {
Self { value }
}

/// Expose the inner value. By exposing the inner value, you take responsibility for ensuring
/// that any copy of the value is zeroized.
pub fn expose(&self) -> &V {
&self.value
}
}

/// Helper to convert a `Decrypted<Vec<u8>>` to a `Decrypted<String>`, care is taken to ensure any
/// intermediate copies are zeroed to avoid leaking sensitive data.
impl TryFrom<DecryptedVec> for DecryptedString {
type Error = CryptoError;

fn try_from(mut v: DecryptedVec) -> Result<Self, CryptoError> {
let value = std::mem::take(&mut v.value);

let rtn = String::from_utf8(value).map_err(|_| CryptoError::InvalidUtf8String);
rtn.map(Decrypted::new)
}
}

impl<V: Zeroize + Default> Default for Decrypted<V> {
fn default() -> Self {
Self::new(V::default())
}
}

impl<V: Zeroize + Serialize> fmt::Debug for Decrypted<V> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Decrypted")
.field("value", &"********")
dani-garcia marked this conversation as resolved.
Show resolved Hide resolved
.finish()
}
}

/// Unfortunately once we serialize a `DecryptedString` we can't control the future memory.
impl<V: Zeroize + Serialize> Serialize for Decrypted<V> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.value.serialize(serializer)
}
}

impl<'de, V: Zeroize + Deserialize<'de>> Deserialize<'de> for Decrypted<V> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::new(V::deserialize(deserializer)?))
}
}

/// Transparently expose the inner value for serialization
impl<V: Zeroize + JsonSchema> JsonSchema for Decrypted<V> {
fn schema_name() -> String {
V::schema_name()
}

fn schema_id() -> Cow<'static, str> {
V::schema_id()
}

fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
V::json_schema(gen)
}
}

/**
impl<K: CryptoKey, V: Zeroize + Clone + KeyEncryptable<K, O>> KeyEncryptable<K, V>
for Decrypted<V>
{
fn encrypt_with_key(self, key: &K) -> Result<O, CryptoError> {
self.value.clone().encrypt_with_key(key)
}
}
**/

impl<T: KeyEncryptable<Key, Output> + Zeroize + Clone, Key: CryptoKey, Output>
KeyEncryptable<Key, Output> for Decrypted<T>
{
fn encrypt_with_key(self, key: &Key) -> Result<Output, CryptoError> {
self.value.clone().encrypt_with_key(key)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Todo: need to validate what happens with the string after encrypting it. If we consume it we should zero it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we should probably add the zeroize in the KeyEncryptable implementation of the EncStrings, which is where the String is consumed finally.

Also, we could avoid the extra clone here by doing

std::mem::take(&mut self.value).encrypt_with_key(key)

It would mean adding a Default bound to the impl, though

}
}

pub type DecryptedVec = Decrypted<Vec<u8>>;
pub type DecryptedString = Decrypted<String>;
28 changes: 16 additions & 12 deletions crates/bitwarden-crypto/src/enc_string/asymmetric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde::Deserialize;

use super::{from_b64_vec, split_enc_string};
use crate::{
decrypted::{DecryptedString, DecryptedVec},
error::{CryptoError, EncStringParseError, Result},
rsa::encrypt_rsa2048_oaep_sha1,
AsymmetricCryptoKey, AsymmetricEncryptable, KeyDecryptable,
Expand Down Expand Up @@ -160,8 +161,8 @@ impl AsymmetricEncString {
}
}

impl KeyDecryptable<AsymmetricCryptoKey, Vec<u8>> for AsymmetricEncString {
fn decrypt_with_key(&self, key: &AsymmetricCryptoKey) -> Result<Vec<u8>> {
impl KeyDecryptable<AsymmetricCryptoKey, DecryptedVec> for AsymmetricEncString {
fn decrypt_with_key(&self, key: &AsymmetricCryptoKey) -> Result<DecryptedVec> {
use AsymmetricEncString::*;
match self {
Rsa2048_OaepSha256_B64 { data } => key.key.decrypt(Oaep::new::<sha2::Sha256>(), data),
Expand All @@ -175,14 +176,15 @@ impl KeyDecryptable<AsymmetricCryptoKey, Vec<u8>> for AsymmetricEncString {
key.key.decrypt(Oaep::new::<sha1::Sha1>(), data)
}
}
.map(DecryptedVec::new)
.map_err(|_| CryptoError::KeyDecrypt)
}
}

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

Expand All @@ -200,6 +202,8 @@ impl schemars::JsonSchema for AsymmetricEncString {

#[cfg(test)]
mod tests {
use crate::DecryptedString;

use super::{AsymmetricCryptoKey, AsymmetricEncString, KeyDecryptable};

const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
Expand Down Expand Up @@ -239,8 +243,8 @@ XKZBokBGnjFnTnKcs7nv/O8=

assert_eq!(enc_string.enc_type(), 3);

let res: String = enc_string.decrypt_with_key(&private_key).unwrap();
assert_eq!(res, "EncryptMe!");
let res: DecryptedString = enc_string.decrypt_with_key(&private_key).unwrap();
assert_eq!(res.expose(), "EncryptMe!");
}

#[test]
Expand All @@ -251,8 +255,8 @@ XKZBokBGnjFnTnKcs7nv/O8=

assert_eq!(enc_string.enc_type(), 4);

let res: String = enc_string.decrypt_with_key(&private_key).unwrap();
assert_eq!(res, "EncryptMe!");
let res: DecryptedString = enc_string.decrypt_with_key(&private_key).unwrap();
assert_eq!(res.expose(), "EncryptMe!");
}

#[test]
Expand All @@ -263,8 +267,8 @@ XKZBokBGnjFnTnKcs7nv/O8=

assert_eq!(enc_string.enc_type(), 6);

let res: String = enc_string.decrypt_with_key(&private_key).unwrap();
assert_eq!(res, "EncryptMe!");
let res: DecryptedString = enc_string.decrypt_with_key(&private_key).unwrap();
assert_eq!(res.expose(), "EncryptMe!");
}

#[test]
Expand Down
27 changes: 14 additions & 13 deletions crates/bitwarden-crypto/src/enc_string/symmetric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use serde::Deserialize;

use super::{check_length, from_b64, from_b64_vec, split_enc_string};
use crate::{
decrypted::{DecryptedString, DecryptedVec},
error::{CryptoError, EncStringParseError, Result},
KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey,
Decrypted, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey,
};

/// # Encrypted string primitive
Expand Down Expand Up @@ -228,13 +229,13 @@ impl KeyEncryptable<SymmetricCryptoKey, EncString> for &[u8] {
}
}

impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
impl KeyDecryptable<SymmetricCryptoKey, DecryptedVec> for EncString {
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<DecryptedVec> {
match self {
EncString::AesCbc256_HmacSha256_B64 { iv, mac, data } => {
let mac_key = key.mac_key.ok_or(CryptoError::InvalidMac)?;
let dec = crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), mac_key, key.key)?;
Ok(dec)
Ok(Decrypted::new(dec))
}
_ => Err(CryptoError::InvalidKey),
}
Expand All @@ -247,10 +248,10 @@ impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
}
}

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, DecryptedString> for EncString {
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<DecryptedString> {
let dec: DecryptedVec = self.decrypt_with_key(key)?;
dec.try_into()
}
}

Expand All @@ -269,17 +270,17 @@ impl schemars::JsonSchema for EncString {
#[cfg(test)]
mod tests {
use super::EncString;
use crate::{derive_symmetric_key, KeyDecryptable, KeyEncryptable};
use crate::{decrypted::DecryptedString, derive_symmetric_key, KeyDecryptable, KeyEncryptable};

#[test]
fn test_enc_string_roundtrip() {
let key = derive_symmetric_key("test");

let test_string = "encrypted_test_string".to_string();
let cipher = test_string.clone().encrypt_with_key(&key).unwrap();
let test_string = "encrypted_test_string";
let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();

let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
assert_eq!(decrypted_str, test_string);
let decrypted_str: DecryptedString = cipher.decrypt_with_key(&key).unwrap();
assert_eq!(decrypted_str.expose(), test_string);
}

#[test]
Expand Down
7 changes: 4 additions & 3 deletions crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ mod tests {
use base64::{engine::general_purpose::STANDARD, Engine};

use crate::{
AsymmetricCryptoKey, AsymmetricEncString, AsymmetricPublicCryptoKey, KeyDecryptable,
AsymmetricCryptoKey, AsymmetricEncString, AsymmetricPublicCryptoKey, DecryptedString,
KeyDecryptable,
};

#[test]
Expand Down Expand Up @@ -201,8 +202,8 @@ DnqOsltgPomWZ7xVfMkm9niL2OA=
let encrypted =
AsymmetricEncString::encrypt_rsa2048_oaep_sha1(plaintext.as_bytes(), &public_key)
.unwrap();
let decrypted: String = encrypted.decrypt_with_key(&private_key).unwrap();
let decrypted: DecryptedString = encrypted.decrypt_with_key(&private_key).unwrap();

assert_eq!(plaintext, decrypted);
assert_eq!(plaintext, decrypted.expose());
}
}
14 changes: 8 additions & 6 deletions crates/bitwarden-crypto/src/keys/device_key.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
error::Result, AsymmetricCryptoKey, AsymmetricEncString, EncString, KeyDecryptable,
KeyEncryptable, SymmetricCryptoKey, UserKey,
decrypted::DecryptedVec, error::Result, AsymmetricCryptoKey, AsymmetricEncString, EncString,
KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, UserKey,
};

/// Device Key
Expand Down Expand Up @@ -60,11 +60,13 @@ impl DeviceKey {
protected_device_private_key: EncString,
protected_user_key: AsymmetricEncString,
) -> Result<UserKey> {
let device_private_key: Vec<u8> = protected_device_private_key.decrypt_with_key(&self.0)?;
let device_private_key = AsymmetricCryptoKey::from_der(device_private_key.as_slice())?;
let device_private_key: DecryptedVec =
protected_device_private_key.decrypt_with_key(&self.0)?;
let device_private_key =
AsymmetricCryptoKey::from_der(device_private_key.expose().as_slice())?;

let dec: Vec<u8> = protected_user_key.decrypt_with_key(&device_private_key)?;
let user_key: SymmetricCryptoKey = dec.as_slice().try_into()?;
let dec: DecryptedVec = protected_user_key.decrypt_with_key(&device_private_key)?;
let user_key: SymmetricCryptoKey = dec.expose().as_slice().try_into()?;

Ok(UserKey(user_key))
}
Expand Down
5 changes: 3 additions & 2 deletions crates/bitwarden-crypto/src/keys/master_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
use sha2::Digest;

use crate::{
decrypted::DecryptedVec,
util::{self, hkdf_expand},
EncString, KeyDecryptable, Result, SymmetricCryptoKey, UserKey,
};
Expand Down Expand Up @@ -59,8 +60,8 @@ impl MasterKey {
pub fn decrypt_user_key(&self, user_key: EncString) -> Result<SymmetricCryptoKey> {
let stretched_key = stretch_master_key(self)?;

let dec: Vec<u8> = user_key.decrypt_with_key(&stretched_key)?;
SymmetricCryptoKey::try_from(dec.as_slice())
let dec: DecryptedVec = user_key.decrypt_with_key(&stretched_key)?;
SymmetricCryptoKey::try_from(dec.expose().as_slice())
}

pub fn encrypt_user_key(&self, user_key: &SymmetricCryptoKey) -> Result<EncString> {
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-crypto/src/keys/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod key_encryptable;
pub use key_encryptable::{KeyDecryptable, KeyEncryptable};
pub use key_encryptable::{CryptoKey, KeyDecryptable, KeyEncryptable};
mod master_key;
pub use master_key::{HashPurpose, Kdf, MasterKey};
mod shareable_key;
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub use util::generate_random_bytes;
mod wordlist;
pub use util::pbkdf2;
pub use wordlist::EFF_LONG_WORD_LIST;
mod decrypted;
pub use decrypted::{Decrypted, DecryptedString, DecryptedVec};

#[cfg(feature = "mobile")]
uniffi::setup_scaffolding!();
Expand Down
6 changes: 4 additions & 2 deletions crates/bitwarden/src/auth/auth_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ pub(crate) fn auth_request_decrypt_user_key(
private_key: String,
user_key: AsymmetricEncString,
) -> Result<SymmetricCryptoKey, Error> {
use bitwarden_crypto::DecryptedString;

let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?;
let key: String = user_key.decrypt_with_key(&key)?;
let key: DecryptedString = user_key.decrypt_with_key(&key)?;

Ok(key.parse()?)
Ok(key.expose().parse()?)
}

/// Approve an auth request.
Expand Down
Loading