Skip to content

Commit

Permalink
fix: do not allow decryption with "Plaintext" algorithm
Browse files Browse the repository at this point in the history
According to
<https://datatracker.ietf.org/doc/html/rfc4880#section-13.4>
"plaintext" MUST NOT be used to encrypt data packets.

We check that "plaintext" algorithm is not used
to decrypt messages and is not used to decrypt session keys
when the key is derived using S2K.
  • Loading branch information
link2xt committed Feb 16, 2024
1 parent 4d9144d commit 175b277
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 26 deletions.
60 changes: 40 additions & 20 deletions src/composed/message/decrypt.rs
Expand Up @@ -9,6 +9,7 @@ use crate::errors::Result;
use crate::packet::SymKeyEncryptedSessionKey;
use crate::types::{KeyTrait, Mpi, SecretKeyRepr, SecretKeyTrait, Tag};

/// Decrypts session key using secret key.
pub fn decrypt_session_key<F>(
locked_key: &(impl SecretKeyTrait + KeyTrait),
key_pw: F,
Expand All @@ -33,8 +34,13 @@ where
}
SecretKeyRepr::EdDSA(_) => unimplemented_err!("EdDSA"),
};
let algorithm = SymmetricKeyAlgorithm::from(decrypted_key[0]);
alg = Some(algorithm);

let session_key_algorithm = SymmetricKeyAlgorithm::from(decrypted_key[0]);
ensure!(
session_key_algorithm != SymmetricKeyAlgorithm::Plaintext,
"session key algorithm cannot be plaintext"
);
alg = Some(session_key_algorithm);
debug!("alg: {:?}", alg);

