Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.0.0] - 2026-10-13

### Added

- One feature per hash family: `blake2`, `blake3`, `md5`, `ripemd`, `sha1`, `sha2`, `sha3`, and `shake`. Each one pulls in only its hash crate (plus `digest`, and `typenum` for `blake2`). All of them are in the default feature set, so a default build behaves as 2.x did.
- With `default-features = false` the crate depends on no hash crate. It still provides the `Multihash` type, its binary and multibase encoding, `HASH_CODECS` and the other codec lists, the digest length checks, and `Builder::with_hash` for digests computed elsewhere.
- `impl From<&Multihash> for Vec<u8>` encodes without cloning. The owned `From<Multihash>` now delegates to it, and binary serde serialization uses it instead of cloning the multihash.

### Changed

- **Breaking:** a build with `default-features = false` no longer computes digests. In 2.x that setting only dropped `serde`. Streaming data with `Builder::update` into a codec whose family feature is off makes `try_build` and `try_build_encoded` return `Error::UnsupportedHash`, where 2.x returned the digest. Add the hash family features you use to keep 2.x behavior.
- `Error::UnsupportedHash` now also covers a codec whose hash family feature is disabled, and its message says so.
- The `serde` feature now enables the optional `serde` dependency through `dep:serde`.
- The benchmark declares `required-features`, so `cargo bench` skips it when a hash family it uses is off.
- Binary serde deserialization decodes straight from the input bytes instead of copying them into an intermediate buffer first.
- Removed three redundant `clone()` calls in the XOF prefix tests that failed `cargo clippy --all-targets -- -D warnings`.

### Dependencies

- `blake2`, `blake3`, `digest`, `md-5`, `ripemd`, `sha1`, `sha2`, `sha3`, `shake`, and `typenum` are now optional.

## [2.0.0] - 2026-10-07

### Added
Expand Down
48 changes: 35 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "multi-hash"
version = "2.0.1"
version = "3.0.0"
edition = "2024"
rust-version = "1.99"
authors = ["Dave Grantham <dwg@linuxprogrammer.org>"]
Expand All @@ -12,30 +12,51 @@ keywords = ["multihash", "multiformats", "hash", "crypto"]
categories = ["cryptography", "encoding"]

[features]
default = ["serde"]
default = [
"blake2",
"blake3",
"md5",
"ripemd",
"serde",
"sha1",
"sha2",
"sha3",
"shake",
]

# each hash family is its own feature; with none enabled the crate still
# provides the Multihash type, its encoding, and Builder::with_hash
blake2 = ["dep:blake2", "dep:digest", "dep:typenum"]
blake3 = ["dep:blake3"]
# expose the FIPS approved codec constants, FIPS_CODECS and SAFE_FIPS_CODECS
fips = []
md5 = ["dep:digest", "dep:md-5"]
ripemd = ["dep:digest", "dep:ripemd"]
serde = ["dep:serde"]
sha1 = ["dep:digest", "dep:sha1"]
sha2 = ["dep:digest", "dep:sha2"]
sha3 = ["dep:digest", "dep:sha3"]
shake = ["dep:digest", "dep:shake"]

[dependencies]
blake2 = "0.11.0"
blake3 = { version = "1.8", features = ["traits-preview", "zeroize"] }
blake2 = { version = "0.11.0", optional = true }
blake3 = { version = "1.8", features = ["traits-preview", "zeroize"], optional = true }
digest = { version = "0.11", features = ["alloc"], optional = true }
hex = "0.4"
md-5 = { version = "0.11", optional = true }
multi-base = "1.1"
multi-codec = "1.6"
multi-trait = "1.1"
multi-util = "1.2"
digest = { version = "0.11", features = ["alloc"] }
hex = "0.4"
md-5 = "0.11"
ripemd = "0.2"
ripemd = { version = "0.2", optional = true }
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true }
sha1 = "0.11"
sha2 = "0.11"
sha3 = "0.12"
shake = "0.1"
sha1 = { version = "0.11", optional = true }
sha2 = { version = "0.11", optional = true }
sha3 = { version = "0.12", optional = true }
shake = { version = "0.1", optional = true }
subtle = "2"
thiserror = { version = "2.0" }
typenum = "1.20"
typenum = { version = "1.20", optional = true }
unsigned-varint = { version = "0.8", features = ["std"] }

