Skip to content

Commit 2a58ae4

Browse files
Enable simultaneous deserialization+decryption of a ChaChaPoly stream
In the upcoming onion messages PR, this will allow us to avoid decrypting onion message encrypted data in an intermediate Vec before decoding it. Instead we decrypt and decode it at the same time using this new ChaChaPolyReadAdapter object. In doing so, we need to adapt the decode_tlv_stream macro such that it will decode a LengthReadableArgs, which is a new trait as well. This trait is necessary because ChaChaPoly needs to know the total length ahead of time to separate out the tag at the end.
1 parent e716218 commit 2a58ae4

File tree

3 files changed

+214
-6
lines changed

3 files changed

+214
-6
lines changed

lightning/src/util/chacha20poly1305rfc.rs

+179-6
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
// This is a port of Andrew Moons poly1305-donna
1111
// https://github.com/floodyberry/poly1305-donna
1212

13-
use util::ser::{Writeable, Writer};
14-
use io::{self, Write};
13+
use ln::msgs::DecodeError;
14+
use util::ser::{FixedLengthReader, LengthRead, LengthReadableArgs, Readable, Writeable, Writer};
15+
use io::{self, Read, Write};
1516

1617
#[cfg(not(fuzzing))]
1718
mod real_chachapoly {
@@ -95,22 +96,48 @@ mod real_chachapoly {
9596
}
9697

9798
pub fn decrypt(&mut self, input: &[u8], output: &mut [u8], tag: &[u8]) -> bool {
98-
assert!(input.len() == output.len());
99-
assert!(self.finished == false);
99+
if self.decrypt_inner(input, Some(output), Some(tag)) {
100+
self.cipher.process(input, output);
101+
return true
102+
}
103+
false
104+
}
100105

101-
self.finished = true;
106+
// Decrypt in place, and check the tag if it's provided. If the tag is not provided, then
107+
// `finish_and_check_tag` may be called to check it later.
108+
pub fn decrypt_in_place(&mut self, input: &mut [u8], tag: Option<&[u8]>) -> bool {
109+
if self.decrypt_inner(input, None, tag) {
110+
self.cipher.process_in_place(input);
111+
return true
112+
}
113+
false
114+
}
115+
116+
fn decrypt_inner(&mut self, input: &[u8], output: Option<&mut [u8]>, tag: Option<&[u8]>) -> bool {
117+
if let Some(output) = output {
118+
assert!(input.len() == output.len());
119+
}
120+
assert!(self.finished == false);
102121

103122
self.mac.input(input);
104123

105124
self.data_len += input.len();
125+
126+
if let Some(tag) = tag {
127+
return self.finish_and_check_tag(tag)
128+
}
129+
true
130+
}
131+
132+
pub fn finish_and_check_tag(&mut self, tag: &[u8]) -> bool {
133+
self.finished = true;
106134
ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len);
107135
self.mac.input(&self.aad_len.to_le_bytes());
108136
self.mac.input(&(self.data_len as u64).to_le_bytes());
109137

110138
let mut calc_tag = [0u8; 16];
111139
self.mac.raw_result(&mut calc_tag);
112140
if fixed_time_eq(&calc_tag, tag) {
113-
self.cipher.process(input, output);
114141
true
115142
} else {
116143
false
@@ -121,6 +148,21 @@ mod real_chachapoly {
121148
#[cfg(not(fuzzing))]
122149
pub use self::real_chachapoly::ChaCha20Poly1305RFC;
123150

151+
pub(crate) struct ChaChaPolyReader<'a, R: Read> {
152+
pub chacha: &'a mut ChaCha20Poly1305RFC,
153+
pub read: R,
154+
}
155+
156+
impl<'a, R: Read> Read for ChaChaPolyReader<'a, R> {
157+
fn read(&mut self, dest: &mut [u8]) -> Result<usize, io::Error> {
158+
let res = self.read.read(dest)?;
159+
if res > 0 {
160+
self.chacha.decrypt_in_place(&mut dest[0..res], None);
161+
}
162+
Ok(res)
163+
}
164+
}
165+
124166
pub(crate) struct ChaChaPolyWriter<'a, W: Writer> {
125167
pub chacha: &'a mut ChaCha20Poly1305RFC,
126168
pub write: &'a mut W,
@@ -164,6 +206,31 @@ impl<'a, T: Writeable> Writeable for ChaChaPolyWriteAdapter<'a, T> {
164206
}
165207
}
166208

209+
pub(crate) struct ChaChaPolyReadAdapter<R: Readable> {
210+
#[allow(unused)] // This will be used soon for onion messages
211+
pub readable: R,
212+
}
213+
214+
impl<T: Readable> LengthReadableArgs<[u8; 32]> for ChaChaPolyReadAdapter<T> {
215+
fn read<R: LengthRead>(mut r: &mut R, secret: [u8; 32]) -> Result<Self, DecodeError> {
216+
if r.total_bytes() < 16 { return Err(DecodeError::InvalidValue) }
217+
218+
let mut chacha = ChaCha20Poly1305RFC::new(&secret, &[0; 12], &[]);
219+
let decrypted_len = r.total_bytes() - 16;
220+
let s = FixedLengthReader::new(&mut r, decrypted_len);
221+
let mut chacha_stream = ChaChaPolyReader { chacha: &mut chacha, read: s };
222+
let readable: T = Readable::read(&mut chacha_stream)?;
223+
224+
let mut tag = [0 as u8; 16];
225+
r.read_exact(&mut tag)?;
226+
if !chacha.finish_and_check_tag(&tag) {
227+
return Err(DecodeError::InvalidValue)
228+
}
229+
230+
Ok(Self { readable })
231+
}
232+
}
233+
167234
#[cfg(fuzzing)]
168235
mod fuzzy_chachapoly {
169236
#[derive(Clone, Copy)]
@@ -219,7 +286,113 @@ mod fuzzy_chachapoly {
219286
self.finished = true;
220287
true
221288
}
289+
290+
pub fn decrypt_in_place(&mut self, _input: &mut [u8], tag: Option<&[u8]>) -> bool {
291+
assert!(self.finished == false);
292+
if let Some(tag) = tag {
293+
if tag[..] != self.tag[..] { return false; }
294+
}
295+
self.finished = true;
296+
true
297+
}
298+
299+
pub fn finish_and_check_tag(&mut self, tag: &[u8]) -> bool {
300+
if tag[..] != self.tag[..] { return false; }
301+
self.finished = true;
302+
true
303+
}
222304
}
223305
}
224306
#[cfg(fuzzing)]
225307
pub use self::fuzzy_chachapoly::ChaCha20Poly1305RFC;
308+
309+
#[cfg(test)]
310+
mod tests {
311+
use ln::msgs::DecodeError;
312+
use super::{ChaChaPolyReadAdapter, ChaChaPolyWriteAdapter};
313+
use util::ser::{self, FixedLengthReader, LengthReadableArgs, Writeable};
314+
315+
// Used for for testing various lengths of serialization.
316+
#[derive(Debug, PartialEq)]
317+
struct TestWriteable {
318+
field1: Vec<u8>,
319+
field2: Vec<u8>,
320+
field3: Vec<u8>,
321+
}
322+
impl_writeable_tlv_based!(TestWriteable, {
323+
(1, field1, vec_type),
324+
(2, field2, vec_type),
325+
(3, field3, vec_type),
326+
});
327+
328+
#[test]
329+
fn test_chacha_stream_adapters() {
330+
// Check that ChaChaPolyReadAdapter and ChaChaPolyWriteAdapter correctly encode and decode an
331+
// encrypted object.
332+
macro_rules! check_object_read_write {
333+
($obj: expr) => {
334+
// First, serialize the object, encrypted with ChaCha20Poly1305.
335+
let rho = [42; 32];
336+
let writeable_len = $obj.serialized_length() as u64 + 16;
337+
let write_adapter = ChaChaPolyWriteAdapter::new(rho, &$obj);
338+
let encrypted_writeable_bytes = write_adapter.encode();
339+
let encrypted_writeable = &encrypted_writeable_bytes[..];
340+
341+
// Now deserialize the object back and make sure it matches the original.
342+
let mut rd = FixedLengthReader::new(encrypted_writeable, writeable_len);
343+
let read_adapter = <ChaChaPolyReadAdapter<TestWriteable>>::read(&mut rd, rho).unwrap();
344+
assert_eq!($obj, read_adapter.readable);
345+
};
346+
}
347+
348+
// Try a big object that will require multiple write buffers.
349+
let big_writeable = TestWriteable {
350+
field1: vec![43],
351+
field2: vec![44; 4192],
352+
field3: vec![45; 4192 + 1],
353+
};
354+
check_object_read_write!(big_writeable);
355+
356+
// Try a small object that fits into one write buffer.
357+
let small_writeable = TestWriteable {
358+
field1: vec![43],
359+
field2: vec![44],
360+
field3: vec![45],
361+
};
362+
check_object_read_write!(small_writeable);
363+
}
364+
365+
fn do_chacha_stream_adapters_ser_macros() -> Result<(), DecodeError> {
366+
// Check that ChaChaPolyReadAdapter and ChaChaPolyWriteAdapter correctly encode and decode an
367+
// encrypted object.
368+
let writeable = TestWriteable {
369+
field1: vec![43],
370+
field2: vec![44; 4192],
371+
field3: vec![45; 4192 + 1],
372+
};
373+
374+
// First, serialize the object into a TLV stream, encrypted with ChaCha20Poly1305.
375+
let rho = [42; 32];
376+
let write_adapter = ChaChaPolyWriteAdapter::new(rho, &writeable);
377+
let mut writer = ser::VecWriter(Vec::new());
378+
encode_tlv_stream!(&mut writer, {
379+
(1, write_adapter, required),
380+
});
381+
382+
// Now deserialize the object back and make sure it matches the original.
383+
let mut read_adapter: Option<ChaChaPolyReadAdapter<TestWriteable>> = None;
384+
decode_tlv_stream!(&writer.0[..], {
385+
(1, read_adapter, (read_arg, rho)),
386+
});
387+
assert_eq!(writeable, read_adapter.unwrap().readable);
388+
389+
Ok(())
390+
}
391+
392+
#[test]
393+
fn chacha_stream_adapters_ser_macros() {
394+
// Test that our stream adapters work as expected with the TLV macros.
395+
// This also serves to test the `LengthReadableArgs` variant of the `decode_tlv` ser macro.
396+
do_chacha_stream_adapters_ser_macros().unwrap()
397+
}
398+
}

