Skip to content
8 changes: 5 additions & 3 deletions crates/bitwarden-core/src/client/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use bitwarden_crypto::KeyStore;
#[cfg(any(feature = "internal", feature = "secrets"))]
use bitwarden_crypto::SymmetricCryptoKey;
#[cfg(feature = "internal")]
use bitwarden_crypto::{CryptoError, EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey};
use bitwarden_crypto::{
CryptoError, EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey,
safe::PasswordProtectedKeyEnvelope,
};
#[cfg(feature = "internal")]
use bitwarden_state::registry::StateRegistry;
use chrono::Utc;
Expand All @@ -26,8 +29,7 @@ use crate::{
},
error::NotAuthenticatedError,
key_management::{
MasterPasswordUnlockData, PasswordProtectedKeyEnvelope, SecurityState, SignedSecurityState,
crypto::InitUserCryptoRequest,
MasterPasswordUnlockData, SecurityState, SignedSecurityState, crypto::InitUserCryptoRequest,
},
};

Expand Down
11 changes: 3 additions & 8 deletions crates/bitwarden-core/src/key_management/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use bitwarden_crypto::{
AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable,
KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, SignatureAlgorithm,
SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey,
UserKey, dangerous_get_v2_rotated_account_keys, safe::PasswordProtectedKeyEnvelopeError,
UserKey, dangerous_get_v2_rotated_account_keys,
safe::{PasswordProtectedKeyEnvelope, PasswordProtectedKeyEnvelopeError},
};
use bitwarden_encoding::B64;
use bitwarden_error::bitwarden_error;
Expand All @@ -26,7 +27,6 @@ use crate::{
key_management::{
AsymmetricKeyId, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId,
master_password::{MasterPasswordAuthenticationData, MasterPasswordUnlockData},
non_generic_wrappers::PasswordProtectedKeyEnvelope,
},
};

