]> code.octet-stream.net Git - hashgood/blob - src/verify.rs
Use paste feature in CI
[hashgood] / src / verify.rs
1 use super::{
2 Algorithm, CandidateHash, CandidateHashes, Hash, MatchLevel, MessageLevel, Opt, Verification,
3 VerificationSource,
4 };
5 #[cfg(feature = "paste")]
6 use clipboard::{ClipboardContext, ClipboardProvider};
7 use regex::Regex;
8 use std::fs::File;
9 use std::io;
10 use std::io::prelude::*;
11 use std::io::BufReader;
12 use std::path::PathBuf;
13
14 /// Calculate a list of candidate hashes based on the options specified.
15 /// If no hash options have been specified returns None.
16 /// It is assumed to be verified previously that at most one mode has been specified.
17 pub fn get_candidate_hashes(opt: &Opt) -> Result<Option<CandidateHashes>, String> {
18 if let Some(hash_string) = &opt.hash {
19 return Ok(Some(get_by_parameter(hash_string)?));
20 } else if opt.get_paste() {
21 return Ok(Some(get_from_clipboard()?));
22 } else if let Some(hash_file) = &opt.hash_file {
23 return Ok(Some(get_from_file(hash_file)?));
24 }
25 Ok(None)
26 }
27
28 /// Generate a candidate hash from the provided command line parameter, or throw an error.
29 fn get_by_parameter(param: &str) -> Result<CandidateHashes, String> {
30 let bytes =
31 hex::decode(&param).map_err(|_| "Provided hash is invalid or truncated hex".to_owned())?;
32 let alg = Algorithm::from_len(bytes.len())?;
33 let candidate = CandidateHash {
34 filename: None,
35 bytes,
36 };
37 Ok(CandidateHashes {
38 alg,
39 hashes: vec![candidate],
40 source: VerificationSource::CommandArgument,
41 })
42 }
43
44 /// Generate a candidate hash from the system clipboard, or throw an error.
45 fn get_from_clipboard() -> Result<CandidateHashes, String> {
46 #[cfg(feature = "paste")] {
47 let mut ctx: ClipboardContext = match ClipboardProvider::new() {
48 Ok(ctx) => ctx,
49 Err(e) => return Err(format!("Error getting system clipboard: {}", e)),
50 };
51
52 let possible_hash = match ctx.get_contents() {
53 Ok(value) => value,
54 Err(e) => format!("Error reading from clipboard: {}", e),
55 };
56
57 let bytes = hex::decode(&possible_hash)
58 .map_err(|_| "Clipboard contains invalid or truncated hex".to_owned())?;
59 let alg = Algorithm::from_len(bytes.len())?;
60 let candidate = CandidateHash {
61 filename: None,
62 bytes,
63 };
64 return Ok(CandidateHashes {
65 alg,
66 hashes: vec![candidate],
67 source: VerificationSource::Clipboard,
68 });
69 }
70 #[cfg(not(feature = "paste"))] {
71 return Err("Paste not implemented".to_owned());
72 }
73 }
74
75 /// Generate a candidate hash from the digests file specified (could be "-" for STDIN), or throw an error.
76 fn get_from_file(path: &PathBuf) -> Result<CandidateHashes, String> {
77 // Get a reader for either standard input or the chosen path
78 let reader: Box<dyn Read> = if path.to_str() == Some("-") {
79 Box::new(std::io::stdin())
80 } else {
81 Box::new(File::open(path).map_err(|_| {
82 format!(
83 "Unable to open check file at path '{}'",
84 path.to_string_lossy()
85 )
86 })?)
87 };
88
89 // Read the first line, trimmed
90 let mut reader = BufReader::new(reader);
91 let mut line = String::new();
92 reader
93 .read_line(&mut line)
94 .map_err(|_| "Error reading from check file".to_owned())?;
95 let line = line.trim().to_owned();
96
97 // Does our first line look like a raw hash on its own? If so, use that
98 if let Some(candidate) = read_raw_candidate_from_file(&line, &path) {
99 return Ok(candidate);
100 }
101
102 // Maybe it's a digests file
103 // Reconstruct the full iterator by joining our already-read line with the others
104 let full_lines = vec![Ok(line)].into_iter().chain(reader.lines());
105
106 // Does the entire file look like a coreutils-style digests file? (SHA1SUMS, etc.)
107 if let Some(candidate) = read_coreutils_digests_from_file(full_lines, &path) {
108 return Ok(candidate);
109 }
110
111 // If neither of these techniques worked this is a fatal error
112 // The user requested we use this input but we couldn't
113 Err(format!(
114 "Provided check file '{}' was neither a hash nor a valid digests file",
115 path.to_string_lossy()
116 ))
117 }
118
119 fn read_raw_candidate_from_file(line: &str, path: &PathBuf) -> Option<CandidateHashes> {
120 // It is a little sad to use a dynamic regex in an otherwise nice Rust program
121 // These deserve to be replaced with a good old fashioned static parser
122 // But let's be honest: the impact is negligible
123 let re = Regex::new(r"^([[:xdigit:]]{32}|[[:xdigit:]]{40}|[[:xdigit:]]{64})$").unwrap();
124 if re.is_match(line) {
125 // These should both always succeed due to the matching
126 let bytes = match hex::decode(line) {
127 Ok(bytes) => bytes,
128 _ => return None,
129 };
130 let alg = match Algorithm::from_len(bytes.len()) {
131 Ok(alg) => alg,
132 _ => return None,
133 };
134 return Some(CandidateHashes {
135 alg,
136 source: VerificationSource::RawFile(path.clone()),
137 hashes: vec![CandidateHash {
138 bytes,
139 filename: None,
140 }],
141 });
142 }
143 None
144 }
145
146 fn read_coreutils_digests_from_file<I>(lines: I, path: &PathBuf) -> Option<CandidateHashes>
147 where
148 I: Iterator<Item = io::Result<String>>,
149 {
150 let re = Regex::new(
151 r"^(?P<hash>([[:xdigit:]]{32}|[[:xdigit:]]{40}|[[:xdigit:]]{64})) .(?P<filename>.+)$",
152 )
153 .unwrap();
154
155 let mut hashes = vec![];
156 let mut alg: Option<Algorithm> = None;
157 for l in lines {
158 if let Ok(l) = l {
159 let l = l.trim();
160 // Allow (ignore) blank lines
161 if l.is_empty() {
162 continue;
163 }
164 // If we can capture a valid line, use it
165 if let Some(captures) = re.captures(&l) {
166 let hash = &captures["hash"];
167 let filename = &captures["filename"];
168 // Decode the hex and algorithm for this line
169 let line_bytes = match hex::decode(hash) {
170 Ok(bytes) => bytes,
171 _ => return None,
172 };
173 let line_alg = match Algorithm::from_len(line_bytes.len()) {
174 Ok(alg) => alg,
175 _ => return None,
176 };
177 if alg.is_some() && alg != Some(line_alg) {
178 // Different algorithms in the same digest file are not supported
179 return None;
180 } else {
181 // If we are the first line, we define the overall algorithm
182 alg = Some(line_alg);
183 }
184 // So far so good - create an entry for this line
185 hashes.push(CandidateHash {
186 bytes: line_bytes,
187 filename: Some(filename.to_owned()),
188 });
189 } else {
190 // But if we have a line with content we cannot parse, this is an error
191 return None;
192 }
193 }
194 }
195
196 // It is a failure if we got zero hashes or we somehow don't know the algorithm
197 if hashes.is_empty() {
198 return None;
199 }
200 let alg = match alg {
201 Some(alg) => alg,
202 _ => return None,
203 };
204
205 // Otherwise all is well and we can return our results
206 Some(CandidateHashes {
207 alg,
208 source: VerificationSource::DigestsFile(path.clone()),
209 hashes,
210 })
211 }
212
213 /// Determine if the calculated hash matches any of the candidates.
214 ///
215 /// Ok result: the hash matches, and if the candidate has a filename, that matches too
216 /// Maybe result: the hash matches but the filename does not
217 /// Fail result: neither of the above
218 pub fn verify_hash<'a>(calculated: &Hash, candidates: &'a CandidateHashes) -> Verification<'a> {
219 let mut ok: Option<&CandidateHash> = None;
220 let mut maybe: Option<&CandidateHash> = None;
221 let mut messages = Vec::new();
222
223 for candidate in &candidates.hashes {
224 if candidate.bytes == calculated.bytes {
225 match candidate.filename {
226 None => ok = Some(candidate),
227 Some(ref candidate_filename) if candidate_filename == &calculated.filename => {
228 ok = Some(candidate)
229 }
230 Some(ref candidate_filename) => {
231 messages.push((
232 MessageLevel::Warning,
233 format!(
234 "The matched hash has filename '{}', which does not match the input.",
235 candidate_filename
236 ),
237 ));
238 maybe = Some(candidate);
239 }
240 }
241 }
242 }
243
244 // Warn that a "successful" MD5 result is not necessarily great
245 if candidates.alg == Algorithm::Md5 && (ok.is_some() || maybe.is_some()) {
246 messages.push((
247 MessageLevel::Note,
248 "MD5 can easily be forged. Use a stronger algorithm if possible.".to_owned(),
249 ))
250 }
251
252 // If we got a full match, great
253 if ok.is_some() {
254 return Verification {
255 match_level: MatchLevel::Ok,
256 comparison_hash: ok,
257 messages,
258 };
259 }
260
261 // Second priority, a "maybe" result
262 if maybe.is_some() {
263 return Verification {
264 match_level: MatchLevel::Maybe,
265 comparison_hash: maybe,
266 messages,
267 };
268 }
269
270 // Otherwise we failed
271 // If we only had one candidate hash, include it
272 let comparison = match candidates.hashes.len() {
273 1 => Some(&candidates.hashes[0]),
274 _ => None,
275 };
276 Verification {
277 match_level: MatchLevel::Fail,
278 comparison_hash: comparison,
279 messages,
280 }
281 }