lightning/src/util/ser.rs

+25
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ impl<R: Read> Read for FixedLengthReader<R> {
134134
}
135135
}
136136

137+
impl<R: Read> LengthRead for FixedLengthReader<R> {
138+
#[inline]
139+
fn total_bytes(&self) -> u64 {
140+
self.total_bytes
141+
}
142+
}
143+
137144
/// A Read which tracks whether any bytes have been read at all. This allows us to distinguish
138145
/// between "EOF reached before we started" and "EOF reached mid-read".
139146
pub(crate) struct ReadTrackingReader<R: Read> {
@@ -220,6 +227,24 @@ pub trait ReadableArgs<P>
220227
fn read<R: Read>(reader: &mut R, params: P) -> Result<Self, DecodeError>;
221228
}
222229

230+
/// A std::io::Read that also provides the total bytes available to read.
231+
pub trait LengthRead where Self: Read {
232+
/// The total number of bytes available to read.
233+
fn total_bytes(&self) -> u64;
234+
}
235+
236+
/// A trait that various higher-level rust-lightning types implement allowing them to be read in
237+
/// from a Read given some additional set of arguments which is required to deserialize, requiring
238+
/// the implementer to provide the total length of the read.
239+
///
240+
/// (C-not exported) as we only export serialization to/from byte arrays instead
241+
pub trait LengthReadableArgs<P>
242+
where Self: Sized
243+
{
244+
/// Reads a Self in from the given LengthRead
245+
fn read<R: LengthRead>(reader: &mut R, params: P) -> Result<Self, DecodeError>;
246+
}
247+
223248
/// A trait that various rust-lightning types implement allowing them to (maybe) be read in from a Read
224249
///
225250
/// (C-not exported) as we only export serialization to/from byte arrays instead

