1use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum IntegrityError {
8 Mismatch {
10 expected: String,
11 actual: String,
13 },
14 Malformed { value: String },
16 Unsupported { value: String },
18 Unavailable { value: String },
21}
22
23impl fmt::Display for IntegrityError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::Mismatch { expected, actual } => {
27 write!(
28 f,
29 "integrity mismatch: expected {expected:?}, got {actual:?}"
30 )
31 }
32 Self::Malformed { value } => write!(f, "malformed integrity value {value:?}"),
33 Self::Unsupported { value } => write!(
34 f,
35 "integrity value {value:?} uses no supported algorithm (sha256, sha384, sha512)"
36 ),
37 Self::Unavailable { value } => write!(
38 f,
39 "cannot check integrity value {value:?}: built without the `integrity` feature"
40 ),
41 }
42 }
43}
44
45impl std::error::Error for IntegrityError {}
46
47pub fn check_integrity(value: &str, bytes: &[u8]) -> Result<(), IntegrityError> {
75 imp::check(value, bytes)
76}
77
78#[cfg(feature = "integrity")]
79mod imp {
80 use base64::engine::general_purpose::STANDARD;
81 use base64::Engine as _;
82 use sha2::{Digest, Sha256, Sha384, Sha512};
83
84 use super::IntegrityError;
85
86 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
87 enum Algorithm {
88 Sha256,
89 Sha384,
90 Sha512,
91 }
92
93 impl Algorithm {
94 fn parse(name: &str) -> Option<Self> {
95 match name {
96 "sha256" => Some(Self::Sha256),
97 "sha384" => Some(Self::Sha384),
98 "sha512" => Some(Self::Sha512),
99 _ => None,
100 }
101 }
102
103 fn name(self) -> &'static str {
104 match self {
105 Self::Sha256 => "sha256",
106 Self::Sha384 => "sha384",
107 Self::Sha512 => "sha512",
108 }
109 }
110
111 fn digest(self, bytes: &[u8]) -> Vec<u8> {
112 match self {
113 Self::Sha256 => Sha256::digest(bytes).to_vec(),
114 Self::Sha384 => Sha384::digest(bytes).to_vec(),
115 Self::Sha512 => Sha512::digest(bytes).to_vec(),
116 }
117 }
118 }
119
120 pub(super) fn check(value: &str, bytes: &[u8]) -> Result<(), IntegrityError> {
121 let malformed = || IntegrityError::Malformed {
122 value: value.to_owned(),
123 };
124 let mut tokens = Vec::new();
125 for token in value.split_ascii_whitespace() {
126 let (name, rest) = token.split_once('-').ok_or_else(malformed)?;
127 let digest = rest.split_once('?').map_or(rest, |(digest, _)| digest);
128 let digest = STANDARD.decode(digest).map_err(|_| malformed())?;
129 if let Some(algorithm) = Algorithm::parse(name) {
130 tokens.push((algorithm, digest));
131 }
132 }
133 if value.split_ascii_whitespace().next().is_none() {
134 return Err(malformed());
135 }
136 let Some(strongest) = tokens.iter().map(|(a, _)| *a).max() else {
137 return Err(IntegrityError::Unsupported {
138 value: value.to_owned(),
139 });
140 };
141 let actual = strongest.digest(bytes);
142 if tokens
143 .iter()
144 .any(|(a, digest)| *a == strongest && *digest == actual)
145 {
146 Ok(())
147 } else {
148 Err(IntegrityError::Mismatch {
149 expected: value.to_owned(),
150 actual: format!("{}-{}", strongest.name(), STANDARD.encode(actual)),
151 })
152 }
153 }
154}
155
156#[cfg(not(feature = "integrity"))]
157mod imp {
158 use super::IntegrityError;
159
160 pub(super) fn check(value: &str, _bytes: &[u8]) -> Result<(), IntegrityError> {
161 Err(IntegrityError::Unavailable {
162 value: value.to_owned(),
163 })
164 }
165}
166
167#[cfg(all(test, feature = "integrity"))]
168mod tests {
169 use super::{check_integrity, IntegrityError};
170
171 const SHA256: &str = "sha256-ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=";
173 const SHA512: &str =
174 "sha512-3a81oZNherrMQXNJriBBMRLm+k6JqX6iCp7u5ktV05ohkpkqJ0/BqDa6PCOj/uu9RU1EI2Q86A4qmslPpUyknw==";
175
176 #[test]
177 fn accepts_matching_digests() {
178 assert_eq!(check_integrity(SHA256, b"abc"), Ok(()));
179 assert_eq!(check_integrity(SHA512, b"abc"), Ok(()));
180 assert_eq!(check_integrity(&format!("{SHA256}?opt"), b"abc"), Ok(()));
181 assert_eq!(check_integrity(&format!(" {SHA256}\n"), b"abc"), Ok(()));
182 assert_eq!(
183 check_integrity(&format!("md5-AAAA {SHA256}"), b"abc"),
184 Ok(())
185 );
186 }
187
188 #[test]
189 fn strongest_algorithm_decides() {
190 let wrong512 = format!("sha512-{}", "A".repeat(86) + "==");
191 let value = format!("{SHA256} {wrong512}");
192 assert!(matches!(
193 check_integrity(&value, b"abc"),
194 Err(IntegrityError::Mismatch { actual, .. }) if actual == SHA512
195 ));
196 }
197
198 #[test]
199 fn rejects_bad_values() {
200 assert!(matches!(
201 check_integrity(SHA256, b"abd"),
202 Err(IntegrityError::Mismatch { actual, .. }) if actual.starts_with("sha256-")
203 ));
204 for value in ["", " ", "sha256", "sha256-not base64!"] {
205 assert!(
206 matches!(
207 check_integrity(value, b"abc"),
208 Err(IntegrityError::Malformed { .. })
209 ),
210 "{value:?}"
211 );
212 }
213 assert!(matches!(
214 check_integrity("md5-AAAA", b"abc"),
215 Err(IntegrityError::Unsupported { .. })
216 ));
217 }
218}