Skip to content

Commit ee805e9

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 945cec3 commit ee805e9

File tree

3 files changed

+212
-2
lines changed

3 files changed

+212
-2
lines changed

lightning/src/util/chacha20poly1305rfc.rs

+181-2
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 {
@@ -115,11 +116,59 @@ mod real_chachapoly {
115116
false
116117
}
117118
}
119+
120+
// Decrypt in place, without checking the tag. Use `finish_and_check_tag` to check it
121+
// later when decryption finishes.
122+
//
123+
// Should never be `pub` because the public API should always enforce tag checking.
124+
pub(super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) {
125+
debug_assert!(self.finished == false);
126+
self.mac.input(input_output);
127+
self.data_len += input_output.len();
128+
self.cipher.process_in_place(input_output);
129+
}
130+
131+
// If we were previously decrypting with `decrypt_in_place`, this method must be used to finish
132+
// decrypting and check the tag. Returns whether or not the tag is valid.
133+
pub(super) fn finish_and_check_tag(&mut self, tag: &[u8]) -> bool {
134+
debug_assert!(self.finished == false);
135+
self.finished = true;
136+
ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len);
137+
self.mac.input(&self.aad_len.to_le_bytes());
138+
self.mac.input(&(self.data_len as u64).to_le_bytes());
139+
140+
let mut calc_tag = [0u8; 16];
141+
self.mac.raw_result(&mut calc_tag);
142+
if fixed_time_eq(&calc_tag, tag) {
143+
true
144+
} else {
145+
false
146+
}
147+
}
118148
}
119149
}
120150
#[cfg(not(fuzzing))]
121151
pub use self::real_chachapoly::ChaCha20Poly1305RFC;
122152

153+
/// Enables simultaneously reading and decrypting a ChaCha20Poly1305RFC stream from a std::io::Read.
154+
struct ChaChaPolyReader<'a, R: Read> {
155+
pub chacha: &'a mut ChaCha20Poly1305RFC,
156+
pub read: R,
157+
}
158+
159+
impl<'a, R: Read> Read for ChaChaPolyReader<'a, R> {
160+
// Decrypt bytes from Self::read into `dest`.
161+
// `ChaCha20Poly1305RFC::finish_and_check_tag` must be called to check the tag after all reads
162+
// complete.
163+
fn read(&mut self, dest: &mut [u8]) -> Result<usize, io::Error> {
164+
let res = self.read.read(dest)?;
165+
if res > 0 {
166+
self.chacha.decrypt_in_place(&mut dest[0..res]);
167+
}
168+
Ok(res)
169+
}
170+
}
171+
123172
/// Enables simultaneously writing and encrypting a byte stream into a Writer.
124173
struct ChaChaPolyWriter<'a, W: Writer> {
125174
pub chacha: &'a mut ChaCha20Poly1305RFC,
@@ -171,6 +220,37 @@ impl<'a, T: Writeable> Writeable for ChaChaPolyWriteAdapter<'a, T> {
171220
}
172221
}
173222

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

lightning/src/util/ser.rs

+22
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,21 @@ 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(crate) trait LengthRead: 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+
pub(crate) trait LengthReadableArgs<P> where Self: Sized
240+
{
241+
/// Reads a Self in from the given LengthRead
242+
fn read<R: LengthRead>(reader: &mut R, params: P) -> Result<Self, DecodeError>;
243+
}
244+
223245
/// A trait that various rust-lightning types implement allowing them to (maybe) be read in from a Read
224246
///
225247
/// (C-not exported) as we only export serialization to/from byte arrays instead

lightning/src/util/ser_macros.rs

+9
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, (option: $trait: ident $(, $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, (option: $trait: ident $(, $read_arg: expr)?)) => {{
151+
// no-op
152+
}};
147153
}
148154

149155
macro_rules! decode_tlv {
@@ -163,6 +169,9 @@ macro_rules! decode_tlv {
163169
($reader: expr, $field: ident, ignorable) => {{
164170
$field = ser::MaybeReadable::read(&mut $reader)?;
165171
}};
172+
($reader: expr, $field: ident, (option: $trait: ident $(, $read_arg: expr)?)) => {{
173+
$field = Some($trait::read(&mut $reader $(, $read_arg)*)?);
174+
}};
166175
}
167176

168177
macro_rules! decode_tlv_stream {

0 commit comments

Comments
 (0)