echoIRCd/src/modules/hashident.rs

81 lines
2.7 KiB
Rust

//! Replaces a user's ident with a stable, opaque 12-character token derived from
//! their IP, so the username field leaks nothing yet stays constant per address.
//!
//! The token is the first 6 bytes of `HMAC-SHA256(hashident_key, ip)`, hex-encoded
//! (12 chars). Off unless `hashident = yes` and a `hashident_key` secret is set;
//! the key is what makes the mapping unforgeable. Applied once, right after connect.
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::sign::Signer;
use crate::module::Module;
use crate::server::Server;
use crate::Uid;
/// `HMAC-SHA256(key, data)` via OpenSSL, or `None` on any OpenSSL error.
fn hmac_sha256(key: &[u8], data: &[u8]) -> Option<Vec<u8>> {
let pkey = PKey::hmac(key).ok()?;
let mut signer = Signer::new(MessageDigest::sha256(), &pkey).ok()?;
signer.update(data).ok()?;
signer.sign_to_vec().ok()
}
/// The 12-char hashed ident for `ip` under `key`, or `None` if HMAC fails.
fn hashed_ident(key: &str, ip: &str) -> Option<String> {
let mac = hmac_sha256(key.as_bytes(), ip.as_bytes())?;
let mut out = String::with_capacity(12);
for b in mac.iter().take(6) {
out.push(char::from_digit((b >> 4) as u32, 16).unwrap());
out.push(char::from_digit((b & 0xf) as u32, 16).unwrap());
}
Some(out)
}
pub struct HashIdent;
impl Module for HashIdent {
fn name(&self) -> &'static str {
"hashident"
}
fn on_user_connect(&mut self, srv: &mut Server, uid: Uid) {
if !srv.conf_bool("hashident", false) {
return;
}
let Some(key) = srv.conf("hashident_key").filter(|k| !k.is_empty()) else {
return; // no secret configured → do nothing (fail safe)
};
let key = key.to_string();
let ip = match srv.users.get(&uid) {
Some(u) => u.addr.ip().to_string(),
None => return,
};
if let Some(ident) = hashed_ident(&key, &ip) {
if let Some(u) = srv.users.get_mut(&uid) {
u.ident = ident;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ident_is_12_hex_chars_and_stable() {
let a = hashed_ident("secretkey", "203.0.113.7").unwrap();
let b = hashed_ident("secretkey", "203.0.113.7").unwrap();
assert_eq!(a, b); // same ip+key → same ident
assert_eq!(a.len(), 12);
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn different_key_or_ip_changes_it() {
let base = hashed_ident("k1", "203.0.113.7").unwrap();
assert_ne!(base, hashed_ident("k2", "203.0.113.7").unwrap());
assert_ne!(base, hashed_ident("k1", "203.0.113.8").unwrap());
}
}