rings_node/onion/circuit/
cell.rs1use bytes::Bytes;
12use rand::CryptoRng;
13use rand::RngCore;
14use rings_core::ecc::elgamal::impls::secp256k1::encrypt_aead_with_rng;
15use rings_core::ecc::elgamal::impls::secp256k1::AeadCiphertext;
16use rings_core::ecc::PublicKey;
17use rings_core::session::SessionSk;
18use serde::Deserialize;
19use serde::Serialize;
20
21use super::codec::OnionWireMessage;
22use crate::error::Error;
23use crate::error::Result;
24use crate::onion::OnionRouteError;
25
26const CELL_LENGTH_PREFIX_BYTES: usize = size_of::<u32>();
27const ONION_CELL_AEAD_NAMESPACE: &[u8] = b"rings-node:onion-cell:v1";
28
29#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
35pub enum OnionCellBucket {
36 KiB4,
38 KiB16,
40 KiB64,
42 KiB256,
44 MiB1,
46 MiB4,
48 MiB12,
52}
53
54impl OnionCellBucket {
55 const ALL: [Self; 7] = [
56 Self::KiB4,
57 Self::KiB16,
58 Self::KiB64,
59 Self::KiB256,
60 Self::MiB1,
61 Self::MiB4,
62 Self::MiB12,
63 ];
64
65 pub const fn plaintext_len(self) -> usize {
67 match self {
68 Self::KiB4 => 4 * 1024,
69 Self::KiB16 => 16 * 1024,
70 Self::KiB64 => 64 * 1024,
71 Self::KiB256 => 256 * 1024,
72 Self::MiB1 => 1024 * 1024,
73 Self::MiB4 => 4 * 1024 * 1024,
74 Self::MiB12 => 12 * 1024 * 1024,
75 }
76 }
77
78 fn smallest_for(encoded_len: usize) -> Result<Self> {
79 let required = encoded_len
80 .checked_add(CELL_LENGTH_PREFIX_BYTES)
81 .ok_or_else(|| Error::OnionRouteError(OnionRouteError::CellPayloadTooLarge))?;
82 Self::ALL
83 .into_iter()
84 .find(|bucket| bucket.plaintext_len() >= required)
85 .ok_or_else(|| Error::OnionRouteError(OnionRouteError::CellPayloadTooLarge))
86 }
87
88 fn accepts(self, encoded_len: usize) -> bool {
89 encoded_len
90 .checked_add(CELL_LENGTH_PREFIX_BYTES)
91 .is_some_and(|required| required <= self.plaintext_len())
92 }
93}
94
95#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
97pub(super) struct OnionWireCell {
98 pub(super) bucket: OnionCellBucket,
99 pub(super) sealed: AeadCiphertext,
100}
101
102pub(super) fn encode_message(message: &OnionWireMessage) -> Result<Bytes> {
103 rings_codec::serialize(message)
104 .map(Bytes::from)
105 .map_err(|_| Error::EncodeError)
106}
107
108pub(super) fn seal_message(
109 message: &OnionWireMessage,
110 recipient: PublicKey<33>,
111 bucket: Option<OnionCellBucket>,
112) -> Result<Bytes> {
113 let encoded = encode_message(message)?;
114 seal_encoded_message(&encoded, recipient, bucket)
115}
116
117pub(super) fn seal_encoded_message(
118 encoded: &[u8],
119 recipient: PublicKey<33>,
120 bucket: Option<OnionCellBucket>,
121) -> Result<Bytes> {
122 let mut rng = rand::thread_rng();
123 seal_encoded_message_with_rng(encoded, recipient, bucket, &mut rng)
124}
125
126pub(super) fn sealed_cell_bucket(payload: &[u8]) -> Result<OnionCellBucket> {
131 rings_codec::deserialize::<OnionWireCell>(payload)
132 .map(|cell| cell.bucket)
133 .map_err(|_| Error::OnionRouteError(OnionRouteError::InvalidCell))
134}
135
136fn seal_encoded_message_with_rng<R: CryptoRng + RngCore>(
137 encoded: &[u8],
138 recipient: PublicKey<33>,
139 bucket: Option<OnionCellBucket>,
140 rng: &mut R,
141) -> Result<Bytes> {
142 let bucket = bucket.map_or_else(|| OnionCellBucket::smallest_for(encoded.len()), Ok)?;
143 if !bucket.accepts(encoded.len()) {
144 return Err(Error::OnionRouteError(OnionRouteError::CellPayloadTooLarge));
145 }
146 let encoded_len = u32::try_from(encoded.len())
147 .map_err(|_| Error::OnionRouteError(OnionRouteError::CellPayloadTooLarge))?;
148 let mut plaintext = vec![0_u8; bucket.plaintext_len()];
149 let encoded_end = CELL_LENGTH_PREFIX_BYTES
150 .checked_add(encoded.len())
151 .ok_or_else(|| Error::OnionRouteError(OnionRouteError::CellPayloadTooLarge))?;
152 plaintext
153 .get_mut(..CELL_LENGTH_PREFIX_BYTES)
154 .ok_or_else(|| Error::OnionRouteError(OnionRouteError::InvalidCell))?
155 .copy_from_slice(&encoded_len.to_le_bytes());
156 plaintext
157 .get_mut(CELL_LENGTH_PREFIX_BYTES..encoded_end)
158 .ok_or_else(|| Error::OnionRouteError(OnionRouteError::InvalidCell))?
159 .copy_from_slice(encoded);
160 rng.fill_bytes(
161 plaintext
162 .get_mut(encoded_end..)
163 .ok_or_else(|| Error::OnionRouteError(OnionRouteError::InvalidCell))?,
164 );
165 let aad = cell_aad(bucket)?;
166 let sealed =
167 encrypt_aead_with_rng(&plaintext, &aad, recipient, rng).map_err(Error::CoreError)?;
168 rings_codec::serialize(&OnionWireCell { bucket, sealed })
169 .map(Bytes::from)
170 .map_err(|_| Error::EncodeError)
171}
172
173pub(super) fn open_cell(
174 session_sk: &SessionSk,
175 bucket: OnionCellBucket,
176 sealed: &AeadCiphertext,
177) -> Result<OnionWireMessage> {
178 let aad = cell_aad(bucket)?;
179 let plaintext = session_sk
180 .decrypt_elgamal_aead(sealed, &aad)
181 .map_err(Error::CoreError)?;
182 if plaintext.len() != bucket.plaintext_len() {
183 return Err(Error::OnionRouteError(OnionRouteError::InvalidCell));
184 }
185 let encoded_len = u32::from_le_bytes(
186 plaintext
187 .get(..CELL_LENGTH_PREFIX_BYTES)
188 .ok_or_else(|| Error::OnionRouteError(OnionRouteError::InvalidCell))?
189 .try_into()
190 .map_err(|_| Error::OnionRouteError(OnionRouteError::InvalidCell))?,
191 ) as usize;
192 if !bucket.accepts(encoded_len) {
193 return Err(Error::OnionRouteError(OnionRouteError::InvalidCell));
194 }
195 let encoded_end = CELL_LENGTH_PREFIX_BYTES
196 .checked_add(encoded_len)
197 .ok_or_else(|| Error::OnionRouteError(OnionRouteError::InvalidCell))?;
198 let encoded = plaintext
199 .get(CELL_LENGTH_PREFIX_BYTES..encoded_end)
200 .ok_or_else(|| Error::OnionRouteError(OnionRouteError::InvalidCell))?;
201 rings_codec::deserialize(encoded).map_err(|_| Error::DecodeError)
202}
203
204fn cell_aad(bucket: OnionCellBucket) -> Result<Vec<u8>> {
205 rings_codec::serialize(&(ONION_CELL_AEAD_NAMESPACE, bucket)).map_err(|_| Error::EncodeError)
206}
207
208#[cfg(test)]
209mod tests {
210 use rings_core::ecc::SecretKey;
211 use rings_core::session::SessionSk;
212
213 use super::*;
214 use crate::onion::circuit::OnionBackwardFrame;
215 use crate::onion::circuit::OnionCircuitId;
216
217 fn session() -> SessionSk {
218 SessionSk::new_with_seckey(&SecretKey::random()).expect("session key")
219 }
220
221 fn backward_message(payload_len: usize) -> OnionWireMessage {
222 let recipient = session();
223 let sealed = encrypt_aead_with_rng(
224 &vec![7_u8; payload_len],
225 b"cell-test",
226 recipient.session_public_key(),
227 &mut rand::thread_rng(),
228 )
229 .expect("encrypt fixture");
230 OnionWireMessage::Backward(OnionBackwardFrame {
231 circuit_id: OnionCircuitId::new([1; 16]),
232 payload: sealed,
233 })
234 }
235
236 #[test]
237 fn test_small_messages_share_one_observable_cell_size() {
238 let recipient = session();
239 let short = seal_message(&backward_message(1), recipient.session_public_key(), None)
240 .expect("seal short");
241 let longer = seal_message(
242 &backward_message(1_000),
243 recipient.session_public_key(),
244 None,
245 )
246 .expect("seal longer");
247 assert_eq!(short.len(), longer.len());
248 }
249
250 #[test]
251 fn test_cell_round_trip_rejects_wrong_recipient() {
252 let recipient = session();
253 let wrong = session();
254 let message = backward_message(1);
255 let encoded =
256 seal_message(&message, recipient.session_public_key(), None).expect("seal message");
257 let cell: OnionWireCell = rings_codec::deserialize(&encoded).expect("decode cell");
258 assert_eq!(
259 open_cell(&recipient, cell.bucket, &cell.sealed).expect("open cell"),
260 message
261 );
262 assert!(open_cell(&wrong, cell.bucket, &cell.sealed).is_err());
263 }
264
265 #[test]
266 fn test_one_hop_cover_is_authenticated_inside_the_same_cell_algebra() {
267 let recipient = session();
268 let encoded = seal_message(
269 &OnionWireMessage::Cover,
270 recipient.session_public_key(),
271 Some(OnionCellBucket::KiB4),
272 )
273 .expect("seal cover");
274 let cell: OnionWireCell = rings_codec::deserialize(&encoded).expect("decode cover cell");
275
276 assert_eq!(
277 open_cell(&recipient, cell.bucket, &cell.sealed).expect("open cover cell"),
278 OnionWireMessage::Cover
279 );
280 assert_eq!(
281 sealed_cell_bucket(&encoded).expect("read public cell bucket"),
282 OnionCellBucket::KiB4
283 );
284 }
285
286 #[test]
287 fn test_local_bucket_selection_is_minimal_at_every_boundary() {
288 for (index, bucket) in OnionCellBucket::ALL.into_iter().enumerate() {
289 let encoded_capacity = bucket.plaintext_len() - CELL_LENGTH_PREFIX_BYTES;
290 assert_eq!(
291 OnionCellBucket::smallest_for(encoded_capacity).ok(),
292 Some(bucket)
293 );
294 match OnionCellBucket::ALL.get(index + 1).copied() {
295 Some(next) => assert_eq!(
296 OnionCellBucket::smallest_for(encoded_capacity + 1).ok(),
297 Some(next)
298 ),
299 None => assert!(OnionCellBucket::smallest_for(encoded_capacity + 1).is_err()),
300 }
301 }
302 }
303}