lightning/src/util/ser_macros.rs

+10
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ macro_rules! check_tlv_order {
118118
($last_seen_type: expr, $typ: expr, $type: expr, $field: ident, ignorable) => {{
119119
// no-op
120120
}};
121+
($last_seen_type: expr, $typ: expr, $type: expr, $field: ident, (read_arg, $read_arg: expr)) => {{
122+
// no-op
123+
}};
121124
}
122125

123126
macro_rules! check_missing_tlv {
@@ -144,6 +147,9 @@ macro_rules! check_missing_tlv {
144147
($last_seen_type: expr, $type: expr, $field: ident, ignorable) => {{
145148
// no-op
146149
}};
150+
($last_seen_type: expr, $type: expr, $field: ident, (read_arg, $read_arg: expr)) => {{
151+
// no-op
152+
}};
147153
}
148154

149155
macro_rules! decode_tlv {
@@ -163,6 +169,10 @@ macro_rules! decode_tlv {
163169
($reader: expr, $field: ident, ignorable) => {{
164170
$field = ser::MaybeReadable::read(&mut $reader)?;
165171
}};
172+
// Read a ser::LengthReadableArgs
173+
($reader: expr, $field: ident, (read_arg, $read_arg: expr)) => {{
174+
$field = Some(ser::LengthReadableArgs::read(&mut $reader, $read_arg)?);
175+
}};
166176
}
167177

168178
macro_rules! decode_tlv_stream {

0 commit comments

Comments
 (0)