use pb_mapper_core::checksum::AesKeyType;
use pb_mapper_core::codec::{Aes256GcmDeCodec, Aes256GcmEnCodec};
use pb_mapper_core::error::Result;
use crate::secure::{HeaderProtocol, protocol_error};
pub const DATA_PROTOCOL_V2: u16 = 2;
#[derive(Clone, Copy)]
pub struct DataCodec {
key: AesKeyType,
protocol: Option<u16>,
}
impl DataCodec {
pub fn legacy(key: AesKeyType) -> Self {
Self {
key,
protocol: None,
}
}
pub fn from_response(
key: Option<AesKeyType>,
protocol: Option<u16>,
offered_v2: bool,
) -> Result<Option<Self>> {
if protocol.is_some()
&& (protocol != Some(DATA_PROTOCOL_V2) || !offered_v2 || key.is_none())
{
return Err(protocol_error("invalid data protocol selection"));
}
Ok(key.map(|key| Self { key, protocol }))
}
pub fn negotiate(key: AesKeyType, offer: Option<u16>, header: HeaderProtocol) -> Result<Self> {
Self::validate_offer(offer, header)?;
Ok(Self {
key,
protocol: offer,
})
}
pub fn validate_offer(offer: Option<u16>, header: HeaderProtocol) -> Result<()> {
if offer.is_some() && (offer != Some(DATA_PROTOCOL_V2) || header != HeaderProtocol::V2) {
return Err(protocol_error("unsupported data protocol offer"));
}
Ok(())
}
pub fn key(self) -> AesKeyType {
self.key
}
pub fn protocol(self) -> Option<u16> {
self.protocol
}
pub fn endpoint_codecs(self) -> Result<(Aes256GcmDeCodec, Aes256GcmEnCodec)> {
self.codecs(false)
}
pub fn relay_codecs(self) -> Result<(Aes256GcmDeCodec, Aes256GcmEnCodec)> {
self.codecs(true)
}
fn codecs(self, relay: bool) -> Result<(Aes256GcmDeCodec, Aes256GcmEnCodec)> {
if self.protocol.is_none() {
return Ok((
crate::get_decodec(&self.key)?,
crate::get_encodec(&self.key)?,
));
}
let (read_prefix, write_prefix) = if relay {
([0; 4], [0, 0, 0, 1])
} else {
([0, 0, 0, 1], [0; 4])
};
Ok((
Aes256GcmDeCodec::try_new_data_v2(&self.key, read_prefix)
.map_err(|_| protocol_error("invalid data-v2 read key"))?,
Aes256GcmEnCodec::try_new_data_v2(&self.key, write_prefix)
.map_err(|_| protocol_error("invalid data-v2 write key"))?,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::command::{MessageSerializer, PbConnRequest, PbConnResponse};
use pb_mapper_core::codec::{Decryptor, Encryptor};
fn seal(codec: &mut Aes256GcmEnCodec, bytes: &[u8]) -> Vec<u8> {
let mut bytes = bytes.to_vec();
let tag = codec.encrypt(&mut bytes).unwrap();
bytes.extend_from_slice(tag.as_ref());
bytes
}
#[test]
fn nonce_prefixes_separate_directions_and_keys_separate_relay_legs() {
let mut ciphertexts = Vec::new();
for secret in [[41; 32], [42; 32]] {
let codec = DataCodec::negotiate(secret, Some(2), HeaderProtocol::V2).unwrap();
let (mut endpoint_read, mut endpoint_write) = codec.endpoint_codecs().unwrap();
let (mut relay_read, mut relay_write) = codec.relay_codecs().unwrap();
let first = seal(&mut endpoint_write, b"same bytes");
let second = seal(&mut relay_write, b"same bytes");
assert_eq!(
relay_read.decrypt(&mut first.clone()).unwrap(),
b"same bytes"
);
assert_eq!(
endpoint_read.decrypt(&mut second.clone()).unwrap(),
b"same bytes"
);
assert!(relay_read.decrypt(&mut first.clone()).is_err());
ciphertexts.extend([first, second]);
}
for (i, left) in ciphertexts.iter().enumerate() {
for right in &ciphertexts[i + 1..] {
assert_ne!(left, right);
}
}
}
#[test]
fn data_v2_wire_vectors_reuse_the_leg_key_with_direction_prefixes() {
let codec = DataCodec::negotiate([41; 32], Some(2), HeaderProtocol::V2).unwrap();
let (_, mut endpoint) = codec.endpoint_codecs().unwrap();
let (_, mut relay) = codec.relay_codecs().unwrap();
for (writer, expected) in [
(
&mut endpoint,
"c510ef0e480d4056924b6e20a9b85e00b219337e7a0a7efa4764",
),
(
&mut relay,
"11bab01eb9f5caa4320d07b0ca94af01960d51d3873d8e7a09be",
),
] {
let actual: String = seal(writer, b"same bytes")
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
assert_eq!(actual, expected);
}
}
#[test]
fn data_selection_requires_an_authenticated_supported_offer() {
assert!(DataCodec::negotiate([0; 32], Some(2), HeaderProtocol::Legacy).is_err());
assert!(DataCodec::negotiate([0; 32], Some(3), HeaderProtocol::V2).is_err());
for (key, selected, offered) in [
(None, Some(2), true),
(Some([0; 32]), Some(3), true),
(Some([0; 32]), Some(2), false),
] {
assert!(DataCodec::from_response(key, selected, offered).is_err());
}
let old = DataCodec::from_response(Some([1; 32]), None, true)
.unwrap()
.unwrap();
assert_eq!(old.protocol(), None);
assert!(
DataCodec::from_response(None, None, true)
.unwrap()
.is_none()
);
}
#[test]
fn legacy_json_ignores_offers_and_new_readers_accept_absent_selections() {
#[derive(serde::Deserialize)]
enum OldRequest {
Subcribe { key: String },
Stream { key: String, dst_id: u32 },
}
for request in [
PbConnRequest::Subcribe {
key: "compat".into(),
data_protocol: Some(2),
},
PbConnRequest::Stream {
key: "compat".into(),
dst_id: 7,
server_generation: 0,
data_protocol: Some(2),
},
] {
match serde_json::from_slice::<OldRequest>(&request.encode().unwrap()).unwrap() {
OldRequest::Subcribe { key } => assert_eq!(key, "compat"),
OldRequest::Stream { key, dst_id } => {
assert_eq!(key, "compat");
assert_eq!(dst_id, 7);
}
}
}
let response = PbConnResponse::decode(br#"{"Stream":{"codec_key":null}}"#).unwrap();
assert!(matches!(
response,
PbConnResponse::Stream {
codec_key: None,
data_protocol: None
}
));
let request = PbConnRequest::decode(br#"{"Subcribe":{"key":"compat"}}"#).unwrap();
assert!(matches!(
request,
PbConnRequest::Subcribe {
data_protocol: None,
..
}
));
let response = PbConnResponse::Stream {
codec_key: None,
data_protocol: None,
}
.encode()
.unwrap();
assert_eq!(response, br#"{"Stream":{"codec_key":null}}"#);
}
}