use crate::ty::*;
use chacha20poly1305::{
aead::{Aead, NewAead},
XChaCha20Poly1305, XNonce,
};
use crc32fast::hash as crc32;
use e_utils::chrono::{DateTime, Duration, FixedOffset, Utc};
const KEY_SIZE: usize = 32;
const NONCE_SIZE: usize = 24;
pub struct ChaCha20Protocol {
secret_key: [u8; KEY_SIZE],
}
impl ChaCha20Protocol {
pub fn new(secret_key: [u8; KEY_SIZE]) -> Self {
Self { secret_key }
}
}
impl LicenseProtocol for ChaCha20Protocol {
fn generate(&self, hours: u64) -> String {
let nonce = e_utils::algorithm!([u8; NONCE_SIZE]);
let expire_time = (Utc::now() + Duration::hours(hours as i64)).timestamp();
let mut data = expire_time.to_le_bytes().to_vec();
let checksum = crc32(&data) ^ 0xDEADBEEF;
data.extend_from_slice(&checksum.to_le_bytes());
let cipher = XChaCha20Poly1305::new_from_slice(&self.secret_key).unwrap();
let ciphertext = cipher.encrypt(&nonce.into(), &*data).unwrap();
let mut output = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
output.extend_from_slice(&nonce);
output.extend(ciphertext);
e_utils::algorithm::base64::encode(output).trim_end_matches('=').to_string()
}
fn verify(&self, license: &str) -> e_utils::Result<DateTime<FixedOffset>> {
let padded_license = {
let padding_len = (4 - license.len() % 4) % 4;
let mut padded = String::with_capacity(license.len() + padding_len);
padded.push_str(license);
padded.extend(std::iter::repeat('=').take(padding_len));
padded
};
let data = e_utils::algorithm::base64::decode(&padded_license).map_err(|_| "Invalid format")?;
if data.len() < NONCE_SIZE + 12 {
return Err("Invalid license".into());
}
let (nonce, ciphertext) = data.split_at(NONCE_SIZE);
let cipher = XChaCha20Poly1305::new_from_slice(&self.secret_key).unwrap();
let plaintext = cipher.decrypt(XNonce::from_slice(nonce), ciphertext).map_err(|_| "Decryption failed")?;
if plaintext.len() != 12 {
return Err("Invalid data length".into());
}
let (time_bytes, checksum_bytes) = plaintext.split_at(8);
let expire_time = i64::from_le_bytes(time_bytes.try_into().unwrap());
let stored_checksum = u32::from_le_bytes(checksum_bytes.try_into().unwrap());
let calculated_checksum = crc32(time_bytes) ^ 0xDEADBEEF;
if stored_checksum != calculated_checksum {
return Err("Checksum mismatch".into());
}
let china_timezone = FixedOffset::east_opt(8 * 3600).unwrap();
let expire = DateTime::from_timestamp(expire_time, 0)
.ok_or("Invalid timestamp")?
.with_timezone(&china_timezone);
if Utc::now().with_timezone(&china_timezone) > expire {
return Err("License expired".into());
}
Ok(expire)
}
}