Skip to content
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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@
"zxcvbn"
],
"rust-analyzer.cargo.targetDir": true,
"rust-analyzer.cargo.features": ["all"]
"rust-analyzer.cargo.features": "all"
}
20 changes: 20 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 @@ -55,6 +55,7 @@ serde_bytes = { workspace = true }
serde_repr.workspace = true
sha1 = ">=0.10.5, <0.11"
sha2 = ">=0.10.6, <0.11"
sha3 = "0.10.8"
subtle = { workspace = true }
thiserror = { workspace = true }
tsify = { workspace = true, optional = true }
Expand Down
236 changes: 236 additions & 0 deletions crates/bitwarden-crypto/examples/organization_v2_protocol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
//! Implements the V2 organization protocol, that enhances cryptographic guarantees with respect to
//! a compromised server. Over the V1 protocol, this decomposes the cryptography to establish trust
//! once and only once, to be able to use this trust for other objects such as policies, and to
//! implement a new key transport mechanism that allows key rotation, and also provides sender
//! authentication.

use bitwarden_crypto::{
AsymmetricCryptoKey, DeriveFingerprint, PublicKeyEncryptionAlgorithm, SerializedMessage, Signature, SignatureAlgorithm, SignedObject, SignedPublicKey, SignedPublicKeyMessage, SigningKey, SigningNamespace, SymmetricCryptoKey, VerifyingKey, safe::IdentitySealedKeyEnvelope
};
use serde::{Deserialize, Serialize};

/// Represents a user's cryptographic identity
struct UserIdentity {
name: String,
signing_key: SigningKey,
verifying_key: VerifyingKey,
private_key: AsymmetricCryptoKey,
signed_public_key: SignedPublicKey,
}

/// Represents an organization's cryptographic identity and key material
struct OrganizationIdentity {
name: String,
signing_key: SigningKey,
verifying_key: VerifyingKey,
symmetric_key: SymmetricCryptoKey,
}

/// A claim that an identity belongs to a specific identifier (e.g., email, organization name)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct IdentityClaim {
identity_fingerprint: bitwarden_crypto::KeyFingerprint,
identifier: String,
}

/// An agreement between a member and an organization, signed by both parties
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MembershipAgreement {
member_identity: bitwarden_crypto::KeyFingerprint,
organization_identity: bitwarden_crypto::KeyFingerprint,
}

fn setup_user() -> UserIdentity {
let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519);
let verifying_key = signing_key.to_verifying_key();
let private_key = AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
let signed_public_key = SignedPublicKeyMessage::from_public_key(&private_key.to_public_key())
.expect("Failed to create signed public key message")
.sign(&signing_key)
.expect("Failed to sign public key");

UserIdentity {
name: "Alice".to_string(),
signing_key,
verifying_key,
private_key,
signed_public_key,
}
}

fn setup_organization() -> OrganizationIdentity {
let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519);
let verifying_key = signing_key.to_verifying_key();
let symmetric_key = SymmetricCryptoKey::make_xchacha20_poly1305_key();

OrganizationIdentity {
name: "My Org Name".to_string(),
signing_key,
verifying_key,
symmetric_key,
}
}

// placeholder for out-of-band fingerprint verification
fn prompt_user_to_verify_fingerprint(
_org: &OrganizationIdentity,
) -> bool {
return true;
}

// placeholder for out-of-band fingerprint verification
fn prompt_organization_to_verify_fingerprint(
_member: &UserIdentity,
) -> bool {
return true;
}

/// Step 2: User accepts the invite by signing an identity claim and receiving a membership agreement
/// NOTE: REQUIRES OUT-OF-BAND VERIFICATION OF THE ORGANIZATION'S IDENTITY FINGERPRINT
fn user_accepts_invite(
org: &OrganizationIdentity,
member: &UserIdentity,
) -> (Signature, SerializedMessage, SignedObject) {
let identity_claim = IdentityClaim {
identity_fingerprint: org.verifying_key.fingerprint(),
identifier: org.name.to_string(),
};

// Admin signs the identity claim to assert ownership
let signed_claim = member
.signing_key
.sign(&identity_claim, &SigningNamespace::IdentityClaim)
.expect("Failed to sign identity claim");

let membership_agreement = MembershipAgreement {
member_identity: member.verifying_key.fingerprint(),
organization_identity: org.verifying_key.fingerprint(),
};

let (signature, serialized_message) = member
.signing_key
.sign_detached(&membership_agreement, &SigningNamespace::MembershipAgreement)
.expect("Failed to sign membership agreement");
(signature, serialized_message, signed_claim)
}

