use std::{io::Read, sync::LazyLock};
use pgp::{
composed::{
ArmorOptions,
DecryptionOptions,
Encryption,
Esk,
Message,
MessageBuilder,
PlainSessionKey,
SignedSecretKey,
TheRing,
VerificationResult,
decrypt_session_key_with_password,
},
crypto::{
aead::{AeadAlgorithm, ChunkSize},
sym::SymmetricKeyAlgorithm,
},
packet::{Features, PacketTrait, Signature},
ser::Serialize,
types::{
EncryptedSecretParams,
KeyDetails,
Password,
SecretParams,
SigningKey,
StringToKey,
Tag,
Timestamp,
VerifyingKey,
},
};
use rand::{CryptoRng, Rng};
use zeroize::Zeroizing;
use crate::{
Error,
certificate::{Certificate, Checked},
key::{ComponentKeyPriv, ComponentKeyPub, SignedComponentKeyPub},
message,
policy::Seipd,
tsk::Tsk,
};
static PW_EMPTY: LazyLock<Password> = LazyLock::new(Password::empty);
pub struct MessageResult {
pub validated: Vec<(Certificate, ComponentKeyPub, Signature)>,
pub session_key: Option<(u8, Vec<u8>)>,
pub cleartext: Vec<u8>,
}
pub fn unpack(
mut msg: Message,
decryptors: Vec<&SignedSecretKey>,
dec_pws: Vec<&Password>,
skesk_passwords: Vec<&Password>,
session_keys: &[(SymmetricKeyAlgorithm, &[u8])],
verifier: &[Certificate],
) -> Result<MessageResult, Error> {
if msg.is_compressed() {
msg = msg.decompress()?;
}
let session_keys: Vec<PlainSessionKey> = session_keys
.iter()
.map(|(algo, key)| PlainSessionKey::V3_4 {
key: (*key).into(),
sym_alg: *algo,
})
.collect();
let mut sk = None;
if msg.is_encrypted() {
let Message::Encrypted { esk, .. } = &msg else {
return Err(Error::Message(
"Inconsistent encryption state of message".to_string(),
));
};
if !session_keys.is_empty() {
sk = Some(session_keys[0].clone());
if session_keys.len() > 1 {
log::warn!("Multiple session keys are not supported, trying the first one")
}
}
let mut tried_locked_decryptor = false;
for esk in esk {
match esk {
Esk::PublicKeyEncryptedSessionKey(pkesk) => {
for ssk in &decryptors {
let pws = if !dec_pws.is_empty() {
dec_pws.as_slice()
} else {
&[&Password::empty()]
};
for pw in pws {
let tsk = Tsk::from((*ssk).clone());
for cks in tsk.decryption_keys_sec() {
if cks.is_locked() {
tried_locked_decryptor = true;
}
if let Ok(s) = cks.decrypt_session_key(pkesk, pw) {
sk = Some(s);
break;
}
}
}
}
}
Esk::SymKeyEncryptedSessionKey(skesk) => {
for pw in &skesk_passwords {
if let Ok(session_key) = decrypt_session_key_with_password(skesk, pw) {
sk = Some(session_key);
break;
}
}
}
}
}
match &sk {
Some(sk) => {
let ring = TheRing {
session_keys: vec![sk.clone()],
decrypt_options: DecryptionOptions::new().enable_gnupg_aead(),
..Default::default()
};
msg = msg.decrypt_the_ring(ring, true)?.0;
}
None => {
if tried_locked_decryptor {
return Err(Error::Message(
"Couldn't unlock secret key packet".to_string(),
));
} else {
return Err(Error::Message("Couldn't decrypt message".to_string()));
}
}
}
}
let mut i = 0;
while msg.is_compressed() && i < 3 {
msg = msg.decompress()?;
i += 1;
}
let cleartext = msg.as_data_vec()?;
let mut verifiers: Vec<(SignedComponentKeyPub, &Certificate)> = vec![];
for vcert in verifier {
vcert
.validation_capable_component_keys()
.for_each(|v| verifiers.push((v, vcert)));
}
let keys: Vec<_> = verifiers
.iter()
.map(|(sck, _)| sck as &dyn VerifyingKey)
.collect();
let verification_result = msg.verify_nested(keys.as_ref())?;
let validations: Vec<(Certificate, ComponentKeyPub, Signature)> = verification_result
.iter()
.zip(verifiers.iter())
.filter(|(vr, _)| {
if let VerificationResult::Valid(sig) = &vr {
crate::signature::signature_acceptable(sig)
} else {
false
}
})
.filter(|(vr, (sck, cert))| {
if let VerificationResult::Valid(sig) = &vr {
if let Some(created) = sig.created() {
if created > Timestamp::now() {
return false;
}
let checked = Checked::from((*cert).clone());
let valid = checked.valid_signing_capable_component_keys_at(created);
valid
.iter()
.any(|sv| sv.as_componentkey().fingerprint() == sck.fingerprint())
} else {
false
}
} else {
false
}
})
.map(|(vr, (sckp, cert))| {
let VerificationResult::Valid(sig) = &vr else {
unreachable!("checked")
};
let ckp: ComponentKeyPub = sckp.clone().into();
((*cert).clone(), ckp, sig.clone())
})
.collect();
let mr = MessageResult {
cleartext,
session_key: sk.and_then(|sk| match &sk {
PlainSessionKey::V3_4 { key, sym_alg } => {
Some(((*sym_alg).into(), key.as_ref().to_vec()))
}
_ => None,
}),
validated: validations,
};
Ok(mr)
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum EncryptionMechanism {
SeipdV1(SymmetricKeyAlgorithm),
SeipdV2(AeadAlgorithm, SymmetricKeyAlgorithm),
}
pub enum SignatureMode {
Binary,
Text,
}
#[allow(clippy::too_many_arguments)]
pub fn encrypt(
seipd: Option<Seipd>,
recipients: Vec<Certificate>,
skesk_passwords: Vec<&Password>,
signers: Vec<Tsk>,
signers_passwords: &[&Password],
source: &mut (dyn Read + Send + Sync),
signature_mode: SignatureMode,
mut sink: &mut (dyn std::io::Write + Send + Sync),
armor: bool,
) -> Result<(Zeroizing<Vec<u8>>, Option<SymmetricKeyAlgorithm>), Error> {
let mut rng = rand::thread_rng();
let checked: Vec<Checked> = recipients.iter().map(|c| c.clone().into()).collect();
let now = Timestamp::now();
let mut symmetric_algorithms_pref = crate::policy::PREFERRED_SYMMETRIC_KEY_ALGORITHMS.to_vec();
let mut aead_algorithms_pref = crate::policy::PREFERRED_AEAD_ALGORITHMS.to_vec();
let mut seipd_pref = crate::policy::PREFERRED_SEIPD_MECHANISMS.to_vec();
for ccert in checked {
if let Some(p) = ccert.preferred_symmetric_key_algo(now) {
symmetric_algorithms_pref.retain(|a| p.contains(a));
}
if let Some(p) = ccert.preferred_aead_algo(now) {
aead_algorithms_pref.retain(|a| p.contains(a));
}
if let Some(feat) = ccert.features(now) {
fn contains(feat: &Features, seipd: Seipd) -> bool {
match seipd {
Seipd::SEIPD1 => feat.seipd_v1(),
Seipd::SEIPD2 => feat.seipd_v2(),
}
}
seipd_pref.retain(|a| contains(feat, *a));
} else {
seipd_pref = vec![Seipd::SEIPD1]
}
}
let mechanism = if !recipients.is_empty() {
let seipd = *seipd_pref.first().unwrap_or(&Seipd::SEIPD1);
match seipd {
Seipd::SEIPD1 => {
let symmetric_algo = symmetric_algorithms_pref
.first()
.cloned()
.unwrap_or(SymmetricKeyAlgorithm::default());
message::EncryptionMechanism::SeipdV1(symmetric_algo)
}
Seipd::SEIPD2 => {
let aead_algo = aead_algorithms_pref
.first()
.unwrap_or(&(SymmetricKeyAlgorithm::AES128, AeadAlgorithm::Ocb));
message::EncryptionMechanism::SeipdV2(aead_algo.1, aead_algo.0)
}
}
} else {
match seipd.unwrap_or(Seipd::SEIPD1) {
Seipd::SEIPD1 => EncryptionMechanism::SeipdV1(SymmetricKeyAlgorithm::default()),
Seipd::SEIPD2 => {
EncryptionMechanism::SeipdV2(AeadAlgorithm::Ocb, SymmetricKeyAlgorithm::default())
}
}
};
let mut builder = MessageBuilder::from_reader("", source);
if matches![signature_mode, SignatureMode::Text] {
builder.sign_text();
}
let session_key;
let sym_alg;
fn pqc_picker(keys: Vec<ComponentKeyPub>) -> Vec<ComponentKeyPub> {
let has_pqc = keys.iter().any(|k| k.algorithm().is_pqc());
if has_pqc {
keys.into_iter()
.filter(|k| k.algorithm().is_pqc())
.collect()
} else {
keys.into_iter()
.filter(|k| !k.algorithm().is_pqc())
.collect()
}
}
match mechanism {
EncryptionMechanism::SeipdV1(sym) => {
let mut builder = builder.seipd_v1(&mut rng, sym);
session_key = builder.session_key().clone();
sym_alg = Some(sym);
for cert in &recipients {
let mut encrypted = false;
let ccert: Checked = cert.clone().into();
let enc = ccert.valid_encryption_capable_component_keys();
for key in pqc_picker(enc) {
if builder.encrypt_to_key(&mut rng, &key).is_ok() {
encrypted = true;
}
}
if !encrypted {
return Err(Error::Message("can't encrypt to this cert".to_string()));
}
}
for pw in skesk_passwords {
builder.encrypt_with_password(StringToKey::new_default(&mut rng), pw)?;
}
sign_and_write(
&mut rng,
builder,
signers,
signers_passwords,
&mut sink,
armor,
)?;
}
EncryptionMechanism::SeipdV2(aead, sym) => {
let mut builder = builder.seipd_v2(&mut rng, sym, aead, ChunkSize::default());
session_key = builder.session_key().clone();
sym_alg = None;
for cert in &recipients {
let mut encrypted = false;
let ccert: Checked = cert.clone().into();
let enc = ccert.valid_encryption_capable_component_keys();
for key in pqc_picker(enc) {
if builder.encrypt_to_key(&mut rng, &key).is_ok() {
encrypted = true;
}
}
if !encrypted {
return Err(Error::Message("can't encrypt to this cert".to_string()));
}
}
for pw in skesk_passwords {
let s2k = StringToKey::new_argon2(&mut rng, 3, 4, 16);
builder.encrypt_with_password(&mut rng, s2k, pw)?;
}
sign_and_write(
&mut rng,
builder,
signers,
signers_passwords,
&mut sink,
armor,
)?;
}
};
Ok((session_key.as_ref().to_vec().into(), sym_alg))
}
fn sign_and_write<R: Read, RAND: CryptoRng + Rng, E: Encryption>(
rng: &mut RAND,
builder: MessageBuilder<R, E>,
signers: Vec<Tsk>,
signers_passwords: &[&Password],
sink: &mut (dyn std::io::Write + Send + Sync),
armor: bool,
) -> Result<(), Error> {
let mut data_signers = vec![];
for signer in &signers {
if let Some(ds) = signer.signing_capable_component_keys().next() {
data_signers.push(ds);
} else {
return Err(Error::Message(
"No signing capable component key found for signer".to_string(),
));
}
}
let mut builder = builder;
for ds in &data_signers {
let key = &ds.key;
fn find_matching_pw<'a>(
sec: &EncryptedSecretParams,
pws: &[&'a Password],
pub_key: &(impl VerifyingKey + Serialize),
secret_tag: Option<Tag>,
) -> Option<&'a Password> {
pws.iter()
.find(|&pw| sec.unlock(pw, pub_key, secret_tag).is_ok())
.map(|pw| &**pw)
}
fn pick_pw<'a>(key: &ComponentKeyPriv, pw: &[&'a Password]) -> Option<&'a Password> {
match key {
ComponentKeyPriv::Subkey(ssk) => {
let sec = ssk.secret_params();
match sec {
SecretParams::Plain(_) => Some(&PW_EMPTY),
SecretParams::Encrypted(sec) => {
find_matching_pw(sec, pw, &ssk.public_key(), Some(ssk.tag()))
}
}
}
ComponentKeyPriv::Primary(sk) => {
let sec = sk.secret_params();
match sec {
SecretParams::Plain(_) => Some(&PW_EMPTY),
SecretParams::Encrypted(sec) => {
find_matching_pw(sec, pw, &sk.public_key(), Some(sk.tag()))
}
}
}
}
}
if let Some(pw) = pick_pw(key, signers_passwords) {
let pw_owned = Password::Static(pw.read());
let hash_algorithm = (*key).hash_alg();
builder.sign(key, pw_owned, hash_algorithm);
} else {
return Err(Error::Message("cannot sign".to_string()));
}
}
if armor {
builder.to_armored_writer(rng, ArmorOptions::default(), sink)?;
} else {
builder.to_writer(rng, sink)?;
}
Ok(())
}