Skip to content

Commit 0107af7

Browse files
authored
[PM-28845] Add PureCrypto functions to replace webcrypto RSA entirely (#592)
## 🎟️ Tracking <!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. --> bitwarden/clients#17742 https://bitwarden.atlassian.net/browse/PM-28845 ## 📔 Objective <!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. --> Preparing to nuke webcrypto's RSA implementation out of orbit. It is unreliable and we have reproduced around 50% of all private keys keys are generated by rustcrypto that are not parseable on webcrypto on Safari (but parseable on chrome / firefox). It is unclear why, most likely safari has some stricter condition for the keys (RSA ASN1 has many conditions that may apply here), or has a bug. We also suspect keys generated on old Xamarin PCLCrypto to be parseable by rust, or at least deliver good error messages on rust, but be unparseable on all of webcrypto. This changes the functions to be exactly the same as webcrypto so that they can be dropped in without refactoring anything else. Note: Usually, implementing crypto directly in the wasm crate would not be a good idea, *however* is is justifiable here because we DO NOT WANT these to be public functions consumeable by any other crate, so they do not belong in the crypto crate. They are also only used by the wasm clients. ## 🚨 Breaking Changes <!-- Does this PR introduce any breaking changes? If so, please describe the impact and migration path for clients. If you're unsure, the automated TypeScript compatibility check will run when you open/update this PR and provide feedback. For breaking changes: 1. Describe what changed in the client interface 2. Explain why the change was necessary 3. Provide migration steps for client developers 4. Link to any paired client PRs if needed Otherwise, you can remove this section. --> ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes
1 parent 868dace commit 0107af7

File tree

3 files changed

+83
-24
lines changed

3 files changed

+83
-24
lines changed

Cargo.lock

Lines changed: 12 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-wasm-internal/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ bitwarden-state = { workspace = true, features = ["wasm"] }
3333
bitwarden-threading = { workspace = true }
3434
bitwarden-vault = { workspace = true, features = ["wasm"] }
3535
console_error_panic_hook = "0.1.7"
36+
rand = ">=0.8.5, <0.9"
37+
rsa = ">=0.9.2, <0.10"
3638
serde = { workspace = true }
39+
sha1 = ">=0.10.5, <0.11"
3740
tracing = { workspace = true }
3841
tracing-subscriber = { workspace = true }
3942
tracing-web = { version = "0.1.3" }

crates/bitwarden-wasm-internal/src/pure_crypto.rs

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ use bitwarden_crypto::{
77
AsymmetricCryptoKey, AsymmetricPublicCryptoKey, BitwardenLegacyKeyBytes, CoseKeyBytes,
88
CoseSerializable, CoseSign1Bytes, CryptoError, Decryptable, EncString, Kdf, KeyDecryptable,
99
KeyEncryptable, KeyStore, MasterKey, OctetStreamBytes, Pkcs8PrivateKeyBytes,
10-
PrimitiveEncryptable, SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes,
11-
SymmetricCryptoKey, UnsignedSharedKey, VerifyingKey,
10+
PrimitiveEncryptable, PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SignedPublicKey,
11+
SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, VerifyingKey,
1212
};
13+
use rsa::{
14+
Oaep, RsaPrivateKey, RsaPublicKey,
15+
pkcs8::{DecodePrivateKey, DecodePublicKey},
16+
};
17+
use sha1::Sha1;
1318
use wasm_bindgen::prelude::*;
1419

1520
/// This module represents a stopgap solution to provide access to primitive crypto functions for JS
@@ -319,23 +324,61 @@ impl PureCrypto {
319324
Ok(result.to_encoded().to_vec())
320325
}
321326

322-
/// Given an encrypted private RSA key and the symmetric key it is wrapped with, this returns
323-
/// the corresponding public RSA key in DER format.
324-
pub fn rsa_extract_public_key(
325-
encrypted_private_key: EncString,
326-
wrapping_key: Vec<u8>,
327-
) -> Result<Vec<u8>, CryptoError> {
328-
let wrapping_key =
329-
SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(wrapping_key))?;
330-
let decrypted_private_key: Vec<u8> =
331-
encrypted_private_key.decrypt_with_key(&wrapping_key)?;
332-
let private_key =
333-
AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(decrypted_private_key))?;
327+
/// Given a decrypted private RSA key PKCS8 DER this
328+
/// returns the corresponding public RSA key in DER format.
329+
pub fn rsa_extract_public_key(private_key: Vec<u8>) -> Result<Vec<u8>, RsaError> {
330+
let private_key = AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(private_key))
331+
.map_err(|_| RsaError::KeyParse)?;
334332
let public_key = private_key.to_public_key();
335-
Ok(public_key.to_der()?.to_vec())
333+
Ok(public_key
334+
.to_der()
335+
.map_err(|_| RsaError::KeySerialize)?
336+
.to_vec())
337+
}
338+
339+
/// Generates a new RSA key pair and returns the private key
340+
pub fn rsa_generate_keypair() -> Result<Vec<u8>, RsaError> {
341+
let private_key = AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
342+
Ok(private_key
343+
.to_der()
344+
.map_err(|_| RsaError::KeySerialize)?
345+
.to_vec())
346+
}
347+
348+
/// Decrypts data using RSAES-OAEP with SHA-1
349+
pub fn rsa_decrypt_data(
350+
encrypted_data: Vec<u8>,
351+
private_key: Vec<u8>,
352+
) -> Result<Vec<u8>, RsaError> {
353+
let private_key = RsaPrivateKey::from_pkcs8_der(private_key.as_slice())
354+
.map_err(|_| RsaError::KeyParse)?;
355+
let padding = Oaep::new::<Sha1>();
356+
private_key
357+
.decrypt(padding, &encrypted_data)
358+
.map_err(|_| RsaError::Decryption)
359+
}
360+
361+
/// Encrypts data using RSAES-OAEP with SHA-1
362+
pub fn rsa_encrypt_data(plain_data: Vec<u8>, public_key: Vec<u8>) -> Result<Vec<u8>, RsaError> {
363+
let public_key = RsaPublicKey::from_public_key_der(public_key.as_slice())
364+
.map_err(|_| RsaError::KeyParse)?;
365+
let padding = Oaep::new::<Sha1>();
366+
let mut rng = rand::thread_rng();
367+
public_key
368+
.encrypt(&mut rng, padding, &plain_data)
369+
.map_err(|_| RsaError::Encryption)
336370
}
337371
}
338372

373+
#[wasm_bindgen]
374+
#[derive(Debug)]
375+
pub enum RsaError {
376+
Decryption,
377+
Encryption,
378+
KeyParse,
379+
KeySerialize,
380+
}
381+
339382
#[cfg(test)]
340383
mod tests {
341384
use std::{num::NonZero, str::FromStr};
@@ -697,4 +740,14 @@ DnqOsltgPomWZ7xVfMkm9niL2OA=
697740
.unwrap();
698741
assert_eq!(user_key.0.to_encoded().to_vec(), decrypted_user_key);
699742
}
743+
744+
#[test]
745+
fn test_rsa_round_trip() {
746+
let private_key = PureCrypto::rsa_generate_keypair().unwrap();
747+
let public_key = PureCrypto::rsa_extract_public_key(private_key.clone()).unwrap();
748+
let plain_data = b"Test RSA encryption data".to_vec();
749+
let encrypted_data = PureCrypto::rsa_encrypt_data(plain_data.clone(), public_key).unwrap();
750+
let decrypted_data = PureCrypto::rsa_decrypt_data(encrypted_data, private_key).unwrap();
751+
assert_eq!(plain_data, decrypted_data);
752+
}
700753
}

0 commit comments

Comments
 (0)