Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: do not allow decryption with "Plaintext" algorithm #287

Merged
merged 1 commit into from Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
76 changes: 70 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,55 @@ 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())
.err()
.unwrap()
.to_string()
.contains("plaintext"));
}

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