diff --git a/Cargo.lock b/Cargo.lock index 7f03988d..d95080a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,6 +168,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "zeroize", +] + [[package]] name = "askama" version = "0.12.1" @@ -424,14 +439,17 @@ dependencies = [ "aes", "argon2", "base64", + "blake3", "cbc", "criterion", + "ed25519-dalek", "generic-array", "hkdf", "hmac", "num-bigint", "num-traits", "pbkdf2", + "pkcs8", "rand", "rand_chacha", "rayon", @@ -441,6 +459,7 @@ dependencies = [ "serde_json", "sha1", "sha2", + "signature", "subtle", "thiserror 1.0.69", "tsify-next", @@ -676,6 +695,20 @@ dependencies = [ "digest", ] +[[package]] +name = "blake3" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "zeroize", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -789,9 +822,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "shlex", ] @@ -1000,6 +1033,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "core-foundation" version = "0.9.4" @@ -1084,9 +1123,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1103,9 +1142,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" @@ -1216,6 +1255,7 @@ dependencies = [ "fiat-crypto", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -1414,8 +1454,12 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core", + "serde", "sha2", + "signature", "subtle", + "zeroize", ] [[package]] @@ -1877,9 +1921,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", @@ -1898,9 +1942,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "f6884a48c6826ec44f524c7456b163cebe9e55a18d7b5e307cb4f100371cc767" dependencies = [ "futures-util", "http", @@ -2898,7 +2942,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.6", + "thiserror 2.0.8", "tokio", "tracing", ] @@ -2917,7 +2961,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.6", + "thiserror 2.0.8", "tinyvec", "tracing", "web-time", @@ -2925,9 +2969,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd4b1eff68bf27940dd39811292c49e007f4d0b4c357358dc9b0197be6b527" +checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" dependencies = [ "cfg_aliases", "libc", @@ -2998,9 +3042,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] @@ -3159,9 +3203,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.19" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "ring", @@ -3195,9 +3239,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" dependencies = [ "web-time", ] @@ -3379,9 +3423,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" dependencies = [ "core-foundation-sys", "libc", @@ -3389,18 +3433,18 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] @@ -3418,9 +3462,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", @@ -3864,11 +3908,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.8", ] [[package]] @@ -3884,9 +3928,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" dependencies = [ "proc-macro2", "quote", @@ -4168,9 +4212,9 @@ checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" @@ -4619,7 +4663,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index 7bf32615..a73ba583 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -27,20 +27,32 @@ argon2 = { version = ">=0.5.0, <0.6", features = [ "zeroize", ], default-features = false } base64 = ">=0.22.1, <0.23" +blake3 = { version = "1.5.5", features = ["zeroize"] } cbc = { version = ">=0.1.2, <0.2", features = ["alloc", "zeroize"] } +ed25519-dalek = { version = "2.1.1", features = [ + "alloc", + "digest", + "pem", + "pkcs8", + "rand_core", + "signature", +] } generic-array = { version = ">=0.14.7, <1.0", features = ["zeroize"] } hkdf = ">=0.12.3, <0.13" hmac = ">=0.12.1, <0.13" num-bigint = ">=0.4, <0.5" num-traits = ">=0.2.15, <0.3" pbkdf2 = { version = ">=0.12.1, <0.13", default-features = false } +pkcs8 = "0.10.2" rand = ">=0.8.5, <0.9" rayon = ">=1.8.1, <2.0" rsa = ">=0.9.2, <0.10" schemars = { workspace = true } serde = { workspace = true } +serde_json.workspace = true sha1 = ">=0.10.5, <0.11" sha2 = ">=0.10.6, <0.11" +signature = "2.2.0" subtle = ">=2.5.0, <3.0" thiserror = { workspace = true } tsify-next = { workspace = true, optional = true } diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 2f9a58b8..5913808e 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -41,6 +41,17 @@ pub enum CryptoError { #[error("Number is zero")] ZeroNumber, + + #[error("Invalid signature algorithm")] + InvalidSignatureAlgorithm, + #[error("Signature Parse Error")] + SignatureParseError, + #[error("Invalid hash algorithm")] + InvalidHashAlgorithm, + #[error("Hash Parse Error")] + HashParseError, + #[error("Invalid signature")] + InvalidSignature, } #[derive(Debug, Error)] diff --git a/crates/bitwarden-crypto/src/keys/key_hash.rs b/crates/bitwarden-crypto/src/keys/key_hash.rs new file mode 100644 index 00000000..9b07fed8 --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/key_hash.rs @@ -0,0 +1,81 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; +use serde::{Deserialize, Serialize}; + +use crate::CryptoError; + +#[derive(PartialEq, Serialize, Deserialize)] +pub(crate) enum KeyHashAlgorithm { + Blake3, +} + +impl KeyHashAlgorithm { + pub(crate) fn from_str(s: &str) -> Option { + match s { + "blake3" => Some(Self::Blake3), + _ => None, + } + } + + pub(crate) fn to_string(&self) -> String { + match self { + Self::Blake3 => "blake3".to_string(), + } + } +} + +#[derive(PartialEq, Serialize, Deserialize)] +pub(crate) struct KeyHash { + pub(crate) hash: Vec, + pub(crate) algorithm: KeyHashAlgorithm, +} + +impl KeyHash { + pub(crate) fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split(':').collect(); + if parts.len() != 2 { + return Err(CryptoError::HashParseError); + } + + let algorithm = + KeyHashAlgorithm::from_str(parts[0]).ok_or(CryptoError::InvalidHashAlgorithm)?; + let hash = STANDARD + .decode(parts[1]) + .map_err(|_| CryptoError::HashParseError)?; + + Ok(Self { algorithm, hash }) + } + + pub(crate) fn to_string(&self) -> String { + format!( + "{}:{}", + self.algorithm.to_string(), + STANDARD.encode(&self.hash) + ) + } + + /// only for debugging + pub(crate) fn default() -> Self { + Self { + hash: vec![0; 32], + algorithm: KeyHashAlgorithm::Blake3, + } + } +} + +pub(crate) trait KeyHashable { + fn hash(&self) -> KeyHash; +} + +impl KeyHashable for T { + fn hash(&self) -> KeyHash { + let hash: [u8; 32] = blake3::hash(&self.hash_data()).into(); + KeyHash { + hash: hash.to_vec(), + algorithm: KeyHashAlgorithm::Blake3, + } + } +} + +pub(crate) trait KeyHashData { + fn hash_data(&self) -> Vec; +} diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index ac173296..5d9c42fa 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -21,4 +21,6 @@ mod device_key; pub use device_key::{DeviceKey, TrustDeviceResponse}; mod pin_key; pub use pin_key::PinKey; +pub(crate) mod key_hash; +pub(crate) mod signing_key; mod utils; diff --git a/crates/bitwarden-crypto/src/keys/signing_key.rs b/crates/bitwarden-crypto/src/keys/signing_key.rs new file mode 100644 index 00000000..b3f324c8 --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/signing_key.rs @@ -0,0 +1,89 @@ +use std::pin::Pin; + +use super::{key_encryptable::CryptoKey, key_hash::KeyHashData}; +use crate::{ + error::Result, + signing::{SignatureAlgorithm, Signer, Verifier}, +}; + +pub trait Verifiable { + fn verifier(&self) -> VerifyingCryptoKey; +} + +pub struct VerifyingCryptoKey { + pub(crate) verifier: Verifier, +} + +impl VerifyingCryptoKey { + pub fn from_spki_der(der: &[u8]) -> Result { + Ok(Self { + verifier: Verifier::from_spki_der(der)?, + }) + } + + pub fn to_spki_der(&self) -> Vec { + self.verifier.to_spki_der() + } +} + +#[derive(Clone)] +pub struct SigningCryptoKey { + pub(crate) signing_key: Pin>, +} + +impl KeyHashData for SigningCryptoKey { + fn hash_data(&self) -> Vec { + self.verifier().to_spki_der() + } +} + +impl KeyHashData for VerifyingCryptoKey { + fn hash_data(&self) -> Vec { + self.to_spki_der() + } +} + +const _: () = { + fn assert_zeroize_on_drop() {} + fn assert_all() { + assert_zeroize_on_drop::(); + } +}; + +impl zeroize::ZeroizeOnDrop for SigningCryptoKey {} + +impl SigningCryptoKey { + pub fn generate(rng: &mut R) -> Self { + let algorithm = SignatureAlgorithm::Ed25519; + Self { + signing_key: Box::pin(Signer::generate(rng, &algorithm)), + } + } + + pub fn from_der(der: &[u8]) -> Result { + Ok(Self { + signing_key: Box::pin(Signer::from_pkcs8_der(der)?), + }) + } + + pub fn to_der(&self) -> Result> { + Ok(self.signing_key.to_pkcs8_der()) + } +} + +impl Verifiable for SigningCryptoKey { + fn verifier(&self) -> VerifyingCryptoKey { + VerifyingCryptoKey { + verifier: self.signing_key.verifier(), + } + } +} + +impl CryptoKey for SigningCryptoKey {} + +// We manually implement these to make sure we don't print any sensitive data +impl std::fmt::Debug for SigningCryptoKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SigningCryptoKey").finish() + } +} diff --git a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs index 971564b0..3f0c5cfa 100644 --- a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs @@ -6,7 +6,7 @@ use generic_array::GenericArray; use rand::Rng; use zeroize::Zeroize; -use super::key_encryptable::CryptoKey; +use super::{key_encryptable::CryptoKey, key_hash::KeyHashData}; use crate::CryptoError; /// A symmetric encryption key. Used to encrypt and decrypt [`EncString`](crate::EncString) @@ -135,6 +135,12 @@ impl std::fmt::Debug for SymmetricCryptoKey { } } +impl KeyHashData for SymmetricCryptoKey { + fn hash_data(&self) -> Vec { + self.to_vec() + } +} + #[cfg(test)] pub fn derive_symmetric_key(name: &str) -> SymmetricCryptoKey { use zeroize::Zeroizing; diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index 40f7a63c..96482092 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -75,6 +75,8 @@ pub use fingerprint::fingerprint; mod keys; pub use keys::*; mod rsa; +mod sign; +pub(crate) mod signing; pub use crate::rsa::RsaKeyPair; mod util; pub use util::{generate_random_alphanumeric, generate_random_bytes, pbkdf2}; diff --git a/crates/bitwarden-crypto/src/sign.rs b/crates/bitwarden-crypto/src/sign.rs new file mode 100644 index 00000000..e49cceea --- /dev/null +++ b/crates/bitwarden-crypto/src/sign.rs @@ -0,0 +1,117 @@ +use serde::{Deserialize, Serialize}; + +use self::key_hash::KeyHashable; +use crate::{ + key_hash::{self, KeyHash}, + signing, + signing_key::{SigningCryptoKey, Verifiable, VerifyingCryptoKey}, + CryptoError, +}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum TrustIdentity { + User(String), + Organization(String), +} + +#[derive(Serialize, Deserialize, PartialEq)] +struct IdentityTrustMessage { + identity: TrustIdentity, + verifying_key_fingerprint: KeyHash, +} + +pub enum SignatureContext { + IdentityTrust(TrustIdentity), +} + +pub struct Signature { + context: SignatureContext, + data: Vec, + signature_data: signing::signature::Signature, + signing_key_hash: KeyHash, +} + +/// Generate a trust signature, that shows that the current user trusts the identity of the peer +/// with the given identity key. +/// # Arguments +/// * `own_signing_key` - The signing key of the current user +/// * `peer_verifying_key` - The verifying key of the peer +/// * `peer_identity` - The identity of the peer +pub fn trust_identity_key( + own_signing_key: &mut SigningCryptoKey, + peer_verifying_key: &VerifyingCryptoKey, + peer_identity: TrustIdentity, +) -> Result { + let message = IdentityTrustMessage { + identity: peer_identity.clone(), + verifying_key_fingerprint: peer_verifying_key.hash(), + }; + + let message_bytes = serde_json::to_vec(&message).map_err(|_| CryptoError::InvalidKey)?; + + Ok(Signature { + context: SignatureContext::IdentityTrust(peer_identity), + data: message_bytes.clone(), + signature_data: own_signing_key.signing_key.sign(&message_bytes), + signing_key_hash: own_signing_key.hash(), + }) +} + +pub fn verify_identity_trust( + own_signing_key: &mut SigningCryptoKey, + peer_verifying_key: &VerifyingCryptoKey, + peer_identity: TrustIdentity, + signature: &Signature, +) -> Result<(), CryptoError> { + if let SignatureContext::IdentityTrust(message_peer_identity) = &signature.context { + if !message_peer_identity.eq(&peer_identity) { + return Err(CryptoError::InvalidSignature); + } + } else { + return Err(CryptoError::InvalidSignature); + } + + let message = IdentityTrustMessage { + identity: peer_identity.clone(), + verifying_key_fingerprint: peer_verifying_key.hash(), + }; + + let message_bytes = serde_json::to_vec(&message).map_err(|_| CryptoError::InvalidKey)?; + if !own_signing_key + .verifier() + .verifier + .verify(&message_bytes, &signature.signature_data) + { + return Err(CryptoError::InvalidSignature); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::signing_key::Verifiable; + + #[test] + fn test_trust_identity_key() { + let mut own_signing_key = SigningCryptoKey::generate(&mut rand::thread_rng()); + let peer_signing_key = SigningCryptoKey::generate(&mut rand::thread_rng()); + let peer_verifying_key = peer_signing_key.verifier(); + let peer_identity = TrustIdentity::User("test_user_id".to_string()); + + let signature = trust_identity_key( + &mut own_signing_key, + &peer_verifying_key, + peer_identity.clone(), + ) + .expect("Failed to generate trust signature"); + let verified = verify_identity_trust( + &mut own_signing_key, + &peer_verifying_key, + peer_identity, + &signature, + ); + assert!(verified.is_ok()); + } +} diff --git a/crates/bitwarden-crypto/src/signing/ed25519.rs b/crates/bitwarden-crypto/src/signing/ed25519.rs new file mode 100644 index 00000000..bbb132b3 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/ed25519.rs @@ -0,0 +1,69 @@ +use ::signature::SignerMut; +use ed25519_dalek::{SigningKey, VerifyingKey}; +use pkcs8::{DecodePrivateKey, EncodePrivateKey}; + +use super::{signature, SignerImpl, VerifierImpl}; +use crate::{error::Result, CryptoError}; + +#[derive(Clone)] +pub(crate) struct Ed25519Verifier { + inner: VerifyingKey, +} + +impl VerifierImpl for Ed25519Verifier { + fn verify(&self, data: &[u8], signature: &signature::Signature) -> bool { + let signature_bytes: [u8; 64] = signature.data.clone().try_into().unwrap(); + let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes); + self.inner.verify_strict(data, &signature).is_ok() + } +} + +impl Ed25519Verifier { + pub(crate) fn from_der(data: &[u8]) -> Result { + let key_bytes: [u8; 32] = data.try_into().map_err(|_| CryptoError::InvalidKey)?; + let verifying_key = + VerifyingKey::from_bytes(&key_bytes).map_err(|_| CryptoError::InvalidKey)?; + Ok(Self { + inner: verifying_key, + }) + } + + pub(crate) fn to_der(&self) -> Vec { + self.inner.to_bytes().to_vec() + } +} + +#[derive(Clone)] +pub(crate) struct Ed25519Signer { + inner: SigningKey, +} + +impl SignerImpl for Ed25519Signer { + fn sign(&mut self, data: &[u8]) -> signature::Signature { + let res = self.inner.sign(data); + signature::Signature::new(super::SignatureAlgorithm::Ed25519, res.to_bytes().to_vec()) + } + + fn verifier(&self) -> Ed25519Verifier { + Ed25519Verifier { + inner: self.inner.verifying_key(), + } + } + + fn generate(rng: &mut R) -> Self { + Self { + inner: SigningKey::generate(rng), + } + } +} + +impl Ed25519Signer { + pub(crate) fn from_der(data: &[u8]) -> Result { + let signing_key = SigningKey::from_pkcs8_der(data).map_err(|_| CryptoError::InvalidKey)?; + Ok(Self { inner: signing_key }) + } + + pub(crate) fn to_der(&self) -> Vec { + self.inner.to_pkcs8_der().unwrap().as_bytes().to_vec() + } +} diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs new file mode 100644 index 00000000..42b89b39 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -0,0 +1,153 @@ +use pkcs8::{der::Decode, PrivateKeyInfo, SubjectPublicKeyInfoRef}; +use zeroize::ZeroizeOnDrop; + +use crate::{key_hash::KeyHash, signing_key::SigningCryptoKey, CryptoError}; + +mod ed25519; +pub mod signature; + +#[derive(Debug, Clone, PartialEq)] +pub enum SignatureAlgorithm { + Ed25519, +} + +#[derive(Clone)] +enum SignatureImpl { + Ed25519(ed25519::Ed25519Signer), +} + +#[derive(Clone)] +enum VerifyImpl { + Ed25519(ed25519::Ed25519Verifier), +} + +#[derive(Clone)] +pub(crate) struct Signer { + impl_: SignatureImpl, +} + +impl ZeroizeOnDrop for Signer {} + +#[derive(Clone)] +pub(crate) struct Verifier { + impl_: VerifyImpl, +} + +impl Signer { + pub(crate) fn generate( + rng: &mut R, + algorithm: &SignatureAlgorithm, + ) -> Self { + match algorithm { + SignatureAlgorithm::Ed25519 => { + let signing_key = ed25519::Ed25519Signer::generate(rng); + Signer { + impl_: SignatureImpl::Ed25519(signing_key), + } + } + } + } + + pub(crate) fn sign(&mut self, data: &[u8]) -> signature::Signature { + match &mut self.impl_ { + SignatureImpl::Ed25519(signer) => signer.sign(data), + } + } + + pub(crate) fn verifier(&self) -> Verifier { + match &self.impl_ { + SignatureImpl::Ed25519(signer) => Verifier { + impl_: VerifyImpl::Ed25519(signer.verifier()), + }, + } + } + + pub(crate) fn from_pkcs8_der(data: &[u8]) -> Result { + let private_key_info = + PrivateKeyInfo::from_der(data).map_err(|_| CryptoError::KeyDecrypt)?; + match private_key_info.algorithm.oid { + ed25519_dalek::ed25519::pkcs8::ALGORITHM_OID => Ok(Signer { + impl_: SignatureImpl::Ed25519(ed25519::Ed25519Signer::from_der(data)?), + }), + _ => Err(CryptoError::InvalidKey), + } + } + + pub(crate) fn to_pkcs8_der(&self) -> Vec { + match &self.impl_ { + SignatureImpl::Ed25519(signer) => signer.to_der(), + } + } +} + +impl Verifier { + pub(crate) fn verify(&self, data: &[u8], signature: &signature::Signature) -> bool { + match &self.impl_ { + VerifyImpl::Ed25519(verifier) => { + if !signature.algorithm().eq(&SignatureAlgorithm::Ed25519) { + return false; + } + verifier.verify(data, signature) + } + } + } + + pub(crate) fn from_spki_der(data: &[u8]) -> Result { + let public_key_info = + SubjectPublicKeyInfoRef::from_der(data).map_err(|_| CryptoError::InvalidKey)?; + match public_key_info.algorithm.oid { + ed25519_dalek::ed25519::pkcs8::ALGORITHM_OID => Ok(Verifier { + impl_: VerifyImpl::Ed25519(ed25519::Ed25519Verifier::from_der(data)?), + }), + _ => Err(CryptoError::InvalidKey), + } + } + + pub(crate) fn to_spki_der(&self) -> Vec { + match &self.impl_ { + VerifyImpl::Ed25519(verifier) => verifier.to_der(), + } + } +} + +impl SignatureAlgorithm { + pub fn from_str(algorithm: &str) -> Option { + match algorithm { + "ed25519" => Some(Self::Ed25519), + _ => None, + } + } + + pub fn to_string(&self) -> String { + match self { + Self::Ed25519 => "ed25519".to_string(), + } + } +} + +trait VerifierImpl { + fn verify(&self, data: &[u8], signature: &signature::Signature) -> bool; +} + +trait SignerImpl { + fn sign(&mut self, data: &[u8]) -> signature::Signature; + fn verifier(&self) -> V; + fn generate(rng: &mut R) -> Self; +} + +#[cfg(test)] +mod tests { + use rand::rngs::OsRng; + + use super::*; + + #[test] + fn test_sign_verify() { + let mut rng = OsRng; + let mut signer = Signer::generate(&mut rng, &SignatureAlgorithm::Ed25519); + let verifier = signer.verifier(); + let data = b"hello world"; + let signature = signer.sign(data); + assert!(verifier.verify(data, &signature)); + } +} diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs new file mode 100644 index 00000000..6856dde4 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -0,0 +1,43 @@ +use base64::{prelude::BASE64_STANDARD, Engine}; + +use super::SignatureAlgorithm; +use crate::CryptoError; + +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct Signature { + algorithm: SignatureAlgorithm, + pub(crate) data: Vec, +} + +impl Signature { + pub fn new(algorithm: SignatureAlgorithm, data: Vec) -> Self { + Self { algorithm, data } + } + + pub fn to_string(&self) -> String { + format!( + "{}:{}", + self.algorithm.to_string(), + BASE64_STANDARD.encode(&self.data) + ) + } + + pub fn from_string(s: &str) -> Result { + let parts: Vec<&str> = s.split(':').collect(); + if parts.len() != 2 { + return Err(CryptoError::SignatureParseError); + } + + let algorithm = + SignatureAlgorithm::from_str(parts[0]).ok_or(CryptoError::InvalidSignatureAlgorithm)?; + let data = BASE64_STANDARD + .decode(parts[1]) + .map_err(|_| CryptoError::SignatureParseError)?; + + Ok(Self { algorithm, data }) + } + + pub fn algorithm(&self) -> &SignatureAlgorithm { + &self.algorithm + } +} diff --git a/crates/bitwarden-wasm-internal/npm/VERSION b/crates/bitwarden-wasm-internal/npm/VERSION new file mode 100644 index 00000000..42d90fed --- /dev/null +++ b/crates/bitwarden-wasm-internal/npm/VERSION @@ -0,0 +1 @@ +611ae0b6bad34196111acf7570feba9cbf861bc1