bc_components/
digest.rs

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