Skip to main content

zerodds_wire/
digest.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The digest the KeyHash uses.
4//!
5//! Its own copy rather than a dependency on the frontend: a runtime that
6//! reached into the frontend for a hash would be a runtime that cannot
7//! ship without it.
8
9/// MD5, as DDSI-RTPS specifies it for the KeyHash.
10///
11/// Not a security primitive here and not used as one: the specification
12/// names MD5 for this, and a different digest would produce hashes that
13/// no other vendor agrees with. DDSI-RTPS 2.5 §9.6.3.8.
14#[allow(clippy::many_single_char_names)]
15pub fn md5(input: &[u8]) -> [u8; 16] {
16    const S: [u32; 64] = [
17        7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5,
18        9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10,
19        15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
20    ];
21    const K: [u32; 64] = [
22        0xd76a_a478,
23        0xe8c7_b756,
24        0x2420_70db,
25        0xc1bd_ceee,
26        0xf57c_0faf,
27        0x4787_c62a,
28        0xa830_4613,
29        0xfd46_9501,
30        0x6980_98d8,
31        0x8b44_f7af,
32        0xffff_5bb1,
33        0x895c_d7be,
34        0x6b90_1122,
35        0xfd98_7193,
36        0xa679_438e,
37        0x49b4_0821,
38        0xf61e_2562,
39        0xc040_b340,
40        0x265e_5a51,
41        0xe9b6_c7aa,
42        0xd62f_105d,
43        0x0244_1453,
44        0xd8a1_e681,
45        0xe7d3_fbc8,
46        0x21e1_cde6,
47        0xc337_07d6,
48        0xf4d5_0d87,
49        0x455a_14ed,
50        0xa9e3_e905,
51        0xfcef_a3f8,
52        0x676f_02d9,
53        0x8d2a_4c8a,
54        0xfffa_3942,
55        0x8771_f681,
56        0x6d9d_6122,
57        0xfde5_380c,
58        0xa4be_ea44,
59        0x4bde_cfa9,
60        0xf6bb_4b60,
61        0xbebf_bc70,
62        0x289b_7ec6,
63        0xeaa1_27fa,
64        0xd4ef_3085,
65        0x0488_1d05,
66        0xd9d4_d039,
67        0xe6db_99e5,
68        0x1fa2_7cf8,
69        0xc4ac_5665,
70        0xf429_2244,
71        0x432a_ff97,
72        0xab94_23a7,
73        0xfc93_a039,
74        0x655b_59c3,
75        0x8f0c_cc92,
76        0xffef_f47d,
77        0x8584_5dd1,
78        0x6fa8_7e4f,
79        0xfe2c_e6e0,
80        0xa301_4314,
81        0x4e08_11a1,
82        0xf753_7e82,
83        0xbd3a_f235,
84        0x2ad7_d2bb,
85        0xeb86_d391,
86    ];
87
88    let mut state = [0x6745_2301_u32, 0xefcd_ab89, 0x98ba_dcfe, 0x1032_5476];
89    let mut blocks = input.chunks_exact(64);
90    for block in &mut blocks {
91        compress(&mut state, block, &S, &K);
92    }
93    // RFC 1321 §3.1–3.2: the rest of the input, one `0x80`, zeros, and the
94    // bit length in the last eight octets — one block when the rest leaves
95    // room for the length, two when it does not. On the stack: the KeyHash
96    // is computed for every write of a keyed sample, and the copy of the
97    // whole input this used to make was a heap allocation per write.
98    let rest = blocks.remainder();
99    let mut tail = [0u8; 128];
100    tail[..rest.len()].copy_from_slice(rest);
101    tail[rest.len()] = 0x80;
102    let tail_len = if rest.len() < 56 { 64 } else { 128 };
103    let bit_length = (input.len() as u64).wrapping_mul(8);
104    tail[tail_len - 8..tail_len].copy_from_slice(&bit_length.to_le_bytes());
105    for block in tail[..tail_len].chunks_exact(64) {
106        compress(&mut state, block, &S, &K);
107    }
108
109    let mut out = [0u8; 16];
110    out[0..4].copy_from_slice(&state[0].to_le_bytes());
111    out[4..8].copy_from_slice(&state[1].to_le_bytes());
112    out[8..12].copy_from_slice(&state[2].to_le_bytes());
113    out[12..16].copy_from_slice(&state[3].to_le_bytes());
114    out
115}
116
117/// One 64-octet block into the state (RFC 1321 §3.4).
118///
119/// The four rounds as four loops, so the round function and the message
120/// index are fixed per loop rather than chosen by a `match` in each of the
121/// 64 steps.
122fn compress(state: &mut [u32; 4], block: &[u8], s: &[u32; 64], k: &[u32; 64]) {
123    let mut words = [0u32; 16];
124    for (index, word) in words.iter_mut().enumerate() {
125        let at = index * 4;
126        *word = u32::from_le_bytes([block[at], block[at + 1], block[at + 2], block[at + 3]]);
127    }
128    let mut v = *state;
129    for i in 0..16 {
130        let [_, b, c, d] = v;
131        step(&mut v, (b & c) | (!b & d), words[i], k[i], s[i]);
132    }
133    for i in 16..32 {
134        let [_, b, c, d] = v;
135        step(
136            &mut v,
137            (d & b) | (!d & c),
138            words[(5 * i + 1) % 16],
139            k[i],
140            s[i],
141        );
142    }
143    for i in 32..48 {
144        let [_, b, c, d] = v;
145        step(&mut v, b ^ c ^ d, words[(3 * i + 5) % 16], k[i], s[i]);
146    }
147    for i in 48..64 {
148        let [_, b, c, d] = v;
149        step(&mut v, c ^ (b | !d), words[(7 * i) % 16], k[i], s[i]);
150    }
151    for (word, add) in state.iter_mut().zip(v) {
152        *word = word.wrapping_add(add);
153    }
154}
155
156/// One step: `(a, b, c, d)` becomes `(d, b + ((a + f + k + word) <<< s), b, c)`.
157#[inline(always)]
158fn step(v: &mut [u32; 4], f: u32, word: u32, k: u32, s: u32) {
159    let [a, b, c, d] = *v;
160    let sum = a.wrapping_add(f).wrapping_add(k).wrapping_add(word);
161    *v = [d, b.wrapping_add(sum.rotate_left(s)), b, c];
162}