rustcrypt 0.3.0-beta.1

Macro-first encryption and obfuscation library for Rust
Documentation
use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Nonce};
use base64::{engine::general_purpose::STANDARD, Engine};
use pbkdf2::pbkdf2_hmac;
use rand::rngs::OsRng;
use rand::TryRngCore;
use sha2::Sha256;
use zeroize::{Zeroize, Zeroizing};

const SALT_LENGTH: u8 = 8;
const DEFAULT_SEPARATOR: &str = "$";
const VERSION: &str = "o1";

#[derive(Debug)]
pub enum TextObfError { Invalid, Unsupported, Encrypt, Decrypt, B64, Rng }

impl From<aes_gcm::Error> for TextObfError { fn from(_: aes_gcm::Error) -> Self { TextObfError::Decrypt } }

#[derive(Clone)]
pub struct TextObfConfig { passphrase: Vec<u8>, salt_length: u8, separator: String, iterations: u32 }

pub struct TextObfuscator { config: TextObfConfig }

impl TextObfuscator {
    pub fn new(passphrase: &[u8]) -> Self {
        if passphrase.is_empty() { panic!("empty passphrase") }
        let config = TextObfConfig { passphrase: passphrase.to_vec(), salt_length: SALT_LENGTH, separator: DEFAULT_SEPARATOR.to_string(), iterations: 1000 };
        Self { config }
    }
    pub fn with_salt_length(mut self, length: u8) -> Self { if length == 0 { panic!("salt length 0") } self.config.salt_length = length; self }
    pub fn with_separator(mut self, separator: &str) -> Self { self.config.separator = separator.to_string(); self }
    pub fn with_iterations(mut self, iterations: u32) -> Self { if iterations == 0 { panic!("iterations 0") } self.config.iterations = iterations; self }
    pub fn obfuscate(&self, text: &str) -> Result<String, TextObfError> {
        let salt = self.gen_salt(self.config.salt_length)?;
        let key = self.derive_key(&self.config.passphrase, &salt);
        let iv = self.gen_iv()?;
        let ct = self.encrypt(text.as_bytes(), &key, &iv)?;
        let es = STANDARD.encode(&salt);
        let ei = STANDARD.encode(&iv);
        let ec = STANDARD.encode(&ct);
        Ok(format!("{}{}{}{}{}{}{}{}", self.config.separator, VERSION, self.config.separator, es, self.config.separator, ei, self.config.separator, ec))
    }
    pub fn unobfuscate(&self, s: &str) -> Result<String, TextObfError> {
        let parts: Vec<&str> = s.split(&self.config.separator).collect();
        let parts = if parts.first().map(|x| x.is_empty()).unwrap_or(false) { &parts[1..] } else { &parts[..] };
        if parts.len() != 4 { return Err(TextObfError::Invalid) }
        match parts[0] { "o1" => { let b = self.decrypt(parts)?; Ok(String::from_utf8_lossy(&b).to_string()) }, _ => Err(TextObfError::Unsupported) }
    }
    pub fn obfuscate_bytes(&self, bytes: &[u8]) -> Result<String, TextObfError> {
        let salt = self.gen_salt(self.config.salt_length)?;
        let key = self.derive_key(&self.config.passphrase, &salt);
        let iv = self.gen_iv()?;
        let ct = self.encrypt(bytes, &key, &iv)?;
        let es = STANDARD.encode(&salt);
        let ei = STANDARD.encode(&iv);
        let ec = STANDARD.encode(&ct);
        Ok(format!("{}{}{}{}{}{}{}{}", self.config.separator, VERSION, self.config.separator, es, self.config.separator, ei, self.config.separator, ec))
    }
    pub fn unobfuscate_bytes(&self, s: &str) -> Result<Zeroizing<Vec<u8>>, TextObfError> {
        let parts: Vec<&str> = s.split(&self.config.separator).collect();
        let parts = if parts.first().map(|x| x.is_empty()).unwrap_or(false) { &parts[1..] } else { &parts[..] };
        if parts.len() != 4 { return Err(TextObfError::Invalid) }
        match parts[0] { "o1" => { let b = self.decrypt(parts)?; Ok(Zeroizing::new(b)) }, _ => Err(TextObfError::Unsupported) }
    }
    pub fn obfuscate_to_parts(&self, input: &[u8]) -> Result<(String, String, String), TextObfError> {
        let salt = self.gen_salt(self.config.salt_length)?;
        let key = self.derive_key(&self.config.passphrase, &salt);
        let iv = self.gen_iv()?;
        let ct = self.encrypt(input, &key, &iv)?;
        Ok((STANDARD.encode(&salt), STANDARD.encode(&iv), STANDARD.encode(&ct)))
    }
    pub fn unobfuscate_from_parts(&self, salt_b64: &str, iv_b64: &str, ct_b64: &str) -> Result<Zeroizing<Vec<u8>>, TextObfError> {
        let salt = STANDARD.decode(salt_b64).map_err(|_| TextObfError::B64)?;
        let iv = STANDARD.decode(iv_b64).map_err(|_| TextObfError::B64)?;
        let ct = STANDARD.decode(ct_b64).map_err(|_| TextObfError::B64)?;
        let key = self.derive_key(&self.config.passphrase, &salt);
        let cipher = Aes256Gcm::new_from_slice(&key).map_err(|_| TextObfError::Encrypt)?;
        let nonce = Nonce::from_slice(&iv);
        let out = cipher.decrypt(nonce, ct.as_ref()).map_err(TextObfError::from)?;
        Ok(Zeroizing::new(out))
    }
    fn decrypt(&self, p: &[&str]) -> Result<Vec<u8>, TextObfError> {
        let salt = STANDARD.decode(p[1]).map_err(|_| TextObfError::B64)?;
        let iv = STANDARD.decode(p[2]).map_err(|_| TextObfError::B64)?;
        let ct = STANDARD.decode(p[3]).map_err(|_| TextObfError::B64)?;
        let key = self.derive_key(&self.config.passphrase, &salt);
        let cipher = Aes256Gcm::new_from_slice(&key).map_err(|_| TextObfError::Encrypt)?;
        let nonce = Nonce::from_slice(&iv);
        let out = cipher.decrypt(nonce, ct.as_ref()).map_err(TextObfError::from)?;
        Ok(out)
    }
    fn encrypt(&self, bytes: &[u8], key: &[u8], iv: &[u8]) -> Result<Vec<u8>, TextObfError> {
        let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| TextObfError::Encrypt)?;
        let nonce = Nonce::from_slice(iv);
        let out = cipher.encrypt(nonce, bytes).map_err(|_| TextObfError::Encrypt)?;
        Ok(out)
    }
    fn gen_salt(&self, len: u8) -> Result<Vec<u8>, TextObfError> { let mut s = vec![0u8; len as usize]; OsRng.try_fill_bytes(&mut s).map_err(|_| TextObfError::Rng)?; Ok(s) }
    fn derive_key(&self, pass: &[u8], salt: &[u8]) -> Vec<u8> { let mut k = [0u8; 32]; pbkdf2_hmac::<Sha256>(pass, salt, self.config.iterations, &mut k); k.to_vec() }
    fn gen_iv(&self) -> Result<Vec<u8>, TextObfError> { let mut iv = vec![0u8; 12]; OsRng.try_fill_bytes(&mut iv).map_err(|_| TextObfError::Rng)?; Ok(iv) }
}

pub const TEXT_OBF_DEFAULT_SEPARATOR: &str = DEFAULT_SEPARATOR;

impl Drop for TextObfuscator { fn drop(&mut self) { self.config.passphrase.zeroize(); } }