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}