rpgpie/msg/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
// SPDX-FileCopyrightText: Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Handling of OpenPGP messages.
use std::io;
use std::io::Read;
use pgp::crypto::aead::AeadAlgorithm;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::packet::{
LiteralData, Packet, PublicKeyEncryptedSessionKey, SymEncryptedProtectedData,
SymKeyEncryptedSessionKey,
};
use pgp::ser::Serialize;
use pgp::types::StringToKey;
use pgp::{decrypt_session_key_with_password, ArmorOptions, PlainSessionKey};
use pgp::{Deserializable, Edata, Esk};
use pgp::{Message, Signature};
use zeroize::Zeroizing;
use crate::key::checked::CheckedCertificate;
use crate::key::component::{ComponentKeyPub, SignedComponentKey};
use crate::key::{Certificate, Tsk};
use crate::policy::MAX_RECURSION;
use crate::Error;
/// Any mechanism that can decrypt a PKESK into a PlainSessionKey.
/// This abstraction allows use of either software-backed or hardware-backed private keys.
pub trait PkeskDecryptor {
fn decrypt(&self, pkesk: &PublicKeyEncryptedSessionKey) -> Option<PlainSessionKey>;
}
pub struct SoftkeyPkeskDecryptor<'a> {
tsk: Tsk,
key_passwords: Vec<&'a [u8]>,
}
impl<'a> SoftkeyPkeskDecryptor<'a> {
pub fn new(tsk: Tsk, key_passwords: Vec<&'a [u8]>) -> Self {
Self { tsk, key_passwords }
}
}
impl PkeskDecryptor for SoftkeyPkeskDecryptor<'_> {
fn decrypt(&self, pkesk: &PublicKeyEncryptedSessionKey) -> Option<PlainSessionKey> {
for key in self.tsk.decryption_capable_component_keys() {
let key_pw = match self.key_passwords.len() {
0 => "".to_string(),
1 => String::from_utf8_lossy(self.key_passwords[0]).to_string(),
_ => {
// FIXME
return None;
}
};
// Match key id/fp or check for wildcard.
if pkesk.match_identity(&key) {
match key {
SignedComponentKey::Sec(s) => {
match s.decrypt_session_key(pkesk, || key_pw.clone()) {
Ok(sk0) => {
return Some(sk0);
}
Err(e) => eprintln!("err {:?}", e),
}
}
SignedComponentKey::Pub(_) => {
// we can't decrypt with this key
}
}
};
}
None
}
}
/// Result of decrypting and/or verifying the signatures of a message with [unpack].
///
/// Depending on the format of the processed message, this result contains:
/// - The cleartext of the message.
/// - The session key, if the message was encrypted.
/// - A list of signatures that were found to be valid.
pub struct MessageResult {
pub validated: Vec<(Certificate, ComponentKeyPub, Signature)>,
pub session_key: Option<(u8, Vec<u8>)>,
pub cleartext: LiteralData,
}
/// Process an existing message: Decrypt message, and/or verify signatures.
///
/// This function handles messages that are encrypted, signed or both.
///
/// More specifically: OpenPGP messages may consist of layers of encryption, signing and
/// compression. This function processes arbitrary combinations of these layers, up to a maximum
/// layering depth.
///
/// The return value encodes (to some degree) the properties the message:
///
/// - It returns the cleartext of the message, and
/// - A list of the signatures on the message that were found to be valid (if any).
///
/// **Decryption**
///
/// Decryption can be attempted using two distinct mechanisms:
///
/// 1. Using a private OpenPGP component key provided via `decryptor`.
/// The OpenPGP component key may optionally be protected with a passphrase. Unlocking such
/// protected keys will be attempted with the passphrases provided in `key_passwords` (if any).
///
/// 2. A symmetric key, represented by a passphrase, in `skesk_passwords` (OpenPGP messages can be
/// encrypted to a recipient who is not using an OpenPGP key, with this method).
///
/// **Signature verification**
///
/// If the message has been signed, signatures will be verified against the certificates in `verifier`.
/// Any correct signatures will be reported in the `MessageResult`.
///
/// **Compression layers**
///
/// If the message has compression layers, they will be unpacked, silently.
pub fn unpack(
msg: Message,
pkesk_decryptors: &[Box<dyn PkeskDecryptor + '_>],
skesk_passwords: Vec<&[u8]>,
verifier: &[Certificate],
) -> Result<MessageResult, Error> {
unpack_int(&msg, pkesk_decryptors, skesk_passwords, verifier, 0)
}
// internal variant of the unpack() function with depth handling and limitation
fn unpack_int(
msg: &Message,
pkesk_decryptors: &[Box<dyn PkeskDecryptor + '_>],
skesk_passwords: Vec<&[u8]>,
verifier: &[Certificate],
depth: usize,
) -> Result<MessageResult, Error> {
if depth > MAX_RECURSION {
return Err(Error::Message(
"Excessive message nesting depth".to_string(),
));
}
match &msg {
Message::Encrypted { ref esk, ref edata } => {
let mut sk = None; // Session key, if any is found
'esks: for e in esk {
match e {
Esk::PublicKeyEncryptedSessionKey(pkesk) => {
for dec in pkesk_decryptors {
if let Some(esk) = dec.decrypt(pkesk) {
sk = Some(esk);
break 'esks;
}
}
}
Esk::SymKeyEncryptedSessionKey(skesk) => {
if !skesk_passwords.is_empty() {
let sym_pw = match skesk_passwords.len() {
0 => "".to_string(),
1 => String::from_utf8_lossy(skesk_passwords[0]).to_string(),
_ => {
// FIXME
return Err(Error::Message(
"More than one SKESK password currently unsupported"
.to_string(),
));
}
};
if let Ok(sk0) = decrypt_session_key_with_password(skesk, || sym_pw) {
sk = Some(sk0);
break 'esks;
}
}
}
}
}
let Some(sk) = sk else {
eprintln!("Failed to get session key");
// FIXME: return more specific error type
return Err(Error::Message("Failed to get session key".to_string()));
};
let (sym_alg, key) = match &sk {
PlainSessionKey::V3_4 { sym_alg, key } => (*sym_alg, key.to_vec()),
PlainSessionKey::V6 { key } => match edata {
Edata::SymEncryptedProtectedData(seipd) => match &seipd.data() {
pgp::packet::Data::V2 { sym_alg, .. } => (*sym_alg, key.to_vec()),
pgp::packet::Data::V1 { .. } => {
return Err(Error::Rpgp(pgp::errors::Error::Message(format!(
"V6 session key with unexpected symmetric data version {}",
seipd.version()
))));
}
},
Edata::SymEncryptedData(_) => {
return Err(Error::Rpgp(pgp::errors::Error::Message(
"SED packet is not supported with V6 session keys".to_string(),
)));
}
},
psk => {
return Err(Error::Rpgp(pgp::errors::Error::Message(format!(
"Unsupported plain session key version {:?}",
psk
))));
}
};
let msg = edata.decrypt(sk.clone())?;
let mut mr = unpack_int(
&msg,
pkesk_decryptors,
skesk_passwords.clone(),
verifier,
depth + 1,
)?;
// Return the session key in MessageResult
mr.session_key = Some(((sym_alg).into(), key));
Ok(mr)
}
Message::Literal(lit) => Ok(MessageResult {
validated: vec![],
session_key: None,
cleartext: lit.clone(), // FIXME: avoid clone
}),
Message::Compressed(cd) => {
let payload = cd.decompress()?;
let msg = Message::from_bytes(payload)?;
unpack_int(&msg, pkesk_decryptors, skesk_passwords, verifier, depth + 1)
}
Message::Signed {
one_pass_signature: _,
message: inner,
signature,
} => {
let mut vals: Vec<(Certificate, ComponentKeyPub, Signature)> = vec![];
// FIXME: handle in streaming mode?
// only consider "signature" if it passes our policy check, skip validation if not
if crate::sig::signature_acceptable(signature) {
// get the data we need to validate this signature against (either the bare literal
// data [without pgp framing], or the raw bytes of the message).
fn grab_data(m: &Message, depth: usize) -> Result<Vec<u8>, Error> {
if depth > MAX_RECURSION {
return Err(Error::Message(
"Excessive message nesting depth".to_string(),
));
}
match m {
Message::Literal(lit) => Ok(lit.data().to_vec()),
Message::Compressed(cd) => {
let payload = cd.decompress()?;
let msg = Message::from_bytes(payload)?;
grab_data(&msg, depth + 1)
}
Message::Signed { message, .. } => {
let Some(message) = message else {
return Err(Error::Message(
"No inner message found in signed message".to_string(),
));
};
// FIXME: return raw message data, if notarizing signature?
grab_data(message.as_ref(), depth + 1)
}
Message::Encrypted { .. } => Ok(m.to_bytes()?),
}
}
let Some(inner) = inner else {
return Err(Error::Message("Inner message is None".to_string()));
};
let data = grab_data(inner.as_ref(), 0)?;
for vcert in verifier {
if let Some(reference) = signature.created() {
// Only consider signers that are valid at signature creation time.
let cv = CheckedCertificate::from(vcert);
let verifiers = cv.valid_signing_capable_component_keys_at(reference);
for v in verifiers {
if v.verify(signature, &data).is_ok() {
vals.push((vcert.clone(), v.into(), signature.clone()));
}
}
}
}
}
let Some(inner) = inner else {
return Err(Error::Message("Inner message is None".to_string()));
};
let mut mr = unpack_int(
inner.as_ref(),
pkesk_decryptors,
skesk_passwords,
verifier,
depth + 1,
)?;
mr.validated.append(&mut vals);
// FIXME: deduplicate validations?
Ok(mr)
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum EncryptionMechanism {
SeipdV1(SymmetricKeyAlgorithm),
SeipdV2(AeadAlgorithm, SymmetricKeyAlgorithm),
}
/// Encrypt (and optionally sign) a message.
///
/// NOTE: `source` is expected to contain raw data, not an OpenPGP Message
///
/// FIXME: we need passwords to unlock signers!
///
/// FIXME: pass recipient primary (to set as intended recipient)
#[allow(clippy::too_many_arguments)]
pub fn encrypt(
mechanism: EncryptionMechanism,
recipients: Vec<ComponentKeyPub>,
skesk_passwords: Vec<&[u8]>,
signers: Vec<Tsk>,
hash_algo: Option<HashAlgorithm>,
source: &mut (dyn Read + Send + Sync),
mut sink: &mut (dyn io::Write + Send + Sync),
armor: bool,
) -> Result<Zeroizing<Vec<u8>>, Error> {
let mut rng = rand::thread_rng();
// 1. Define a new session key
let session_key = match mechanism {
EncryptionMechanism::SeipdV1(sym) | EncryptionMechanism::SeipdV2(_, sym) => {
sym.new_session_key(&mut rng)
}
};
let mut esk = vec![];
// 2. Encrypt (pub) the session key, to each PublicKey.
for recipient in recipients {
let pkesk = match mechanism {
EncryptionMechanism::SeipdV1(sym) => {
recipient.pkesk_from_session_key_v3(&mut rng, &session_key, sym)?
}
EncryptionMechanism::SeipdV2(_, _) => {
recipient.pkesk_from_session_key_v6(&mut rng, &session_key)?
}
};
esk.push(Esk::PublicKeyEncryptedSessionKey(pkesk));
}
// 3. Encrypt the session key to each symmetric password
for pw in skesk_passwords {
let pass = String::from_utf8_lossy(pw).to_string();
let skesk = match mechanism {
EncryptionMechanism::SeipdV1(sym) => SymKeyEncryptedSessionKey::encrypt_v4(
|| pass,
&session_key,
StringToKey::new_default(&mut rng),
sym,
)?,
EncryptionMechanism::SeipdV2(aead, sym) => {
// "If much less memory is available, a uniformly safe option is Argon2id with
// t=3 iterations, p=4 lanes, m=2^(16) (64 MiB of RAM),
// 128-bit salt, and 256-bit tag size. This is the SECOND RECOMMENDED option."
// FIXME: move these settings up to rPGP, as part of a convenience constructor?
let s2k = StringToKey::new_argon2(&mut rng, 3, 4, 16);
SymKeyEncryptedSessionKey::encrypt_v6(
&mut rng,
|| pass,
&session_key,
s2k,
sym,
aead,
)?
}
};
esk.push(Esk::SymKeyEncryptedSessionKey(skesk));
}
// 4. Wrap the plaintext into a literal
let mut data = vec![];
source.read_to_end(&mut data)?;
let lit = LiteralData::from_bytes((&[]).into(), &data);
// 5. Maybe sign the message.
let hash_algo = hash_algo.unwrap_or_default();
let msg = if !signers.is_empty() {
// FIXME: set Intended Recipient Fingerprint? (at least for v6)
// "The OpenPGP Key fingerprint of the intended recipient primary key"
// "When generating this subpacket in a version 6 signature, it SHOULD be marked as critical."
let mut packets = vec![];
packets.push(Packet::from(lit.clone()));
for signer in signers {
let data_signers: Vec<_> = signer.signing_capable_component_keys().collect();
// FIXME: use all signing capable keys?
if let Some(ds) = data_signers.first() {
// FIXME: key decryption password!
if let Message::Signed {
one_pass_signature,
signature,
..
} = ds.sign_msg(Message::Literal(lit.clone()), String::default, hash_algo)?
{
if let Some(mut ops) = one_pass_signature {
if packets.len() > 1 {
// only the innermost signature should be marked "last",
// so we mark all others as non-last.
ops.last = 0;
}
packets.insert(0, Packet::from(ops));
}
packets.push(Packet::from(signature));
}
} else {
return Err(Error::Message(
"No signing capable component key found for signer".to_string(),
));
}
}
if let Some(Ok(msg)) = Message::from_packets(packets.into_iter().map(Ok).peekable()).next()
{
msg
} else {
// This shouldn't happen, if we construct a reasonable "packets"
return Err(Error::Message(
"Failed to construct Message from packets".to_string(),
));
}
} else {
// we're not signing
Message::Literal(lit)
};
// 6. Symmetrically Encrypt the message to the session key.
let edata = match mechanism {
EncryptionMechanism::SeipdV1(sym) => {
Edata::SymEncryptedProtectedData(SymEncryptedProtectedData::encrypt_seipdv1(
&mut rng,
sym,
&session_key,
&msg.to_bytes()?,
)?)
}
EncryptionMechanism::SeipdV2(aead, sym) => {
Edata::SymEncryptedProtectedData(SymEncryptedProtectedData::encrypt_seipdv2(
&mut rng,
sym,
aead,
crate::policy::AEAD_CHUNK_SIZE,
&session_key,
&msg.to_bytes()?,
)?)
}
};
// 7. Put together encrypted message
let msg = Message::Encrypted { esk, edata };
// 8. Write encrypted message to sink
match armor {
true => msg.to_armored_writer(&mut sink, ArmorOptions::default())?,
false => msg.to_writer(&mut sink)?,
}
// 9. Return session key
Ok(session_key)
}