diff --git a/.github/changelog.sh b/.github/changelog.sh new file mode 100755 index 0000000..f21e927 --- /dev/null +++ b/.github/changelog.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +m_branch=m; +changelog_file=CHANGELOG.md; + +# fetch master since we might be in a shallow clone +git fetch origin "$m_branch:$m_branch" --depth=1 + +changed=0; +for log in */"$changelog_file"; do + dir=$(dirname "$log"); + # check if version changed + if git diff "$m_branch" -- "$dir/Cargo.toml" | grep -q "^-version = "; then + # check if changelog updated + if git diff --exit-code --no-patch "$m_branch" -- "$log"; then + echo "$dir version changed, but $log is not updated" + changed=1; + fi + fi +done + +exit "$changed"; diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fb9f034..ce0dc55 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,29 +9,31 @@ env: CARGO_NET_GIT_FETCH_WITH_CLI: true jobs: - build-and-test: + check-and-test: strategy: matrix: - features: ["", "derive", "derive,alloc", "derive,std"] + features: ["", "derive", "derive,alloc", "derive,std", "digest", "digest,std"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 with: cache-on-failure: "true" - - name: Build - run: cargo build -p udigest --features "${{ matrix.features }}" + - name: Check + run: cargo check -p udigest --features "${{ matrix.features }}" - name: Run tests run: cargo test -p udigest --lib --tests --features "${{ matrix.features }}" - doctest: + test-all-features: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 with: cache-on-failure: "true" - - name: Doctests - run: cargo test --doc --all-features + - name: Check + run: cargo check -p udigest --all-features + - name: Run tests + run: cargo test -p udigest --all-features run-examples: runs-on: ubuntu-latest steps: @@ -64,4 +66,10 @@ jobs: with: cache-on-failure: "true" - name: Run clippy - run: cargo clippy -- -D clippy::all + run: cargo clippy --all-features -- -D clippy::all + check-changelog: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check changelogs + run: ./.github/changelog.sh diff --git a/Cargo.lock b/Cargo.lock index 2524a71..e959c99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -44,6 +53,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -62,6 +72,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.149" @@ -97,6 +116,22 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "2.0.38" @@ -116,11 +151,13 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "udigest" -version = "0.1.0" +version = "0.2.0-rc1" dependencies = [ + "blake2", "digest", "hex", "sha2", + "sha3", "udigest-derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/Makefile b/Makefile index c0cdc25..6515e91 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,14 @@ +.PHONY: docs docs-open + +docs: + RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --no-deps --all-features + +docs-open: + RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --no-deps --all-features --open + +docs-private: + RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --no-deps --all-features --document-private-items + readme: cargo readme -i src/lib.rs -r udigest/ -t ../docs/readme.tpl \ | perl -ne 's/\[(.+?)\]\((?!https).+?\)/\1/g; print;' \ diff --git a/README.md b/README.md index 60451a0..2966a7f 100644 --- a/README.md +++ b/README.md @@ -17,34 +17,34 @@ data can be anything that implements `Digestable` trait: The trait is intentionally not implemented for certain types: -* `HashMap`, `HashSet` as they can not be traversed in determenistic order -* `usize`, `isize` as their byte size varies on differnet platforms +* `HashMap`, `HashSet` as they can not be traversed in deterministic order +* `usize`, `isize` as their byte size varies on different platforms The `Digestable` trait can be implemented for the struct using a macro: ```rust -use udigest::{Tag, udigest}; -use sha2::Sha256; - #[derive(udigest::Digestable)] struct Person { name: String, job_title: String, } -let alice = &Person { +let alice = Person { name: "Alice".into(), job_title: "cryptographer".into(), }; -let tag = Tag::::new("udigest.example"); -let hash = udigest(tag, &alice); +let hash = udigest::hash::(&alice); ``` The crate intentionally does not try to follow any existing standards for unambiguous -encoding. The format for encoding was desingned specifically for `udigest` to provide +encoding. The format for encoding was designed specifically for `udigest` to provide a better usage experience in Rust. The details of encoding format can be found in `encoding` module. ### Features +* `digest` enables support of hash functions that implement `digest` traits \ + If feature is not enabled, the crate is still usable via `Digestable` trait that + generically implements unambiguous encoding +* `inline-struct` is required to use `inline_struct!` macro * `std` implements `Digestable` trait for types in standard library * `alloc` implements `Digestable` trait for type in `alloc` crate * `derive` enables `Digestable` proc macro diff --git a/cspell.yml b/cspell.yml new file mode 100644 index 0000000..139373c --- /dev/null +++ b/cspell.yml @@ -0,0 +1,11 @@ +words: +- digestable +- udigest +- VecDeque +- bytestring +- bytestrings +- biglen +- sublist +- docsrs +- concated +- inlines diff --git a/udigest/CHANGELOG.md b/udigest/CHANGELOG.md new file mode 100644 index 0000000..c05564e --- /dev/null +++ b/udigest/CHANGELOG.md @@ -0,0 +1,14 @@ +## v0.2.0 +* Breaking change: remove `udigest::Tag` [#4] +* Breaking change: rename `udigest::udigest` function to `udigest::hash` [#4] +* Add support of all hash functions compatible with `digest` crate: + hash functions with fixed output, with extendable output, and with + variable output [#4] +* Add `udigest::inline_struct!` macro [#4] +* fix: handle cases when `EncodeValue` is dropped without being used + +[#4]: https://github.com/dfns/udigest/pull/4 + +## v0.1.0 + +The first release! diff --git a/udigest/Cargo.toml b/udigest/Cargo.toml index 3360bb3..0737865 100644 --- a/udigest/Cargo.toml +++ b/udigest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "udigest" -version = "0.1.0" +version = "0.2.0-rc1" edition = "2021" license = "MIT OR Apache-2.0" description = "Unambiguously digest structured data" @@ -11,27 +11,44 @@ readme = "../README.md" [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--cfg", "docsrs"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -digest = { version = "0.10", default-features = false } +digest = { version = "0.10", default-features = false, optional = true } udigest-derive = { version = "0.1", optional = true } [dev-dependencies] -sha2 = "0.10" hex = "0.4" +sha2 = "0.10" +sha3 = "0.10" +blake2 = "0.10" + [features] +default = ["digest", "std", "inline-struct"] + std = ["alloc"] alloc = [] derive = ["dep:udigest-derive"] +digest = ["dep:digest"] +inline-struct = [] + [[test]] name = "derive" -required-features = ["std", "derive"] +required-features = ["std", "derive", "digest"] + +[[test]] +name = "deterministic_hash" +required-features = ["derive", "digest"] + +[[test]] +name = "inline_struct" +required-features = ["derive", "inline-struct"] [[example]] name = "derivation" -required-features = ["std", "derive"] \ No newline at end of file +required-features = ["std", "derive", "digest"] diff --git a/udigest/examples/derivation.rs b/udigest/examples/derivation.rs index f5b4fe2..2f2c18c 100644 --- a/udigest/examples/derivation.rs +++ b/udigest/examples/derivation.rs @@ -1,6 +1,3 @@ -use sha2::Sha256; -use udigest::udigest; - #[derive(udigest::Digestable)] #[udigest(tag = "udigest.example.Person.v1")] struct Person { @@ -15,7 +12,6 @@ fn main() { job_title: "cryptographer".into(), }; - let tag = udigest::Tag::::new("udigest.example"); - let hash = udigest(tag, &person); + let hash = udigest::hash::(&person); println!("{}", hex::encode(hash)); } diff --git a/udigest/src/encoding.rs b/udigest/src/encoding.rs index 9bf6445..9403622 100644 --- a/udigest/src/encoding.rs +++ b/udigest/src/encoding.rs @@ -3,7 +3,7 @@ //! The core of the crate is functionality to unambiguously encode any structured data //! into bytes. It's then used to digest any data into a hash. Note that this module //! provides low-level implementation details which you normally don't need to know -//! unless you manually implemenet [Digestable](crate::Digestable) trait. +//! unless you manually implement [Digestable](crate::Digestable) trait. //! //! Any structured `value` is encoded either as a bytestring or as a list (each element //! within the list is either a bytestring or a list). The simplified grammar can be seen as @@ -18,7 +18,7 @@ //! Encoding goal is to distinguish `["12", "3"]` from `["1", "23"]`, `["1", [], "2"]` //! from `["1", "2"]` and so on. Now, we only need to map any structured data onto //! that grammar to have an unambiguous encoding. Below, we will show how Rust structures -//! can be mapped onto the lists, and then we descrive how exactly encoding works. +//! can be mapped onto the lists, and then we describe how exactly encoding works. //! //! # Mapping Rust types onto lists //! @@ -188,9 +188,25 @@ pub trait Buffer { fn write(&mut self, bytes: &[u8]); } -impl Buffer for D { +/// Wraps [`digest::Digest`] and implements [`Buffer`] +#[cfg(feature = "digest")] +pub struct BufferDigest(pub D); + +#[cfg(feature = "digest")] +impl Buffer for BufferDigest { + fn write(&mut self, bytes: &[u8]) { + self.0.update(bytes) + } +} + +/// Wraps [`digest::Update`] and implements [`Buffer`] +#[cfg(feature = "digest")] +pub struct BufferUpdate(pub D); + +#[cfg(feature = "digest")] +impl Buffer for BufferUpdate { fn write(&mut self, bytes: &[u8]) { - self.update(bytes) + self.0.update(bytes) } } @@ -199,23 +215,27 @@ impl Buffer for D { /// Can be used to encode (only) a single value. Value can be a leaf (bytestring) or a list of values. #[must_use = "encoder must be used to encode a value"] pub struct EncodeValue<'b, B: Buffer> { - buffer: &'b mut B, + buffer: Option<&'b mut B>, } impl<'b, B: Buffer> EncodeValue<'b, B> { /// Constructs an encoder pub fn new(buffer: &'b mut B) -> Self { - Self { buffer } + Self { + buffer: Some(buffer), + } } /// Encodes a list - pub fn encode_list(self) -> EncodeList<'b, B> { - EncodeList::new(self.buffer) + pub fn encode_list(mut self) -> EncodeList<'b, B> { + #[allow(clippy::expect_used)] + EncodeList::new(self.buffer.take().expect("buffer must be available")) } /// Encodes a leaf (bytestring) - pub fn encode_leaf(self) -> EncodeLeaf<'b, B> { - EncodeLeaf::new(self.buffer) + pub fn encode_leaf(mut self) -> EncodeLeaf<'b, B> { + #[allow(clippy::expect_used)] + EncodeLeaf::new(self.buffer.take().expect("buffer must be available")) } /// Encodes a leaf value @@ -228,15 +248,26 @@ impl<'b, B: Buffer> EncodeValue<'b, B> { /// Encodes a struct /// /// Struct is represented as a list: `[field_name1, field_value1, ...]` - pub fn encode_struct(self) -> EncodeStruct<'b, B> { - EncodeStruct::new(self.buffer) + pub fn encode_struct(mut self) -> EncodeStruct<'b, B> { + #[allow(clippy::expect_used)] + EncodeStruct::new(self.buffer.take().expect("buffer must be available")) } /// Encodes an enum /// /// Enum is represented as a list: `["variant", variant_name, field_name1, field_value1, ...]` - pub fn encode_enum(self) -> EncodeEnum<'b, B> { - EncodeEnum::new(self.buffer) + pub fn encode_enum(mut self) -> EncodeEnum<'b, B> { + #[allow(clippy::expect_used)] + EncodeEnum::new(self.buffer.take().expect("buffer must be available")) + } +} + +impl<'b, B: Buffer> Drop for EncodeValue<'b, B> { + fn drop(&mut self) { + if let Some(buffer) = &mut self.buffer { + // buffer is not consumed -- we write an empty leaf + EncodeLeaf::new(*buffer).finish() + } } } @@ -312,15 +343,15 @@ impl<'b, B: Buffer> EncodeStruct<'b, B> { self } - /// Adds a fiels to the structure + /// Adds a fields to the structure /// - /// Returns an encoder that shall be used to encode the fiels value + /// Returns an encoder that shall be used to encode the fields value pub fn add_field(&mut self, field_name: impl AsRef<[u8]>) -> EncodeValue { self.list.add_leaf().chain(field_name); self.list.add_item() } - /// Finilizes the encoding, puts the necessary metadata to the buffer + /// Finalizes the encoding, puts the necessary metadata to the buffer /// /// It's an alias to dropping the encoder pub fn finish(self) {} @@ -369,15 +400,20 @@ impl<'b, B: Buffer> EncodeLeaf<'b, B> { /// Appends a bytestring /// /// Encoded value will correspond to concatenation of all the chained bytestrings + /// + /// ## Panic + /// Panics if total length of the leaf overflows `usize` + #[allow(clippy::expect_used)] pub fn update(&mut self, bytes: &[u8]) { self.buffer.write(bytes); + self.len = self .len .checked_add(bytes.len()) .expect("leaf length overflows `usize`") } - /// Finilizes the encoding, puts the necessary metadata to the buffer + /// Finalizes the encoding, puts the necessary metadata to the buffer /// /// It's an alias to dropping the encoder pub fn finish(self) {} @@ -433,6 +469,10 @@ impl<'b, B: Buffer> EncodeList<'b, B> { /// Adds an item to the list /// /// Returns an encoder that shall be used to encode a value of the item + /// + /// ## Panic + /// Panics if list length overflows `usize` + #[allow(clippy::expect_used)] pub fn add_item(&mut self) -> EncodeValue { self.len = self.len.checked_add(1).expect("list len overflows usize"); EncodeValue::new(self.buffer) @@ -452,7 +492,7 @@ impl<'b, B: Buffer> EncodeList<'b, B> { self.add_item().encode_list() } - /// Finilizes the encoding, puts the necessary metadata to the buffer + /// Finalizes the encoding, puts the necessary metadata to the buffer /// /// It's an alias to dropping the encoder pub fn finish(self) {} @@ -475,7 +515,7 @@ impl<'b, B: Buffer> Drop for EncodeList<'b, B> { /// Encodes length of list or leaf /// -/// Altough we expose how the length is encoded, normally you should use [EncodeList] +/// Although we expose how the length is encoded, normally you should use [EncodeList] /// and [EncodeLeaf] which use this function internally pub fn encode_len(buffer: &mut impl Buffer, len: usize) { match u32::try_from(len) { @@ -487,7 +527,11 @@ pub fn encode_len(buffer: &mut impl Buffer, len: usize) { let len = len.to_be_bytes(); let leading_zeroes = len.iter().take_while(|b| **b == 0).count(); let len = &len[leading_zeroes..]; - let len_of_len = u8::try_from(len.len()).expect("usize is more than 256 bytes long"); + + #[allow(clippy::expect_used)] + let len_of_len = u8::try_from(len.len()) + .expect("it's impossible that usize is more than 256 bytes long"); + buffer.write(len); buffer.write(&[len_of_len]); buffer.write(&[BIGLEN]); diff --git a/udigest/src/inline_struct.rs b/udigest/src/inline_struct.rs new file mode 100644 index 0000000..ff2869b --- /dev/null +++ b/udigest/src/inline_struct.rs @@ -0,0 +1,196 @@ +//! Digestable inline structs +//! +//! If you find yourself in situation in which you have to define a struct just +//! to use it only once for `udigest` hashing, [`inline_struct!`] macro can be +//! used instead: +//! +//! ```rust +//! let hash = udigest::hash::(&udigest::inline_struct!({ +//! name: "Alice", +//! age: 24_u32, +//! })); +//! ``` +//! +//! Which will produce identical hash as below: +//! +//! ```rust +//! #[derive(udigest::Digestable)] +//! struct Person { +//! name: &'static str, +//! age: u32, +//! } +//! +//! let hash = udigest::hash::(&Person { +//! name: "Alice", +//! age: 24, +//! }); +//! ``` +//! +//! See [`inline_struct!`] macro for more examples. + +/// Inline structure +/// +/// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. +pub struct InlineStruct<'a, F: FieldsList + 'a = Nil> { + fields_list: F, + tag: Option<&'a [u8]>, +} + +impl InlineStruct<'static> { + /// Creates inline struct with no fields + /// + /// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + fields_list: Nil, + tag: None, + } + } +} + +impl<'a, F: FieldsList + 'a> InlineStruct<'a, F> { + /// Adds field to the struct + /// + /// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. + pub fn add_field( + self, + field_name: &'a str, + field_value: &'a V, + ) -> InlineStruct<'a, impl FieldsList + 'a> + where + F: 'a, + V: crate::Digestable, + { + InlineStruct { + fields_list: cons(field_name, field_value, self.fields_list), + tag: self.tag, + } + } + + /// Sets domain-separation tag + /// + /// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. + pub fn set_tag>(mut self, tag: &'a T) -> Self { + self.tag = Some(tag.as_ref()); + self + } +} + +impl<'a, F: FieldsList + 'a> crate::Digestable for InlineStruct<'a, F> { + fn unambiguously_encode(&self, encoder: crate::encoding::EncodeValue) { + let mut struct_encode = encoder.encode_struct(); + if let Some(tag) = self.tag { + struct_encode.set_tag(tag); + } + self.fields_list.encode(&mut struct_encode); + } +} + +/// Creates digestable inline struct +/// +/// Macro creates "inlined" (anonymous) struct instance containing specified fields and their +/// values. The inlined struct implements [`Digestable` trait](crate::Digestable), and therefore +/// can be unambiguously hashed, for instance, using [`udigest::hash`](crate::hash). It helps +/// reducing amount of code when otherwise you'd have to define a separate struct which would +/// only be used one. +/// +/// ## Usage +/// The code snippet below inlines `struct Person { name: &str, age: u32 }`. +/// ```rust +/// let hash = udigest::hash::(&udigest::inline_struct!({ +/// name: "Alice", +/// age: 24_u32, +/// })); +/// ``` +/// +/// You may add a domain separation tag: +/// ```rust +/// let hash = udigest::hash::( +/// &udigest::inline_struct!("some tag" { +/// name: "Alice", +/// age: 24_u32, +/// }) +/// ); +/// ``` +/// +/// Several structs may be embedded in each other: +/// ```rust +/// let hash = udigest::hash::(&udigest::inline_struct!({ +/// name: "Alice", +/// age: 24_u32, +/// preferences: udigest::inline_struct!({ +/// display_email: false, +/// receive_newsletter: false, +/// }), +/// })); +/// ``` +#[macro_export] +macro_rules! inline_struct { + ({$($field_name:ident: $field_value:expr),*$(,)?}) => {{ + $crate::inline_struct::InlineStruct::new() + $(.add_field(stringify!($field_name), &$field_value))* + }}; + ($tag:tt {$($field_name:ident: $field_value:expr),*$(,)?}) => {{ + $crate::inline_struct::InlineStruct::new() + .set_tag($tag) + $(.add_field(stringify!($field_name), &$field_value))* + + }}; +} + +pub use crate::inline_struct; + +mod sealed { + pub trait Sealed {} +} + +/// List of fields in inline struct +/// +/// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. +pub trait FieldsList: sealed::Sealed { + /// Encodes all fields in order from the first to last + fn encode(&self, encoder: &mut crate::encoding::EncodeStruct); +} + +/// Empty list of fields +/// +/// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. +pub struct Nil; +impl sealed::Sealed for Nil {} +impl FieldsList for Nil { + fn encode(&self, _encoder: &mut crate::encoding::EncodeStruct) { + // Empty list - do nothing + } +} + +fn cons<'a, V, T>(field_name: &'a str, field_value: &'a V, tail: T) -> impl FieldsList + 'a +where + V: crate::Digestable, + T: FieldsList + 'a, +{ + struct Cons<'a, V, T: 'a> { + field_name: &'a str, + field_value: &'a V, + tail: T, + } + + impl<'a, V, T: 'a> sealed::Sealed for Cons<'a, V, T> {} + + impl<'a, V: crate::Digestable, T: FieldsList + 'a> FieldsList for Cons<'a, V, T> { + fn encode(&self, encoder: &mut crate::encoding::EncodeStruct) { + // Since we store fields from last to first, we need to encode the tail first + // to reverse order of fields + self.tail.encode(encoder); + + let value_encoder = encoder.add_field(self.field_name); + self.field_value.unambiguously_encode(value_encoder); + } + } + + Cons { + field_name, + field_value, + tail, + } +} diff --git a/udigest/src/lib.rs b/udigest/src/lib.rs index 38ff8dc..b315962 100644 --- a/udigest/src/lib.rs +++ b/udigest/src/lib.rs @@ -17,40 +17,43 @@ //! //! The trait is intentionally not implemented for certain types: //! -//! * `HashMap`, `HashSet` as they can not be traversed in determenistic order -//! * `usize`, `isize` as their byte size varies on differnet platforms +//! * `HashMap`, `HashSet` as they can not be traversed in deterministic order +//! * `usize`, `isize` as their byte size varies on different platforms //! //! The `Digestable` trait can be implemented for the struct using [a macro](derive@Digestable): //! ```rust -//! use udigest::{Tag, udigest}; -//! use sha2::Sha256; -//! //! #[derive(udigest::Digestable)] //! struct Person { //! name: String, //! job_title: String, //! } -//! let alice = &Person { +//! let alice = Person { //! name: "Alice".into(), //! job_title: "cryptographer".into(), //! }; //! -//! let tag = Tag::::new("udigest.example"); -//! let hash = udigest(tag, &alice); +//! let hash = udigest::hash::(&alice); //! ``` //! //! The crate intentionally does not try to follow any existing standards for unambiguous -//! encoding. The format for encoding was desingned specifically for `udigest` to provide +//! encoding. The format for encoding was designed specifically for `udigest` to provide //! a better usage experience in Rust. The details of encoding format can be found in //! [`encoding` module](encoding). //! //! ## Features +//! * `digest` enables support of hash functions that implement [`digest`] traits \ +//! If feature is not enabled, the crate is still usable via [`Digestable`] trait that +//! generically implements unambiguous encoding +//! * `inline-struct` is required to use [`inline_struct!`] macro //! * `std` implements `Digestable` trait for types in standard library //! * `alloc` implements `Digestable` trait for type in `alloc` crate //! * `derive` enables `Digestable` proc macro #![no_std] #![forbid(missing_docs)] +#![cfg_attr(not(test), forbid(unused_crate_dependencies))] +#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #[cfg(feature = "alloc")] extern crate alloc; @@ -80,12 +83,9 @@ pub use encoding::Buffer; /// attribute. /// * Fields are hashed exactly in the order in which they are defined, so changing /// the fields order will change the hashing -/// * Hashing differnet types, generally, may result into the same hash if they have +/// * Hashing different types, generally, may result into the same hash if they have /// the same byte encoding. For instance: /// ```rust -/// use udigest::{udigest, Tag}; -/// use sha2::Sha256; -/// /// #[derive(udigest::Digestable, Debug)] /// struct PersonA { name: String } /// #[derive(udigest::Digestable, Debug)] @@ -94,15 +94,14 @@ pub use encoding::Buffer; /// let person_a = PersonA{ name: "Alice".into() }; /// let person_b = PersonB{ name: b"Alice".to_vec() }; /// -/// let tag = Tag::new("udigest.example"); /// assert_eq!( -/// udigest::(tag.clone(), &person_a), -/// udigest::(tag, &person_b), +/// udigest::hash::(&person_a), +/// udigest::hash::(&person_b), /// ) /// ``` /// `person_a` and `person_b` have exactly the same hash as they have the same bytes -/// representation. For that reason, make sure that the [Tag] is unique per application. -/// You may also specify a tag per data type using `#[udigest(tag = "...")]` attribute. +/// representation. If you need to distinguish them, you can specify a domain-separation +/// tag using `#[udigest(tag = "...")]` attribute. /// /// ### Container attributes /// * `#[udigest(tag = "...")]` \ @@ -111,7 +110,7 @@ pub use encoding::Buffer; /// tag may include a version to distinguish hashes of the same structures across different versions. /// * `#[udigest(bound = "...")]` \ /// Specifies which generic bounds to use. By default, `udigest` will generate `T: Digestable` bound per -/// each generic `T`. This behavior can be overriden via this attribute. Example: +/// each generic `T`. This behavior can be overridden via this attribute. Example: /// ```rust /// #[derive(udigest::Digestable)] /// #[udigest(bound = "")] @@ -203,78 +202,91 @@ pub use encoding::Buffer; pub use udigest_derive::Digestable; pub mod encoding; +#[cfg(feature = "inline-struct")] +pub mod inline_struct; + +/// Digests a structured `value` using fixed-output hash function (like sha2-256) +#[cfg(feature = "digest")] +pub fn hash(value: &impl Digestable) -> digest::Output { + let mut hash = encoding::BufferDigest(D::new()); + value.unambiguously_encode(encoding::EncodeValue::new(&mut hash)); + hash.0.finalize() +} -/// Domain separation tag (DST) -/// -/// The tag is used to distinguish different applications and provide better hygiene. -/// Having different tags will result into different hashes even if the value being -/// hashed is the same. -/// -/// Tag can be constructed from a bytestring (using constructor [`new`](Self::new)), -/// or from any structured data (using constructor [`new_structured`](Self::new_structured)). -#[derive(Clone)] -pub struct Tag(D); - -impl Tag { - /// Constructs a new tag from a bytestring - /// - /// If the tag is represented by a structured data, [`Tag::new_structured`] - /// constructor can be used instead. - pub fn new(tag: impl AsRef<[u8]>) -> Self { - Self::new_structured(Bytes(tag)) - } - - /// Constructs a new tag from a structured data - pub fn new_structured(tag: impl Digestable) -> Self { - Self::with_digest_and_structured_tag(D::new(), tag) - } - - /// Constructs a new tag - /// - /// Similar to [`Tag::new_structured`] but takes also a digest to use - pub fn with_digest_and_structured_tag(mut hash: D, tag: impl Digestable) -> Self { - let mut header = encoding::EncodeStruct::new(&mut hash).with_tag(b"udigest.header"); - header.add_field("udigest_version").encode_leaf().chain("1"); - let tag_encoder = header.add_field("tag"); - tag.unambiguously_encode(tag_encoder); - header.finish(); - - Self(hash) +/// Digests a list of structured data using fixed-output hash function (like sha2-256) +#[cfg(feature = "digest")] +pub fn hash_iter( + iter: impl IntoIterator, +) -> digest::Output { + let mut hash = encoding::BufferDigest(D::new()); + let mut encoder = encoding::EncodeList::new(&mut hash).with_tag(b"udigest.list"); + for value in iter { + let item_encoder = encoder.add_item(); + value.unambiguously_encode(item_encoder); } + encoder.finish(); + hash.0.finalize() +} - /// Digests a structured `value` - /// - /// Alias to [`udigest`] in root of the crate - pub fn digest(self, value: impl Digestable) -> digest::Output { - udigest(self, value) - } +/// Digests a structured `value` using extendable-output hash function (like shake-256) +#[cfg(feature = "digest")] +pub fn hash_xof(value: &impl Digestable) -> D::Reader +where + D: Default + digest::Update + digest::ExtendableOutput, +{ + let mut hash = encoding::BufferUpdate(D::default()); + value.unambiguously_encode(encoding::EncodeValue::new(&mut hash)); + hash.0.finalize_xof() +} - /// Digests a list of structured data - /// - /// Alias to [`udigest_iter`] in root of the crate - pub fn digest_iter(self, iter: impl IntoIterator) -> digest::Output { - udigest_iter(self, iter) +/// Digests a list of structured data using extendable-output hash function (like shake-256) +#[cfg(feature = "digest")] +pub fn hash_xof_iter(iter: impl IntoIterator) -> D::Reader +where + D: Default + digest::Update + digest::ExtendableOutput, +{ + let mut hash = encoding::BufferUpdate(D::default()); + let mut encoder = encoding::EncodeList::new(&mut hash).with_tag(b"udigest.list"); + for value in iter { + let item_encoder = encoder.add_item(); + value.unambiguously_encode(item_encoder); } + encoder.finish(); + hash.0.finalize_xof() } -/// Digests a structured `value` -pub fn udigest(mut tag: Tag, value: impl Digestable) -> digest::Output { - value.unambiguously_encode(encoding::EncodeValue::new(&mut tag.0)); - tag.0.finalize() +/// Digests a structured `value` using variable-output hash function (like blake2b) +#[cfg(feature = "digest")] +pub fn hash_vof(value: &impl Digestable, out: &mut [u8]) -> Result<(), digest::InvalidOutputSize> +where + D: digest::VariableOutput + digest::Update, +{ + let mut hash = encoding::BufferUpdate(D::new(out.len())?); + value.unambiguously_encode(encoding::EncodeValue::new(&mut hash)); + hash.0 + .finalize_variable(out) + .map_err(|_| digest::InvalidOutputSize) } -/// Digests a list of structured data -pub fn udigest_iter( - mut tag: Tag, +/// Digests a list of structured data using variable-output hash function (like blake2b) +#[cfg(feature = "digest")] +pub fn hash_vof_iter( iter: impl IntoIterator, -) -> digest::Output { - let mut encoder = encoding::EncodeList::new(&mut tag.0).with_tag(b"udigest.list"); + out: &mut [u8], +) -> Result<(), digest::InvalidOutputSize> +where + D: digest::VariableOutput + digest::Update, +{ + let mut hash = encoding::BufferUpdate(D::new(out.len())?); + let mut encoder = encoding::EncodeList::new(&mut hash).with_tag(b"udigest.list"); for value in iter { let item_encoder = encoder.add_item(); value.unambiguously_encode(item_encoder); } encoder.finish(); - tag.0.finalize() + hash.0 + .finalize_variable(out) + .map_err(|_| digest::InvalidOutputSize) } /// A value that can be unambiguously digested @@ -312,6 +324,12 @@ macro_rules! digestable_integers { digestable_integers!(i8, u8, i16, u16, i32, u32, i64, u64, i128, u128); +impl Digestable for bool { + fn unambiguously_encode(&self, encoder: encoding::EncodeValue) { + u8::from(*self).unambiguously_encode(encoder) + } +} + impl Digestable for char { fn unambiguously_encode(&self, encoder: encoding::EncodeValue) { // Any char can be represented using two bytes, but strangely Rust does not provide diff --git a/udigest/tests/derive.rs b/udigest/tests/derive.rs index bd7d776..66760e8 100644 --- a/udigest/tests/derive.rs +++ b/udigest/tests/derive.rs @@ -38,9 +38,9 @@ pub enum EnumExample { something_else: SomeValue, }, Variant2(String, #[udigest(as_bytes)] Vec, #[udigest(skip)] Empty), - Vartiant3 {}, + Variant3 {}, Variant4(), - Vartiant5, + Variant5, } #[derive(udigest::Digestable)] diff --git a/udigest/tests/deterministic_hash.rs b/udigest/tests/deterministic_hash.rs new file mode 100644 index 0000000..f2184ce --- /dev/null +++ b/udigest/tests/deterministic_hash.rs @@ -0,0 +1,78 @@ +#[derive(udigest::Digestable)] +pub struct Person { + name: &'static str, + age: u16, + job_title: &'static str, +} + +const ALICE: Person = Person { + name: "Alice", + age: 24, + job_title: "cryptographer", +}; + +const BOB: Person = Person { + name: "Bob", + age: 25, + job_title: "research engineer", +}; + +#[test] +fn sha2_256() { + let alice_hash = udigest::hash::(&ALICE); + assert_eq!( + hex::encode(alice_hash.as_slice()), + "99e258d6a6ccc430a50dcbf4e9c8cfb59ad0b94b96b83f0182a9a68eb1c5438f", + ); + + let bob_hash = udigest::hash::(&BOB); + assert_eq!( + hex::encode(bob_hash.as_slice()), + "28474b5dec79b222b74badc2d78f9f81c0fbfd1ee04a134947cd07f44237ade3", + ); +} + +#[test] +fn shake256() { + use digest::XofReader; + + let mut hash = [0u8; 123]; + let mut alice_hash_reader = udigest::hash_xof::(&ALICE); + alice_hash_reader.read(&mut hash); + assert_eq!( + hex::encode(&hash), + "54809cf7b06438f9508785fb5e46bdfd7714b39b026e86fa7cc8a8442ae10bd5\ + 49baeced19ff0642b042ae4e92636536baec5748dad99e71fc53a4361734973ae\ + 2c4f1547305a76addd5b6076509ddbf91bd5beb71ba09598e265704d1e9a1c0c3\ + 5fae7f8e4958ceb38962fc8e6fc56e32bef4e88f64bc8a88f88a" + ); + + let mut bob_hash_reader = udigest::hash_xof::(&BOB); + bob_hash_reader.read(&mut hash); + assert_eq!( + hex::encode(&hash), + "f68ca9eeb7e09657fc54a5cbbd50acdd6d9fccd29ec1a3eb460b673ea59d64a9\ + b2ec8be97c7d7858ad6724cf8c27299569bd72193c77bb339883214a4477c0762\ + f9cf31a2d698562f57dff5ede03d6928feba694975445e7dabe3d67e67b710f26\ + 11f4f14471917bd447d199c32eb93dbcaf1fdbefe05132911991" + ); +} + +#[test] +fn blake2b() { + let mut out = [0u8; 63]; + + udigest::hash_vof::(&ALICE, &mut out).unwrap(); + assert_eq!( + hex::encode(&out), + "91d1ce144fd46ed5400895c8db5f2b39c95870020c6627af034a9fa09c2f2cc3\ + f4c8c7d4e8d38ff16e4f54360b4387c0439cf30c51c21c78f904cda9205023" + ); + + udigest::hash_vof::(&BOB, &mut out).unwrap(); + assert_eq!( + hex::encode(&out), + "2f916c687c82c0f37d31df061c0453e98d0655e1877d4a55ec1507514822a2c4\ + b7cac3ca66a5e3deb678f915210e93f2fc14591b987f121083623ab024ece4" + ); +} diff --git a/udigest/tests/inline_struct.rs b/udigest/tests/inline_struct.rs new file mode 100644 index 0000000..ad84b4e --- /dev/null +++ b/udigest/tests/inline_struct.rs @@ -0,0 +1,77 @@ +#[test] +fn no_tag() { + #[derive(udigest::Digestable)] + struct Person { + name: &'static str, + age: u32, + } + + let hash_expected = udigest::hash::(&Person { + name: "Alice", + age: 24, + }); + + let hash_actual = udigest::hash::(&udigest::inline_struct!({ + name: "Alice", + age: 24_u32, + })); + + assert_eq!(hex::encode(hash_expected), hex::encode(hash_actual)); +} + +#[test] +fn with_tag() { + #[derive(udigest::Digestable)] + #[udigest(tag = "some_tag")] + struct Person { + name: &'static str, + age: u32, + } + + let hash_expected = udigest::hash::(&Person { + name: "Alice", + age: 24, + }); + + let hash_actual = udigest::hash::(&udigest::inline_struct!("some_tag" { + name: "Alice", + age: 24_u32, + })); + + assert_eq!(hex::encode(hash_expected), hex::encode(hash_actual)); +} + +#[test] +fn embedded_structs() { + #[derive(udigest::Digestable)] + struct Person { + name: &'static str, + age: u32, + preferences: Preferences, + } + #[derive(udigest::Digestable)] + struct Preferences { + display_email: bool, + receive_newsletter: bool, + } + + let hash_expected = udigest::hash::(&Person { + name: "Alice", + age: 24, + preferences: Preferences { + display_email: false, + receive_newsletter: false, + }, + }); + + let hash_actual = udigest::hash::(&udigest::inline_struct!({ + name: "Alice", + age: 24_u32, + preferences: udigest::inline_struct!({ + display_email: false, + receive_newsletter: false, + }) + })); + + assert_eq!(hex::encode(hash_expected), hex::encode(hash_actual)); +}