Skip to content

Commit

Permalink
Merge pull request #7023 from freedomofpress/redwood-no-armor
Browse files Browse the repository at this point in the history
redwood: Don't armor encrypted files/messages
  • Loading branch information
cfm committed Oct 27, 2023
2 parents 3a665de + a7a0513 commit 6fae718
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 36 deletions.
4 changes: 3 additions & 1 deletion redwood/redwood/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...

Expand Down
45 changes: 21 additions & 24 deletions redwood/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,10 @@ pub fn encrypt_message(
recipients: Vec<String>,
plaintext: String,
destination: PathBuf,
armor: Option<bool>,
) -> 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.
Expand All @@ -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.
Expand All @@ -144,6 +145,7 @@ fn encrypt(
recipients: &[String],
mut plaintext: impl Read,
destination: &Path,
armor: Option<bool>,
) -> Result<()> {
let mut certs = vec![];
let mut recipient_keys = vec![];
Expand All @@ -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()?;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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?"
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 8 additions & 8 deletions securedrop/tests/test_encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
12 changes: 9 additions & 3 deletions securedrop/tests/test_journalist_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down

0 comments on commit 6fae718

Please sign in to comment.