use std::fmt;
use argon2::Argon2;
use argon2::password_hash::phc::PasswordHash;
use argon2::password_hash::{PasswordHasher, PasswordVerifier};
use base64ct::{Base64, Encoding};
use chacha20poly1305::aead::{Aead, Generate, KeyInit};
use chacha20poly1305::{XChaCha20Poly1305, XNonce};
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
use crate::config::Config;
pub struct Hash;
impl Hash {
pub fn make(password: &str) -> String {
Argon2::default()
.hash_password(password.as_bytes())
.expect("password hashing needs the operating system random source")
.to_string()
}
pub fn check(password: &str, hash: &str) -> bool {
PasswordHash::new(hash).is_ok_and(|parsed| {
Argon2::default()
.verify_password(password.as_bytes(), &parsed)
.is_ok()
})
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Key([u8; 32]);
impl Key {
#[cfg(all(feature = "auth", feature = "mail"))]
pub(crate) fn bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn generate() -> Self {
let mut bytes = [0; 32];
getrandom::fill(&mut bytes).expect("keys need the operating system random source");
Self(bytes)
}
pub fn parse(text: &str) -> Option<Self> {
let encoded = text.trim().strip_prefix("base64:").unwrap_or(text.trim());
let bytes = Base64::decode_vec(encoded).ok()?;
Some(Self(bytes.try_into().ok()?))
}
pub fn from_config(config: &Config) -> Self {
let text = config
.get("APP_KEY")
.unwrap_or_else(|| panic!("config key APP_KEY is missing; run clamp key:generate"));
Self::parse(text)
.unwrap_or_else(|| panic!("config key APP_KEY is set but is not a base64: key"))
}
}
impl fmt::Display for Key {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "base64:{}", Base64::encode_string(&self.0))
}
}
impl fmt::Debug for Key {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Key(..)")
}
}
pub struct Crypt {
cipher: XChaCha20Poly1305,
}
impl Crypt {
pub fn new(key: &Key) -> Self {
Self {
cipher: XChaCha20Poly1305::new_from_slice(&key.0).expect("keys are 32 bytes"),
}
}
pub fn encrypt(&self, plaintext: &[u8]) -> String {
let nonce = XNonce::generate();
let sealed = self
.cipher
.encrypt(&nonce, plaintext)
.expect("encryption of an in-memory buffer does not fail");
let mut out = nonce.to_vec();
out.extend(sealed);
Base64::encode_string(&out)
}
pub fn decrypt(&self, sealed: &str) -> Option<Vec<u8>> {
let bytes = Base64::decode_vec(sealed.trim()).ok()?;
if bytes.len() < 24 {
return None;
}
let (nonce, ciphertext) = bytes.split_at(24);
let nonce = XNonce::try_from(nonce).ok()?;
self.cipher.decrypt(&nonce, ciphertext).ok()
}
}
pub fn sha256(data: &[u8]) -> String {
hex(&Sha256::digest(data))
}
pub fn hmac_sha256(key: &[u8], data: &[u8]) -> String {
let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC takes any key length");
mac.update(data);
hex(&mac.finalize().into_bytes())
}
pub fn hmac_verify(key: &[u8], data: &[u8], signature: &str) -> bool {
let Some(expected) = unhex(signature) else {
return false;
};
let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC takes any key length");
mac.update(data);
mac.verify_slice(&expected).is_ok()
}
pub fn random_bytes(len: usize) -> Vec<u8> {
let mut bytes = vec![0; len];
getrandom::fill(&mut bytes).expect("tokens need the operating system random source");
bytes
}
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
a.len() == b.len()
&& std::hint::black_box(a.iter().zip(b).fold(0, |diff, (x, y)| diff | (x ^ y))) == 0
}
pub fn sha1(data: &[u8]) -> [u8; 20] {
let mut state: [u32; 5] = [
0x6745_2301,
0xEFCD_AB89,
0x98BA_DCFE,
0x1032_5476,
0xC3D2_E1F0,
];
let mut message = data.to_vec();
message.push(0x80);
while message.len() % 64 != 56 {
message.push(0);
}
message.extend_from_slice(&(data.len() as u64 * 8).to_be_bytes());
for block in message.as_chunks::<64>().0 {
let mut w = [0u32; 80];
for (i, word) in block.as_chunks::<4>().0.iter().enumerate() {
w[i] = u32::from_be_bytes(*word);
}
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] = state;
for (i, word) in w.iter().enumerate() {
let (f, k) = match i {
0..20 => ((b & c) | (!b & d), 0x5A82_7999),
20..40 => (b ^ c ^ d, 0x6ED9_EBA1),
40..60 => ((b & c) | (b & d) | (c & d), 0x8F1B_BCDC),
_ => (b ^ c ^ d, 0xCA62_C1D6),
};
let t = a
.rotate_left(5)
.wrapping_add(f)
.wrapping_add(e)
.wrapping_add(k)
.wrapping_add(*word);
(e, d, c, b, a) = (d, c, b.rotate_left(30), a, t);
}
for (s, v) in state.iter_mut().zip([a, b, c, d, e]) {
*s = s.wrapping_add(v);
}
}
let mut digest = [0; 20];
for (out, word) in digest.as_chunks_mut::<4>().0.iter_mut().zip(state) {
*out = word.to_be_bytes();
}
digest
}
pub fn base64_encode(bytes: &[u8]) -> String {
Base64::encode_string(bytes)
}
pub fn base64_decode(text: &str) -> Option<Vec<u8>> {
Base64::decode_vec(text).ok()
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
fn unhex(text: &str) -> Option<Vec<u8>> {
if !text.len().is_multiple_of(2) {
return None;
}
(0..text.len())
.step_by(2)
.map(|index| u8::from_str_radix(text.get(index..index + 2)?, 16).ok())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn protocol_primitives() {
let accept = sha1(b"dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
assert_eq!(base64_encode(&accept), "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
assert_eq!(hex(&sha1(b"")), "da39a3ee5e6b4b0d3255bfef95601890afd80709");
assert_eq!(
hex(&sha1(b"abc")),
"a9993e364706816aba3e25717850c26c9cd0d89d"
);
assert_eq!(
hex(&sha1(
b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
)),
"84983e441c3bd26ebaae4aa1f95129e5e54670f1"
);
assert_eq!(
hex(&sha1(&[b'a'; 1000])),
"291e9a6c66994949b57ba5e650361e98fc36b1ba"
);
assert_eq!(base64_decode("aGk=").as_deref(), Some(&b"hi"[..]));
assert_eq!(base64_decode("not base64!"), None);
assert!(constant_time_eq(b"key-1", b"key-1"));
assert!(!constant_time_eq(b"key-1", b"key-2"));
assert!(!constant_time_eq(b"key", b"key-1"));
assert_eq!(random_bytes(32).len(), 32);
assert_ne!(random_bytes(16), random_bytes(16));
}
#[test]
fn digests_match_known_answers() {
assert_eq!(
sha256(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
assert_eq!(
hmac_sha256(b"Jefe", b"what do ya want for nothing?"),
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
);
let signature = hmac_sha256(b"key", b"/download/7");
assert!(hmac_verify(b"key", b"/download/7", &signature));
assert!(!hmac_verify(b"key", b"/download/8", &signature));
assert!(!hmac_verify(b"key", b"/download/7", "zz"));
}
#[test]
fn keys_round_trip_and_stay_secret() {
let key = Key::generate();
let text = key.to_string();
assert!(text.starts_with("base64:"));
assert_eq!(Key::parse(&text), Some(key.clone()));
assert_eq!(format!("{key:?}"), "Key(..)");
assert_eq!(Key::parse("base64:c2hvcnQ="), None, "too short");
let config = Config::parse(&format!("APP_KEY={text}"));
assert_eq!(Key::from_config(&config), key);
}
#[test]
fn tampering_fails_to_decrypt() {
let crypt = Crypt::new(&Key::generate());
let sealed = crypt.encrypt(b"secret");
assert_ne!(sealed, crypt.encrypt(b"secret"), "a fresh nonce each time");
let mut bytes = Base64::decode_vec(&sealed).unwrap();
let last = bytes.len() - 1;
bytes[last] ^= 1;
assert!(crypt.decrypt(&Base64::encode_string(&bytes)).is_none());
assert!(crypt.decrypt("not base64!").is_none());
assert!(crypt.decrypt("AAAA").is_none());
}
#[test]
fn malformed_hash_is_a_mismatch() {
assert!(!Hash::check("x", "not a hash"));
assert!(Hash::make("same") != Hash::make("same"), "salted");
}
#[test]
#[should_panic(expected = "APP_KEY is missing")]
fn missing_key_stops_the_app() {
Key::from_config(&Config::default());
}
}