bc_components/
digest.rs

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