let (k, checksum) = match *priv_key {
Expand All @@ -46,7 +52,7 @@ where
)
}
_ => {
let key_size = algorithm.key_size();
let key_size = session_key_algorithm.key_size();
(
&decrypted_key[1..=key_size],
&decrypted_key[key_size + 1..key_size + 3],
Expand All @@ -63,6 +69,10 @@ where
Ok((key, alg.expect("failed to unlock")))
}

/// Decrypts session key from SKESK packet.
///
/// Returns decrypted or derived session key
/// and symmetric algorithm of the key.
pub fn decrypt_session_key_with_password<F>(
packet: &SymKeyEncryptedSessionKey,
msg_pw: F,
Expand All @@ -72,25 +82,35 @@ where
{
debug!("decrypting session key");

let packet_algorithm = packet.sym_algorithm();
ensure!(
packet_algorithm != SymmetricKeyAlgorithm::Plaintext,
"SKESK packet encryption algorithm cannot be plaintext"
);

let key = packet
.s2k()
.derive_key(&msg_pw(), packet.sym_algorithm().key_size())?;

match packet.encrypted_key() {
Some(ref encrypted_key) => {
let mut decrypted_key = encrypted_key.to_vec();
// packet.sym_algorithm().decrypt(&key, &mut decrypted_key)?;
let iv = vec![0u8; packet.sym_algorithm().block_size()];
packet
.sym_algorithm()
.decrypt_with_iv_regular(&key, &iv, &mut decrypted_key)?;

let alg = SymmetricKeyAlgorithm::from(decrypted_key[0]);

Ok((decrypted_key[1..].to_vec(), alg))
}
None => Ok((key, packet.sym_algorithm())),
}
.derive_key(&msg_pw(), packet_algorithm.key_size())?;

let Some(ref encrypted_key) = packet.encrypted_key() else {
// There is no encrypted session key.
//
// S2K-derived key is the session key.
return Ok((key, packet_algorithm));
};

let mut decrypted_key = encrypted_key.to_vec();
// packet.sym_algorithm().decrypt(&key, &mut decrypted_key)?;
let iv = vec![0u8; packet.sym_algorithm().block_size()];
packet_algorithm.decrypt_with_iv_regular(&key, &iv, &mut decrypted_key)?;

let session_key_algorithm = SymmetricKeyAlgorithm::from(decrypted_key[0]);
ensure!(
session_key_algorithm != SymmetricKeyAlgorithm::Plaintext,
"session key algorithm cannot be plaintext"
);

Ok((decrypted_key[1..].to_vec(), session_key_algorithm))
}

pub struct MessageDecrypter<'a> {
Expand Down
71 changes: 65 additions & 6 deletions src/composed/message/types.rs
Expand Up @@ -270,7 +270,7 @@ impl Message {
self.encrypt_symmetric(rng, esk, alg, session_key)
}

/// Encrytp the message using the given password.
/// Encrypt the message using the given password.
pub fn encrypt_with_password<R, F>(
&self,
rng: &mut R,
Expand Down Expand Up @@ -533,8 +533,8 @@ impl Message {
ensure!(!session_keys.is_empty(), "failed to decrypt session key");

// make sure all the keys are the same, otherwise we are in a bad place
let (session_key, alg) = {
let k0 = &session_keys[0].1;
let (session_key, session_key_algorithm) = {
let (_key_id, k0) = &session_keys[0];
if !session_keys.iter().skip(1).all(|(_, k)| k0 == k) {
bail!("found inconsistent session keys, possible message corruption");
}
Expand All @@ -544,8 +544,15 @@ impl Message {
};

let ids = session_keys.into_iter().map(|(k, _)| k).collect();
ensure!(
session_key_algorithm != SymmetricKeyAlgorithm::Plaintext,
"session key algorithm cannot be plaintext"
);

Ok((MessageDecrypter::new(session_key, alg, edata), ids))
Ok((
MessageDecrypter::new(session_key, session_key_algorithm, edata),
ids,
))
}
}
}
Expand Down Expand Up @@ -573,10 +580,18 @@ impl Message {

ensure!(skesk.is_some(), "message is not password protected");

let (session_key, alg) =
let (session_key, session_key_algorithm) =
decrypt_session_key_with_password(skesk.expect("checked above"), msg_pw)?;
ensure!(
session_key_algorithm != SymmetricKeyAlgorithm::Plaintext,
"session key algorithm cannot be plaintext"
);

Ok(MessageDecrypter::new(session_key, alg, edata))
Ok(MessageDecrypter::new(
session_key,
session_key_algorithm,
edata,
))
}
}
}
Expand Down Expand Up @@ -803,6 +818,50 @@ mod tests {
assert_eq!(compressed_msg, decrypted);
}

#[test]
fn test_no_plaintext_decryption() {
// Invalid message "encrypted" with plaintext algorithm.
// Generated with the Python script below.
let msg_raw = b"\xc3\x04\x04\x00\x00\x08\xd2-\x01\x00\x00\xcb\x12b\x00\x00\x00\x00\x00Hello world!\xd3\x14\xc3\xadw\x022\x05\x0ek'k\x8d\x12\xaa8\r'\x8d\xc0\x82)";
/*
import hashlib
import sys
data = (
b"\xc3" # PTag = 11000011, new packet format, tag 3 = SKESK
b"\x04" # Packet length, 4
b"\x04" # Version number, 4
b"\x00" # Algorithm, plaintext
b"\x00\x08" # S2K specifier, Simple S2K, SHA256
b"\xd2" # PTag = 1101 0010, new packet format, tag 18 = SEIPD
b"\x2d" # Packet length, 45
b"\x01" # Version number, 1
)
inner_data = (
b"\x00\x00" # IV
b"\xcb" # PTag = 11001011, new packet format, tag 11 = literal data packet
b"\x12" # Packet length, 18
b"\x62" # Binary data ('b')
b"\x00" # No filename, empty filename length
b"\x00\x00\x00\x00" # Date
b"Hello world!"
)
data += inner_data
data += (
b"\xd3" # Modification Detection Code packet, tag 19
b"\x14" # MDC packet length, 20 bytes
)
data += hashlib.new("SHA1", inner_data + b"\xd3\x14").digest()
print(data)
*/

let msg = Message::from_bytes(&msg_raw[..]).unwrap();

// Before the fix message eventually decrypted to
// Literal(LiteralData { packet_version: New, mode: Binary, created: 1970-01-01T00:00:00Z, file_name: "", data: "48656c6c6f20776f726c6421" })
// where "48656c6c6f20776f726c6421" is an encoded "Hello world!" string.
assert!(msg.decrypt_with_password(|| "foobarbaz".into()).is_err());
}

#[test]
fn test_x25519_signing_string() {
let (skey, _headers) = SignedSecretKey::from_armor_single(
Expand Down

0 comments on commit 175b277

Please sign in to comment.