From 341fdd29a9863e793c560e2a7207989c4f61d772 Mon Sep 17 00:00:00 2001 From: Nikhil Benesch Date: Thu, 10 Dec 2020 20:46:58 -0500 Subject: openpgp: Add a RustCrypto backend. - This adds a cryptographic backend based on the RustCrypto crates. The backend is marked as experimental, as the RustCrypto crates' authors state that they have not been audited and may not perform computations in constant time. Nevertheless, it may be useful in certain environments, e.g. WebAssembly. - The backend implements RSA, EdDSA and ECDH over Curve25519, IDEA, 3DES, CAST5, Blowfish, AES, Twofish, EAX, MD5, SHA1, RipeMD160, and the SHA2 family. - Notably missing are DSA, ElGamal, and ECDSA and ECDH over the NIST curves. - See #333. --- openpgp/src/crypto/backend/rust.rs | 64 ++++ openpgp/src/crypto/backend/rust/aead.rs | 134 ++++++++ openpgp/src/crypto/backend/rust/asymmetric.rs | 473 ++++++++++++++++++++++++++ openpgp/src/crypto/backend/rust/ecdh.rs | 99 ++++++ openpgp/src/crypto/backend/rust/hash.rs | 80 +++++ openpgp/src/crypto/backend/rust/symmetric.rs | 199 +++++++++++ 6 files changed, 1049 insertions(+) create mode 100644 openpgp/src/crypto/backend/rust.rs create mode 100644 openpgp/src/crypto/backend/rust/aead.rs create mode 100644 openpgp/src/crypto/backend/rust/asymmetric.rs create mode 100644 openpgp/src/crypto/backend/rust/ecdh.rs create mode 100644 openpgp/src/crypto/backend/rust/hash.rs create mode 100644 openpgp/src/crypto/backend/rust/symmetric.rs (limited to 'openpgp/src/crypto/backend') diff --git a/openpgp/src/crypto/backend/rust.rs b/openpgp/src/crypto/backend/rust.rs new file mode 100644 index 00000000..a661c14e --- /dev/null +++ b/openpgp/src/crypto/backend/rust.rs @@ -0,0 +1,64 @@ +//! Implementation of Sequoia crypto API using pure Rust cryptographic +//! libraries. + +use crate::types::*; + +pub mod aead; +pub mod asymmetric; +pub mod ecdh; +pub mod hash; +pub mod symmetric; + +/// Fills the given buffer with random data. +/// +/// Fills the given buffer with random data produced by a +/// cryptographically secure pseudorandom number generator (CSPRNG). +/// The output may be used as session keys or to derive long-term +/// cryptographic keys from. +pub fn random>(mut buf: B) { + use rand::rngs::OsRng; + use rand::RngCore; + + OsRng.fill_bytes(buf.as_mut()) +} + +impl PublicKeyAlgorithm { + pub(crate) fn is_supported_by_backend(&self) -> bool { + use PublicKeyAlgorithm::*; + #[allow(deprecated)] + match &self { + RSAEncryptSign | RSAEncrypt | RSASign | ECDH | EdDSA + => true, + DSA | ECDSA + => false, + ElGamalEncrypt | ElGamalEncryptSign | Private(_) | Unknown(_) + => false, + } + } +} + +impl Curve { + pub(crate) fn is_supported_by_backend(&self) -> bool { + use self::Curve::*; + match &self { + NistP256 | NistP384 | NistP521 + => false, + Ed25519 | Cv25519 + => true, + BrainpoolP256 | BrainpoolP512 | Unknown(_) + => false, + } + } +} + +impl AEADAlgorithm { + pub(crate) fn is_supported_by_backend(&self) -> bool { + use self::AEADAlgorithm::*; + match &self { + EAX + => true, + OCB | Private(_) | Unknown(_) + => false, + } + } +} diff --git a/openpgp/src/crypto/backend/rust/aead.rs b/openpgp/src/crypto/backend/rust/aead.rs new file mode 100644 index 00000000..ab2ca208 --- /dev/null +++ b/openpgp/src/crypto/backend/rust/aead.rs @@ -0,0 +1,134 @@ +//! Implementation of AEAD using pure Rust cryptographic libraries. + +use std::cmp; + +use cipher::{BlockCipher, NewBlockCipher}; +use cipher::block::Block; +use cipher::consts::U16; +use eax::online::{Eax, Encrypt, Decrypt}; +use generic_array::{ArrayLength, GenericArray}; + +use crate::{Error, Result}; +use crate::crypto::aead::{Aead, CipherOp}; +use crate::seal; +use crate::types::{AEADAlgorithm, SymmetricAlgorithm}; + +trait GenericArrayExt { + const LEN: usize; +} + +impl> GenericArrayExt for GenericArray { + const LEN: usize = N::USIZE; +} + +impl Aead for Eax +where + Cipher: BlockCipher + NewBlockCipher + Clone, + Cipher::ParBlocks: ArrayLength>, +{ + fn update(&mut self, ad: &[u8]) { + self.update_assoc(ad) + } + + fn digest_size(&self) -> usize { + eax::Tag::LEN + } + + fn digest(&mut self, digest: &mut [u8]) { + let tag = self.tag_clone(); + digest[..tag.len()].copy_from_slice(&tag[..]); + } + + fn encrypt(&mut self, dst: &mut [u8], src: &[u8]) { + let len = cmp::min(dst.len(), src.len()); + dst[..len].copy_from_slice(&src[..len]); + Self::encrypt(self, &mut dst[..len]) + } + + fn decrypt(&mut self, _dst: &mut [u8], _src: &[u8]) { + panic!("AEAD decryption called in the encryption context") + } +} + +impl Aead for Eax +where + Cipher: BlockCipher + NewBlockCipher + Clone, + Cipher::ParBlocks: ArrayLength>, +{ + fn update(&mut self, ad: &[u8]) { + self.update_assoc(ad) + } + + fn digest_size(&self) -> usize { + eax::Tag::LEN + } + + fn digest(&mut self, digest: &mut [u8]) { + let tag = self.tag_clone(); + digest[..tag.len()].copy_from_slice(&tag[..]); + } + + fn encrypt(&mut self, _dst: &mut [u8], _src: &[u8]) { + panic!("AEAD encryption called in the decryption context") + } + + fn decrypt(&mut self, dst: &mut [u8], src: &[u8]) { + let len = core::cmp::min(dst.len(), src.len()); + dst[..len].copy_from_slice(&src[..len]); + self.decrypt_unauthenticated_hazmat(&mut dst[..len]) + } +} + +impl seal::Sealed for Eax +where + Cipher: BlockCipher + NewBlockCipher + Clone, + Cipher::ParBlocks: ArrayLength>, + Op: eax::online::CipherOp, +{} + +impl AEADAlgorithm { + pub(crate) fn context( + &self, + sym_algo: SymmetricAlgorithm, + key: &[u8], + nonce: &[u8], + op: CipherOp, + ) -> Result> { + match self { + AEADAlgorithm::EAX => match sym_algo { + SymmetricAlgorithm::AES128 => match op { + CipherOp::Encrypt => Ok(Box::new( + Eax::::with_key_and_nonce(key.into(), nonce.into()))), + CipherOp::Decrypt => Ok(Box::new( + Eax::::with_key_and_nonce(key.into(), nonce.into()))), + } + SymmetricAlgorithm::AES192 => match op { + CipherOp::Encrypt => Ok(Box::new( + Eax::::with_key_and_nonce(key.into(), nonce.into()))), + CipherOp::Decrypt => Ok(Box::new( + Eax::::with_key_and_nonce(key.into(), nonce.into()))), + } + SymmetricAlgorithm::AES256 => match op { + CipherOp::Encrypt => Ok(Box::new( + Eax::::with_key_and_nonce(key.into(), nonce.into()))), + CipherOp::Decrypt => Ok(Box::new( + Eax::::with_key_and_nonce(key.into(), nonce.into()))), + } + | SymmetricAlgorithm::IDEA + | SymmetricAlgorithm::TripleDES + | SymmetricAlgorithm::CAST5 + | SymmetricAlgorithm::Blowfish + | SymmetricAlgorithm::Twofish + | SymmetricAlgorithm::Camellia128 + | SymmetricAlgorithm::Camellia192 + | SymmetricAlgorithm::Camellia256 + | SymmetricAlgorithm::Private(_) + | SymmetricAlgorithm::Unknown(_) + | SymmetricAlgorithm::Unencrypted => + Err(Error::UnsupportedSymmetricAlgorithm(sym_algo).into()), + }, + AEADAlgorithm::OCB | AEADAlgorithm::Private(_) | AEADAlgorithm::Unknown(_) => + Err(Error::UnsupportedAEADAlgorithm(*self).into()), + } + } +} diff --git a/openpgp/src/crypto/backend/rust/asymmetric.rs b/openpgp/src/crypto/backend/rust/asymmetric.rs new file mode 100644 index 00000000..b09f6214 --- /dev/null +++ b/openpgp/src/crypto/backend/rust/asymmetric.rs @@ -0,0 +1,473 @@ +//! Holds the implementation of [`Signer`] and [`Decryptor`] for [`KeyPair`]. +//! +//! [`Signer`]: ../../asymmetric/trait.Signer.html +//! [`Decryptor`]: ../../asymmetric/trait.Decryptor.html +//! [`KeyPair`]: ../../asymmetric/struct.KeyPair.html + +use std::convert::TryFrom; +use std::time::SystemTime; + +use num_bigint_dig::{traits::ModInverse, BigUint}; +use rand::rngs::OsRng; +use rsa::{PaddingScheme, RSAPublicKey, RSAPrivateKey, PublicKey, PublicKeyParts, Hash}; + +use crate::{Error, Result}; +use crate::crypto::asymmetric::{KeyPair, Decryptor, Signer}; +use crate::crypto::mem::Protected; +use crate::crypto::mpi::{self, MPI, ProtectedMPI}; +use crate::crypto::SessionKey; +use crate::packet::{key, Key}; +use crate::packet::key::{Key4, SecretParts}; +use crate::types::{Curve, HashAlgorithm, PublicKeyAlgorithm, SymmetricAlgorithm}; + +const CURVE25519_SIZE: usize = 32; + +fn pkcs1_padding(hash_algo: HashAlgorithm) -> Result { + let hash = match hash_algo { + HashAlgorithm::MD5 => Hash::MD5, + HashAlgorithm::SHA1 => Hash::SHA1, + HashAlgorithm::SHA224 => Hash::SHA2_224, + HashAlgorithm::SHA256 => Hash::SHA2_256, + HashAlgorithm::SHA384 => Hash::SHA2_384, + HashAlgorithm::SHA512 => Hash::SHA2_512, + HashAlgorithm::RipeMD => Hash::RIPEMD160, + _ => return Err(Error::InvalidArgument(format!( + "Algorithm {:?} not representable", hash_algo)).into()), + }; + Ok(PaddingScheme::PKCS1v15Sign { + hash: Some(hash) + }) +} + +fn rsa_public_key(e: &MPI, n: &MPI) -> Result { + let n = BigUint::from_bytes_be(n.value()); + let e = BigUint::from_bytes_be(e.value()); + Ok(RSAPublicKey::new(n, e)?) +} + +fn rsa_private_key(e: &MPI, n: &MPI, p: &ProtectedMPI, q: &ProtectedMPI, d: &ProtectedMPI) + -> RSAPrivateKey +{ + let n = BigUint::from_bytes_be(n.value()); + let e = BigUint::from_bytes_be(e.value()); + let p = BigUint::from_bytes_be(p.value()); + let q = BigUint::from_bytes_be(q.value()); + let d = BigUint::from_bytes_be(d.value()); + RSAPrivateKey::from_components(n, e, d, vec![p, q]) +} + +impl Signer for KeyPair { + fn public(&self) -> &Key { + KeyPair::public(self) + } + + fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) + -> Result + { + use crate::PublicKeyAlgorithm::*; + #[allow(deprecated)] + self.secret().map(|secret| match (self.public().pk_algo(), self.public().mpis(), secret) { + (RSAEncryptSign, + mpi::PublicKey::RSA { e, n }, + mpi::SecretKeyMaterial::RSA { p, q, d, .. }) | + (RSASign, + mpi::PublicKey::RSA { e, n }, + mpi::SecretKeyMaterial::RSA { p, q, d, .. }) => { + let key = rsa_private_key(e, n, p, q, d); + let padding = pkcs1_padding(hash_algo)?; + let sig = key.sign(padding, digest)?; + Ok(mpi::Signature::RSA { + s: mpi::MPI::new(&sig), + }) + }, + + (PublicKeyAlgorithm::DSA, + mpi:: PublicKey::DSA { .. }, + mpi::SecretKeyMaterial::DSA { .. }) => { + Err(Error::UnsupportedPublicKeyAlgorithm(PublicKeyAlgorithm::DSA).into()) + }, + + (PublicKeyAlgorithm::ECDSA, + mpi::PublicKey::ECDSA { .. }, + mpi::SecretKeyMaterial::ECDSA { .. }) => { + Err(Error::UnsupportedPublicKeyAlgorithm(PublicKeyAlgorithm::ECDSA).into()) + }, + + (EdDSA, + mpi::PublicKey::EdDSA { curve, q }, + mpi::SecretKeyMaterial::EdDSA { scalar }) => match curve + { + Curve::Ed25519 => { + use ed25519_dalek::{Keypair, Signer}; + use ed25519_dalek::{PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH}; + + let (public, ..) = q.decode_point(&Curve::Ed25519)?; + assert_eq!(public.len(), PUBLIC_KEY_LENGTH); + + // It's expected for the private key to be exactly + // SECRET_KEY_LENGTH bytes long but OpenPGP allows leading + // zeros to be stripped. + // Padding has to be unconditional; otherwise we have a + // secret-dependent branch. + let mut keypair = Protected::from( + vec![0u8; SECRET_KEY_LENGTH + PUBLIC_KEY_LENGTH] + ); + keypair.as_mut()[..SECRET_KEY_LENGTH] + .copy_from_slice( + &scalar.value_padded(SECRET_KEY_LENGTH)); + keypair.as_mut()[SECRET_KEY_LENGTH..] + .copy_from_slice(&public); + let pair = Keypair::from_bytes(&keypair)?; + + let sig = pair.sign(digest).to_bytes(); + + // https://tools.ietf.org/html/rfc8032#section-5.1.6 + let (r, s) = sig.split_at(sig.len() / 2); + Ok(mpi::Signature::EdDSA { + r: mpi::MPI::new(r), + s: mpi::MPI::new(s), + }) + }, + _ => Err(Error::UnsupportedEllipticCurve(curve.clone()).into()), + }, + + (pk_algo, _, _) => Err(Error::InvalidOperation(format!( + "unsupported combination of algorithm {:?}, key {:?}, \ + and secret key {:?}", + pk_algo, self.public(), self.secret())))?, + }) + } +} + +impl Decryptor for KeyPair { + fn public(&self) -> &Key { + KeyPair::public(self) + } + + fn decrypt(&mut self, ciphertext: &mpi::Ciphertext, + _plaintext_len: Option) + -> Result + { + use crate::PublicKeyAlgorithm::*; + self.secret().map(|secret| match (self.public().mpis(), secret, ciphertext) { + (mpi::PublicKey::RSA { e, n }, + mpi::SecretKeyMaterial::RSA { p, q, d, .. }, + mpi::Ciphertext::RSA { c }) => { + let key = rsa_private_key(e, n, p, q, d); + let padding = PaddingScheme::PKCS1v15Encrypt; + let decrypted = key.decrypt(padding, c.value())?; + Ok(SessionKey::from(decrypted)) + } + + (mpi::PublicKey::ElGamal { .. }, + mpi::SecretKeyMaterial::ElGamal { .. }, + mpi::Ciphertext::ElGamal { .. }) => + Err(Error::UnsupportedPublicKeyAlgorithm(ElGamalEncrypt).into()), + + (mpi::PublicKey::ECDH { .. }, + mpi::SecretKeyMaterial::ECDH { .. }, + mpi::Ciphertext::ECDH { .. }) => + crate::crypto::ecdh::decrypt(self.public(), secret, ciphertext), + + (public, secret, ciphertext) => + Err(Error::InvalidOperation(format!( + "unsupported combination of key pair {:?}/{:?} \ + and ciphertext {:?}", + public, secret, ciphertext)).into()), + }) + } +} + + +impl Key { + /// Encrypts the given data with this key. + pub fn encrypt(&self, data: &SessionKey) -> Result { + use PublicKeyAlgorithm::*; + #[allow(deprecated)] + match self.pk_algo() { + RSAEncryptSign | RSAEncrypt => match self.mpis() { + mpi::PublicKey::RSA { e, n } => { + // The ciphertext has the length of the modulus. + let ciphertext_len = n.value().len(); + if data.len() + 11 > ciphertext_len { + return Err(Error::InvalidArgument( + "Plaintext data too large".into()).into()); + } + let key = rsa_public_key(e, n)?; + let padding = PaddingScheme::PKCS1v15Encrypt; + let ciphertext = key.encrypt(&mut OsRng, padding, data.as_ref())?; + Ok(mpi::Ciphertext::RSA { + c: mpi::MPI::new(&ciphertext) + }) + } + pk => Err(Error::MalformedPacket(format!( + "Key: Expected RSA public key, got {:?}", pk)).into()) + } + ECDH => crate::crypto::ecdh::encrypt(self.parts_as_public(), data), + algo => Err(Error::UnsupportedPublicKeyAlgorithm(algo).into()), + } + } + + /// Verifies the given signature. + pub fn verify(&self, sig: &mpi::Signature, hash_algo: HashAlgorithm, + digest: &[u8]) -> Result<()> + { + match (self.mpis(), sig) { + (mpi::PublicKey::RSA { e, n }, mpi::Signature::RSA { s }) => { + let key = rsa_public_key(e, n)?; + let padding = pkcs1_padding(hash_algo)?; + key.verify(padding, digest, s.value())?; + Ok(()) + } + (mpi::PublicKey::DSA { .. }, + mpi::Signature::DSA { .. }) => { + Err(Error::UnsupportedPublicKeyAlgorithm(PublicKeyAlgorithm::DSA).into()) + }, + (mpi::PublicKey::ECDSA { .. }, + mpi::Signature::ECDSA { .. }) => { + Err(Error::UnsupportedPublicKeyAlgorithm(PublicKeyAlgorithm::ECDSA).into()) + }, + (mpi::PublicKey::EdDSA { curve, q }, + mpi::Signature::EdDSA { r, s }) => match curve { + Curve::Ed25519 => { + use ed25519_dalek::{PublicKey, Signature, SIGNATURE_LENGTH}; + use ed25519_dalek::{Verifier}; + + let (public, ..) = q.decode_point(&Curve::Ed25519)?; + assert_eq!(public.len(), 32); + + let key = PublicKey::from_bytes(public).map_err(|e| { + Error::InvalidKey(e.to_string()) + })?; + + // OpenPGP encodes R and S separately, but our + // cryptographic library expects them to be + // concatenated. + let mut sig_bytes = [0u8; SIGNATURE_LENGTH]; + + // We need to zero-pad them at the front, because + // the MPI encoding drops leading zero bytes. + let half = SIGNATURE_LENGTH / 2; + sig_bytes[..half].copy_from_slice( + &r.value_padded(half).map_err(bad)?); + sig_bytes[half..].copy_from_slice( + &s.value_padded(half).map_err(bad)?); + + let signature = Signature::from(sig_bytes); + + key.verify(digest, &signature) + .map_err(|e| Error::BadSignature(e.to_string()))?; + Ok(()) + }, + _ => Err(Error::UnsupportedEllipticCurve(curve.clone()).into()), + }, + _ => Err(Error::MalformedPacket(format!( + "unsupported combination of key {} and signature {:?}.", + self.pk_algo(), sig)).into()), + } + } +} + +impl Key4 + where R: key::KeyRole, +{ + + /// Creates a new OpenPGP secret key packet for an existing X25519 key. + /// + /// The ECDH key will use hash algorithm `hash` and symmetric + /// algorithm `sym`. If one or both are `None` secure defaults + /// will be used. The key will have it's creation date set to + /// `ctime` or the current time if `None` is given. + pub fn import_secret_cv25519(private_key: &[u8], + hash: H, sym: S, ctime: T) + -> Result where H: Into>, + S: Into>, + T: Into> + { + use x25519_dalek::{PublicKey, StaticSecret}; + + let secret = StaticSecret::from(<[u8; 32]>::try_from(private_key)?); + let public_key = PublicKey::from(&secret); + + let mut private_key = Vec::from(private_key); + private_key.reverse(); + + Self::with_secret( + ctime.into().unwrap_or_else(SystemTime::now), + PublicKeyAlgorithm::ECDH, + mpi::PublicKey::ECDH { + curve: Curve::Cv25519, + hash: hash.into().unwrap_or(HashAlgorithm::SHA512), + sym: sym.into().unwrap_or(SymmetricAlgorithm::AES256), + q: MPI::new_compressed_point(&*public_key.as_bytes()), + }, + mpi::SecretKeyMaterial::ECDH { + scalar: private_key.into(), + }.into()) + } + + /// Creates a new OpenPGP secret key packet for an existing Ed25519 key. + /// + /// The ECDH key will use hash algorithm `hash` and symmetric + /// algorithm `sym`. If one or both are `None` secure defaults + /// will be used. The key will have it's creation date set to + /// `ctime` or the current time if `None` is given. + pub fn import_secret_ed25519(private_key: &[u8], ctime: T) + -> Result where T: Into> + { + use ed25519_dalek::{PublicKey, SecretKey}; + + let private = SecretKey::from_bytes(private_key).map_err(|e| { + Error::InvalidKey(e.to_string()) + })?; + + // Mark MPI as compressed point with 0x40 prefix. See + // https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-07#section-13.2. + let mut public = [0u8; 1 + CURVE25519_SIZE]; + public[0] = 0x40; + &mut public[1..].copy_from_slice(Into::::into(&private).as_bytes()); + + Self::with_secret( + ctime.into().unwrap_or_else(SystemTime::now), + PublicKeyAlgorithm::EdDSA, + mpi::PublicKey::EdDSA { + curve: Curve::Ed25519, + q: mpi::MPI::new(&public) + }, + mpi::SecretKeyMaterial::EdDSA { + scalar: mpi::MPI::new(&private_key).into(), + }.into() + ) + } + + /// Creates a new OpenPGP public key packet for an existing RSA key. + /// + /// The RSA key will use public exponent `e` and modulo `n`. The key will + /// have it's creation date set to `ctime` or the current time if `None` + /// is given. + pub fn import_secret_rsa(d: &[u8], p: &[u8], q: &[u8], ctime: T) + -> Result where T: Into> + { + // RFC 4880: `p < q` + let (p, q) = if p < q { (p, q) } else { (q, p) }; + + // RustCrypto can't compute the public key from the private one, so do it ourselves + let big_p = BigUint::from_bytes_be(p); + let big_q = BigUint::from_bytes_be(q); + let n = big_p.clone() * big_q.clone(); + + let big_d = BigUint::from_bytes_be(d); + let big_phi = (big_p.clone() - 1u32) * (big_q.clone() - 1u32); + let e = big_d.mod_inverse(big_phi) // e ≡ d⁻¹ (mod 𝜙) + .and_then(|x| x.to_biguint()) + .ok_or_else(|| Error::MalformedMPI("RSA: `d` and `(p-1)(q-1)` aren't coprime".into()))?; + + let u: BigUint = big_p.mod_inverse(big_q) // RFC 4880: u ≡ p⁻¹ (mod q) + .and_then(|x| x.to_biguint()) + .ok_or_else(|| Error::MalformedMPI("RSA: `p` and `q` aren't coprime".into()))?; + + Self::with_secret( + ctime.into().unwrap_or_else(SystemTime::now), + PublicKeyAlgorithm::RSAEncryptSign, + mpi::PublicKey::RSA { + e: mpi::MPI::new(&e.to_bytes_be()), + n: mpi::MPI::new(&n.to_bytes_be()), + }, + mpi::SecretKeyMaterial::RSA { + d: mpi::MPI::new(d).into(), + p: mpi::MPI::new(p).into(), + q: mpi::MPI::new(q).into(), + u: mpi::MPI::new(&u.to_bytes_be()).into(), + }.into() + ) + } + + /// Generates a new RSA key with a public modulos of size `bits`. + pub fn generate_rsa(bits: usize) -> Result { + let key = RSAPrivateKey::new(&mut OsRng, bits)?; + let (p, q) = match key.primes() { + [p, q] => (p, q), + _ => panic!("RSA key generation resulted in wrong number of primes"), + }; + let u = p.mod_inverse(q) // RFC 4880: u ≡ p⁻¹ (mod q) + .and_then(|x| x.to_biguint()) + .expect("rsa crate did not generate coprime p and q"); + + let public = mpi::PublicKey::RSA { + e: mpi::MPI::new(&key.to_public_key().e().to_bytes_be()), + n: mpi::MPI::new(&key.to_public_key().n().to_bytes_be()), + }; + + let private = mpi::SecretKeyMaterial::RSA { + p: mpi::MPI::new(&p.to_bytes_be()).into(), + q: mpi::MPI::new(&q.to_bytes_be()).into(), + d: mpi::MPI::new(&key.d().to_bytes_be()).into(), + u: mpi::MPI::new(&u.to_bytes_be()).into(), + }; + + Self::with_secret( + SystemTime::now(), + PublicKeyAlgorithm::RSAEncryptSign, + public, + private.into(), + ) + } + + /// Generates a new ECC key over `curve`. + /// + /// If `for_signing` is false a ECDH key, if it's true either a + /// EdDSA or ECDSA key is generated. Giving `for_signing == true` and + /// `curve == Cv25519` will produce an error. Likewise + /// `for_signing == false` and `curve == Ed25519` will produce an error. + pub fn generate_ecc(for_signing: bool, curve: Curve) -> Result { + let (algo, public, private) = match (&curve, for_signing) { + (Curve::Ed25519, true) => { + use ed25519_dalek::Keypair; + + let Keypair { public, secret } = Keypair::generate(&mut OsRng); + + let secret: Protected = secret.as_bytes().as_ref().into(); + + // Mark MPI as compressed point with 0x40 prefix. See + // https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-07#section-13.2. + let mut compressed_public = [0u8; 1 + CURVE25519_SIZE]; + compressed_public[0] = 0x40; + &mut compressed_public[1..].copy_from_slice(public.as_bytes()); + + ( + PublicKeyAlgorithm::EdDSA, + mpi::PublicKey::EdDSA { curve, q: mpi::MPI::new(&compressed_public) }, + mpi::SecretKeyMaterial::EdDSA { scalar: secret.into() }, + ) + } + + (Curve::Cv25519, false) => { + use x25519_dalek::{StaticSecret, PublicKey}; + + let private_key = StaticSecret::new(OsRng); + let public_key = PublicKey::from(&private_key); + + let mut private_key = Vec::from(private_key.to_bytes()); + private_key.reverse(); + + let public_mpis = mpi::PublicKey::ECDH { + curve: Curve::Cv25519, + q: MPI::new_compressed_point(&*public_key.as_bytes()), + hash: HashAlgorithm::SHA256, + sym: SymmetricAlgorithm::AES256, + }; + let private_mpis = mpi::SecretKeyMaterial::ECDH { + scalar: private_key.into(), + }; + + (PublicKeyAlgorithm::ECDH, public_mpis, private_mpis.into()) + } + + _ => { + return Err(Error::UnsupportedEllipticCurve(curve).into()); + } + }; + Self::with_secret(SystemTime::now(), algo, public, private.into()) + } +} + diff --git a/openpgp/src/crypto/backend/rust/ecdh.rs b/openpgp/src/crypto/backend/rust/ecdh.rs new file mode 100644 index 00000000..519782f4 --- /dev/null +++ b/openpgp/src/crypto/backend/rust/ecdh.rs @@ -0,0 +1,99 @@ +//! Elliptic Curve Diffie-Hellman. + +use std::convert::TryInto; + +use rand::rngs::OsRng; + +use crate::{Error, Result}; +use crate::crypto::SessionKey; +use crate::crypto::mem::Protected; +use crate::crypto::ecdh::{encrypt_wrap, decrypt_unwrap}; +use crate::crypto::mpi::{self, Ciphertext, SecretKeyMaterial, MPI}; +use crate::packet::{key, Key}; +use crate::types::Curve; + +const CURVE25519_SIZE: usize = 32; + +/// Wraps a session key using Elliptic Curve Diffie-Hellman. +#[allow(non_snake_case)] +pub fn encrypt(recipient: &Key, + session_key: &SessionKey) + -> Result + where R: key::KeyRole +{ + let (curve, q) = match recipient.mpis() { + mpi::PublicKey::ECDH { curve, q, .. } => (curve, q), + _ => return Err(Error::InvalidArgument("Expected an ECDHPublicKey".into()).into()), + }; + + let (VB, shared) = match curve { + Curve::Cv25519 => { + use x25519_dalek::{EphemeralSecret, PublicKey}; + + // Decode the recipient's public key. + let R: [u8; CURVE25519_SIZE] = q.decode_point(curve)?.0.try_into()?; + let recipient_key = PublicKey::from(R); + + // Generate a keypair and perform Diffie-Hellman. + let secret = EphemeralSecret::new(OsRng); + let public = PublicKey::from(&secret); + let shared = secret.diffie_hellman(&recipient_key); + + // Encode our public key. We need to add an encoding + // octet in front of the key. + let mut VB = [0; 1 + CURVE25519_SIZE]; + VB[0] = 0x40; + &mut VB[1..].copy_from_slice(public.as_bytes()); + let VB = MPI::new(&VB); + + // Encode the shared secret. + let shared: &[u8] = shared.as_bytes(); + let shared = Protected::from(shared); + + (VB, shared) + }, + _ => + return Err(Error::UnsupportedEllipticCurve(curve.clone()).into()), + }; + + encrypt_wrap(recipient, session_key, VB, &shared) +} + +/// Unwraps a session key using Elliptic Curve Diffie-Hellman. +#[allow(non_snake_case)] +pub fn decrypt(recipient: &Key, + recipient_sec: &SecretKeyMaterial, + ciphertext: &Ciphertext) + -> Result + where R: key::KeyRole +{ + let (curve, scalar, e) = match (recipient.mpis(), recipient_sec, ciphertext) { + (mpi::PublicKey::ECDH { ref curve, ..}, + SecretKeyMaterial::ECDH { ref scalar, }, + Ciphertext::ECDH { ref e, .. }) => (curve, scalar, e), + _ => return Err(Error::InvalidArgument("Expected an ECDHPublicKey".into()).into()), + }; + + let S: Protected = match curve { + Curve::Cv25519 => { + use x25519_dalek::{PublicKey, StaticSecret}; + + // Get the public part V of the ephemeral key. + let V: [u8; CURVE25519_SIZE] = e.decode_point(curve)?.0.try_into()?; + let V = PublicKey::from(V); + + let mut scalar: [u8; CURVE25519_SIZE] = + scalar.value_padded(CURVE25519_SIZE).as_ref().try_into()?; + scalar.reverse(); + let r = StaticSecret::from(scalar); + + let secret = r.diffie_hellman(&V); + Vec::from(secret.to_bytes()).into() + }, + _ => { + return Err(Error::UnsupportedEllipticCurve(curve.clone()).into()); + }, + }; + + decrypt_unwrap(recipient, &S, ciphertext) +} diff --git a/openpgp/src/crypto/backend/rust/hash.rs b/openpgp/src/crypto/backend/rust/hash.rs new file mode 100644 index 00000000..fa3b9f2c --- /dev/null +++ b/openpgp/src/crypto/backend/rust/hash.rs @@ -0,0 +1,80 @@ +use std::cmp; + +use digest::Digest as _; + +use crate::{Error, Result}; +use crate::crypto::hash::Digest; +use crate::types::HashAlgorithm; + +macro_rules! impl_digest_for { + ($ty:ty, $algo:ident) => { + impl Digest for $ty { + fn algo(&self) -> HashAlgorithm { + HashAlgorithm::$algo + } + + fn digest_size(&self) -> usize { + Self::output_size() + } + + fn update(&mut self, data: &[u8]) { + digest::Digest::update(self, data) + } + + fn digest(&mut self, digest: &mut [u8]) -> Result<()> { + let buf = self.finalize_reset(); + let n = cmp::min(buf.len(), digest.len()); + digest[..n].copy_from_slice(&buf[..n]); + Ok(()) + } + } + } +} + +impl_digest_for!(md5::Md5, MD5); +impl_digest_for!(ripemd160::Ripemd160, RipeMD); +impl_digest_for!(sha1::Sha1, SHA1); +impl_digest_for!(sha2::Sha224, SHA224); +impl_digest_for!(sha2::Sha256, SHA256); +impl_digest_for!(sha2::Sha384, SHA384); +impl_digest_for!(sha2::Sha512, SHA512); + +impl HashAlgorithm { + /// Whether Sequoia supports this algorithm. + pub fn is_supported(self) -> bool { + match self { + HashAlgorithm::SHA1 => true, + HashAlgorithm::SHA224 => true, + HashAlgorithm::SHA256 => true, + HashAlgorithm::SHA384 => true, + HashAlgorithm::SHA512 => true, + HashAlgorithm::RipeMD => true, + HashAlgorithm::MD5 => true, + HashAlgorithm::Private(_) => false, + HashAlgorithm::Unknown(_) => false, + } + } + + /// Creates a new hash context for this algorithm. + /// + /// # Errors + /// + /// Fails with `Error::UnsupportedHashAlgorithm` if Sequoia does + /// not support this algorithm. See + /// [`HashAlgorithm::is_supported`]. + /// + /// [`HashAlgorithm::is_supported`]: #method.is_supported + pub(crate) fn new_hasher(self) -> Result> { + match self { + HashAlgorithm::SHA1 => Ok(Box::new(sha1::Sha1::new())), + HashAlgorithm::SHA224 => Ok(Box::new(sha2::Sha224::new())), + HashAlgorithm::SHA256 => Ok(Box::new(sha2::Sha256::new())), + HashAlgorithm::SHA384 => Ok(Box::new(sha2::Sha384::new())), + HashAlgorithm::SHA512 => Ok(Box::new(sha2::Sha512::new())), + HashAlgorithm::RipeMD => Ok(Box::new(ripemd160::Ripemd160::new())), + HashAlgorithm::MD5 => Ok(Box::new(md5::Md5::new())), + HashAlgorithm::Private(_) | HashAlgorithm::Unknown(_) => + Err(Error::UnsupportedHashAlgorithm(self).into()), + } + } +} diff --git a/openpgp/src/crypto/backend/rust/symmetric.rs b/openpgp/src/crypto/backend/rust/symmetric.rs new file mode 100644 index 00000000..6f6d027c --- /dev/null +++ b/openpgp/src/crypto/backend/rust/symmetric.rs @@ -0,0 +1,199 @@ +use std::slice; + +use block_modes::{BlockMode, Cfb, Ecb}; +use block_padding::ZeroPadding; +use cipher::{BlockCipher, NewBlockCipher}; +use generic_array::{ArrayLength, GenericArray}; +use typenum::Unsigned; + +use crate::{Error, Result}; +use crate::crypto::symmetric::Mode; +use crate::types::SymmetricAlgorithm; + +macro_rules! impl_mode { + ($mode:ident) => { + impl Mode for $mode + where + C: BlockCipher + NewBlockCipher + Send + Sync, + { + fn block_size(&self) -> usize { + C::BlockSize::to_usize() + } + + fn encrypt( + &mut self, + dst: &mut [u8], + src: &[u8], + ) -> Result<()> { + debug_assert_eq!(dst.len(), src.len()); + let bs = self.block_size(); + let missing = (bs - (dst.len() % bs)) % bs; + if missing > 0 { + let mut buf = vec![0u8; src.len() + missing]; + buf[..src.len()].copy_from_slice(src); + self.encrypt_blocks(to_blocks(&mut buf)); + dst.copy_from_slice(&buf[..dst.len()]); + } else { + dst.copy_from_slice(src); + self.encrypt_blocks(to_blocks(dst)); + } + Ok(()) + } + + fn decrypt( + &mut self, + dst: &mut [u8], + src: &[u8], + ) -> Result<()> { + debug_assert_eq!(dst.len(), src.len()); + let bs = self.block_size(); + let missing = (bs - (dst.len() % bs)) % bs; + if missing > 0 { + let mut buf = vec![0u8; src.len() + missing]; + buf[..src.len()].copy_from_slice(src); + self.decrypt_blocks(to_blocks(&mut buf)); + dst.copy_from_slice(&buf[..dst.len()]); + } else { + dst.copy_from_slice(src); + self.decrypt_blocks(to_blocks(dst)); + } + Ok(()) + } + } + } +} + +impl_mode!(Cfb); +impl_mode!(Ecb); + +fn to_blocks(data: &mut [u8]) -> &mut [GenericArray] +where + N: ArrayLength, +{ + let n = N::to_usize(); + debug_assert!(data.len() % n == 0); + unsafe { + slice::from_raw_parts_mut(data.as_ptr() as *mut GenericArray, data.len() / n) + } +} + +impl SymmetricAlgorithm { + /// Returns whether this algorithm is supported by the crypto backend. + /// + /// All backends support all the AES variants. + /// + /// # Examples + /// + /// ```rust + /// use sequoia_openpgp as openpgp; + /// use openpgp::types::SymmetricAlgorithm; + /// + /// assert!(SymmetricAlgorithm::AES256.is_supported()); + /// assert!(SymmetricAlgorithm::TripleDES.is_supported()); + /// assert!(SymmetricAlgorithm::IDEA.is_supported()); + /// + /// assert!(!SymmetricAlgorithm::Unencrypted.is_supported()); + /// assert!(!SymmetricAlgorithm::Private(101).is_supported()); + /// ``` + pub fn is_supported(&self) -> bool { + use SymmetricAlgorithm::*; + match self { + IDEA => true, + TripleDES => true, + CAST5 => true, + Blowfish => true, + AES128 => true, + AES192 => true, + AES256 => true, + Twofish => true, + Camellia128 => false, + Camellia192 => false, + Camellia256 => false, + Private(_) => false, + Unknown(_) => false, + Unencrypted => false, + } + } + + /// Length of a key for this algorithm in bytes. + /// + /// Fails if Sequoia does not support this algorithm. + pub fn key_size(self) -> Result { + use SymmetricAlgorithm::*; + match self { + IDEA => Ok(::KeySize::to_usize()), + TripleDES => Ok(::KeySize::to_usize()), + CAST5 => Ok(::KeySize::to_usize()), + Blowfish => Ok(::KeySize::to_usize()), + AES128 => Ok(::KeySize::to_usize()), + AES192 => Ok(::KeySize::to_usize()), + AES256 => Ok(::KeySize::to_usize()), + Twofish => Ok(::KeySize::to_usize()), + Camellia128 | Camellia192 | Camellia256 | Private(_) | Unknown(_) | Unencrypted => + Err(Error::UnsupportedSymmetricAlgorithm(self).into()), + } + } + + /// Length of a block for this algorithm in bytes. + /// + /// Fails if Sequoia does not support this algorithm. + pub fn block_size(self) -> Result { + use SymmetricAlgorithm::*; + match self { + IDEA => Ok(::BlockSize::to_usize()), + TripleDES => Ok(::BlockSize::to_usize()), + CAST5 => Ok(::BlockSize::to_usize()), + Blowfish => Ok(::BlockSize::to_usize()), + AES128 => Ok(::BlockSize::to_usize()), + AES192 => Ok(::BlockSize::to_usize()), + AES256 => Ok(::BlockSize::to_usize()), + Twofish => Ok(::BlockSize::to_usize()), + Camellia128 | Camellia192 | Camellia256 | Private(_) | Unknown(_) | Unencrypted => + Err(Error::UnsupportedSymmetricAlgorithm(self).into()), + } + } + + /// Creates a context for encrypting in CFB mode. + pub(crate) fn make_encrypt_cfb(self, key: &[u8], iv: Vec) -> Result> { + use SymmetricAlgorithm::*; + match self { + IDEA => Ok(Box::new(Cfb::::new_var(key, &iv)?)), + TripleDES => Ok(Box::new(Cfb::::new_var(key, &iv)?)), + CAST5 => Ok(Box::new(Cfb::::new_var(key, &iv)?)), + Blowfish => Ok(Box::new(Cfb::::new_var(key, &iv)?)), + AES128 => Ok(Box::new(Cfb::::new_var(key, &iv)?)), + AES192 => Ok(Box::new(Cfb::::new_var(key, &iv)?)), + AES256 => Ok(Box::new(Cfb::::new_var(key, &iv)?)), + Twofish => Ok(Box::new(Cfb::::new_var(key, &iv)?)), + Camellia128 | Camellia192 | Camellia256 | Private(_) | Unknown(_) | Unencrypted => + Err(Error::UnsupportedSymmetricAlgorithm(self).into()), + } + } + + /// Creates a context for decrypting in CFB mode. + pub(crate) fn make_decrypt_cfb(self, key: &[u8], iv: Vec) -> Result> { + self.make_encrypt_cfb(key, iv) + } + + /// Creates a context for encrypting in ECB mode. + pub(crate) fn make_encrypt_ecb(self, key: &[u8]) -> Result> { + use SymmetricAlgorithm::*; + match self { + IDEA => Ok(Box::new(Ecb::::new_var(key, &[])?)), + TripleDES => Ok(Box::new(Ecb::::new_var(key, &[])?)), + CAST5 => Ok(Box::new(Ecb::::new_var(key, &[])?)), + Blowfish => Ok(Box::new(Ecb::::new_var(key, &[])?)), + AES128 => Ok(Box::new(Ecb::::new_var(key, &[])?)), + AES192 => Ok(Box::new(Ecb::::new_var(key, &[])?)), + AES256 => Ok(Box::new(Ecb::::new_var(key, &[])?)), + Twofish => Ok(Box::new(Ecb::::new_var(key, &[])?)), + Camellia128 | Camellia192 | Camellia256 | Private(_) | Unknown(_) | Unencrypted => + Err(Error::UnsupportedSymmetricAlgorithm(self).into()), + } + } + + /// Creates a context for decrypting in ECB mode. + pub(crate) fn make_decrypt_ecb(self, key: &[u8]) -> Result> { + self.make_encrypt_ecb(key) + } +} -- cgit v1.2.3