diff --git a/redwood/redwood/__init__.pyi b/redwood/redwood/__init__.pyi index 8b74f179c5..eb6908b6b4 100644 --- a/redwood/redwood/__init__.pyi +++ b/redwood/redwood/__init__.pyi @@ -5,7 +5,9 @@ from typing import BinaryIO def generate_source_key_pair(passphrase: str, email: str) -> tuple[str, str, str]: ... def is_valid_public_key(input: str) -> str: ... -def encrypt_message(recipients: list[str], plaintext: str, destination: Path) -> None: ... +def encrypt_message( + recipients: list[str], plaintext: str, destination: Path, *, armor: bool = False +) -> None: ... def encrypt_stream(recipients: list[str], plaintext: BinaryIO, destination: Path) -> None: ... def decrypt(ciphertext: bytes, secret_key: str, passphrase: str) -> bytes: ... diff --git a/redwood/src/lib.rs b/redwood/src/lib.rs index 9f38e34737..93ff39d847 100644 --- a/redwood/src/lib.rs +++ b/redwood/src/lib.rs @@ -119,9 +119,10 @@ pub fn encrypt_message( recipients: Vec, plaintext: String, destination: PathBuf, + armor: Option, ) -> Result<()> { let plaintext = plaintext.as_bytes(); - encrypt(&recipients, plaintext, &destination) + encrypt(&recipients, plaintext, &destination, armor) } /// Encrypt a Python stream (`typing.BinaryIO`) for the specified recipients. @@ -134,7 +135,7 @@ pub fn encrypt_stream( destination: PathBuf, ) -> Result<()> { let stream = stream::Stream { reader: plaintext }; - encrypt(&recipients, stream, &destination) + encrypt(&recipients, stream, &destination, None) } /// Helper function to encrypt readable things. @@ -144,6 +145,7 @@ fn encrypt( recipients: &[String], mut plaintext: impl Read, destination: &Path, + armor: Option, ) -> Result<()> { let mut certs = vec![]; let mut recipient_keys = vec![]; @@ -165,7 +167,11 @@ fn encrypt( .open(destination)?; let mut writer = BufWriter::new(sink); let message = Message::new(&mut writer); - let message = Armorer::new(message).build()?; + let message = if armor.unwrap_or(false) { + Armorer::new(message).build()? + } else { + message + }; let message = Encryptor::for_recipients(message, recipient_keys).build()?; let mut message = LiteralWriter::new(message).build()?; @@ -278,6 +284,7 @@ mod tests { vec![good_key, BAD_KEY.to_string()], SECRET_MESSAGE.to_string(), tmp_dir.path().join("message.asc"), + None, ) .unwrap_err(); assert_eq!(err.to_string(), expected_err_msg); @@ -325,42 +332,31 @@ mod tests { vec![public_key1, public_key2], SECRET_MESSAGE.to_string(), tmp.clone(), + None, ) .unwrap(); - let ciphertext = std::fs::read_to_string(tmp).unwrap(); - // Verify ciphertext looks like an encrypted message - assert!(ciphertext.starts_with("-----BEGIN PGP MESSAGE-----\n")); + let ciphertext = std::fs::read(tmp).unwrap(); // Decrypt as key 1 - let plaintext = decrypt( - ciphertext.clone().into_bytes(), - secret_key1, - PASSPHRASE.to_string(), - ) - .unwrap(); + let plaintext = + decrypt(ciphertext.clone(), secret_key1, PASSPHRASE.to_string()) + .unwrap(); // Verify message is what we put in originally assert_eq!( SECRET_MESSAGE, String::from_utf8(plaintext.to_vec()).unwrap() ); // Decrypt as key 2 - let plaintext = decrypt( - ciphertext.clone().into_bytes(), - secret_key2, - PASSPHRASE.to_string(), - ) - .unwrap(); + let plaintext = + decrypt(ciphertext.clone(), secret_key2, PASSPHRASE.to_string()) + .unwrap(); // Verify message is what we put in originally assert_eq!( SECRET_MESSAGE, String::from_utf8(plaintext.to_vec()).unwrap() ); // Try to decrypt as key 3, expect an error - let err = decrypt( - ciphertext.into_bytes(), - secret_key3, - PASSPHRASE.to_string(), - ) - .unwrap_err(); + let err = decrypt(ciphertext, secret_key3, PASSPHRASE.to_string()) + .unwrap_err(); assert_eq!( err.to_string(), "OpenPGP error: no matching pkesk, wrong secret key provided?" @@ -391,6 +387,7 @@ mod tests { key, // missing or malformed recipient key "Look ma, no key".to_string(), tmp_dir.path().join("message.asc"), + None, ) .unwrap_err(); assert_eq!(err.to_string(), error); diff --git a/securedrop/tests/test_encryption.py b/securedrop/tests/test_encryption.py index a22a9e3869..d423c78fa0 100644 --- a/securedrop/tests/test_encryption.py +++ b/securedrop/tests/test_encryption.py @@ -114,9 +114,9 @@ def test_encrypt_source_message(self, config, tmp_path): message_in=message, encrypted_message_path_out=encrypted_message_path ) - # And the output file contains the encrypted data + # And the output file doesn't contain the message plaintext encrypted_message = encrypted_message_path.read_bytes() - assert encrypted_message.startswith(b"-----BEGIN PGP MESSAGE-----") + assert message.encode() not in encrypted_message # And the journalist is able to decrypt the message decrypted_message = utils.decrypt_as_journalist(encrypted_message).decode() @@ -138,9 +138,9 @@ def test_encrypt_source_file(self, config, tmp_path): encrypted_file_path_out=encrypted_file_path, ) - # And the output file contains the encrypted data + # And the output file doesn't contain the file plaintext encrypted_file = encrypted_file_path.read_bytes() - assert encrypted_file.startswith(b"-----BEGIN PGP MESSAGE-----") + assert file_to_encrypt_path.read_bytes() not in encrypted_file # And the journalist is able to decrypt the file decrypted_file = utils.decrypt_as_journalist(encrypted_file) @@ -173,9 +173,9 @@ def test_encrypt_and_decrypt_journalist_reply( encrypted_reply_path_out=encrypted_reply_path, ) - # And the output file contains the encrypted data + # And the output file doesn't contain the reply plaintext encrypted_reply = encrypted_reply_path.read_bytes() - assert encrypted_reply.startswith(b"-----BEGIN PGP MESSAGE-----") + assert journalist_reply.encode() not in encrypted_reply # And source1 is able to decrypt the reply decrypted_reply = encryption_mgr.decrypt_journalist_reply( @@ -224,9 +224,9 @@ def test_gpg_encrypt_and_decrypt_journalist_reply( encrypted_reply_path_out=encrypted_reply_path, ) - # And the output file contains the encrypted data + # And the output file doesn't contain the reply plaintext encrypted_reply = encrypted_reply_path.read_bytes() - assert encrypted_reply.startswith(b"-----BEGIN PGP MESSAGE-----") + assert journalist_reply.encode() not in encrypted_reply # And source1 is able to decrypt the reply decrypted_reply = encryption_mgr.decrypt_journalist_reply( diff --git a/securedrop/tests/test_journalist_api.py b/securedrop/tests/test_journalist_api.py index 151e090690..9e09668e6e 100644 --- a/securedrop/tests/test_journalist_api.py +++ b/securedrop/tests/test_journalist_api.py @@ -777,10 +777,16 @@ def test_authorized_user_can_add_reply( # First we must encrypt the reply, or it will get rejected # by the server. - encryption_mgr = EncryptionManager.get_default() reply_path = tmp_path / "message.gpg" - encryption_mgr.encrypt_journalist_reply( - test_source["source"], "This is a plaintext reply", reply_path + # Use redwood directly, so we can generate an armored message. + redwood.encrypt_message( + recipients=[ + test_source["source"].public_key, + EncryptionManager.get_default().get_journalist_public_key(), + ], + plaintext="This is an encrypted reply", + destination=reply_path, + armor=True, ) reply_content = reply_path.read_text()