modules: port password_hash (md5/sha1/sha2/pbkdf2 + MKPASSWD, hashed OPER) and hashident
This commit is contained in:
parent
dc22dcd072
commit
c71c28457f
4 changed files with 300 additions and 1 deletions
83
src/modules/hashident.rs
Normal file
83
src/modules/hashident.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
//! hashident — replace a user's ident with a stable, opaque 12-character token
|
||||
//! derived from their IP, so the username field leaks nothing (no `~guest`, no
|
||||
//! probed identd name) yet stays constant per address. reverse's own module.
|
||||
//!
|
||||
//! 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 —
|
||||
//! without the key it does nothing (the key is what makes the mapping unforgeable).
|
||||
//! Applied once, right after the user finishes connecting; all config-driven.
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue