From d9e279d8eda121b30a11c44b4c041cb4859f1dbf Mon Sep 17 00:00:00 2001 From: reverse Date: Wed, 29 Jul 2026 18:29:56 +0000 Subject: [PATCH] Port skybot hash plugin as a module Co-Authored-By: Claude Opus 4.8 --- README.md | 9 +- src/bot/modules/hash.rs | 250 ++++++++++++++++++++++++++++++++++++++++ src/bot/modules/mod.rs | 2 + tests/hash.rs | 76 ++++++++++++ 4 files changed, 333 insertions(+), 4 deletions(-) create mode 100644 src/bot/modules/hash.rs create mode 100644 tests/hash.rs diff --git a/README.md b/README.md index 92f4327..0958c4e 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,11 @@ pub fn all() -> Vec> { ``` Shipped modules: **`builtins`** (`ping`, `echo`, `hello`), **`dice`** -(`roll [NdM]`), and **`weather`** (`add ` then `w [location]` via -wttr.in; per-user locations saved to a per-network JSON file, path from -`RUSTBOT_DATA_DIR`). Module tests live in `tests/` — construct a module and -assert on the `Action`s it returns, no network required. +(`roll [NdM]`), **`hash`** (`md5`/`sha1`/`sha256`/`hash `, hand-rolled), +and **`weather`** (`add ` then `w [location]` via wttr.in; per-user +locations saved to a per-network JSON file, path from `RUSTBOT_DATA_DIR`). +Module tests live in `tests/` — construct a module and assert on the `Action`s +it returns, no network required. ## Build & run diff --git a/src/bot/modules/hash.rs b/src/bot/modules/hash.rs new file mode 100644 index 0000000..985db90 --- /dev/null +++ b/src/bot/modules/hash.rs @@ -0,0 +1,250 @@ +use crate::bot::module::{Action, Command, CommandSpec, Module}; + +pub struct Hash; + +const COMMANDS: &[CommandSpec] = &[ + CommandSpec { name: "md5", usage: "md5 ", about: "MD5 hex digest of " }, + CommandSpec { name: "sha1", usage: "sha1 ", about: "SHA1 hex digest of " }, + CommandSpec { name: "sha256", usage: "sha256 ", about: "SHA256 hex digest of " }, + CommandSpec { name: "hash", usage: "hash ", about: "md5, sha1 and sha256 of " }, +]; + +impl Module for Hash { + fn name(&self) -> &'static str { + "hash" + } + + fn commands(&self) -> &'static [CommandSpec] { + COMMANDS + } + + fn on_command(&mut self, cmd: &Command) -> Vec { + let input = cmd.args.join(" "); + if input.is_empty() { + return vec![Action::reply(format!("usage: {}{} ", cmd.prefix, cmd.name))]; + } + let b = input.as_bytes(); + let reply = match cmd.name { + "md5" => md5(b), + "sha1" => sha1(b), + "sha256" => sha256(b), + "hash" => format!("md5: {}, sha1: {}, sha256: {}", md5(b), sha1(b), sha256(b)), + _ => return vec![], + }; + vec![Action::reply(reply)] + } +} + +fn pad(data: &[u8], little_endian_len: bool) -> Vec { + let bits = (data.len() as u64).wrapping_mul(8); + let mut m = data.to_vec(); + m.push(0x80); + while m.len() % 64 != 56 { + m.push(0); + } + if little_endian_len { + m.extend_from_slice(&bits.to_le_bytes()); + } else { + m.extend_from_slice(&bits.to_be_bytes()); + } + m +} + +fn to_hex(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for b in bytes { + s.push_str(&format!("{b:02x}")); + } + s +} + +pub fn md5(data: &[u8]) -> String { + const S: [u32; 64] = [ + 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, + 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, + 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, + ]; + const K: [u32; 64] = [ + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, + 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, + 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, + 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, + 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, + 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244, + 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, + 0xeb86d391, + ]; + + let (mut a0, mut b0, mut c0, mut d0) = + (0x67452301u32, 0xefcdab89u32, 0x98badcfeu32, 0x10325476u32); + let msg = pad(data, true); + for chunk in msg.chunks(64) { + let mut m = [0u32; 16]; + for (i, word) in m.iter_mut().enumerate() { + *word = u32::from_le_bytes([ + chunk[i * 4], + chunk[i * 4 + 1], + chunk[i * 4 + 2], + chunk[i * 4 + 3], + ]); + } + let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0); + for i in 0..64 { + let (f, g) = if i < 16 { + ((b & c) | (!b & d), i) + } else if i < 32 { + ((d & b) | (!d & c), (5 * i + 1) % 16) + } else if i < 48 { + (b ^ c ^ d, (3 * i + 5) % 16) + } else { + (c ^ (b | !d), (7 * i) % 16) + }; + let f = f.wrapping_add(a).wrapping_add(K[i]).wrapping_add(m[g]); + a = d; + d = c; + c = b; + b = b.wrapping_add(f.rotate_left(S[i])); + } + a0 = a0.wrapping_add(a); + b0 = b0.wrapping_add(b); + c0 = c0.wrapping_add(c); + d0 = d0.wrapping_add(d); + } + + let mut out = Vec::with_capacity(16); + for v in [a0, b0, c0, d0] { + out.extend_from_slice(&v.to_le_bytes()); + } + to_hex(&out) +} + +pub fn sha1(data: &[u8]) -> String { + let (mut h0, mut h1, mut h2, mut h3, mut h4) = ( + 0x67452301u32, + 0xEFCDAB89u32, + 0x98BADCFEu32, + 0x10325476u32, + 0xC3D2E1F0u32, + ); + let msg = pad(data, false); + for chunk in msg.chunks(64) { + let mut w = [0u32; 80]; + for i in 0..16 { + w[i] = u32::from_be_bytes([ + chunk[i * 4], + chunk[i * 4 + 1], + chunk[i * 4 + 2], + chunk[i * 4 + 3], + ]); + } + for i in 16..80 { + w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1); + } + let (mut a, mut b, mut c, mut d, mut e) = (h0, h1, h2, h3, h4); + for (i, &word) in w.iter().enumerate() { + let (f, k) = if i < 20 { + ((b & c) | (!b & d), 0x5A827999u32) + } else if i < 40 { + (b ^ c ^ d, 0x6ED9EBA1u32) + } else if i < 60 { + ((b & c) | (b & d) | (c & d), 0x8F1BBCDCu32) + } else { + (b ^ c ^ d, 0xCA62C1D6u32) + }; + let temp = a + .rotate_left(5) + .wrapping_add(f) + .wrapping_add(e) + .wrapping_add(k) + .wrapping_add(word); + e = d; + d = c; + c = b.rotate_left(30); + b = a; + a = temp; + } + h0 = h0.wrapping_add(a); + h1 = h1.wrapping_add(b); + h2 = h2.wrapping_add(c); + h3 = h3.wrapping_add(d); + h4 = h4.wrapping_add(e); + } + + let mut out = Vec::with_capacity(20); + for v in [h0, h1, h2, h3, h4] { + out.extend_from_slice(&v.to_be_bytes()); + } + to_hex(&out) +} + +pub fn sha256(data: &[u8]) -> String { + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2, + ]; + + let mut h: [u32; 8] = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, + 0x5be0cd19, + ]; + let msg = pad(data, false); + for chunk in msg.chunks(64) { + let mut w = [0u32; 64]; + for i in 0..16 { + w[i] = u32::from_be_bytes([ + chunk[i * 4], + chunk[i * 4 + 1], + chunk[i * 4 + 2], + chunk[i * 4 + 3], + ]); + } + for i in 16..64 { + let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3); + let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + .wrapping_add(s0) + .wrapping_add(w[i - 7]) + .wrapping_add(s1); + } + let mut v = h; + for i in 0..64 { + let s1 = v[4].rotate_right(6) ^ v[4].rotate_right(11) ^ v[4].rotate_right(25); + let ch = (v[4] & v[5]) ^ (!v[4] & v[6]); + let temp1 = v[7] + .wrapping_add(s1) + .wrapping_add(ch) + .wrapping_add(K[i]) + .wrapping_add(w[i]); + let s0 = v[0].rotate_right(2) ^ v[0].rotate_right(13) ^ v[0].rotate_right(22); + let maj = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]); + let temp2 = s0.wrapping_add(maj); + v[7] = v[6]; + v[6] = v[5]; + v[5] = v[4]; + v[4] = v[3].wrapping_add(temp1); + v[3] = v[2]; + v[2] = v[1]; + v[1] = v[0]; + v[0] = temp1.wrapping_add(temp2); + } + for i in 0..8 { + h[i] = h[i].wrapping_add(v[i]); + } + } + + let mut out = Vec::with_capacity(32); + for v in h { + out.extend_from_slice(&v.to_be_bytes()); + } + to_hex(&out) +} diff --git a/src/bot/modules/mod.rs b/src/bot/modules/mod.rs index c12f042..fdf7052 100644 --- a/src/bot/modules/mod.rs +++ b/src/bot/modules/mod.rs @@ -1,5 +1,6 @@ pub mod builtins; pub mod dice; +pub mod hash; pub mod weather; use super::module::Module; @@ -8,6 +9,7 @@ pub fn all(network: &str) -> Vec> { vec![ Box::new(builtins::Builtins), Box::new(dice::Dice::new()), + Box::new(hash::Hash), Box::new(weather::Weather::new(network)), ] } diff --git a/tests/hash.rs b/tests/hash.rs new file mode 100644 index 0000000..49b9351 --- /dev/null +++ b/tests/hash.rs @@ -0,0 +1,76 @@ +use rustbot::bot::module::{Action, Command, Module}; +use rustbot::bot::modules::hash::{md5, sha1, sha256, Hash}; + +fn cmd<'a>(name: &'a str, args: &'a [&'a str]) -> Command<'a> { + Command { sender: "u", reply_to: "#c", name, args, prefix: ">", network: "t" } +} + +fn reply(actions: &[Action]) -> &str { + match actions { + [Action::Reply(s)] => s, + _ => panic!("expected exactly one Reply"), + } +} + +const MULTI: &[u8] = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; + +#[test] +fn md5_vectors() { + assert_eq!(md5(b""), "d41d8cd98f00b204e9800998ecf8427e"); + assert_eq!(md5(b"abc"), "900150983cd24fb0d6963f7d28e17f72"); + assert_eq!( + md5(b"The quick brown fox jumps over the lazy dog"), + "9e107d9d372bb6826bd81d3542a419d6" + ); + assert_eq!( + md5(b"12345678901234567890123456789012345678901234567890123456789012345678901234567890"), + "57edf4a22be3c955ac49da2e2107b67a" + ); +} + +#[test] +fn sha1_vectors() { + assert_eq!(sha1(b""), "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + assert_eq!(sha1(b"abc"), "a9993e364706816aba3e25717850c26c9cd0d89d"); + assert_eq!(sha1(MULTI), "84983e441c3bd26ebaae4aa1f95129e5e54670f1"); +} + +#[test] +fn sha256_vectors() { + assert_eq!( + sha256(b""), + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ); + assert_eq!( + sha256(b"abc"), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ); + assert_eq!( + sha256(MULTI), + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1" + ); +} + +#[test] +fn module_single_hash() { + let mut m = Hash; + assert_eq!( + reply(&m.on_command(&cmd("sha256", &["abc"]))), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ); +} + +#[test] +fn module_all_hashes() { + let mut m = Hash; + let r = reply(&m.on_command(&cmd("hash", &["abc"]))).to_string(); + assert!(r.contains("md5: 900150983cd24fb0d6963f7d28e17f72"), "{r}"); + assert!(r.contains("sha1: a9993e364706816aba3e25717850c26c9cd0d89d"), "{r}"); + assert!(r.contains("sha256: ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"), "{r}"); +} + +#[test] +fn module_usage_when_empty() { + let mut m = Hash; + assert!(reply(&m.on_command(&cmd("md5", &[]))).contains("usage")); +}