Expand Down Expand Up @@ -429,12 +429,7 @@ pub(super) fn enroll_pin(
let key_store = client.internal.get_key_store();
let mut ctx = key_store.context_mut();

let key_envelope =
PasswordProtectedKeyEnvelope(bitwarden_crypto::safe::PasswordProtectedKeyEnvelope::seal(
SymmetricKeyId::User,
&pin,
&ctx,
)?);
let key_envelope = PasswordProtectedKeyEnvelope::seal(SymmetricKeyId::User, &pin, &ctx)?;
let encrypted_pin = pin.encrypt(&mut ctx, SymmetricKeyId::User)?;
Ok(EnrollPinResponse {
pin_protected_user_key_envelope: key_envelope,
Expand Down
4 changes: 2 additions & 2 deletions crates/bitwarden-core/src/key_management/crypto_client.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[cfg(feature = "wasm")]
use bitwarden_crypto::safe::PasswordProtectedKeyEnvelope;
use bitwarden_crypto::{CryptoError, Decryptable, Kdf};
#[cfg(feature = "internal")]
use bitwarden_crypto::{EncString, UnsignedSharedKey};
Expand All @@ -10,8 +12,6 @@ use super::crypto::{
MakeKeyPairResponse, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
derive_key_connector, make_key_pair, verify_asymmetric_keys,
};
#[cfg(any(feature = "wasm", test))]
use crate::key_management::PasswordProtectedKeyEnvelope;
#[cfg(feature = "internal")]
use crate::key_management::{
SymmetricKeyId,
Expand Down
4 changes: 0 additions & 4 deletions crates/bitwarden-core/src/key_management/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ pub use master_password::MasterPasswordError;
#[cfg(feature = "internal")]
pub(crate) use master_password::{MasterPasswordAuthenticationData, MasterPasswordUnlockData};
#[cfg(feature = "internal")]
mod non_generic_wrappers;
#[cfg(feature = "internal")]
pub(crate) use non_generic_wrappers::*;
#[cfg(feature = "internal")]
mod security_state;
#[cfg(feature = "internal")]
pub use security_state::{SecurityState, SignedSecurityState};
Expand Down

This file was deleted.

11 changes: 3 additions & 8 deletions crates/bitwarden-core/src/uniffi_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

use std::{num::NonZeroU32, str::FromStr};

use bitwarden_crypto::safe;
use bitwarden_uniffi_error::convert_result;
use uuid::Uuid;

use crate::key_management::{PasswordProtectedKeyEnvelope, SignedSecurityState};
use crate::key_management::SignedSecurityState;

uniffi::use_remote_type!(bitwarden_crypto::NonZeroU32);
uniffi::use_remote_type!(bitwarden_crypto::safe::PasswordProtectedKeyEnvelope);

type DateTime = chrono::DateTime<chrono::Utc>;
uniffi::custom_type!(DateTime, std::time::SystemTime, { remote });
Expand All @@ -33,10 +35,3 @@ uniffi::custom_type!(SignedSecurityState, String, {
},
lower: |obj| obj.into(),
});

uniffi::custom_type!(PasswordProtectedKeyEnvelope, String, {
remote,
try_lift: |val| convert_result(bitwarden_crypto::safe::PasswordProtectedKeyEnvelope::from_str(&val)
.map(PasswordProtectedKeyEnvelope)),
lower: |obj| obj.0.into(),
});
11 changes: 5 additions & 6 deletions crates/bitwarden-crypto/examples/protect_key_with_password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,11 @@ fn main() {
ctx.clear_local();

// Load the envelope from disk and unseal it with the PIN, and store it in the context.
let deserialized: PasswordProtectedKeyEnvelope<ExampleIds> =
PasswordProtectedKeyEnvelope::try_from(
disk.load("vault_key_envelope")
.expect("Loading from disk should work"),
)
.expect("Deserializing envelope should work");
let deserialized: PasswordProtectedKeyEnvelope = PasswordProtectedKeyEnvelope::try_from(
disk.load("vault_key_envelope")
.expect("Loading from disk should work"),
)
.expect("Deserializing envelope should work");
deserialized
.unseal(ExampleSymmetricKey::VaultKey, pin, &mut ctx)
.expect("Unsealing should work");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//! single recipient's unprotected headers. The output from the KDF - "envelope key", is used to
//! wrap the symmetric key, that is sealed by the envelope.

use std::{marker::PhantomData, num::TryFromIntError, str::FromStr};
use std::{num::TryFromIntError, str::FromStr};

use argon2::Params;
use bitwarden_encoding::{B64, FromStrVisitor};
Expand All @@ -22,6 +22,8 @@ use coset::{CborSerializable, CoseError, Header, HeaderBuilder};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(feature = "wasm")]
use wasm_bindgen::convert::FromWasmAbi;

use crate::{
BitwardenLegacyKeyBytes, ContentFormat, CoseKeyBytes, EncodedSymmetricKey, KeyIds,
Expand All @@ -47,17 +49,16 @@ const ENVELOPE_ARGON2_OUTPUT_KEY_SIZE: usize = 32;
/// be provided.
///
/// Internally, Argon2 is used as the KDF and XChaCha20-Poly1305 is used to encrypt the key.
pub struct PasswordProtectedKeyEnvelope<Ids: KeyIds> {
_phantom: PhantomData<Ids>,
pub struct PasswordProtectedKeyEnvelope {
cose_encrypt: coset::CoseEncrypt,
}

impl<Ids: KeyIds> PasswordProtectedKeyEnvelope<Ids> {
impl PasswordProtectedKeyEnvelope {
/// Seals a symmetric key with a password, using the current default KDF parameters and a random
/// salt.
///
/// This should never fail, except for memory allocation error, when running the KDF.
pub fn seal(
pub fn seal<Ids: KeyIds>(
key_to_seal: Ids::Symmetric,
password: &str,
ctx: &KeyStoreContext<Ids>,
Expand Down Expand Up @@ -129,15 +130,12 @@ impl<Ids: KeyIds> PasswordProtectedKeyEnvelope<Ids> {
.build();
cose_encrypt.unprotected.iv = nonce.into();

Ok(PasswordProtectedKeyEnvelope {
_phantom: PhantomData,
cose_encrypt,
})
Ok(PasswordProtectedKeyEnvelope { cose_encrypt })
}

/// Unseals a symmetric key from the password-protected envelope, and stores it in the key store
/// context.
pub fn unseal(
pub fn unseal<Ids: KeyIds>(
&self,
target_keyslot: Ids::Symmetric,
password: &str,
Expand Down Expand Up @@ -229,36 +227,33 @@ impl<Ids: KeyIds> PasswordProtectedKeyEnvelope<Ids> {
}
}

impl<Ids: KeyIds> From<&PasswordProtectedKeyEnvelope<Ids>> for Vec<u8> {
fn from(val: &PasswordProtectedKeyEnvelope<Ids>) -> Self {
impl From<&PasswordProtectedKeyEnvelope> for Vec<u8> {
fn from(val: &PasswordProtectedKeyEnvelope) -> Self {
val.cose_encrypt
.clone()
.to_vec()
.expect("Serialization to cose should not fail")
}
}

impl<Ids: KeyIds> TryFrom<&Vec<u8>> for PasswordProtectedKeyEnvelope<Ids> {
impl TryFrom<&Vec<u8>> for PasswordProtectedKeyEnvelope {
type Error = CoseError;

fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
let cose_encrypt = coset::CoseEncrypt::from_slice(value)?;
Ok(PasswordProtectedKeyEnvelope {
_phantom: PhantomData,
cose_encrypt,
})
Ok(PasswordProtectedKeyEnvelope { cose_encrypt })
}
}

impl<Ids: KeyIds> std::fmt::Debug for PasswordProtectedKeyEnvelope<Ids> {
impl std::fmt::Debug for PasswordProtectedKeyEnvelope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PasswordProtectedKeyEnvelope")
.field("cose_encrypt", &self.cose_encrypt)
.finish()
}
}

impl<Ids: KeyIds> FromStr for PasswordProtectedKeyEnvelope<Ids> {
impl FromStr for PasswordProtectedKeyEnvelope {
type Err = PasswordProtectedKeyEnvelopeError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Expand All @@ -275,14 +270,14 @@ impl<Ids: KeyIds> FromStr for PasswordProtectedKeyEnvelope<Ids> {
}
}

impl<Ids: KeyIds> From<PasswordProtectedKeyEnvelope<Ids>> for String {
fn from(val: PasswordProtectedKeyEnvelope<Ids>) -> Self {
impl From<PasswordProtectedKeyEnvelope> for String {
fn from(val: PasswordProtectedKeyEnvelope) -> Self {
let serialized: Vec<u8> = (&val).into();
B64::from(serialized).to_string()
}
}

impl<'de, Ids: KeyIds> Deserialize<'de> for PasswordProtectedKeyEnvelope<Ids> {
impl<'de> Deserialize<'de> for PasswordProtectedKeyEnvelope {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
Expand All @@ -291,7 +286,7 @@ impl<'de, Ids: KeyIds> Deserialize<'de> for PasswordProtectedKeyEnvelope<Ids> {
}
}

impl<Ids: KeyIds> Serialize for PasswordProtectedKeyEnvelope<Ids> {
impl Serialize for PasswordProtectedKeyEnvelope {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
Expand Down Expand Up @@ -447,6 +442,30 @@ impl From<TryFromIntError> for PasswordProtectedKeyEnvelopeError {
}
}

#[cfg(feature = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
const TS_CUSTOM_TYPES: &'static str = r#"
export type PasswordProtectedKeyEnvelope = Tagged<string, "PasswordProtectedKeyEnvelope">;
"#;

#[cfg(feature = "wasm")]
impl wasm_bindgen::describe::WasmDescribe for PasswordProtectedKeyEnvelope {
fn describe() {
<String as wasm_bindgen::describe::WasmDescribe>::describe();
}
}

#[cfg(feature = "wasm")]
impl FromWasmAbi for PasswordProtectedKeyEnvelope {
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@quexten quexten Oct 15, 2025

Choose a reason for hiding this comment

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

Previously, because we had the newtype wrapper, we could use tsify to auto-generate it. For just working with the type directly, I did not find a good way to use tsify other than creating a newtype in the same file, which did not seem like the right solution.

That said, tsify just generates FromWasmAbi. https://github.com/madonoharu/tsify/blob/ab399fe86761e94f99bdb57a0b18a78d54b08671/tsify-macros/src/wasm_bindgen.rs#L202

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@Thomas-Avery Thomas-Avery Oct 15, 2025

Choose a reason for hiding this comment

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

Thank you for the context. I guess if it breaks in the future we can just follow whatever tsify comes up with to solve for it. I would like someone with more wasm bindgen experience than me to review.

Copy link
Contributor Author

@quexten quexten Oct 15, 2025

Choose a reason for hiding this comment

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

Leaving this comment open until the review from @bitwarden/team-sdk-sme

Copy link
Member

Choose a reason for hiding this comment

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

Yea it's unstable but a lot of things depends on it and we'll know if it stops compiling.

Copy link
Member

Choose a reason for hiding this comment

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

Sadly TSify doesn't support this type of string type-alias pattern we're doing, so manual implementation seems like the way to go. If we find ourselves doing it a lot we could consider introducing a macro for that.

We might also want to implement IntoWasmAbi for symmetry, even if it's not used yet.

impl IntoWasmAbi for PasswordProtectedKeyEnvelope {
    type Abi = <String as IntoWasmAbi>::Abi;
    fn into_abi(self) -> Self::Abi {
        let string: String = self.into();
        string.into_abi()
    }
}

type Abi = <String as FromWasmAbi>::Abi;

unsafe fn from_abi(abi: Self::Abi) -> Self {
use wasm_bindgen::UnwrapThrowExt;
let string = unsafe { String::from_abi(abi) };
PasswordProtectedKeyEnvelope::from_str(&string).unwrap_throw()
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -547,7 +566,7 @@ mod tests {
let serialized: Vec<u8> = (&envelope).into();

// Unseal the key from the envelope
let deserialized: PasswordProtectedKeyEnvelope<TestIds> =
let deserialized: PasswordProtectedKeyEnvelope =
PasswordProtectedKeyEnvelope::try_from(&serialized).unwrap();
deserialized
.unseal(TestSymmKey::A(1), password, &mut ctx)
Expand Down Expand Up @@ -580,7 +599,7 @@ mod tests {
let serialized: Vec<u8> = (&envelope).into();

// Unseal the key from the envelope
let deserialized: PasswordProtectedKeyEnvelope<TestIds> =
let deserialized: PasswordProtectedKeyEnvelope =
PasswordProtectedKeyEnvelope::try_from(&serialized).unwrap();
deserialized
.unseal(TestSymmKey::A(1), password, &mut ctx)
Expand All @@ -607,7 +626,7 @@ mod tests {
let new_password = "new_test_password";

// Seal the key with a password
let envelope: PasswordProtectedKeyEnvelope<TestIds> =
let envelope: PasswordProtectedKeyEnvelope =
PasswordProtectedKeyEnvelope::seal_ref(&key, password).expect("Sealing should work");

// Reseal
Expand Down Expand Up @@ -635,7 +654,7 @@ mod tests {
let envelope = PasswordProtectedKeyEnvelope::seal(test_key, password, &ctx).unwrap();

// Attempt to unseal with the wrong password
let deserialized: PasswordProtectedKeyEnvelope<TestIds> =
let deserialized: PasswordProtectedKeyEnvelope =
PasswordProtectedKeyEnvelope::try_from(&(&envelope).into()).unwrap();
assert!(matches!(
deserialized.unseal(TestSymmKey::A(1), wrong_password, &mut ctx),
Expand Down
10 changes: 9 additions & 1 deletion crates/bitwarden-crypto/src/uniffi_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use std::{num::NonZeroU32, str::FromStr};

use bitwarden_uniffi_error::convert_result;

use crate::{CryptoError, EncString, SignedPublicKey, UnsignedSharedKey};
use crate::{
CryptoError, EncString, SignedPublicKey, UnsignedSharedKey, safe::PasswordProtectedKeyEnvelope,
};

uniffi::custom_type!(NonZeroU32, u32, {
remote,
Expand Down Expand Up @@ -32,3 +34,9 @@ uniffi::custom_type!(SignedPublicKey, String, {
},
lower: |obj| obj.into(),
});

uniffi::custom_type!(PasswordProtectedKeyEnvelope, String, {
remote,
try_lift: |val| convert_result(PasswordProtectedKeyEnvelope::from_str(&val)),
lower: |obj| obj.into(),
});
Loading