bc_components/
digest.rs

1use std::borrow::Cow;
2use bc_crypto::hash::sha256;
3use crate::{ digest_provider::DigestProvider, tags };
4use anyhow::{ bail, Result };
5use dcbor::prelude::*;
6
7/// A cryptographically secure digest, implemented with SHA-256.
8///
9/// A `Digest` represents the cryptographic hash of some data. In this implementation, SHA-256 is used,
10/// which produces a 32-byte hash value. Digests are used throughout the crate for data verification
11/// and as unique identifiers derived from data.
12///
13/// # CBOR Serialization
14///
15/// `Digest` implements the `CBORTaggedCodable` trait, which means it can be serialized to and
16/// deserialized from CBOR with a specific tag. The tag used is `TAG_DIGEST` defined in the `tags` module.
17///
18/// # UR Serialization
19///
20/// When serialized as a Uniform Resource (UR), a `Digest` is represented as a binary blob with the type "digest".
21///
22/// # Examples
23///
24/// Creating a digest from data:
25///
26/// ```
27/// use bc_components::Digest;
28///
29/// // Create a digest from a string
30/// let data = "hello world";
31/// let digest = Digest::from_image(data.as_bytes());
32///
33/// // Validate that the digest matches the original data
34/// assert!(digest.validate(data.as_bytes()));
35/// ```
36///
37/// Creating and using a digest with hexadecimal representation:
38///
39/// ```
40/// use bc_components::Digest;
41///
42/// // Create a digest from a hex string
43/// let hex_string = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
44/// let digest = Digest::from_hex(hex_string);
45///
46/// // Retrieve the digest as hex
47/// assert_eq!(digest.hex(), hex_string);
48/// ```
49#[derive(Clone, PartialEq, Eq, Hash)]
50pub struct Digest([u8; Self::DIGEST_SIZE]);
51
52impl Digest {
53    pub const DIGEST_SIZE: usize = 32;
54
55    /// Create a new digest from data.
56    pub fn from_data(data: [u8; Self::DIGEST_SIZE]) -> Self {
57        Self(data)
58    }
59
60    /// Create a new digest from data.
61    ///
62    /// Returns `None` if the data is not the correct length.
63    pub fn from_data_ref(data: impl AsRef<[u8]>) -> Result<Self> {
64        let data = data.as_ref();
65        if data.len() != Self::DIGEST_SIZE {
66            bail!("Invalid digest size");
67        }
68        let mut arr = [0u8; Self::DIGEST_SIZE];
69        arr.copy_from_slice(data.as_ref());
70        Ok(Self::from_data(arr))
71    }
72
73    /// Create a new digest from the given image.
74    ///
75    /// The image is hashed with SHA-256.
76    pub fn from_image(image: impl AsRef<[u8]>) -> Self {
77        Self::from_data(sha256(image.as_ref()))
78    }
79
80    /// Create a new digest from an array of data items.
81    ///
82    /// The image parts are concatenated and hashed with SHA-256.
83    pub fn from_image_parts(image_parts: &[&[u8]]) -> Self {
84        let mut buf = Vec::new();
85        for part in image_parts {
86            buf.extend_from_slice(part);
87        }
88        Self::from_image(&buf)
89    }
90
91    /// Create a new digest from an array of Digests.
92    ///
93    /// The image parts are concatenated and hashed with SHA-256.
94    pub fn from_digests(digests: &[Digest]) -> Self {
95        let mut buf = Vec::new();
96        for digest in digests {
97            buf.extend_from_slice(digest.data());
98        }
99        Self::from_image(&buf)
100    }
101
102    /// Get the data of the digest.
103    pub fn data(&self) -> &[u8; Self::DIGEST_SIZE] {
104        self.into()
105    }
106
107    /// Validate the digest against the given image.
108    ///
109    /// The image is hashed with SHA-256 and compared to the digest.
110    /// Returns `true` if the digest matches the image.
111    pub fn validate(&self, image: impl AsRef<[u8]>) -> bool {
112        self == &Self::from_image(image)
113    }
114
115    /// Create a new digest from the given hexadecimal string.
116    ///
117    /// # Panics
118    /// Panics if the string is not exactly 64 hexadecimal digits.
119    pub fn from_hex(hex: impl AsRef<str>) -> Self {
120        Self::from_data_ref(hex::decode(hex.as_ref()).unwrap()).unwrap()
121    }
122
123    /// The data as a hexadecimal string.
124    pub fn hex(&self) -> String {
125        hex::encode(self.0)
126    }
127
128    /// The first four bytes of the digest as a hexadecimal string.
129    pub fn short_description(&self) -> String {
130        hex::encode(&self.0[0..4])
131    }
132
133    /// Validate the given data against the digest, if any.
134    ///
135    /// Returns `true` if the digest is `None` or if the digest matches the image's digest.
136    /// Returns `false` if the digest does not match the image's digest.
137    pub fn validate_opt(image: impl AsRef<[u8]>, digest: Option<&Digest>) -> bool {
138        match digest {
139            Some(digest) => digest.validate(image),
140            None => true,
141        }
142    }
143}
144
145/// Allows accessing the underlying data as a fixed-size byte array reference.
146impl<'a> From<&'a Digest> for &'a [u8; Digest::DIGEST_SIZE] {
147    fn from(value: &'a Digest) -> Self {
148        &value.0
149    }
150}
151
152/// Allows accessing the underlying data as a byte slice reference.
153impl<'a> From<&'a Digest> for &'a [u8] {
154    fn from(value: &'a Digest) -> Self {
155        &value.0
156    }
157}
158
159/// Allows using a Digest as a reference to a byte slice.
160impl AsRef<[u8]> for Digest {
161    fn as_ref(&self) -> &[u8] {
162        &self.0
163    }
164}
165
166/// Provides a self-reference, enabling API consistency with other types.
167impl AsRef<Digest> for Digest {
168    fn as_ref(&self) -> &Digest {
169        self
170    }
171}
172
173/// Enables partial ordering of Digests by comparing their underlying bytes.
174impl std::cmp::PartialOrd for Digest {
175    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
176        Some(self.0.cmp(&other.0))
177    }
178}
179
180/// Enables total ordering of Digests by comparing their underlying bytes lexicographically.
181impl std::cmp::Ord for Digest {
182    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
183        self.0.cmp(&other.0)
184    }
185}
186
187/// Implements DigestProvider to return itself without copying, as a Digest is already a digest.
188impl DigestProvider for Digest {
189    fn digest(&self) -> Cow<'_, Digest> {
190        Cow::Borrowed(self)
191    }
192}
193
194/// Provides a debug representation showing the digest's hexadecimal value.
195impl std::fmt::Debug for Digest {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        write!(f, "Digest({})", self.hex())
198    }
199}
200
201/// Provides a string representation showing the digest's hexadecimal value.
202impl std::fmt::Display for Digest {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        write!(f, "Digest({})", self.hex())
205    }
206}
207
208/// Identifies the CBOR tags used for Digest serialization.
209impl CBORTagged for Digest {
210    fn cbor_tags() -> Vec<Tag> {
211        tags_for_values(&[tags::TAG_DIGEST])
212    }
213}
214
215/// Enables conversion of a Digest into a tagged CBOR value.
216impl From<Digest> for CBOR {
217    fn from(value: Digest) -> Self {
218        value.tagged_cbor()
219    }
220}
221
222/// Defines how a Digest is encoded as CBOR (as a byte string).
223impl CBORTaggedEncodable for Digest {
224    fn untagged_cbor(&self) -> CBOR {
225        CBOR::to_byte_string(self.0)
226    }
227}
228
229/// Enables conversion from CBOR to Digest, with proper error handling.
230impl TryFrom<CBOR> for Digest {
231    type Error = dcbor::Error;
232
233    fn try_from(cbor: CBOR) -> Result<Self, Self::Error> {
234        Self::from_tagged_cbor(cbor)
235    }
236}
237
238/// Defines how a Digest is decoded from CBOR.
239impl CBORTaggedDecodable for Digest {
240    fn from_untagged_cbor(cbor: CBOR) -> dcbor::Result<Self> {
241        let data = CBOR::try_into_byte_string(cbor)?;
242        Ok(Self::from_data_ref(data)?)
243    }
244}
245
246/// Enables cloning a Digest from a reference using From trait.
247impl From<&Digest> for Digest {
248    fn from(digest: &Digest) -> Self {
249        digest.clone()
250    }
251}
252
253/// Converts a Digest into a `Vec<u8>` containing the digest bytes.
254impl From<Digest> for Vec<u8> {
255    fn from(digest: Digest) -> Self {
256        digest.0.to_vec()
257    }
258}
259
260/// Converts a Digest reference into a `Vec<u8>` containing the digest bytes.
261impl From<&Digest> for Vec<u8> {
262    fn from(digest: &Digest) -> Self {
263        digest.0.to_vec()
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use bc_ur::prelude::*;
271    use hex_literal::hex;
272
273    #[test]
274    fn test_digest() {
275        let data = "hello world";
276        let digest = Digest::from_image(data.as_bytes());
277        assert_eq!(digest.data().len(), Digest::DIGEST_SIZE);
278        assert_eq!(*digest.data(), sha256(data.as_bytes()));
279        assert_eq!(
280            *digest.data(),
281            hex!("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9")
282        );
283    }
284
285    #[test]
286    fn test_digest_from_hex() {
287        let digest = Digest::from_hex(
288            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
289        );
290        assert_eq!(digest.data().len(), Digest::DIGEST_SIZE);
291        assert_eq!(*digest.data(), sha256("hello world".as_bytes()));
292        assert_eq!(
293            *digest.data(),
294            hex!("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9")
295        );
296    }
297
298    #[test]
299    fn test_ur() {
300        crate::register_tags();
301        let data = "hello world";
302        let digest = Digest::from_image(data.as_bytes());
303        let ur_string = digest.ur_string();
304        let expected_ur_string =
305            "ur:digest/hdcxrhgtdirhmugtfmayondmgmtstnkipyzssslrwsvlkngulawymhloylpsvowssnwlamnlatrs";
306        assert_eq!(ur_string, expected_ur_string);
307        let digest2 = Digest::from_ur_string(&ur_string).unwrap();
308        assert_eq!(digest, digest2);
309    }
310
311    #[test]
312    fn test_digest_equality() {
313        let digest1 = Digest::from_hex(
314            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
315        );
316        let digest2 = Digest::from_hex(
317            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
318        );
319        assert_eq!(digest1, digest2);
320    }
321
322    #[test]
323    fn test_digest_inequality() {
324        let digest1 = Digest::from_hex(
325            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
326        );
327        let digest2 = Digest::from_hex(
328            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
329        );
330        assert_ne!(digest1, digest2);
331    }
332
333    #[test]
334    #[should_panic]
335    fn test_invalid_hex_string() {
336        let _ = Digest::from_hex("invalid_hex_string");
337    }
338
339    #[test]
340    #[should_panic]
341    fn test_new_from_invalid_ur_string() {
342        let invalid_ur = "ur:not_digest/invalid";
343        let _ = Digest::from_ur_string(invalid_ur).unwrap();
344    }
345}