aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-binary/src
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 15:31:57 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 15:31:57 +0300
commitaa1b809bd804655da1f5662c1553698883a92b52 (patch)
tree0567ad9b171a582835e84243fcb7df9476c1100e /crates/fparkan-binary/src
parentf69c893a401730339ad72610c573e20282573045 (diff)
downloadfparkan-aa1b809bd804655da1f5662c1553698883a92b52.tar.xz
fparkan-aa1b809bd804655da1f5662c1553698883a92b52.zip
fix: strengthen resource fingerprints
Diffstat (limited to 'crates/fparkan-binary/src')
-rw-r--r--crates/fparkan-binary/src/lib.rs192
1 files changed, 192 insertions, 0 deletions
diff --git a/crates/fparkan-binary/src/lib.rs b/crates/fparkan-binary/src/lib.rs
index ef5a0e4..793719a 100644
--- a/crates/fparkan-binary/src/lib.rs
+++ b/crates/fparkan-binary/src/lib.rs
@@ -3,6 +3,9 @@
use std::fmt;
+/// SHA-256 digest bytes.
+pub type Sha256Digest = [u8; 32];
+
/// Parser limits shared by binary formats.
#[derive(Clone, Copy, Debug)]
pub struct Limits {
@@ -262,6 +265,183 @@ pub fn read_lp_bytes(cursor: &mut Cursor<'_>, max: u32) -> Result<Vec<u8>, Decod
Ok(cursor.read_exact(len)?.to_vec())
}
+/// Computes a SHA-256 content digest without external dependencies.
+#[must_use]
+pub fn sha256(bytes: &[u8]) -> Sha256Digest {
+ const K: [u32; 64] = [
+ 0x428a_2f98,
+ 0x7137_4491,
+ 0xb5c0_fbcf,
+ 0xe9b5_dba5,
+ 0x3956_c25b,
+ 0x59f1_11f1,
+ 0x923f_82a4,
+ 0xab1c_5ed5,
+ 0xd807_aa98,
+ 0x1283_5b01,
+ 0x2431_85be,
+ 0x550c_7dc3,
+ 0x72be_5d74,
+ 0x80de_b1fe,
+ 0x9bdc_06a7,
+ 0xc19b_f174,
+ 0xe49b_69c1,
+ 0xefbe_4786,
+ 0x0fc1_9dc6,
+ 0x240c_a1cc,
+ 0x2de9_2c6f,
+ 0x4a74_84aa,
+ 0x5cb0_a9dc,
+ 0x76f9_88da,
+ 0x983e_5152,
+ 0xa831_c66d,
+ 0xb003_27c8,
+ 0xbf59_7fc7,
+ 0xc6e0_0bf3,
+ 0xd5a7_9147,
+ 0x06ca_6351,
+ 0x1429_2967,
+ 0x27b7_0a85,
+ 0x2e1b_2138,
+ 0x4d2c_6dfc,
+ 0x5338_0d13,
+ 0x650a_7354,
+ 0x766a_0abb,
+ 0x81c2_c92e,
+ 0x9272_2c85,
+ 0xa2bf_e8a1,
+ 0xa81a_664b,
+ 0xc24b_8b70,
+ 0xc76c_51a3,
+ 0xd192_e819,
+ 0xd699_0624,
+ 0xf40e_3585,
+ 0x106a_a070,
+ 0x19a4_c116,
+ 0x1e37_6c08,
+ 0x2748_774c,
+ 0x34b0_bcb5,
+ 0x391c_0cb3,
+ 0x4ed8_aa4a,
+ 0x5b9c_ca4f,
+ 0x682e_6ff3,
+ 0x748f_82ee,
+ 0x78a5_636f,
+ 0x84c8_7814,
+ 0x8cc7_0208,
+ 0x90be_fffa,
+ 0xa450_6ceb,
+ 0xbef9_a3f7,
+ 0xc671_78f2,
+ ];
+ let mut h = [
+ 0x6a09_e667,
+ 0xbb67_ae85,
+ 0x3c6e_f372,
+ 0xa54f_f53a,
+ 0x510e_527f,
+ 0x9b05_688c,
+ 0x1f83_d9ab,
+ 0x5be0_cd19,
+ ];
+
+ let bit_len = (bytes.len() as u64).wrapping_mul(8);
+ let mut chunks = bytes.chunks_exact(64);
+ for chunk in &mut chunks {
+ compress_sha256_chunk(&mut h, chunk, &K);
+ }
+
+ let tail = chunks.remainder();
+ let mut block = [0u8; 128];
+ block[..tail.len()].copy_from_slice(tail);
+ block[tail.len()] = 0x80;
+ let padded_len = if tail.len() < 56 { 64 } else { 128 };
+ block[padded_len - 8..padded_len].copy_from_slice(&bit_len.to_be_bytes());
+ for chunk in block[..padded_len].chunks_exact(64) {
+ compress_sha256_chunk(&mut h, chunk, &K);
+ }
+
+ let mut out = [0u8; 32];
+ for (idx, word) in h.iter().enumerate() {
+ out[idx * 4..idx * 4 + 4].copy_from_slice(&word.to_be_bytes());
+ }
+ out
+}
+
+/// Renders a SHA-256 digest as lowercase hexadecimal.
+#[must_use]
+pub fn sha256_hex(digest: &Sha256Digest) -> String {
+ const HEX: &[u8; 16] = b"0123456789abcdef";
+ let mut out = String::with_capacity(64);
+ for byte in digest {
+ out.push(char::from(HEX[usize::from(byte >> 4)]));
+ out.push(char::from(HEX[usize::from(byte & 0x0f)]));
+ }
+ out
+}
+
+#[allow(clippy::many_single_char_names)]
+fn compress_sha256_chunk(h: &mut [u32; 8], chunk: &[u8], k: &[u32; 64]) {
+ let mut w = [0u32; 64];
+ for (idx, word) in w.iter_mut().take(16).enumerate() {
+ let base = idx * 4;
+ *word = u32::from_be_bytes([
+ chunk[base],
+ chunk[base + 1],
+ chunk[base + 2],
+ chunk[base + 3],
+ ]);
+ }
+ for idx in 16..64 {
+ let s0 = w[idx - 15].rotate_right(7) ^ w[idx - 15].rotate_right(18) ^ (w[idx - 15] >> 3);
+ let s1 = w[idx - 2].rotate_right(17) ^ w[idx - 2].rotate_right(19) ^ (w[idx - 2] >> 10);
+ w[idx] = w[idx - 16]
+ .wrapping_add(s0)
+ .wrapping_add(w[idx - 7])
+ .wrapping_add(s1);
+ }
+
+ let mut a = h[0];
+ let mut b = h[1];
+ let mut c = h[2];
+ let mut d = h[3];
+ let mut e = h[4];
+ let mut f = h[5];
+ let mut g = h[6];
+ let mut hh = h[7];
+
+ for idx in 0..64 {
+ let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
+ let ch = (e & f) ^ ((!e) & g);
+ let temp1 = hh
+ .wrapping_add(s1)
+ .wrapping_add(ch)
+ .wrapping_add(k[idx])
+ .wrapping_add(w[idx]);
+ let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
+ let maj = (a & b) ^ (a & c) ^ (b & c);
+ let temp2 = s0.wrapping_add(maj);
+
+ hh = g;
+ g = f;
+ f = e;
+ e = d.wrapping_add(temp1);
+ d = c;
+ c = b;
+ b = a;
+ a = temp1.wrapping_add(temp2);
+ }
+
+ h[0] = h[0].wrapping_add(a);
+ h[1] = h[1].wrapping_add(b);
+ h[2] = h[2].wrapping_add(c);
+ h[3] = h[3].wrapping_add(d);
+ h[4] = h[4].wrapping_add(e);
+ h[5] = h[5].wrapping_add(f);
+ h[6] = h[6].wrapping_add(g);
+ h[7] = h[7].wrapping_add(hh);
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -305,4 +485,16 @@ mod tests {
);
assert_eq!(cursor.offset(), 4);
}
+
+ #[test]
+ fn sha256_matches_known_vectors() {
+ assert_eq!(
+ sha256_hex(&sha256(b"")),
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ );
+ assert_eq!(
+ sha256_hex(&sha256(b"abc")),
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
+ );
+ }
}