From fa8e6d726df9bb9b8499bf078ac6bdb0c1378999 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 25 Aug 2023 01:32:06 -0500 Subject: [PATCH] keyfork-frame: add asyncext to allow AsyncRead/AsyncWrite --- keyfork-frame/Cargo.toml | 5 ++++ keyfork-frame/src/asyncext.rs | 45 +++++++++++++++++++++++++++++++++++ keyfork-frame/src/lib.rs | 45 +++++++++++++++++++++++------------ 3 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 keyfork-frame/src/asyncext.rs diff --git a/keyfork-frame/Cargo.toml b/keyfork-frame/Cargo.toml index d09136f..4d63b1f 100644 --- a/keyfork-frame/Cargo.toml +++ b/keyfork-frame/Cargo.toml @@ -5,10 +5,15 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["async"] +async = ["dep:tokio"] + [dependencies] hex = "0.4.3" sha2 = "0.10.7" thiserror = "1.0.47" +tokio = { version = "1.32.0", optional = true, features = ["io-util"] } [dev-dependencies] insta = "1.31.0" diff --git a/keyfork-frame/src/asyncext.rs b/keyfork-frame/src/asyncext.rs new file mode 100644 index 0000000..9800818 --- /dev/null +++ b/keyfork-frame/src/asyncext.rs @@ -0,0 +1,45 @@ +use std::marker::Unpin; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +use super::{hash, verify_checksum, DecodeError, EncodeError}; + +/// Decode a framed message into a `Vec`. +/// +/// # Errors +/// An error may be returned if: +/// * The given `data` does not contain enough data to parse a length, +/// * The given `data` does not contain the given length's worth of data, +/// * The given `data` has a checksum that does not match what we build locally. +pub async fn try_decode_from( + readable: &mut (impl AsyncRead + Unpin), +) -> Result, DecodeError> { + let len = readable.read_u32().await?; + + let mut data = Vec::with_capacity(len as usize); + readable.read_exact(&mut data[..]).await?; + + let content = verify_checksum(&data[..])?; + + // Note: Optimizing this isn't *too* practical, we could probably pop the first 32 bytes off + // the front of the Vec, but it might not even be worth it as opposed to one reallocation. + Ok(content.to_vec()) +} + +/// Encode a &[u8] into a framed message +/// +/// # Errors +/// An error may be returned if: +/// * The given `data` is more than [`u32::MAX`] bytes. This is a constraint on a protocol level. +/// * The resulting data was unable to be written to the given `writable`. +pub async fn try_encode_to( + data: &[u8], + writable: &mut (impl AsyncWrite + Unpin), +) -> Result<(), EncodeError> { + let hash = hash(data); + let len = u32::try_from(hash.len() + data.len()) + .map_err(|_| EncodeError::InputTooLarge(hash.len() + data.len()))?; + writable.write_u32(len).await?; + writable.write_all(&hash[..]).await?; + writable.write_all(data).await?; + Ok(()) +} diff --git a/keyfork-frame/src/lib.rs b/keyfork-frame/src/lib.rs index 22b403b..f83d09b 100644 --- a/keyfork-frame/src/lib.rs +++ b/keyfork-frame/src/lib.rs @@ -12,10 +12,12 @@ //! | checksum: [u8; 32] sha256 hash of `raw_data` | raw_data: &[u8] | //! ``` +#[cfg(feature = "async")] +pub mod asyncext; use sha2::{Digest, Sha256}; -#[derive(Debug, Clone, thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum DecodeError { /// There were not enough bytes to determine the length of the data slice. #[error("Invalid length: {0}")] @@ -32,18 +34,26 @@ pub enum DecodeError { /// The provided checksum of the data did not match the locally-generated checksum. #[error("Checksum did not match: Their {0} != Our {1}")] BadChecksum(String, String), + + /// Data could not be read from the input source. + #[error("Data could not be read from the input source: {0}")] + Io(#[from] std::io::Error), } -#[derive(Debug, Clone, thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum EncodeError { /// The given input was larger than could be encoded by this protocol. #[error("Input too large to encode: {0}")] InputTooLarge(usize), + + /// Data could not be written to the output sink. + #[error("Data could not be written to the output sink: {0}")] + Io(#[from] std::io::Error), } const LEN_SIZE: usize = std::mem::size_of::(); -fn hash(data: &[u8]) -> Vec { +pub(crate) fn hash(data: &[u8]) -> Vec { let mut hashobj = Sha256::new(); hashobj.update(data); hashobj.finalize().to_vec() @@ -65,6 +75,22 @@ pub fn try_encode(data: &[u8]) -> Result, EncodeError> { Ok(result) } +pub(crate) fn verify_checksum(data: &[u8]) -> Result<&[u8], DecodeError> { + let checksum: &[u8; 32] = &data[..32] + .try_into() + .map_err(DecodeError::InvalidChecksum)?; + + let content = &data[32..]; + let our_checksum = hash(content); + if our_checksum != checksum { + return Err(DecodeError::BadChecksum( + hex::encode(checksum), + hex::encode(our_checksum), + )); + } + Ok(content) +} + /// Decode a framed message into a `Vec`. /// /// # Errors @@ -83,18 +109,7 @@ pub fn try_decode(data: &[u8]) -> Result, DecodeError> { } let data = &data[LEN_SIZE..]; - let checksum: &[u8; 32] = &data[..32] - .try_into() - .map_err(DecodeError::InvalidChecksum)?; - - let content = &data[32..]; - let our_checksum = hash(content); - if our_checksum != checksum { - return Err(DecodeError::BadChecksum( - hex::encode(checksum), - hex::encode(our_checksum), - )); - } + let content = verify_checksum(data)?; Ok(content.to_vec()) }