Skip to main content

securedrop_protocol_minimal/client/
plaintext.rs

1use crate::metadata::MetadataPublicKey;
2use crate::primitives::ristretto255::DHPublicKey;
3use alloc::vec::Vec;
4use anyhow::Error;
5
6#[derive(Debug, Clone)]
7/// Toy pt structure
8pub struct Plaintext {
9    /// Fetching key: $pk_S^{fetch}$ in the spec
10    pub sender_fetch_key: DHPublicKey,
11    /// Metadata key: $pk_S^{PKE}$ in the spec
12    pub sender_reply_pubkey_hybrid: MetadataPublicKey,
13    /// Message
14    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    // Toy parsing only
33    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            // Assemble the wire bytes independently of Plaintext::to_bytes.
88            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}