Skip to main content

rustolio_utils/crypto/
hash.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11#[cfg(debug_assertions)]
12use std::panic::Location;
13use std::{cell::RefCell, hash::Hash};
14
15use sha3::{digest::Digest as _, Keccak256};
16
17use crate::prelude::*;
18
19thread_local! {
20    /// A thread-local instance of the Keccak256 hasher.
21    /// This is so the hasher does not need to be initialized for every hash.
22    #[doc(hidden)]
23    static LOCAL_HASHER: RefCell<Inner> = RefCell::new(Inner::new());
24}
25
26#[derive(Debug, Clone, Copy, Eq, Encode, Decode)]
27pub struct Digest([u8; 32]);
28
29impl Digest {
30    pub fn to_bytes(&self) -> [u8; 32] {
31        self.0
32    }
33}
34
35impl<T: AsRef<[u8]>> PartialEq<T> for Digest {
36    fn eq(&self, other: &T) -> bool {
37        self.as_ref() == other.as_ref()
38    }
39}
40
41impl AsRef<[u8]> for Digest {
42    fn as_ref(&self) -> &[u8] {
43        &self.0
44    }
45}
46
47pub struct Hasher(
48    std::marker::PhantomData<()>, // Must be constructed using `::new()`
49);
50
51struct Inner {
52    keccak: Keccak256,
53
54    #[cfg(debug_assertions)]
55    locked: Option<&'static Location<'static>>,
56}
57
58impl std::hash::Hasher for Inner {
59    fn finish(&self) -> u64 {
60        unimplemented!()
61    }
62    fn write(&mut self, bytes: &[u8]) {
63        self.update(bytes);
64    }
65}
66
67impl Inner {
68    fn new() -> Self {
69        Self {
70            keccak: Keccak256::new(),
71            #[cfg(debug_assertions)]
72            locked: None,
73        }
74    }
75
76    fn update(&mut self, bytes: &[u8]) {
77        #[cfg(debug_assertions)]
78        if self.locked.is_none() {
79            panic!("Must be locked at this point")
80        }
81
82        self.keccak.update(bytes);
83    }
84}
85
86impl Hasher {
87    #[track_caller]
88    pub fn new() -> Hasher {
89        #[cfg(debug_assertions)]
90        {
91            let caller = Location::caller();
92            LOCAL_HASHER.with_borrow_mut(|h| {
93                if let Some(caller) = h.locked {
94                    panic!(
95                        "Cannot hash multiple values at once. Already hashing: {}",
96                        caller
97                    )
98                }
99                h.locked = Some(caller);
100            });
101        }
102
103        Self(std::marker::PhantomData)
104    }
105
106    pub fn once(b: impl Hash) -> Digest {
107        Hasher::new().update(b).finalize()
108    }
109
110    pub fn once_raw(b: impl AsRef<[u8]>) -> Digest {
111        Hasher::new().update_raw(b).finalize()
112    }
113
114    pub fn update(self, b: impl Hash) -> Self {
115        LOCAL_HASHER.with_borrow_mut(|h| b.hash(h));
116        self
117    }
118
119    pub fn update_raw(self, b: impl AsRef<[u8]>) -> Self {
120        LOCAL_HASHER.with_borrow_mut(|h| h.update(b.as_ref()));
121        self
122    }
123
124    pub fn finalize(self) -> Digest {
125        #[cfg(debug_assertions)]
126        LOCAL_HASHER.with_borrow_mut(|h| h.locked = None);
127
128        Digest(
129            LOCAL_HASHER
130                .with_borrow_mut(|h| h.keccak.finalize_reset())
131                .into(),
132        )
133    }
134}
135
136#[cfg(debug_assertions)]
137impl Drop for Hasher {
138    fn drop(&mut self) {
139        LOCAL_HASHER.with_borrow_mut(|h| h.locked = None);
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[derive(Debug, Hash)]
148    struct ToHash {
149        u: usize,
150        s: String,
151        c: char,
152    }
153
154    #[test]
155    fn test_hasher() {
156        let value = b"abc";
157        let expected = [
158            0x4e, 0x3, 0x65, 0x7a, 0xea, 0x45, 0xa9, 0x4f, 0xc7, 0xd4, 0x7b, 0xa8, 0x26, 0xc8,
159            0xd6, 0x67, 0xc0, 0xd1, 0xe6, 0xe3, 0x3a, 0x64, 0xa0, 0x36, 0xec, 0x44, 0xf5, 0x8f,
160            0xa1, 0x2d, 0x6c, 0x45,
161        ];
162
163        let digest = Hasher::once_raw(value);
164        assert_eq!(digest, expected);
165
166        let digest = Hasher::new().update_raw(value).finalize();
167        assert_eq!(digest, expected);
168
169        let value = ToHash {
170            u: 654,
171            s: String::from("tohash"),
172            c: 'h',
173        };
174        let multiple = Hasher::new().update(&value).finalize();
175        let once = Hasher::once(&value);
176        assert_eq!(multiple, once);
177    }
178
179    #[test]
180    #[should_panic]
181    #[cfg(debug_assertions)]
182    fn test_multiple_hasher() {
183        let _h = Hasher::new();
184        let _h = Hasher::new();
185    }
186
187    #[test]
188    fn test_hash_eq() {
189        let value = b"abcdef";
190        let digest = Hasher::once_raw(value);
191        let digest2 = Hasher::new()
192            .update_raw(&value[0..3])
193            .update_raw(&value[3..6])
194            .finalize();
195        assert_eq!(digest, digest2);
196    }
197}