ender-eye 1.0.0

Encrypt and decrypt messages using SGA encoding + AES-256-GCM, inspired by the Standard Galactic Alphabet from Minecraft.
Documentation
use aes_gcm::{
    Aes256Gcm, Key, Nonce,
    aead::{Aead, KeyInit},
};
use argon2::Argon2;
use rand::RngCore;
use rand::rngs::OsRng;
use zeroize::Zeroizing;

use crate::error::Result;
use crate::error::ValidationErrors;

pub fn derive_key(password: &str, salt: &[u8]) -> Result<Zeroizing<[u8; 32]>> {
    let mut key = Zeroizing::new([0u8; 32]);

    Argon2::default().hash_password_into(password.as_bytes(), salt, &mut *key)?;

    Ok(key)
}

pub fn generate_salt() -> [u8; 16] {
    let mut salt = [0u8; 16];
    OsRng.fill_bytes(&mut salt);
    salt
}

pub fn generate_nonce() -> [u8; 12] {
    let mut nonce = [0u8; 12];
    OsRng.fill_bytes(&mut nonce);
    nonce
}

pub fn encrypt(message: &str, password: &str) -> Result<(Vec<u8>, [u8; 16], [u8; 12])> {
    let salt = generate_salt();
    let nonce = generate_nonce();
    let key = derive_key(password, &salt)?;

    let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&*key));

    let ciphertext = cipher
        .encrypt(Nonce::from_slice(&nonce), message.as_bytes())
        .map_err(|_| ValidationErrors::EncryptionFailed)?;

    Ok((ciphertext, salt, nonce))
}

pub fn decrypt(ciphertext: &[u8], password: &str, salt: &[u8], nonce: &[u8]) -> Result<String> {
    if password.is_empty() {
        return Err(ValidationErrors::EmptyCharacters);
    }

    let key = derive_key(password, salt)?;

    let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&*key));

    let plaintext_bytes = cipher
        .decrypt(Nonce::from_slice(&nonce), ciphertext)
        .map_err(|_| ValidationErrors::PlainTextDecryptationFailed)?;

    String::from_utf8(plaintext_bytes).map_err(|_| ValidationErrors::StringConversionFailed)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn salts_are_unique() {
        let salt1 = generate_salt();
        let salt2 = generate_salt();
        assert_ne!(salt1, salt2);
    }

    #[test]
    fn encrypt_valid_input_returns_ok() {
        let result = encrypt("hello", "password123");
        assert!(result.is_ok());
    }

    #[test]
    fn encrypt_same_password_twice() {
        let first_result = encrypt("cat", "john_doe").unwrap();
        let second_result = encrypt("cat", "john_doe").unwrap();

        assert_ne!(first_result, second_result);
    }

    #[test]
    fn roundtrip_encrypt_decrypt() {
        let message = "Hello World";
        let password = "password123";

        let (ciphertext, salt, nonce) = encrypt(message, password).unwrap();
        let result = decrypt(&ciphertext, password, &salt, &nonce).unwrap();

        assert_eq!(result, message);
    }
}