ferric_crypto_lib 0.2.7

A library for Ferric Crypto
Documentation
use crate::error::CharacterParseError;
use crate::prelude::{ALPHABET, ALPHABET_LEN};
use nalgebra::DMatrix;
use num_integer::gcd;

#[cfg(feature = "python-integration")]
use pyo3::prelude::*;

/// Enum representing possible errors in the Hill cipher.
///
/// # Variants
///
/// * `InvalidKey` - Represents an error where the key matrix is invalid. This could be due to the matrix not being square,
///                  having elements outside the range 0..28, or having a determinant that is not coprime with 28.
/// * `InvalidFiller` - Represents an error where the filler character is invalid. This is the case when the filler character
///                     is not in the alphabet.
/// * `InvalidDirection` - Represents an error where the direction of operation is invalid. This is the case when the direction
///                        is not Vertical or Horizontal.
#[derive(Debug)]
pub enum HillError {
    InvalidKey,
    InvalidFiller,
    InvalidDirection,
    CharacterParseError(CharacterParseError),
}

/// Enum representing the direction of operation in the Hill cipher.
///
/// # Variants
///
/// * `Vertical` - Represents a vertical operation. In this case, the key matrix is multiplied with the input vector from the right.
/// * `Horizontal` - Represents a horizontal operation. In this case, the key matrix is multiplied with the input vector from the left.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "python-integration", pyclass(get_all))]
pub enum HillDirection {
    Vertical,
    Horizontal,
}

/// Represents a Hill cipher.
///
/// A Hill cipher is a polygraphic substitution cipher based on linear algebra.
/// Each letter is represented by a number modulo 28. A block of letters is
/// then converted into a vector, and multiplied by a matrix, resulting in a
/// new vector. The numbers in the new vector are then converted back into
/// letters, forming the ciphertext.
///
/// # Fields
///
/// * `key` - A `Vec<Vec<usize>>` that represents the key matrix used for encryption and decryption.
/// * `filler` - A `char` that is used as the padding character. The default value is `x`.
/// * `direction` - A `HillDirection` enum that specifies the direction of operation (`Vertical` or `Horizontal`).
#[derive(Debug, Clone)]
#[cfg_attr(feature = "python-integration", pyclass(get_all))]
pub struct Hill {
    pub key: Vec<Vec<usize>>,
    pub filler: char,
    pub direction: HillDirection,
}

#[cfg(not(feature = "python-integration"))]
impl Hill {
    /// Creates a new Hill cipher with the given key, filler, and direction.
    ///
    /// # Arguments
    ///
    /// * `key` - A `Vec<Vec<usize>>` that represents the key matrix.
    /// * `filler` - A `char` that is used as the padding character.
    /// * `direction` - A `HillDirection` enum that specifies the direction of operation (`Vertical` or `Horizontal`).
    ///
    /// # Returns
    ///
    /// * A new Hill cipher with the specified key, filler, and direction.
    pub fn new(key: Vec<Vec<usize>>, filler: char, direction: HillDirection) -> Self {
        Self {
            key,
            filler,
            direction,
        }
    }

    /// Checks if the key of the Hill cipher is valid.
    ///
    /// This function checks if the key matrix is square and non-empty, if all elements are in the
    /// range 0..28, and if the determinant of the key matrix is coprime with the length of the alphabet.
    ///
    /// # Returns
    ///
    /// * A bool indicating whether the key is valid. Returns true if the key is valid, and false otherwise.
    pub(crate) fn is_valid_key(&self) -> bool {
        let size = self.key.len();

        // Check if matrix is square, non-empty, and all elements are in the range 0..28
        if size == 0
            || self
                .key
                .iter()
                .any(|row| row.len() != size || row.iter().any(|&elem| elem >= *ALPHABET_LEN))
        {
            return false;
        }

        // Convert key to a DMatrix
        let matrix = DMatrix::from_fn(size, size, |i, j| self.key[i][j] as f64);

        // Calculate determinant and check if it's coprime with the length of the alphabet
        let determinant = matrix.determinant().round() as isize; // Round to handle floating point errors

        gcd(determinant.abs(), (*ALPHABET_LEN).try_into().unwrap()) == 1
    }

    /// Checks if the filler of the Hill cipher is valid.
    ///
    /// This function checks if the filler character is in the alphabet.
    ///
    /// # Returns
    ///
    /// * A bool indicating whether the filler is valid. Returns true if the filler is valid, and false otherwise.
    pub(crate) fn is_valid_filler(&self) -> bool {
        // check if filler is in the alphabet
        if ALPHABET.contains(self.filler) {
            return true;
        }

        false
    }
}

