Skip to content

Commit aeaf125

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 df31474 commit aeaf125

File tree

3 files changed

+223
-6
lines changed

3 files changed

+223
-6
lines changed

lightning/src/util/chacha20poly1305rfc.rs

Lines changed: 186 additions & 6 deletions
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 {
@@ -93,22 +94,48 @@ mod real_chachapoly {
9394
}
9495

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

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

101120
self.mac.input(input);
102121

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

108136
let mut calc_tag = [0u8; 16];
109137
self.mac.raw_result(&mut calc_tag);
110138
if fixed_time_eq(&calc_tag, tag) {
111-
self.cipher.process(input, output);
112139
true
113140
} else {
114141
false
@@ -119,6 +146,25 @@ mod real_chachapoly {
119146
#[cfg(not(fuzzing))]
120147
pub use self::real_chachapoly::ChaCha20Poly1305RFC;
121148

149+
/// Enables simultaneously reading and decrypting a ChaCha20Poly1305RFC stream from a std::io::Read.
150+
pub(crate) struct ChaChaPolyReader<'a, R: Read> {
151+
pub chacha: &'a mut ChaCha20Poly1305RFC,
152+
pub read: R,
153+
}
154+
155+
impl<'a, R: Read> Read for ChaChaPolyReader<'a, R> {
156+
// Decrypt bytes from Self::read into `dest`.
157+
// `ChaCha20Poly1305RFC::finish_and_check_tag` must be called to check the tag after all reads
158+
// complete.
159+
fn read(&mut self, dest: &mut [u8]) -> Result<usize, io::Error> {
160+
let res = self.read.read(dest)?;
161+
if res > 0 {
162+
self.chacha.decrypt_in_place(&mut dest[0..res], None);
163+
}
164+
Ok(res)
165+
}
166+
}
167+
122168
/// Enables simultaneously writing and encrypting a byte stream into a Writer.
123169
pub(crate) struct ChaChaPolyWriter<'a, W: Writer> {
124170
pub chacha: &'a mut ChaCha20Poly1305RFC,
@@ -173,6 +219,36 @@ impl<'a, T: Writeable> Writeable for ChaChaPolyWriteAdapter<'a, T> {
173219
}
174220
}
175221

222+
/// Enables the use of the serialization macros for objects that need to be simultaneously decrypted and
223+
/// deserialized. This allows us to avoid an intermediate Vec allocation.
224+
pub(crate) struct ChaChaPolyReadAdapter<R: Readable> {
225+
#[allow(unused)] // This will be used soon for onion messages
226+
pub readable: R,
227+
}
228+
229+
impl<T: Readable> LengthReadableArgs<[u8; 32]> for ChaChaPolyReadAdapter<T> {
230+
// Simultaneously read and decrypt an object from a LengthRead, storing it in Self::readable.
231+
// LengthRead must be used instead of std::io::Read because we need the total length to separate
232+
// out the tag at the end.
233+
fn read<R: LengthRead>(mut r: &mut R, secret: [u8; 32]) -> Result<Self, DecodeError> {
234+
if r.total_bytes() < 16 { return Err(DecodeError::InvalidValue) }
235+
236+
let mut chacha = ChaCha20Poly1305RFC::new(&secret, &[0; 12], &[]);
237+
let decrypted_len = r.total_bytes() - 16;
238+
let s = FixedLengthReader::new(&mut r, decrypted_len);
239+
let mut chacha_stream = ChaChaPolyReader { chacha: &mut chacha, read: s };
240+
let readable: T = Readable::read(&mut chacha_stream)?;
241+
242+
let mut tag = [0 as u8; 16];
243+
r.read_exact(&mut tag)?;
244+
if !chacha.finish_and_check_tag(&tag) {
245+
return Err(DecodeError::InvalidValue)
246+
}
247+
248+
Ok(Self { readable })
249+
}
250+
}
251+
176252
#[cfg(fuzzing)]
177253
mod fuzzy_chachapoly {
178254
#[derive(Clone, Copy)]
@@ -228,7 +304,111 @@ mod fuzzy_chachapoly {
228304
self.finished = true;
229305
true
230306
}
307+
308+
pub fn decrypt_in_place(&mut self, _input: &mut [u8], tag: Option<&[u8]>) -> bool {
309+
assert!(self.finished == false);
310+
if let Some(tag) = tag {
311+
if tag[..] != self.tag[..] { return false; }
312+
}
313+
self.finished = true;
314+
true
315+
}
316+
317+
pub fn finish_and_check_tag(&mut self, tag: &[u8]) -> bool {
318+
if tag[..] != self.tag[..] { return false; }
319+
self.finished = true;
320+
true
321+
}
231322
}
232323
}
233324
#[cfg(fuzzing)]
234325
pub use self::fuzzy_chachapoly::ChaCha20Poly1305RFC;
326+
327+
#[cfg(test)]
328+
mod tests {
329+
use ln::msgs::DecodeError;
330+
use super::{ChaChaPolyReadAdapter, ChaChaPolyWriteAdapter};
331+
use util::ser::{self, FixedLengthReader, LengthReadableArgs, Writeable};
332+
333+
// Used for for testing various lengths of serialization.
334+
#[derive(Debug, PartialEq)]
335+
struct TestWriteable {
336+
field1: Vec<u8>,
337+
field2: Vec<u8>,
338+
field3: Vec<u8>,
339+
}
340+
impl_writeable_tlv_based!(TestWriteable, {
341+
(1, field1, vec_type),
342+
(2, field2, vec_type),
343+
(3, field3, vec_type),
344+
});
345+
346+
#[test]
347+
fn test_chacha_stream_adapters() {
348+
// Check that ChaChaPolyReadAdapter and ChaChaPolyWriteAdapter correctly encode and decode an
349+
// encrypted object.
350+
macro_rules! check_object_read_write {
351+
($obj: expr) => {
352+
// First, serialize the object, encrypted with ChaCha20Poly1305.
353+
let rho = [42; 32];
354+
let writeable_len = $obj.serialized_length() as u64 + 16;
355+
let write_adapter = ChaChaPolyWriteAdapter::new(rho, &$obj);
356+
let encrypted_writeable_bytes = write_adapter.encode();
357+
let encrypted_writeable = &encrypted_writeable_bytes[..];
358+
359+
// Now deserialize the object back and make sure it matches the original.
360+
let mut rd = FixedLengthReader::new(encrypted_writeable, writeable_len);
361+
let read_adapter = <ChaChaPolyReadAdapter<TestWriteable>>::read(&mut rd, rho).unwrap();
362+
assert_eq!($obj, read_adapter.readable);
363+
};
364+
}
365+
366+
// Try a big object that will require multiple write buffers.
367+
let big_writeable = TestWriteable {
368+
field1: vec![43],
369+
field2: vec![44; 4192],
370+
field3: vec![45; 4192 + 1],
371+
};
372+
check_object_read_write!(big_writeable);
373+
374+
// Try a small object that fits into one write buffer.
375+
let small_writeable = TestWriteable {
376+
field1: vec![43],
377+
field2: vec![44],
378+
field3: vec![45],
379+
};
380+
check_object_read_write!(small_writeable);
381+
}
382+
383+
fn do_chacha_stream_adapters_ser_macros() -> Result<(), DecodeError> {
384+
let writeable = TestWriteable {
385+
field1: vec![43],
386+
field2: vec![44; 4192],
387+
field3: vec![45; 4192 + 1],
388+
};
389+
390+
// First, serialize the object into a TLV stream, encrypted with ChaCha20Poly1305.
391+
let rho = [42; 32];
392+
let write_adapter = ChaChaPolyWriteAdapter::new(rho, &writeable);
393+
let mut writer = ser::VecWriter(Vec::new());
394+
encode_tlv_stream!(&mut writer, {
395+
(1, write_adapter, required),
396+
});
397+
398+
// Now deserialize the object back and make sure it matches the original.
399+
let mut read_adapter: Option<ChaChaPolyReadAdapter<TestWriteable>> = None;
400+
decode_tlv_stream!(&writer.0[..], {
401+
(1, read_adapter, (read_arg, rho)),
402+
});
403+
assert_eq!(writeable, read_adapter.unwrap().readable);
404+
405+
Ok(())
406+
}
407+
408+
#[test]
409+
fn chacha_stream_adapters_ser_macros() {
410+
// Test that our stream adapters work as expected with the TLV macros.
411+
// This also serves to test the `LengthReadableArgs` variant of the `decode_tlv` ser macro.
412+
do_chacha_stream_adapters_ser_macros().unwrap()
413+
}
414+
}

lightning/src/util/ser.rs

Lines changed: 27 additions & 0 deletions
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,26 @@ 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+
///
232+
/// (C-not exported) as we only export serialization to/from byte arrays instead
233+
pub trait LengthRead where Self: Read {
234+
/// The total number of bytes available to read.
235+
fn total_bytes(&self) -> u64;
236+
}
237+
238+
/// A trait that various higher-level rust-lightning types implement allowing them to be read in
239+
/// from a Read given some additional set of arguments which is required to deserialize, requiring
240+
/// the implementer to provide the total length of the read.
241+
///
242+
/// (C-not exported) as we only export serialization to/from byte arrays instead
243+
pub trait LengthReadableArgs<P>
244+
where Self: Sized
245+
{
246+
/// Reads a Self in from the given LengthRead
247+
fn read<R: LengthRead>(reader: &mut R, params: P) -> Result<Self, DecodeError>;
248+
}
249+
223250
/// A trait that various rust-lightning types implement allowing them to (maybe) be read in from a Read
224251
///
225252
/// (C-not exported) as we only export serialization to/from byte arrays instead

lightning/src/util/ser_macros.rs

Lines changed: 10 additions & 0 deletions
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)