1use sha2::{Digest, Sha256};
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum IntegrityError {
29 DigestMismatch {
31 expected: String,
33 actual: String,
35 },
36 MalformedDigest(String),
38}
39
40impl std::fmt::Display for IntegrityError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 Self::DigestMismatch { expected, actual } => write!(
44 f,
45 "integrity check failed: expected sha256 {expected}, got {actual}. \
46 The file was NOT written. Either the source changed or the transfer \
47 was tampered with; do not retry without establishing which."
48 ),
49 Self::MalformedDigest(d) => write!(
50 f,
51 "malformed expected digest {d:?}: a sha256 is 64 hex characters"
52 ),
53 }
54 }
55}
56
57impl std::error::Error for IntegrityError {}
58
59#[must_use]
61pub fn sha256_hex(bytes: &[u8]) -> String {
62 let mut hasher = Sha256::new();
63 hasher.update(bytes);
64 hasher
65 .finalize()
66 .iter()
67 .fold(String::with_capacity(64), |mut s, b| {
68 use std::fmt::Write as _;
69 let _ = write!(s, "{b:02x}");
70 s
71 })
72}
73
74pub fn verify_sha256(bytes: &[u8], expected_hex: &str) -> Result<(), IntegrityError> {
83 let expected = expected_hex.trim().to_ascii_lowercase();
84 if expected.len() != 64 || !expected.bytes().all(|b| b.is_ascii_hexdigit()) {
85 return Err(IntegrityError::MalformedDigest(expected_hex.to_string()));
86 }
87
88 let actual = sha256_hex(bytes);
89
90 let diff = actual
92 .bytes()
93 .zip(expected.bytes())
94 .fold(0u8, |acc, (a, b)| acc | (a ^ b));
95
96 if diff == 0 {
97 Ok(())
98 } else {
99 Err(IntegrityError::DigestMismatch { expected, actual })
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 const EMPTY: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
111 const ABC: &str = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
112
113 #[test]
114 fn known_answer_vectors() {
115 assert_eq!(sha256_hex(b""), EMPTY);
116 assert_eq!(sha256_hex(b"abc"), ABC);
117 }
118
119 #[test]
120 fn accepts_matching_digest() {
121 assert_eq!(verify_sha256(b"abc", ABC), Ok(()));
122 }
123
124 #[test]
125 fn accepts_uppercase_and_padded_digest() {
126 assert_eq!(
127 verify_sha256(b"abc", &format!(" {} ", ABC.to_uppercase())),
128 Ok(())
129 );
130 }
131
132 #[test]
133 fn rejects_wrong_content() {
134 let err = verify_sha256(b"abd", ABC).expect_err("must reject");
136 match err {
137 IntegrityError::DigestMismatch { expected, actual } => {
138 assert_eq!(expected, ABC);
139 assert_ne!(actual, ABC);
140 }
141 other => panic!("wrong error: {other:?}"),
142 }
143 }
144
145 #[test]
146 fn rejects_single_flipped_bit() {
147 let mut a = vec![7u8; 4096];
149 let good = sha256_hex(&a);
150 a[2048] ^= 0x01;
151 assert!(verify_sha256(&a, &good).is_err());
152 }
153
154 #[test]
155 fn rejects_truncated_transfer() {
156 let full = vec![3u8; 8192];
157 let good = sha256_hex(&full);
158 assert!(verify_sha256(&full[..4096], &good).is_err());
159 }
160
161 #[test]
162 fn malformed_expectation_is_an_error_not_a_pass() {
163 for bad in ["", "abc", "zz", &"f".repeat(63), &"g".repeat(64)] {
164 assert!(
165 matches!(
166 verify_sha256(b"anything", bad),
167 Err(IntegrityError::MalformedDigest(_))
168 ),
169 "expected {bad:?} to be rejected as malformed"
170 );
171 }
172 }
173
174 #[test]
175 fn error_message_says_the_file_was_not_written() {
176 let msg = verify_sha256(b"abd", ABC).unwrap_err().to_string();
179 assert!(msg.contains("NOT written"), "message was: {msg}");
180 }
181}