cipher_utils/
score.rs

1use itertools::Itertools as _;
2
3use crate::{frequency, Analyze};
4
5/// A possible plaintext. The `PossiblePlaintext` struct provides utilities for analyzing
6/// and scoring texts that may be plaintexts. This is useful for brute-forcing ciphers, when
7/// you need a system to find the decryption outputs that are most likely to be correct.
8#[derive(Debug, Hash, PartialEq, Eq, Clone)]
9pub struct PossiblePlaintext(String);
10
11impl PossiblePlaintext {
12    /// Creates a new `PossiblePlaintext` with the given text as the possible plaintext.
13    ///
14    /// # Parameters
15    /// - `plaintext` - The possible plaintext.
16    ///
17    /// # Returns
18    /// The created `PossiblePlaintext` object
19    pub fn new(plaintext: &str) -> Self {
20        Self(plaintext.to_owned())
21    }
22
23    /// Returns the "score" of this plaintext. The score is based on cryptographic analysis, and a higher score
24    /// indicates a better plaintext. The score is calculated from:
25    ///
26    /// - Index of coincidence
27    /// - Monogram Frequency
28    /// - Bigram Frequency
29    /// - Trigram Frequency
30    /// - Quadram Frequency
31    pub fn score(&self) -> f64 {
32        let ioc_score = 1. - (self.0.index_of_coincidence() - 0.0667).abs() / 0.9333;
33        let frequency_distribution_score = frequency::distribution_score(&self.0);
34        let frequency_character_score = frequency::character_score(&self.0);
35
36        3. * ioc_score + frequency_character_score + frequency_distribution_score / 5.
37    }
38
39    /// Returns the original text of this plaintext.
40    ///
41    /// # Returns
42    /// A reference to the stored text in this plaintext.
43    pub fn text(&self) -> &str {
44        &self.0
45    }
46
47    /// Returns the best plaintext from the given slice based on cryptographic analysis. To get the best `n` plaintexts,
48    /// use `Plaintexts::best_n`.
49    ///
50    /// # Parameters
51    /// - `plaintexts` - The plaintexts to find the best of
52    ///
53    /// # Returns
54    /// The best plaintext of the given slice, or `None` if it's empty.
55    pub fn best<T: AsRef<str>>(plaintexts: &[T]) -> Option<String> {
56        plaintexts
57            .iter()
58            .map(|plaintext| Self(plaintext.as_ref().to_owned()))
59            .max()
60            .map(|plaintext| plaintext.text().to_owned())
61    }
62
63    /// Returns the most top `n` plaintexts in order from best to worst based on cryptographic analysis. To get only the
64    /// best one, use `Plaintext::best`.
65    ///
66    /// # Parameters
67    /// - `plaintexts` - The plaintexts to find the best of
68    /// - `n` - The number of best plaintexts to return
69    ///
70    /// # Returns
71    /// The `n` best plaintexts in order from best to worst.
72    ///
73    /// # Errors
74    /// If the given `n` is greater than the number of plaintexts (or 0), or if the given plaintext slice is empty.
75    pub fn best_n<T: AsRef<str>>(plaintexts: &[T], n: usize) -> anyhow::Result<Vec<String>> {
76        if n == 0 {
77            anyhow::bail!("Attempted to get the best 0 plaintexts; Use a natural number instead.");
78        }
79
80        if plaintexts.is_empty() {
81            anyhow::bail!("Attempted to get the best {n} plaintexts of an empty plaintext list.");
82        }
83
84        let sorted = plaintexts.iter().map(|plaintext| Self(plaintext.as_ref().to_owned())).sorted().rev().collect_vec();
85        sorted
86            .get(sorted.len() - n..sorted.len())
87            .ok_or_else(|| anyhow::anyhow!("Error getting best n plaintexts: Index {n} is out of range of {} plaintexts", plaintexts.len()))
88            .map(|ok| ok.iter().map(|plaintext| plaintext.text().to_owned()).collect())
89    }
90}
91
92impl std::cmp::PartialOrd for PossiblePlaintext {
93    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
94        Some(self.cmp(other))
95    }
96}
97
98impl std::cmp::Ord for PossiblePlaintext {
99    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
100        self.score().total_cmp(&other.score())
101    }
102}