/// Step 3: Member verifies and counter-signs the membership agreement
/// NOTE: REQUIRES ADMIN TO FIRST CONFIRM THE MEMBERS NAME TO THE FINGERPRINT OUT-OF-BAND
fn admin_confirms_join(
member: &UserIdentity,
org: &OrganizationIdentity,
signature: &Signature,
serialized_message: &SerializedMessage,
) -> (Signature, IdentitySealedKeyEnvelope, SignedObject) {
// Verify admin's signature
assert!(
signature.verify(
serialized_message.as_bytes(),
&member.verifying_key,
&SigningNamespace::MembershipAgreement,
),
"Failed to verify admin's membership signature"
);

let identity_claim = IdentityClaim {
identity_fingerprint: member.verifying_key.fingerprint(),
identifier: member.name.to_string(),
};
let signed_member_claim = org
.signing_key
.sign(&identity_claim, &SigningNamespace::IdentityClaim)
.expect("Failed to sign member identity claim");

// Counter-sign to indicate acceptance
let counter_signature = org
.signing_key
.counter_sign_detached(
serialized_message.as_bytes().to_vec(),
signature,
&SigningNamespace::MembershipAgreement,
)
.expect("Failed to counter-sign membership agreement");
let envelope = IdentitySealedKeyEnvelope::seal(
&org.signing_key,
&member.verifying_key,
&member.signed_public_key,
&org.symmetric_key,
)
.expect("Failed to seal organization key");
(counter_signature, envelope, signed_member_claim)
}

/// Step 5: Member loads the organization key by verifying all signatures
fn load_shared_vault_key(
member: &UserIdentity,
org: &OrganizationIdentity,
admin_signature: &Signature,
member_signature: &Signature,
serialized_message: &SerializedMessage,
envelope: &IdentitySealedKeyEnvelope,
) -> SymmetricCryptoKey {
// Verify both signatures on the membership agreement
assert!(
admin_signature.verify(
serialized_message.as_bytes(),
&org.verifying_key,
&SigningNamespace::MembershipAgreement,
),
"Failed to verify admin's membership signature"
);
assert!(
member_signature.verify(
serialized_message.as_bytes(),
&member.verifying_key,
&SigningNamespace::MembershipAgreement,
),
"Failed to verify member's membership signature"
);

// Unseal the organization key
let key = envelope
.unseal(
&org.verifying_key,
&member.verifying_key,
&member.private_key,
)
.expect("Failed to unseal organization key");
key
}

fn main() {
// Setup identities
let alice = setup_user();
let org = setup_organization();

// Step 2: Alice accepts the invite
if !prompt_user_to_verify_fingerprint(&org) {
panic!("User did not verify organization fingerprint");
}
let (alice_signature, serialized_message, _signed_org_claim) = user_accepts_invite(&org, &alice);
// upload: alice_signature, serialized_message, _signed_org_claim

// Step 3: Admin confirms alice
if !prompt_organization_to_verify_fingerprint(&alice) {
panic!("Organization did not verify member fingerprint");
}
let (admin_signature, envelope, _signed_member_claim) = admin_confirms_join(&alice, &org, &alice_signature, &serialized_message);
// upload: admin_signature, envelope, _signed_member_claim

// Alice loads her vault, including the organization key
let loaded_vault_key = load_shared_vault_key(
&alice,
&org,
&admin_signature,
&alice_signature,
&serialized_message,
&envelope,
);
assert_eq!(
org.symmetric_key,
loaded_vault_key,
"Loaded key does not match original organization key"
);
}
4 changes: 3 additions & 1 deletion crates/bitwarden-crypto/src/cose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ pub(crate) const ARGON2_SALT: i64 = -71001;
pub(crate) const ARGON2_ITERATIONS: i64 = -71002;
pub(crate) const ARGON2_MEMORY: i64 = -71003;
pub(crate) const ARGON2_PARALLELISM: i64 = -71004;
pub(crate) const IDENTITY_SEALED_ENVELOPE_RECIPIENT_FINGERPRINT: i64 = -71005;
pub(crate) const IDENTITY_SEALED_ENVELOPE_SENDER_FINGERPRINT: i64 = -71006;

