bc_components/
digest.rs

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