Skip to main content

pb_mapper_protocol/
data.rs

1//! Optional data encryption, negotiated independently on each relay leg.
2//! The authenticated setup response selects a format before application I/O.
3//!
4//! ```text
5//!                  one shared key K per relay leg
6//! Endpoint  ---- nonce = 00000000 | counter:u64be ----> Relay
7//! Endpoint  <--- nonce = 00000001 | counter:u64be ----- Relay
8//!                        4 bytes       8 bytes
9//!
10//! Setup:    install K and a fixed prefix; no data-key derivation or extra RTT.
11//! Per frame: increment the local counter, then one AES-GCM operation.
12//! Wire:     checksum:u32 | length:u32 | ciphertext | tag[16] (unchanged).
13//! ```
14//!
15//! Counters start at zero independently. Prefixes keep their nonce spaces
16//! disjoint under K. The other relay leg has its own fresh key and counters.
17
18use pb_mapper_core::checksum::AesKeyType;
19use pb_mapper_core::codec::{Aes256GcmDeCodec, Aes256GcmEnCodec};
20use pb_mapper_core::error::Result;
21
22use crate::secure::{HeaderProtocol, protocol_error};
23
24/// Version with disjoint directional nonce prefixes and checked 64-bit counters.
25pub const DATA_PROTOCOL_V2: u16 = 2;
26
27/// Negotiated key material for exactly one endpoint-to-relay connection.
28/// Debug output intentionally never includes the secret.
29#[derive(Clone, Copy)]
30pub struct DataCodec {
31    key: AesKeyType,
32    protocol: Option<u16>,
33}
34
35impl DataCodec {
36    /// Wrap an existing legacy key without changing its wire encoding.
37    pub fn legacy(key: AesKeyType) -> Self {
38        Self {
39            key,
40            protocol: None,
41        }
42    }
43
44    /// Validate an authenticated response. Unknown selections are errors and
45    /// must never cause another connection in a weaker mode.
46    pub fn from_response(
47        key: Option<AesKeyType>,
48        protocol: Option<u16>,
49        offered_v2: bool,
50    ) -> Result<Option<Self>> {
51        if protocol.is_some()
52            && (protocol != Some(DATA_PROTOCOL_V2) || !offered_v2 || key.is_none())
53        {
54            return Err(protocol_error("invalid data protocol selection"));
55        }
56        Ok(key.map(|key| Self { key, protocol }))
57    }
58
59    /// Select only a capability received through authenticated control-v2.
60    pub fn negotiate(key: AesKeyType, offer: Option<u16>, header: HeaderProtocol) -> Result<Self> {
61        Self::validate_offer(offer, header)?;
62        Ok(Self {
63            key,
64            protocol: offer,
65        })
66    }
67
68    /// Reject unknown or unauthenticated selections before any setup response.
69    pub fn validate_offer(offer: Option<u16>, header: HeaderProtocol) -> Result<()> {
70        if offer.is_some() && (offer != Some(DATA_PROTOCOL_V2) || header != HeaderProtocol::V2) {
71            return Err(protocol_error("unsupported data protocol offer"));
72        }
73        Ok(())
74    }
75
76    /// The per-leg secret sent inside the authenticated setup response.
77    pub fn key(self) -> AesKeyType {
78        self.key
79    }
80
81    /// Omit the field completely when interoperating with a legacy peer.
82    pub fn protocol(self) -> Option<u16> {
83        self.protocol
84    }
85
86    /// Receiving and sending codecs from the local endpoint's perspective.
87    pub fn endpoint_codecs(self) -> Result<(Aes256GcmDeCodec, Aes256GcmEnCodec)> {
88        self.codecs(false)
89    }
90
91    /// Receiving and sending codecs from the relay's perspective.
92    pub fn relay_codecs(self) -> Result<(Aes256GcmDeCodec, Aes256GcmEnCodec)> {
93        self.codecs(true)
94    }
95
96    fn codecs(self, relay: bool) -> Result<(Aes256GcmDeCodec, Aes256GcmEnCodec)> {
97        if self.protocol.is_none() {
98            return Ok((
99                crate::get_decodec(&self.key)?,
100                crate::get_encodec(&self.key)?,
101            ));
102        }
103        // Both directions reuse the per-leg key. Disjoint nonce prefixes avoid
104        // key derivation at setup and leave the per-frame encryption path intact.
105        let (read_prefix, write_prefix) = if relay {
106            ([0; 4], [0, 0, 0, 1])
107        } else {
108            ([0, 0, 0, 1], [0; 4])
109        };
110        Ok((
111            Aes256GcmDeCodec::try_new_data_v2(&self.key, read_prefix)
112                .map_err(|_| protocol_error("invalid data-v2 read key"))?,
113            Aes256GcmEnCodec::try_new_data_v2(&self.key, write_prefix)
114                .map_err(|_| protocol_error("invalid data-v2 write key"))?,
115        ))
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::command::{MessageSerializer, PbConnRequest, PbConnResponse};
123    use pb_mapper_core::codec::{Decryptor, Encryptor};
124
125    fn seal(codec: &mut Aes256GcmEnCodec, bytes: &[u8]) -> Vec<u8> {
126        let mut bytes = bytes.to_vec();
127        let tag = codec.encrypt(&mut bytes).unwrap();
128        bytes.extend_from_slice(tag.as_ref());
129        bytes
130    }
131
132    #[test]
133    fn nonce_prefixes_separate_directions_and_keys_separate_relay_legs() {
134        let mut ciphertexts = Vec::new();
135        for secret in [[41; 32], [42; 32]] {
136            let codec = DataCodec::negotiate(secret, Some(2), HeaderProtocol::V2).unwrap();
137            let (mut endpoint_read, mut endpoint_write) = codec.endpoint_codecs().unwrap();
138            let (mut relay_read, mut relay_write) = codec.relay_codecs().unwrap();
139            let first = seal(&mut endpoint_write, b"same bytes");
140            let second = seal(&mut relay_write, b"same bytes");
141            assert_eq!(
142                relay_read.decrypt(&mut first.clone()).unwrap(),
143                b"same bytes"
144            );
145            assert_eq!(
146                endpoint_read.decrypt(&mut second.clone()).unwrap(),
147                b"same bytes"
148            );
149            // A replay is not the next frame in this direction.
150            assert!(relay_read.decrypt(&mut first.clone()).is_err());
151            ciphertexts.extend([first, second]);
152        }
153        for (i, left) in ciphertexts.iter().enumerate() {
154            for right in &ciphertexts[i + 1..] {
155                assert_ne!(left, right);
156            }
157        }
158    }
159
160    #[test]
161    fn data_v2_wire_vectors_reuse_the_leg_key_with_direction_prefixes() {
162        // Independently generated AES-256-GCM vectors: key = [41; 32],
163        // plaintext = "same bytes", counter = 0, empty additional data.
164        let codec = DataCodec::negotiate([41; 32], Some(2), HeaderProtocol::V2).unwrap();
165        let (_, mut endpoint) = codec.endpoint_codecs().unwrap();
166        let (_, mut relay) = codec.relay_codecs().unwrap();
167        for (writer, expected) in [
168            (
169                &mut endpoint,
170                "c510ef0e480d4056924b6e20a9b85e00b219337e7a0a7efa4764",
171            ),
172            (
173                &mut relay,
174                "11bab01eb9f5caa4320d07b0ca94af01960d51d3873d8e7a09be",
175            ),
176        ] {
177            let actual: String = seal(writer, b"same bytes")
178                .iter()
179                .map(|byte| format!("{byte:02x}"))
180                .collect();
181            assert_eq!(actual, expected);
182        }
183    }
184
185    #[test]
186    fn data_selection_requires_an_authenticated_supported_offer() {
187        assert!(DataCodec::negotiate([0; 32], Some(2), HeaderProtocol::Legacy).is_err());
188        assert!(DataCodec::negotiate([0; 32], Some(3), HeaderProtocol::V2).is_err());
189        for (key, selected, offered) in [
190            (None, Some(2), true),
191            (Some([0; 32]), Some(3), true),
192            (Some([0; 32]), Some(2), false),
193        ] {
194            assert!(DataCodec::from_response(key, selected, offered).is_err());
195        }
196        let old = DataCodec::from_response(Some([1; 32]), None, true)
197            .unwrap()
198            .unwrap();
199        assert_eq!(old.protocol(), None);
200        assert!(
201            DataCodec::from_response(None, None, true)
202                .unwrap()
203                .is_none()
204        );
205    }
206
207    #[test]
208    fn legacy_json_ignores_offers_and_new_readers_accept_absent_selections() {
209        #[derive(serde::Deserialize)]
210        enum OldRequest {
211            Subcribe { key: String },
212            Stream { key: String, dst_id: u32 },
213        }
214        for request in [
215            PbConnRequest::Subcribe {
216                key: "compat".into(),
217                data_protocol: Some(2),
218            },
219            PbConnRequest::Stream {
220                key: "compat".into(),
221                dst_id: 7,
222                server_generation: 0,
223                data_protocol: Some(2),
224            },
225        ] {
226            match serde_json::from_slice::<OldRequest>(&request.encode().unwrap()).unwrap() {
227                OldRequest::Subcribe { key } => assert_eq!(key, "compat"),
228                OldRequest::Stream { key, dst_id } => {
229                    assert_eq!(key, "compat");
230                    assert_eq!(dst_id, 7);
231                }
232            }
233        }
234        let response = PbConnResponse::decode(br#"{"Stream":{"codec_key":null}}"#).unwrap();
235        assert!(matches!(
236            response,
237            PbConnResponse::Stream {
238                codec_key: None,
239                data_protocol: None
240            }
241        ));
242        let request = PbConnRequest::decode(br#"{"Subcribe":{"key":"compat"}}"#).unwrap();
243        assert!(matches!(
244            request,
245            PbConnRequest::Subcribe {
246                data_protocol: None,
247                ..
248            }
249        ));
250        let response = PbConnResponse::Stream {
251            codec_key: None,
252            data_protocol: None,
253        }
254        .encode()
255        .unwrap();
256        assert_eq!(response, br#"{"Stream":{"codec_key":null}}"#);
257    }
258}