// 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";
pub(crate) const CONTENT_TYPE_PADDED_CBOR: &str = "application/x.bitwarden.cbor-padded";
const CONTENT_TYPE_BITWARDEN_LEGACY_KEY: &str = "application/x.bitwarden.legacy-key";
pub(crate) 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
Expand Down
35 changes: 34 additions & 1 deletion crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::pin::Pin;

use rsa::{RsaPrivateKey, RsaPublicKey, pkcs8::DecodePublicKey};
use rsa::{RsaPrivateKey, RsaPublicKey, pkcs8::DecodePublicKey, traits::PublicKeyParts};
use serde_repr::{Deserialize_repr, Serialize_repr};
use sha2::Digest as _;

use super::key_encryptable::CryptoKey;
use crate::{
Pkcs8PrivateKeyBytes, SpkiPublicKeyBytes,
error::{CryptoError, Result},
traits::DeriveFingerprint,
};

/// Algorithm / public key encryption scheme used for encryption/decryption.
Expand Down Expand Up @@ -58,6 +60,37 @@ impl AsymmetricPublicCryptoKey {
}
}

impl DeriveFingerprint for AsymmetricPublicCryptoKey {
fn fingerprint(&self) -> crate::traits::KeyFingerprint {
match &self.inner {
RawPublicKey::RsaOaepSha1(key) => {
// An RSA key has two components - the exponent e and the modulus n. To create a
// canonical representation, we serialize both components in
// big-endian byte order and concatenate. However, to prevent collisions,
// we prefix each of these with the length of the component as a 2-byte big-endian
// integer.
let e = key.e().to_bytes_be();
let e_len = e.len() as u16;
let n = key.n().to_bytes_be();
let n_len = n.len() as u16;
let mut canonical_representation = Vec::with_capacity(4 + e.len() + n.len());
canonical_representation.extend_from_slice(&e_len.to_be_bytes());
canonical_representation.extend_from_slice(&e);
canonical_representation.extend_from_slice(&n_len.to_be_bytes());
canonical_representation.extend_from_slice(&n);

// Now hash the canonical representation with SHA-256 to get the fingerprint
let digest = sha2::Sha256::digest(&canonical_representation);
let arr: [u8; 32] = digest
.as_slice()
.try_into()
.expect("SHA-256 digest should be 32 bytes");
crate::traits::KeyFingerprint(arr)
}
}
}
}

#[derive(Clone)]
pub(crate) enum RawPrivateKey {
// RsaPrivateKey is not a Copy type so this isn't completely necessary, but
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub use signing::*;
mod traits;
mod xchacha20;
pub use traits::{
CompositeEncryptable, Decryptable, IdentifyKey, KeyId, KeyIds, LocalId, PrimitiveEncryptable,
CompositeEncryptable, Decryptable, IdentifyKey, KeyId, KeyIds, LocalId, PrimitiveEncryptable, KeyFingerprint, DeriveFingerprint
};
pub use zeroizing_alloc::ZeroAlloc as ZeroizingAllocator;

Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-crypto/src/rsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub(crate) fn make_key_pair(key: &SymmetricCryptoKey) -> Result<RsaKeyPair> {
}

/// Encrypt data using RSA-OAEP-SHA1 with a 2048 bit key
pub(super) fn encrypt_rsa2048_oaep_sha1(public_key: &RsaPublicKey, data: &[u8]) -> Result<Vec<u8>> {
pub(crate) fn encrypt_rsa2048_oaep_sha1(public_key: &RsaPublicKey, data: &[u8]) -> Result<Vec<u8>> {
let mut rng = rand::thread_rng();

let padding = Oaep::new::<Sha1>();
Expand Down
9 changes: 9 additions & 0 deletions crates/bitwarden-crypto/src/safe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,12 @@ Use the data envelope to protect a struct (document) of data. Examples include:
The serialization of the data and the creation of a content encryption key is handled internally.
Calling the API with a decrypted struct, the content encryption key ID and the encrypted data are
returned.

## Identity-sealed key envelope

Use the identity sealed key envelope to share a symmetric key from one cryptographic identity to another cryptographic identity. Example use-cases include:
- Sharing a symmetric key for emergency access
- Sharing a symmetric key for organization membership
- Sharing a symmetric key for ad-hoc item sharing

This provides sender authentication, so that the recipient knows that the key was intended for them, and knows who it was sent by.
Loading
Loading