use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Fixity {
Off,
#[default]
Payloads,
Full,
}
impl Fixity {
pub fn covers_payloads(self) -> bool {
matches!(self, Self::Payloads | Self::Full)
}
pub fn covers_bodies(self) -> bool {
matches!(self, Self::Full)
}
pub fn from_config_str(value: &str) -> Option<Self> {
match value {
"off" => Some(Self::Off),
"attachments" => Some(Self::Payloads),
"all" => Some(Self::Full),
_ => None,
}
}
pub fn as_config_str(self) -> &'static str {
match self {
Self::Off => "off",
Self::Payloads => "attachments",
Self::Full => "all",
}
}
}
pub fn digest(bytes: &[u8]) -> String {
let mut s = String::with_capacity(7 + 64);
s.push_str("sha256:");
for byte in Sha256::digest(bytes) {
s.push(char::from_digit((byte >> 4) as u32, 16).unwrap());
s.push(char::from_digit((byte & 0xf) as u32, 16).unwrap());
}
s
}
pub fn verify(bytes: &[u8], recorded: &str) -> bool {
match recorded.strip_prefix("sha256:") {
Some(_) => digest(bytes) == recorded,
None if recorded.is_empty() => true,
None => true,
}
}
pub fn is_recognized(recorded: &str) -> bool {
recorded.starts_with("sha256:")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_the_published_sha256_vectors() {
assert_eq!(
digest(b""),
"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(
digest(b"abc"),
"sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
assert_eq!(
digest(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
"sha256:248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
);
}
#[test]
fn crosses_a_block_boundary_correctly() {
let million_a = vec![b'a'; 1_000_000];
assert_eq!(
digest(&million_a),
"sha256:cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"
);
}
#[test]
fn verify_accepts_the_matching_digest_and_rejects_a_changed_byte() {
let recorded = digest(b"the original bytes");
assert!(verify(b"the original bytes", &recorded));
assert!(!verify(b"the corrupted bytes", &recorded));
}
#[test]
fn verify_never_cries_wolf_over_an_unrecorded_or_unknown_digest() {
assert!(verify(b"anything", ""));
assert!(verify(b"anything", "blake3:deadbeef"));
assert!(!is_recognized("blake3:deadbeef"));
assert!(is_recognized("sha256:e3b0c442"));
}
}