Files
chatmail-core/src/pgp.rs
2022-12-23 18:43:24 +00:00

583 lines
19 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
#![allow(missing_docs)]
use std::collections::{BTreeMap, HashSet};
use std::io;
use std::io::Cursor;
use anyhow::{bail, format_err, Context as _, Result};
use pgp::armor::BlockType;
use pgp::composed::{
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
SignedPublicSubKey, SignedSecretKey, StandaloneSignature, SubkeyParamsBuilder,
};
use pgp::crypto::{HashAlgorithm, SymmetricKeyAlgorithm};
use pgp::types::{
CompressionAlgorithm, KeyTrait, Mpi, PublicKeyTrait, SecretKeyTrait, StringToKey,
};
use rand::{thread_rng, CryptoRng, Rng};
use tokio::runtime::Handle;
use crate::constants::KeyGenType;
use crate::key::{DcKey, Fingerprint};
use crate::keyring::Keyring;
use crate::tools::EmailAddress;
pub const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
pub const HEADER_SETUPCODE: &str = "passphrase-begin";
/// A wrapper for rPGP public key types
#[derive(Debug)]
enum SignedPublicKeyOrSubkey<'a> {
Key(&'a SignedPublicKey),
Subkey(&'a SignedPublicSubKey),
}
impl<'a> KeyTrait for SignedPublicKeyOrSubkey<'a> {
fn fingerprint(&self) -> Vec<u8> {
match self {
Self::Key(k) => k.fingerprint(),
Self::Subkey(k) => k.fingerprint(),
}
}
fn key_id(&self) -> pgp::types::KeyId {
match self {
Self::Key(k) => k.key_id(),
Self::Subkey(k) => k.key_id(),
}
}
fn algorithm(&self) -> pgp::crypto::PublicKeyAlgorithm {
match self {
Self::Key(k) => k.algorithm(),
Self::Subkey(k) => k.algorithm(),
}
}
}
impl<'a> PublicKeyTrait for SignedPublicKeyOrSubkey<'a> {
fn verify_signature(
&self,
hash: HashAlgorithm,
data: &[u8],
sig: &[Mpi],
) -> pgp::errors::Result<()> {
match self {
Self::Key(k) => k.verify_signature(hash, data, sig),
Self::Subkey(k) => k.verify_signature(hash, data, sig),
}
}
fn encrypt<R: Rng + CryptoRng>(
&self,
rng: &mut R,
plain: &[u8],
) -> pgp::errors::Result<Vec<Mpi>> {
match self {
Self::Key(k) => k.encrypt(rng, plain),
Self::Subkey(k) => k.encrypt(rng, plain),
}
}
fn to_writer_old(&self, writer: &mut impl io::Write) -> pgp::errors::Result<()> {
match self {
Self::Key(k) => k.to_writer_old(writer),
Self::Subkey(k) => k.to_writer_old(writer),
}
}
}
/// Split data from PGP Armored Data as defined in <https://tools.ietf.org/html/rfc4880#section-6.2>.
///
/// Returns (type, headers, base64 encoded body).
pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, String>, Vec<u8>)> {
use std::io::Read;
let cursor = Cursor::new(buf);
let mut dearmor = pgp::armor::Dearmor::new(cursor);
let mut bytes = Vec::with_capacity(buf.len());
dearmor.read_to_end(&mut bytes)?;
let typ = dearmor.typ.context("failed to parse type")?;
// normalize headers
let headers = dearmor
.headers
.into_iter()
.map(|(key, value)| (key.trim().to_lowercase(), value.trim().to_string()))
.collect();
Ok((typ, headers, bytes))
}
/// A PGP keypair.
///
/// This has it's own struct to be able to keep the public and secret
/// keys together as they are one unit.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct KeyPair {
pub addr: EmailAddress,
pub public: SignedPublicKey,
pub secret: SignedSecretKey,
}
/// Create a new key pair.
pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Result<KeyPair> {
let (secret_key_type, public_key_type) = match keygen_type {
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
};
let user_id = format!("<{}>", addr);
let key_params = SecretKeyParamsBuilder::default()
.key_type(secret_key_type)
.can_create_certificates(true)
.can_sign(true)
.primary_user_id(user_id)
.passphrase(None)
.preferred_symmetric_algorithms(smallvec![
SymmetricKeyAlgorithm::AES256,
SymmetricKeyAlgorithm::AES192,
SymmetricKeyAlgorithm::AES128,
])
.preferred_hash_algorithms(smallvec![
HashAlgorithm::SHA2_256,
HashAlgorithm::SHA2_384,
HashAlgorithm::SHA2_512,
HashAlgorithm::SHA2_224,
HashAlgorithm::SHA1,
])
.preferred_compression_algorithms(smallvec![
CompressionAlgorithm::ZLIB,
CompressionAlgorithm::ZIP,
])
.subkey(
SubkeyParamsBuilder::default()
.key_type(public_key_type)
.can_encrypt(true)
.passphrase(None)
.build()
.map_err(|e| format_err!("{}", e))
.context("failed to build subkey parameters")?,
)
.build()
.map_err(|e| format_err!("{}", e))
.context("invalid key params")?;
let key = key_params.generate().context("invalid params")?;
let private_key = key
.sign(|| "".into())
.map_err(|e| format_err!("{}", e))
.context("failed to sign secret key")?;
let public_key = private_key.public_key();
let public_key = public_key
.sign(&private_key, || "".into())
.map_err(|e| format_err!("{}", e))
.context("failed to sign public key")?;
private_key
.verify()
.map_err(|e| format_err!("{}", e))
.context("invalid private key generated")?;
public_key
.verify()
.map_err(|e| format_err!("{}", e))
.context("invalid public key generated")?;
Ok(KeyPair {
addr,
public: public_key,
secret: private_key,
})
}
/// Select public key or subkey to use for encryption.
///
/// First, tries to use subkeys. If none of the subkeys are suitable
/// for encryption, tries to use primary key. Returns `None` if the public
/// key cannot be used for encryption.
///
/// TODO: take key flags and expiration dates into account
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<SignedPublicKeyOrSubkey> {
key.public_subkeys
.iter()
.find(|subkey| subkey.is_encryption_key())
.map_or_else(
|| {
// No usable subkey found, try primary key
if key.is_encryption_key() {
Some(SignedPublicKeyOrSubkey::Key(key))
} else {
None
}
},
|subkey| Some(SignedPublicKeyOrSubkey::Subkey(subkey)),
)
}
/// Encrypts `plain` text using `public_keys_for_encryption`
/// and signs it using `private_key_for_signing`.
pub async fn pk_encrypt(
plain: &[u8],
public_keys_for_encryption: Keyring<SignedPublicKey>,
private_key_for_signing: Option<SignedSecretKey>,
) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain);
Handle::current()
.spawn_blocking(move || {
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
.keys()
.iter()
.filter_map(select_pk_for_encryption)
.collect();
let pkeys_refs: Vec<&SignedPublicKeyOrSubkey> = pkeys.iter().collect();
let mut rng = thread_rng();
// TODO: measure time
let encrypted_msg = if let Some(ref skey) = private_key_for_signing {
lit_msg
.sign(skey, || "".into(), Default::default())
.and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB))
.and_then(|msg| msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs))
} else {
lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs)
};
let msg = encrypted_msg?;
let encoded_msg = msg.to_armored_string(None)?;
Ok(encoded_msg)
})
.await?
}
/// Decrypts the message with keys from the private key keyring.
///
/// Receiver private keys are provided in
/// `private_keys_for_decryption`.
///
/// Returns decrypted message and fingerprints
/// of all keys from the `public_keys_for_validation` keyring that
/// have valid signatures there.
#[allow(clippy::implicit_hasher)]
pub fn pk_decrypt(
ctext: Vec<u8>,
private_keys_for_decryption: &Keyring<SignedSecretKey>,
public_keys_for_validation: &Keyring<SignedPublicKey>,
) -> Result<(Vec<u8>, HashSet<Fingerprint>)> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
let cursor = Cursor::new(ctext);
let (msg, _) = Message::from_armor_single(cursor)?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.keys().iter().collect();
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
if let Some(msg) = msgs.into_iter().next() {
// get_content() will decompress the message if needed,
// but this avoids decompressing it again to check signatures
let msg = msg.decompress()?;
let content = match msg.get_content()? {
Some(content) => content,
None => bail!("The decrypted message is empty"),
};
if !public_keys_for_validation.is_empty() {
let pkeys = public_keys_for_validation.keys();
let mut fingerprints: Vec<Fingerprint> = Vec::new();
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
for pkey in pkeys {
if signed_msg.verify(&pkey.primary_key).is_ok() {
let fp = DcKey::fingerprint(pkey);
fingerprints.push(fp);
}
}
}
ret_signature_fingerprints.extend(fingerprints);
}
Ok((content, ret_signature_fingerprints))
} else {
bail!("No valid messages found");
}
}
/// Validates detached signature.
pub fn pk_validate(
content: &[u8],
signature: &[u8],
public_keys_for_validation: &Keyring<SignedPublicKey>,
) -> Result<HashSet<Fingerprint>> {
let mut ret: HashSet<Fingerprint> = Default::default();
let standalone_signature = StandaloneSignature::from_armor_single(Cursor::new(signature))?.0;
let pkeys = public_keys_for_validation.keys();
// Remove trailing CRLF before the delimiter.
// According to RFC 3156 it is considered to be part of the MIME delimiter for the purpose of
// OpenPGP signature calculation.
let content = content
.get(..content.len().saturating_sub(2))
.context("index is out of range")?;
for pkey in pkeys {
if standalone_signature.verify(pkey, content).is_ok() {
let fp = DcKey::fingerprint(pkey);
ret.insert(fp);
}
}
Ok(ret)
}
/// Symmetric encryption.
pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain);
let passphrase = passphrase.to_string();
tokio::task::spawn_blocking(move || {
let mut rng = thread_rng();
let s2k = StringToKey::new_default(&mut rng);
let msg =
lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?;
let encoded_msg = msg.to_armored_string(None)?;
Ok(encoded_msg)
})
.await?
}
/// Symmetric decryption.
pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
passphrase: &str,
ctext: T,
) -> Result<Vec<u8>> {
let (enc_msg, _) = Message::from_armor_single(ctext)?;
let passphrase = passphrase.to_string();
tokio::task::spawn_blocking(move || {
let decryptor = enc_msg.decrypt_with_password(|| passphrase)?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
if let Some(msg) = msgs.first() {
match msg.get_content()? {
Some(content) => Ok(content),
None => bail!("Decrypted message is empty"),
}
} else {
bail!("No valid messages found")
}
})
.await?
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{alice_keypair, bob_keypair};
use once_cell::sync::Lazy;
use tokio::sync::OnceCell;
#[test]
fn test_split_armored_data_1() {
let (typ, _headers, base64) = split_armored_data(
b"-----BEGIN PGP MESSAGE-----\nNoVal:\n\naGVsbG8gd29ybGQ=\n-----END PGP MESSAGE----",
)
.unwrap();
assert_eq!(typ, BlockType::Message);
assert!(!base64.is_empty());
assert_eq!(
std::string::String::from_utf8(base64).unwrap(),
"hello world"
);
}
#[test]
fn test_split_armored_data_2() {
let (typ, headers, base64) = split_armored_data(
b"-----BEGIN PGP PRIVATE KEY BLOCK-----\nAutocrypt-Prefer-Encrypt: mutual \n\naGVsbG8gd29ybGQ=\n-----END PGP PRIVATE KEY BLOCK-----"
)
.unwrap();
assert_eq!(typ, BlockType::PrivateKey);
assert!(!base64.is_empty());
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
}
#[test]
fn test_create_keypair() {
let keypair0 = create_keypair(
EmailAddress::new("foo@bar.de").unwrap(),
KeyGenType::Default,
)
.unwrap();
let keypair1 = create_keypair(
EmailAddress::new("two@zwo.de").unwrap(),
KeyGenType::Default,
)
.unwrap();
assert_ne!(keypair0.public, keypair1.public);
}
/// [Key] objects to use in tests.
struct TestKeys {
alice_secret: SignedSecretKey,
alice_public: SignedPublicKey,
bob_secret: SignedSecretKey,
bob_public: SignedPublicKey,
}
impl TestKeys {
fn new() -> TestKeys {
let alice = alice_keypair();
let bob = bob_keypair();
TestKeys {
alice_secret: alice.secret.clone(),
alice_public: alice.public,
bob_secret: bob.secret.clone(),
bob_public: bob.public,
}
}
}
/// The original text of [CTEXT_SIGNED]
static CLEARTEXT: &[u8] = b"This is a test";
/// Initialised [TestKeys] for tests.
static KEYS: Lazy<TestKeys> = Lazy::new(TestKeys::new);
static CTEXT_SIGNED: OnceCell<String> = OnceCell::const_new();
static CTEXT_UNSIGNED: OnceCell<String> = OnceCell::const_new();
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
async fn ctext_signed() -> &'static String {
CTEXT_SIGNED
.get_or_init(|| async {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))
.await
.unwrap()
})
.await
}
/// A cyphertext encrypted to Alice & Bob, not signed.
async fn ctext_unsigned() -> &'static String {
CTEXT_UNSIGNED
.get_or_init(|| async {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
pk_encrypt(CLEARTEXT, keyring, None).await.unwrap()
})
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypt_signed() {
assert!(!ctext_signed().await.is_empty());
assert!(ctext_signed()
.await
.starts_with("-----BEGIN PGP MESSAGE-----"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypt_unsigned() {
assert!(!ctext_unsigned().await.is_empty());
assert!(ctext_unsigned()
.await
.starts_with("-----BEGIN PGP MESSAGE-----"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_singed() {
// Check decrypting as Alice
let mut decrypt_keyring: Keyring<SignedSecretKey> = Keyring::new();
decrypt_keyring.add(KEYS.alice_secret.clone());
let mut sig_check_keyring: Keyring<SignedPublicKey> = Keyring::new();
sig_check_keyring.add(KEYS.alice_public.clone());
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::new();
sig_check_keyring.add(KEYS.alice_public.clone());
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_no_sig_check() {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_secret.clone());
let empty_keyring = Keyring::new();
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&keyring,
&empty_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_signed_no_key() {
// The validation does not have the public key of the signer.
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::new();
sig_check_keyring.add(KEYS.bob_public.clone());
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_unsigned() {
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let sig_check_keyring = Keyring::new();
let (plain, valid_signatures) = pk_decrypt(
ctext_unsigned().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
}