1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// wire-rs: encrypted protocol between Ark and host
// Copyright 2026 Dark Bio AG. All rights reserved.
//! Established session state and the sealing of protobuf messages through it.
use crate::{Error, MAX_MESSAGE_SIZE};
use darkbio_crypto::xhpke;
use prost::Message;
/// Established session, holding the xHPKE contexts of the two directions.
pub(crate) struct Session {
pub sender: xhpke::Sender, // Outbound context, sealing messages to the peer
pub receiver: xhpke::Receiver, // Inbound context, opening messages from the peer
}
impl Session {
/// Bytes the session's AEAD adds to a sealed message (the Poly1305 tag).
/// Sealing advances the HPKE sequence, so message sizes are bounded with
/// this before sealing rather than by rejecting the sealed output afterwards.
pub const SEAL_OVERHEAD: usize = 16;
/// Protobuf encodes a message into the scratch buffer and seals it for the
/// peer. Messages above MAX_MESSAGE_SIZE are rejected before sealing,
/// leaving the HPKE sequence untouched; a failure of the sealing itself
/// leaves the context unusable.
pub fn seal<M: Message>(&mut self, msg: &M, scratch: &mut Vec<u8>) -> Result<Vec<u8>, Error> {
let len = msg.encoded_len();
if len > MAX_MESSAGE_SIZE {
return Err(Error::PacketTooLarge(len));
}
scratch.clear();
msg.encode(scratch).map_err(Error::PacketEncodingFailed)?;
self.sender
.seal(scratch, &[])
.map_err(|err| Error::EncryptionFailed(err.to_string()))
}
/// Opens a sealed packet from the peer and protobuf decodes it. A failure
/// to decrypt means the HPKE sequence can no longer be followed, whereas a
/// packet that decrypts but does not decode leaves the session intact.
pub fn open<M: Message + Default>(&mut self, packet: &[u8]) -> Result<M, Error> {
let blob = self
.receiver
.open(packet, &[])
.map_err(|err| Error::EncryptionFailed(err.to_string()))?;
M::decode(&blob[..]).map_err(Error::PacketDecodingFailed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MAX_FRAME_SIZE;
use darkbio_cobs as cobs;
// Tests that the sealing overhead constant matches what the session AEAD
// actually adds, so a crypto upgrade cannot silently break size bounds.
#[test]
fn test_seal_overhead() {
let secret = xhpke::SecretKey::generate();
let (mut sender, _) = secret.public_key().new_sender(b"test").unwrap();
for size in [0, 1, 255, 4096] {
let sealed = sender.seal(&vec![0x42; size], &[]).unwrap();
assert_eq!(
sealed.len(),
size + Session::SEAL_OVERHEAD,
"overhead mismatch"
);
}
}
// Tests that the message limit is exact against the frame limit, the sealed
// and framed size of a maximal message fitting a frame with the next byte
// pushing it over.
#[test]
fn test_message_limit() {
let framed = |size: usize| cobs::encode_buffer(size + Session::SEAL_OVERHEAD);
assert!(framed(MAX_MESSAGE_SIZE) <= MAX_FRAME_SIZE, "limit too high");
assert!(
framed(MAX_MESSAGE_SIZE + 1) > MAX_FRAME_SIZE,
"limit too low"
);
}
// Tests that a message at the limit seals into a frame sized packet, that
// one over the limit is rejected before sealing, and that the rejection
// leaves the HPKE sequence untouched so the session stays in sync.
#[test]
fn test_seal_bounds() {
/// Throwaway message carrying an arbitrary payload.
#[derive(Clone, PartialEq, Message)]
struct Blob {
#[prost(bytes = "vec", tag = "1")]
data: Vec<u8>,
}
/// Blob whose protobuf encoding is exactly the given size.
fn blob_of(size: usize) -> Blob {
let mut blob = Blob {
data: vec![0x42; size],
};
blob.data.truncate(size - (blob.encoded_len() - size));
assert_eq!(blob.encoded_len(), size, "blob sizing failed");
blob
}
let secret = xhpke::SecretKey::generate();
let (sender, encap) = secret.public_key().new_sender(b"test").unwrap();
let receiver = secret.new_receiver(&encap, b"test").unwrap();
let mut session = Session { sender, receiver };
let mut scratch = Vec::new();
// One byte over the limit must be rejected up front
let blob = blob_of(MAX_MESSAGE_SIZE + 1);
assert!(
matches!(
session.seal(&blob, &mut scratch),
Err(Error::PacketTooLarge(_))
),
"expected oversized packet rejection"
);
// A maximal message must seal, frame within the limit and, the
// sequence not having advanced, open as the first message on the
// receiving side
let blob = blob_of(MAX_MESSAGE_SIZE);
let sealed = session.seal(&blob, &mut scratch).unwrap();
assert!(
cobs::encode_buffer(sealed.len()) <= MAX_FRAME_SIZE,
"sealed message does not fit a frame"
);
let opened: Blob = session.open(&sealed).unwrap();
assert_eq!(opened, blob, "message mismatch");
}
}