#[cfg(feature = "python-integration")]
mod python_integration {
    use super::*;
    use crate::Traits::{Decrypt, Encrypt};
    use pyo3::{prelude::*, pyclass, pymethods, PyResult};
    use rand::prelude::SliceRandom;
    use std::collections::HashMap;

    #[pymethods]
    impl Hill {
        /// Creates a new Hill cipher with the given key, filler, and direction.
        ///
        /// # Arguments
        ///
        /// * `key` - A `Vec<Vec<usize>>` that represents the key matrix.
        /// * `filler` - A `char` that is used as the padding character.
        /// * `direction` - A `HillDirection` enum that specifies the direction of operation (`Vertical` or `Horizontal`).
        ///
        /// # Returns
        ///
        /// * A new Hill cipher with the specified key, filler, and direction.
        #[new]
        pub fn new(key: Vec<Vec<usize>>, filler: char, direction: HillDirection) -> Self {
            Self {
                key,
                filler,
                direction,
            }
        }

        /// Checks if the key of the Hill cipher is valid.
        ///
        /// This function checks if the key matrix is square and non-empty, if all elements are in the
        /// range 0..28, and if the determinant of the key matrix is coprime with the length of the alphabet.
        ///
        /// # Returns
        ///
        /// * A bool indicating whether the key is valid. Returns true if the key is valid, and false otherwise.
        pub(crate) fn is_valid_key(&self) -> bool {
            let size = self.key.len();

            // Check if matrix is square, non-empty, and all elements are in the range 0..28
            if size == 0
                || self
                    .key
                    .iter()
                    .any(|row| row.len() != size || row.iter().any(|&elem| elem >= *ALPHABET_LEN))
            {
                return false;
            }

            // Convert key to a DMatrix
            let matrix = DMatrix::from_fn(size, size, |i, j| self.key[i][j] as f64);

            // Calculate determinant and check if it's coprime with the length of the alphabet
            let determinant = matrix.determinant().round() as isize; // Round to handle floating point errors

            gcd(determinant.abs(), (*ALPHABET_LEN).try_into().unwrap()) == 1
        }

        /// Checks if the filler of the Hill cipher is valid.
        ///
        /// This function checks if the filler character is in the alphabet.
        ///
        /// # Returns
        ///
        /// * A bool indicating whether the filler is valid. Returns true if the filler is valid, and false otherwise.
        pub(crate) fn is_valid_filler(&self) -> bool {
            // check if filler is in the alphabet
            if ALPHABET.contains(self.filler) {
                return true;
            }

            false
        }

        pub fn encrypt(&self, input: String) -> PyResult<String> {
            match Encrypt::encrypt(self, input) {
                Ok(s) => Ok(s),
                Err(e) => Err(pyo3::exceptions::PyException::new_err(format!("{:?}", e))),
            }
        }

        pub fn decrypt(&self, input: String) -> PyResult<String> {
            match Decrypt::decrypt(self, input) {
                Ok(s) => Ok(s),
                Err(e) => Err(pyo3::exceptions::PyException::new_err(format!("{:?}", e))),
            }
        }

        // Define Python specific methods here, static methods require the pyo3 decorator `#[staticmethod]`
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::crypto_systems::hill_crypto::HillDirection;

    #[test]
    fn valid_key_returns_true() {
        let key = vec![vec![5, 17, 6], vec![2, 21, 14], vec![19, 3, 11]];
        let hill = Hill::new(key, 'x', HillDirection::Vertical);
        assert!(hill.is_valid_key());
    }

    #[test]
    fn invalid_key_returns_false() {
        let key = vec![vec![6, 24, 1], vec![13, 16, 10], vec![20, 17, 30]];
        let hill = Hill::new(key, 'x', HillDirection::Vertical);
        assert!(!hill.is_valid_key());
    }

    #[test]
    fn non_square_key_returns_false() {
        let key = vec![vec![6, 24, 1], vec![13, 16, 10]];
        let hill = Hill::new(key, 'x', HillDirection::Vertical);
        assert!(!hill.is_valid_key());
    }

    #[test]
    fn valid_filler_returns_true() {
        let key = vec![vec![6, 24, 1], vec![13, 16, 10], vec![20, 17, 15]];
        let hill = Hill::new(key, 'x', HillDirection::Vertical);
        assert!(hill.is_valid_filler());
    }

    #[test]
    fn invalid_filler_returns_false() {
        let key = vec![vec![6, 24, 1], vec![13, 16, 10], vec![20, 17, 15]];
        let hill = Hill::new(key, '1', HillDirection::Vertical);
        assert!(!hill.is_valid_filler());
    }
}