[dev-dependencies]
Expand All @@ -62,3 +83,4 @@ unsafe_code = "deny"
name = "multihash_bench"
harness = false
path = "benches/multihash_bench.rs"
required-features = ["blake3", "md5", "sha2", "sha3", "shake"]
46 changes: 37 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Multihash is a self-describing format. It pairs a hash algorithm identifier (a m

- [Features](#features)
- [Install](#install)
- [Upgrading from 2.x](#upgrading-from-2x)
- [Supported Algorithms](#supported-algorithms)
- [Usage](#usage)
- [Computing a Hash](#computing-a-hash)
Expand All @@ -37,11 +38,11 @@ Multihash is a self-describing format. It pairs a hash algorithm identifier (a m

## Features

- 25 hash algorithms. SHA1, SHA2 family, SHA3 family, Blake2, Blake3, MD5, RIPEMD. `Blake3`, `Shake128`, and `Shake256` are extendable-output functions (XOF) with a caller-chosen digest length.
- 25 hash algorithms. SHA1, SHA2 family, SHA3 family, Blake2, Blake3, MD5, RIPEMD, each family behind its own [feature](#feature-flags). `Blake3`, `Shake128`, and `Shake256` are extendable-output functions (XOF) with a caller-chosen digest length.
- Builder pattern. A fluent API to create multihashes from raw data or existing digests.
- Multibase encoding. The `EncodedMultihash` smart pointer gives a base-encoded string representation via `BaseEncoded` from `multi-util`.
- Serde support. JSON gives the codec name string. Binary gives the varint bytes. The `serde` feature gates it.
- Binary round-trip. `Into<Vec<u8>>` and `TryFrom<&[u8]>` for the raw wire format.
- Binary round-trip. `From<&Multihash>` and `From<Multihash>` for `Vec<u8>`, and `TryFrom<&[u8]>`, for the raw wire format. Encoding by reference does not clone the multihash.
- Type-safe newtypes. `HashDigest` and `AlgorithmId` wrappers.
- Zero unsafe code. `#![deny(unsafe_code)]` is set at the crate root.
- Thread-safe. All types are `Send + Sync`.
Expand All @@ -52,18 +53,28 @@ Add this to your `Cargo.toml`:

```toml
[dependencies]
multi-hash = "2.0"
multi-hash = "3.0"
```

To disable serde support:
To pick only the hash families you need, turn off the defaults and list them (see [Feature Flags](#feature-flags)):

```toml
[dependencies]
multi-hash = { version = "2.0", default-features = false }
multi-hash = { version = "3.0", default-features = false, features = ["sha2", "sha3"] }
```

MSRV: Rust 1.99 (Edition 2024).

## Upgrading from 2.x

3.0 puts each hash family behind its own feature. The default build has every family on, so `multi-hash = "3.0"` behaves as 2.x did.

- If you used `default-features = false` only to drop serde, add back the families you hash with, e.g. `features = ["sha2", "blake3"]`. Without them, `try_build` returns `Error::UnsupportedHash` for a streamed codec, where 2.x returned the digest.
- `Builder::with_hash` and decoding work for every codec in any build, so a crate that only wraps or parses digests can drop all hash crates.
- Encode by reference with `Vec::<u8>::from(&mh)` to avoid cloning. The owned `From<Multihash>` still works.

See [CHANGELOG.md](CHANGELOG.md) for the full list.

## Supported Algorithms

### Secure algorithms (recommended for cryptographic use)
Expand Down Expand Up @@ -193,8 +204,8 @@ let mut builder = Builder::new(Codec::Sha2256).unwrap();
builder.update(b"data");
let mh1 = builder.try_build().unwrap();

// Encode to binary (varint wire format)
let bytes: Vec<u8> = mh1.clone().into();
// Encode to binary (varint wire format); by reference, so mh1 is not cloned
let bytes: Vec<u8> = (&mh1).into();

// Decode from binary
let mh2 = Multihash::try_from(bytes.as_ref()).unwrap();
Expand Down Expand Up @@ -393,21 +404,38 @@ CI collects coverage with `cargo-llvm-cov` and uploads the result to Codecov.

## Feature Flags

Each hash family is its own feature, and all of them are on by default:

| Feature | Codecs | Crate |
|---|---|---|
| `blake2` | `Blake2B224`, `Blake2B256`, `Blake2B384`, `Blake2B512`, `Blake2S224`, `Blake2S256` | `blake2` |
| `blake3` | `Blake3` (XOF) | `blake3` |
| `md5` | `Md5` | `md-5` |
| `ripemd` | `Ripemd128`, `Ripemd160`, `Ripemd256`, `Ripemd320` | `ripemd` |
| `sha1` | `Sha1` | `sha1` |
| `sha2` | `Sha2224`, `Sha2256`, `Sha2384`, `Sha2512`, `Sha2512224`, `Sha2512256` | `sha2` |
| `sha3` | `Sha3224`, `Sha3256`, `Sha3384`, `Sha3512` | `sha3` |
| `shake` | `Shake128`, `Shake256` (XOF) | `shake` |

A family feature is only needed to compute a digest. With `default-features = false` and no family enabled, the crate still provides the `Multihash` type, its binary and multibase encoding, the codec lists and output-length checks, and `Builder::with_hash` for digests computed elsewhere. Streaming data with `Builder::update` into a codec whose family is off makes `try_build` return `Error::UnsupportedHash`.

- `serde` (default). Enables serde serialization and deserialization. When on, `Multihash` implements `Serialize` and `Deserialize`. Human-readable formats give the codec name and the hex digest. Binary formats give the varint bytes.
- `fips` (additive, off by default). Exports `FIPS_CODECS` (13 codecs) and `SAFE_FIPS_CODECS` (8 codecs). They list the NIST FIPS approved hash algorithms. `SAFE_FIPS_CODECS` omits SHA-1 and the 224-bit algorithms that SP 800-131A restricts. `Blake2`, `Blake3`, `Md5`, and the RIPEMD codecs are not FIPS approved. SHA-1 is verification-only under NIST SP 800-131A Rev. 2. The SP 800-131A Rev. 3 draft deprecates SHA-1 and the 224-bit hashes through 2030 and disallows them after 2030.

The `fips` feature composes with the default `serde` feature:

```toml
[dependencies]
multi-hash = { version = "2.0", features = ["fips"] }
multi-hash = { version = "3.0", features = ["fips"] }
```

### Disabling Default Features

This build has no hash crates and no serde; it decodes, encodes, and wraps existing digests only:

```toml
[dependencies]
multi-hash = { version = "2.0", default-features = false }
multi-hash = { version = "3.0", default-features = false }
```

## Security
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The `multi-hash` crate gives self-describing cryptographic hash digests. It foll

## std-only Status

The crate is std-only. The hash path holds each hasher as a concrete type in a private `Hasher` enum. The enum dispatches between the hashers without a trait object. The BLAKE3 arm holds a boxed `blake3::Hasher`. The boxed hasher and the digest buffers need the allocator in `std`. A `no_std` conversion is not planned for this crate.
The crate is std-only. The hash path holds each hasher as a concrete type in a private `Hasher` enum. The enum dispatches between the hashers without a trait object. Each arm exists only when its hash family feature (`blake2`, `blake3`, `md5`, `ripemd`, `sha1`, `sha2`, `sha3`, `shake`) is enabled. The BLAKE3 arm holds a boxed `blake3::Hasher`. The boxed hasher and the digest buffers need the allocator in `std`. A `no_std` conversion is not planned for this crate.

## Security Properties

Expand Down
14 changes: 11 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ pub enum Error {

/// Unsupported hash algorithm
///
/// The specified codec is not a supported cryptographic hash algorithm
/// or is not implemented in this crate.
/// The specified codec is not a supported cryptographic hash algorithm,
/// is not implemented in this crate, or belongs to a hash family whose
/// feature is disabled in this build.
///
/// # Context
///
Expand All @@ -90,7 +91,8 @@ pub enum Error {
#[error(
"Unsupported hash algorithm: {codec:?}\n\
The codec {codec:?} is not a supported cryptographic hash algorithm.\n\
See HASH_CODECS or SAFE_HASH_CODECS for supported algorithms."
See HASH_CODECS or SAFE_HASH_CODECS for supported algorithms;\n\
computing a digest also needs the codec's hash family feature."
)]
UnsupportedHash {
/// The unsupported codec that was requested
Expand Down Expand Up @@ -433,6 +435,7 @@ mod tests {
assert_ne!(context, "");
}

#[cfg(feature = "sha2")]
#[test]
fn test_invalid_digest_length_error() {
let err = Error::invalid_digest_length(Codec::Sha2256, 32, 16);
Expand All @@ -442,13 +445,15 @@ mod tests {
assert!(msg.contains("16"));
}

#[cfg(feature = "sha2")]
#[test]
fn test_hash_compute_failed_error() {
let err = Error::hash_compute_failed(Codec::Sha2256, "test failure");
assert_eq!(err.kind(), "HashComputeFailed");
assert!(err.to_string().contains("test failure"));
}

#[cfg(feature = "shake")]
#[test]
fn test_output_len_required_error() {
let err = Error::output_len_required(Codec::Shake128);
Expand All @@ -459,6 +464,7 @@ mod tests {
assert!(context.contains("Shake128") || context.contains("shake-128"));
}

#[cfg(feature = "shake")]
#[test]
fn test_output_len_invalid_error() {
let err = Error::output_len_invalid(Codec::Shake256, 0, 16 * 1024 * 1024);
Expand All @@ -471,6 +477,7 @@ mod tests {
assert!(context.contains("Shake256") || context.contains("shake-256"));
}

#[cfg(all(feature = "sha2", feature = "shake"))]
#[test]
fn test_error_kind_uniqueness() {
let errors = [
Expand All @@ -495,6 +502,7 @@ mod tests {
}
}

#[cfg(feature = "sha2")]
#[test]
fn test_error_context_informative() {
let err = Error::unsupported_hash(Codec::Sha2256);
Expand Down
Loading
Loading