securedrop_protocol_minimal/client/
plaintext.rs1use crate::metadata::MetadataPublicKey;
2use crate::primitives::ristretto255::DHPublicKey;
3use alloc::vec::Vec;
4use anyhow::Error;
5
6#[derive(Debug, Clone)]
7pub struct Plaintext {
9 pub sender_fetch_key: DHPublicKey,
11 pub sender_reply_pubkey_hybrid: MetadataPublicKey,
13 pub msg: Vec<u8>,
15}
16
17impl Plaintext {
18 pub fn to_bytes(&self) -> alloc::vec::Vec<u8> {
19 let mut buf = Vec::new();
20
21 buf.extend_from_slice(&self.sender_fetch_key.into_bytes());
22 buf.extend_from_slice(self.sender_reply_pubkey_hybrid.as_bytes());
23 buf.extend_from_slice(&self.msg);
24
25 buf
26 }
27
28 pub fn len(&self) -> usize {
29 DHPublicKey::LEN + MetadataPublicKey::LEN + self.msg.len()
30 }
31
32 pub fn from_bytes(pt_bytes: &[u8]) -> Result<Self, Error> {
34 if pt_bytes.len() < DHPublicKey::LEN + MetadataPublicKey::LEN {
35 return Err(anyhow::anyhow!("plaintext is shorter than its key header"));
36 }
37
38 let mut offset = 0;
39
40 let mut fetch_key_bytes = [0u8; DHPublicKey::LEN];
41 fetch_key_bytes.copy_from_slice(&pt_bytes[offset..offset + DHPublicKey::LEN]);
42
43 let sender_fetch_key = DHPublicKey::decode(fetch_key_bytes)?;
44 offset += DHPublicKey::LEN;
45
46 let sender_reply_pubkey_hybrid =
47 MetadataPublicKey::from_bytes(&pt_bytes[offset..offset + MetadataPublicKey::LEN])?;
48 offset += MetadataPublicKey::LEN;
49
50 let msg = pt_bytes[offset..].to_vec();
51
52 Ok(Plaintext {
53 sender_fetch_key,
54 sender_reply_pubkey_hybrid,
55 msg,
56 })
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use crate::primitives::ristretto255::random_dh_public_key;
64 use rand_chacha::ChaCha20Rng;
65 use rand_core::SeedableRng;
66
67 #[test]
68 fn test_plaintext_rejects_truncated_header() {
69 let header_len = DHPublicKey::LEN + MetadataPublicKey::LEN;
70 let bytes = alloc::vec![0u8; header_len];
71
72 for len in 0..header_len {
73 assert!(
74 Plaintext::from_bytes(&bytes[..len]).is_err(),
75 "accepted a truncated plaintext header of {len} bytes"
76 );
77 }
78 }
79
80 #[test]
81 fn test_plaintext_preserves_complete_header_and_message() {
82 let mut rng = ChaCha20Rng::seed_from_u64(42);
83 let fetch_key_bytes = random_dh_public_key(&mut rng).into_bytes();
84 let reply_key_bytes = [0x42u8; MetadataPublicKey::LEN];
85
86 for message in [b"".as_slice(), b"\x00\xffbinary message".as_slice()] {
87 let mut bytes = fetch_key_bytes.to_vec();
89 bytes.extend_from_slice(&reply_key_bytes);
90 bytes.extend_from_slice(message);
91
92 let restored = Plaintext::from_bytes(&bytes).expect("complete plaintext");
93 assert_eq!(restored.sender_fetch_key.into_bytes(), fetch_key_bytes);
94 assert_eq!(
95 restored.sender_reply_pubkey_hybrid.as_bytes(),
96 reply_key_bytes
97 );
98 assert_eq!(restored.msg, message);
99 }
100 }
101
102 #[test]
103 fn test_plaintext_rejects_invalid_fetch_key() {
104 let mut bytes = alloc::vec![0u8; DHPublicKey::LEN + MetadataPublicKey::LEN];
105 bytes[..DHPublicKey::LEN].fill(0xff);
106
107 assert!(Plaintext::from_bytes(&bytes).is_err());
108 }
109}