use alloc::string::String;
use core::{marker::PhantomData, ops::Deref};
use blake2::{Blake2b, Blake2bVar};
use digest::{
Digest,
FixedOutput,
FixedOutputReset,
Output,
OutputSizeUser,
Update,
consts::{U32, U64},
};
use sha3::Sha3_256;
use crate::{
alloc::string::ToString,
errors::{HashingError, SliceError},
keys::SecretKey,
};
pub trait DomainSeparation {
fn version() -> u8;
fn domain() -> &'static str;
fn domain_separation_tag<S: AsRef<str>>(label: S) -> String {
if !label.as_ref().is_empty() {
return format!("{}.v{}.{}", Self::domain(), Self::version(), label.as_ref());
}
format!("{}.v{}", Self::domain(), Self::version())
}
fn add_domain_separation_tag<S: AsRef<[u8]>, D: Digest>(digest: &mut D, label: S) {
let label = if label.as_ref().is_empty() { &[] } else { label.as_ref() };
let domain = Self::domain();
let (version_offset, version) = byte_to_decimal_ascii_bytes(Self::version());
let len = if label.is_empty() {
domain.len() + (3 - version_offset) + 2
} else {
domain.len() + (3 - version_offset) + label.len() + 3
};
let len = (len as u64).to_le_bytes();
digest.update(len);
digest.update(domain);
digest.update(b".v");
digest.update(&version[version_offset..]);
if !label.is_empty() {
digest.update(b".");
digest.update(label);
}
}
}
fn byte_to_decimal_ascii_bytes(mut byte: u8) -> (usize, [u8; 3]) {
const ZERO_ASCII_CHAR: u8 = 48;
let mut bytes = [0u8, 0u8, ZERO_ASCII_CHAR];
let mut pos = 3usize;
if byte == 0 {
return (2, bytes);
}
while byte > 0 {
let rem = byte % 10;
byte /= 10;
bytes[pos - 1] = ZERO_ASCII_CHAR + rem;
pos -= 1;
}
(pos, bytes)
}
pub struct DomainSeparatedHash<D: Digest> {
output: Output<D>,
}
impl<D: Digest> DomainSeparatedHash<D> {
fn new(output: Output<D>) -> Self {
Self { output }
}
}
impl<D: Digest> AsRef<[u8]> for DomainSeparatedHash<D> {
fn as_ref(&self) -> &[u8] {
self.output.as_slice()
}
}
#[derive(Debug, Clone, Default)]
pub struct DomainSeparatedHasher<D, M> {
inner: D,
label: &'static str,
_dst: PhantomData<M>,
}
impl<D: Digest, M: DomainSeparation> DomainSeparatedHasher<D, M> {
pub fn new() -> Self {
Self::new_with_label("")
}
pub fn new_with_label(label: &'static str) -> Self {
let mut inner = D::new();
M::add_domain_separation_tag(&mut inner, label);
Self {
inner,
label,
_dst: PhantomData,
}
}
pub fn update(&mut self, data: impl AsRef<[u8]>) {
let len = (data.as_ref().len() as u64).to_le_bytes();
self.inner.update(len);
self.inner.update(data);
}
#[must_use]
pub fn chain(mut self, data: impl AsRef<[u8]>) -> Self {
self.update(data);
self
}
pub fn finalize(self) -> DomainSeparatedHash<D> {
let output = self.inner.finalize();
DomainSeparatedHash::new(output)
}
pub fn digest(mut self, data: &[u8]) -> DomainSeparatedHash<D> {
self.update(data);
self.finalize()
}
}
impl<D: Digest, M: DomainSeparation> PartialEq for DomainSeparatedHasher<D, M> {
fn eq(&self, other: &Self) -> bool {
self.label == other.label
}
}
impl<D: Digest, M: DomainSeparation> Eq for DomainSeparatedHasher<D, M> {}
pub trait AsFixedBytes<const I: usize>: AsRef<[u8]> {
fn as_fixed_bytes(&self) -> Result<[u8; I], SliceError> {
let hash_vec = self.as_ref();
if hash_vec.is_empty() || hash_vec.len() < I {
let hash_vec_length = if hash_vec.is_empty() { 0 } else { hash_vec.len() };
return Err(SliceError::CopyFromSlice {
target: I,
provided: hash_vec_length,
});
}
let mut buffer: [u8; I] = [0; I];
buffer.copy_from_slice(&hash_vec[..I]);
Ok(buffer)
}
}
impl<TInnerDigest: OutputSizeUser, TDomain: DomainSeparation> OutputSizeUser
for DomainSeparatedHasher<TInnerDigest, TDomain>
{
type OutputSize = TInnerDigest::OutputSize;
}
impl<TInnerDigest: Update, TDomain: DomainSeparation> Update for DomainSeparatedHasher<TInnerDigest, TDomain> {
fn update(&mut self, data: &[u8]) {
self.inner.update(data);
}
}
impl<const I: usize, D: Digest> AsFixedBytes<I> for DomainSeparatedHash<D> {}
impl<TInnerDigest: FixedOutput, TDomain: DomainSeparation> FixedOutput
for DomainSeparatedHasher<TInnerDigest, TDomain>
{
fn finalize_into(self, out: &mut Output<Self>) {
self.inner.finalize_into(out);
}
}
impl<D: FixedOutputReset, M: DomainSeparation> DomainSeparatedHasher<D, M> {
pub fn finalize_into_reset(&mut self, out: &mut Output<Self>) {
self.inner.finalize_into_reset(out);
}
}
impl<TInnerDigest: Digest + FixedOutputReset, TDomain: DomainSeparation> Digest
for DomainSeparatedHasher<TInnerDigest, TDomain>
{
fn new() -> Self {
DomainSeparatedHasher::<TInnerDigest, TDomain>::new()
}
fn new_with_prefix(data: impl AsRef<[u8]>) -> Self {
let hasher = DomainSeparatedHasher::<TInnerDigest, TDomain>::new();
hasher.chain_update(data)
}
fn update(&mut self, data: impl AsRef<[u8]>) {
self.update(data);
}
fn chain_update(self, data: impl AsRef<[u8]>) -> Self
where Self: Sized {
self.chain(data)
}
fn finalize(self) -> Output<Self> {
self.finalize().output
}
fn finalize_reset(&mut self) -> Output<Self> {
let value = self.inner.finalize_reset();
TDomain::add_domain_separation_tag(&mut self.inner, self.label);
value
}
fn finalize_into_reset(&mut self, out: &mut Output<Self>) {
Digest::finalize_into_reset(&mut self.inner, out);
}
fn finalize_into(self, out: &mut Output<Self>) {
Digest::finalize_into(self.inner, out);
}
fn reset(&mut self) {
Digest::reset(&mut self.inner);
TDomain::add_domain_separation_tag(&mut self.inner, self.label);
}
fn output_size() -> usize {
<TInnerDigest as Digest>::output_size()
}
fn digest(data: impl AsRef<[u8]>) -> Output<Self> {
let mut hasher = Self::new();
hasher.update(data);
hasher.finalize().output
}
}
pub trait LengthExtensionAttackResistant {}
impl LengthExtensionAttackResistant for Blake2bVar {}
impl LengthExtensionAttackResistant for Sha3_256 {}
impl LengthExtensionAttackResistant for Blake2b<U32> {}
impl LengthExtensionAttackResistant for Blake2b<U64> {}
pub struct MacDomain;
impl DomainSeparation for MacDomain {
fn version() -> u8 {
1
}
fn domain() -> &'static str {
"com.tari.mac"
}
}
pub struct Mac<D: Digest> {
hmac: DomainSeparatedHash<D>,
}
impl<D> Mac<D>
where D: Digest + Update + LengthExtensionAttackResistant
{
pub fn generate<K, S>(key: K, msg: S, label: &'static str) -> Self
where
K: AsRef<[u8]>,
S: AsRef<[u8]>,
{
let hmac = DomainSeparatedHasher::<D, MacDomain>::new_with_label(label)
.chain(key.as_ref())
.chain(msg.as_ref())
.finalize();
Self { hmac }
}
}
impl<D: Digest> Deref for Mac<D> {
type Target = DomainSeparatedHash<D>;
fn deref(&self) -> &Self::Target {
&self.hmac
}
}
pub trait DerivedKeyDomain: DomainSeparation {
type DerivedKeyType: SecretKey;
fn generate<D>(primary_key: &[u8], data: &[u8], label: &'static str) -> Result<Self::DerivedKeyType, HashingError>
where
Self: Sized,
D: Digest + Update,
{
if primary_key.len() < <Self::DerivedKeyType as SecretKey>::KEY_LEN {
return Err(HashingError::InputTooShort {});
}
if <D as Digest>::output_size() != <Self::DerivedKeyType as SecretKey>::WIDE_REDUCTION_LEN {
return Err(HashingError::InputTooShort {});
}
let hash = DomainSeparatedHasher::<D, Self>::new_with_label(label)
.chain(primary_key)
.chain(data)
.finalize();
let derived_key = Self::DerivedKeyType::from_uniform_bytes(hash.as_ref())
.map_err(|e| HashingError::ConversionFromBytes { reason: e.to_string() })?;
Ok(derived_key)
}
}
#[macro_export]
macro_rules! hash_domain {
($name:ident, $domain:expr, $version: expr) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name;
impl $crate::hashing::DomainSeparation for $name {
fn version() -> u8 {
$version
}
fn domain() -> &'static str {
$domain
}
}
};
($name:ident, $domain:expr) => {
hash_domain!($name, $domain, 1);
};
}
#[macro_export]
macro_rules! hasher {
($digest:ty, $name:ident, $domain:expr, $version: expr, $mod_name:ident) => {
mod $mod_name {
$crate::hash_domain!(__HashDomain, $domain, $version);
}
pub type $name = $crate::hashing::DomainSeparatedHasher<$digest, $mod_name::__HashDomain>;
};
($digest: ty, $name:ident, $domain:expr, $version: expr) => {
hasher!($digest, $name, $domain, $version, __inner_hasher_impl);
};
($digest: ty, $name:ident, $domain:expr) => {
hasher!($digest, $name, $domain, 1, __inner_hasher_impl);
};
}
pub fn create_hasher_with_label<D: Digest, HD: DomainSeparation>(label: &'static str) -> DomainSeparatedHasher<D, HD> {
DomainSeparatedHasher::<D, HD>::new_with_label(label)
}
pub fn create_hasher<D: Digest, HD: DomainSeparation>() -> DomainSeparatedHasher<D, HD> {
DomainSeparatedHasher::<D, HD>::new()
}
#[cfg(test)]
mod test {
use blake2::Blake2b;
use digest::{
Digest,
Update,
consts::{U32, U64},
generic_array::GenericArray,
};
use tari_utilities::hex::{from_hex, to_hex};
use crate::hashing::{
AsFixedBytes,
DomainSeparatedHasher,
DomainSeparation,
Mac,
MacDomain,
byte_to_decimal_ascii_bytes,
};
mod util {
use digest::Digest;
use tari_utilities::hex::to_hex;
pub(crate) fn hash_test<D: Digest>(data: &[u8], expected: &str) {
let mut hasher = D::new();
hasher.update(data);
let hash = hasher.finalize();
assert_eq!(to_hex(&hash), expected);
}
pub(crate) fn hash_from_digest<D: Digest>(mut hasher: D, data: &[u8], expected: &str) {
hasher.update(data);
let hash = hasher.finalize();
assert_eq!(to_hex(&hash), expected);
}
}
#[test]
fn hasher_macro_tests() {
{
hasher!(Blake2b<U32>, MyDemoHasher, "com.macro.test");
util::hash_from_digest(
MyDemoHasher::new(),
&[0, 0, 0],
"d4cbf5b6b97485a991973db8a6ce4d3fc660db5dff5f55f2b0cb363fca34b0a2",
);
}
{
hasher!(Blake2b<U32>, MyDemoHasher2, "com.macro.test", 1);
util::hash_from_digest(
MyDemoHasher2::new(),
&[0, 0, 0],
"d4cbf5b6b97485a991973db8a6ce4d3fc660db5dff5f55f2b0cb363fca34b0a2",
);
}
}
#[test]
fn mac_domain_metadata() {
assert_eq!(MacDomain::version(), 1);
assert_eq!(MacDomain::domain(), "com.tari.mac");
assert_eq!(MacDomain::domain_separation_tag(""), "com.tari.mac.v1");
assert_eq!(MacDomain::domain_separation_tag("test"), "com.tari.mac.v1.test");
}
#[test]
fn finalize_into() {
hash_domain!(TestHasher, "com.example.test");
let mut hasher = DomainSeparatedHasher::<Blake2b<U32>, TestHasher>::new();
hasher.update([0, 0, 0]);
let mut output = GenericArray::<u8, U32>::default();
hasher.finalize_into(&mut output);
}
#[test]
fn finalize_into_reset() {
hash_domain!(TestHasher, "com.example.test");
let mut hasher = DomainSeparatedHasher::<Blake2b<U32>, TestHasher>::new();
hasher.update([0, 0, 0]);
let mut output = GenericArray::<u8, U32>::default();
hasher.finalize_into_reset(&mut output);
}
#[test]
fn test_safe_array() {
use tari_utilities::{hidden::Hidden, hidden_type, safe_array::SafeArray};
use zeroize::Zeroize;
hash_domain!(TestHasher, "com.example.test");
let mut hasher = DomainSeparatedHasher::<Blake2b<U32>, TestHasher>::new();
hasher.update([0, 0, 0]);
hidden_type!(Key, SafeArray<u8, 32>);
let mut key = Key::from(SafeArray::default()); hasher.finalize_into_reset(GenericArray::from_mut_slice(key.reveal_mut()));
}
#[test]
fn dst_hasher() {
hash_domain!(GenericHashDomain, "com.tari.generic");
assert_eq!(GenericHashDomain::domain_separation_tag(""), "com.tari.generic.v1");
let hash = DomainSeparatedHasher::<Blake2b<U32>, GenericHashDomain>::new_with_label("test_hasher")
.chain("some foo")
.finalize();
let mut hash2 = DomainSeparatedHasher::<Blake2b<U32>, GenericHashDomain>::new_with_label("test_hasher");
hash2.update("some foo");
let hash2 = hash2.finalize();
assert_eq!(hash.as_ref(), hash2.as_ref());
assert_eq!(
to_hex(hash.as_ref()),
"a8326620e305430a0b632a0a5e33c6c1124d7513b4bd84736faaa3a0b9ba557f"
);
let hash_1 =
DomainSeparatedHasher::<Blake2b<U32>, GenericHashDomain>::new_with_label("mynewtest").digest(b"rincewind");
let hash_2 = DomainSeparatedHasher::<Blake2b<U32>, GenericHashDomain>::new_with_label("mynewtest")
.chain(b"rincewind")
.finalize();
assert_eq!(hash_1.as_ref(), hash_2.as_ref());
}
#[test]
fn digest_is_the_same_as_standard_api() {
hash_domain!(MyDemoHasher, "com.macro.test");
assert_eq!(MyDemoHasher::domain_separation_tag(""), "com.macro.test.v1");
util::hash_test::<DomainSeparatedHasher<Blake2b<U32>, MyDemoHasher>>(
&[0, 0, 0],
"d4cbf5b6b97485a991973db8a6ce4d3fc660db5dff5f55f2b0cb363fca34b0a2",
);
let mut hasher = DomainSeparatedHasher::<Blake2b<U32>, MyDemoHasher>::new();
hasher.update([0, 0, 0]);
let hash = hasher.finalize();
assert_eq!(
to_hex(hash.as_ref()),
"d4cbf5b6b97485a991973db8a6ce4d3fc660db5dff5f55f2b0cb363fca34b0a2"
);
let mut hasher = DomainSeparatedHasher::<Blake2b<U32>, MyDemoHasher>::new_with_label("");
hasher.update([0, 0, 0]);
let hash = hasher.finalize();
assert_eq!(
to_hex(hash.as_ref()),
"d4cbf5b6b97485a991973db8a6ce4d3fc660db5dff5f55f2b0cb363fca34b0a2"
);
}
#[test]
fn can_be_used_as_digest() {
hash_domain!(MyDemoHasher, "com.macro.test");
assert_eq!(MyDemoHasher::domain_separation_tag(""), "com.macro.test.v1");
util::hash_test::<DomainSeparatedHasher<Blake2b<U32>, MyDemoHasher>>(
&[0, 0, 0],
"d4cbf5b6b97485a991973db8a6ce4d3fc660db5dff5f55f2b0cb363fca34b0a2",
);
hash_domain!(MyDemoHasher2, "com.macro.test", 2);
assert_eq!(MyDemoHasher2::domain_separation_tag(""), "com.macro.test.v2");
util::hash_test::<DomainSeparatedHasher<Blake2b<U32>, MyDemoHasher2>>(
&[0, 0, 0],
"ce327b02271d035bad4dcc1e69bc292392ee4ee497f1f8467d54bf4b4c72639a",
);
hash_domain!(TariHasher, "com.tari.hasher");
assert_eq!(TariHasher::domain_separation_tag(""), "com.tari.hasher.v1");
util::hash_test::<DomainSeparatedHasher<Blake2b<U32>, TariHasher>>(
&[0, 0, 0],
"ae359f05bb76c646c6767d25f53893fc38b0c7b56f8a74a1cbb008ea3ffc183f",
);
}
#[test]
fn hash_to_fixed_bytes_conversion() {
hash_domain!(TestDomain, "com.tari.generic");
let hash = DomainSeparatedHasher::<Blake2b<U32>, TestDomain>::new_with_label("mytest")
.chain("some data")
.finalize();
let hash_to_bytes_7: [u8; 7] = hash.as_fixed_bytes().unwrap();
assert_eq!(hash_to_bytes_7, hash.as_fixed_bytes().unwrap());
let hash_to_bytes_23: [u8; 23] = hash.as_fixed_bytes().unwrap();
assert_eq!(hash_to_bytes_23, hash.as_fixed_bytes().unwrap());
let hash_to_bytes_32: [u8; 32] = hash.as_fixed_bytes().unwrap();
assert_eq!(hash_to_bytes_32, hash.as_fixed_bytes().unwrap());
}
#[test]
fn deconstruction() {
hash_domain!(TestDomain, "com.tari.generic");
let hash = DomainSeparatedHasher::<Blake2b<U32>, TestDomain>::new_with_label("mytest")
.chain("rincewind")
.chain("hex")
.finalize();
let expected = Blake2b::<U32>::new()
.chain(26u64.to_le_bytes())
.chain("com.tari.generic.v1.mytest".as_bytes())
.chain(9u64.to_le_bytes())
.chain("rincewind".as_bytes())
.chain(3u64.to_le_bytes())
.chain("hex".as_bytes())
.finalize();
assert_eq!(hash.as_ref(), expected.as_slice());
}
#[test]
fn domain_separation_tag_hashing() {
struct MyDemoHasher;
impl DomainSeparation for MyDemoHasher {
fn version() -> u8 {
42
}
fn domain() -> &'static str {
"com.discworld"
}
}
let domain = "com.discworld.v42.turtles";
assert_eq!(MyDemoHasher::domain_separation_tag("turtles"), domain);
let hash = DomainSeparatedHasher::<Blake2b<U32>, MyDemoHasher>::new_with_label("turtles").finalize();
let expected = Blake2b::<U32>::default()
.chain((domain.len() as u64).to_le_bytes())
.chain(domain)
.finalize();
assert_eq!(hash.as_ref(), expected.as_slice());
}
#[test]
fn update_domain_separation_tag() {
hash_domain!(TestDomain, "com.test");
let s_tag = TestDomain::domain_separation_tag("mytest");
let expected_hash = Blake2b::<U32>::default()
.chain(s_tag.len().to_le_bytes())
.chain(s_tag)
.finalize();
let mut digest = Blake2b::<U32>::default();
TestDomain::add_domain_separation_tag(&mut digest, "mytest");
assert_eq!(digest.finalize(), expected_hash);
}
#[test]
fn application_hasher() {
struct MyDemoHasher;
impl DomainSeparation for MyDemoHasher {
fn version() -> u8 {
42
}
fn domain() -> &'static str {
"com.discworld"
}
}
let hash = DomainSeparatedHasher::<Blake2b<U64>, MyDemoHasher>::new_with_label("turtles")
.chain("elephants")
.finalize();
assert_eq!(
to_hex(hash.as_ref()),
"64a89c7160a1076a725fac97d3f67803abd0991d82518a595072fa62df4c870bddee9160f591231c381087831bf6925616013de317ce0b02846585caf41942ac"
);
}
#[test]
fn incompatible_tags() {
let key = from_hex("b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c").unwrap();
let mac = Mac::<Blake2b<U32>>::generate(key, "test message", "test");
assert_eq!(MacDomain::domain_separation_tag("test"), "com.tari.mac.v1.test");
assert_eq!(
to_hex(mac.as_ref()),
"9bcfbe2bad73b14ac42f673ddca34e82ce03cbbac69d34526004f5d108dff061"
)
}
#[test]
fn check_bytes_to_decimal_ascii_bytes() {
assert_eq!(byte_to_decimal_ascii_bytes(0), (2, [0u8, 0, 48]));
assert_eq!(byte_to_decimal_ascii_bytes(42), (1, [0u8, 52, 50]));
assert_eq!(byte_to_decimal_ascii_bytes(255), (0, [50u8, 53, 53]));
}
}