diff --git a/CHANGELOG.md b/CHANGELOG.md index 246820a..abeb70a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,120 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.12] - 2023-11-20 +- Update the CHANGELOG. +- Update the version. +- Update the README. + +## [0.9.11] - 2023-11-14 + +### Added +- Add the function that return all key combinations for a possible key vector. + +### Changed +- Rename the function from get-possible-keys to get-all-combinations-for-blocks. + +## [0.9.10] - 2023-11-13 + +### Added +- Add a function to get all permutations for a block. + +## [0.9.9] - 2023-11-12 + +### Added +- Add a function to convert a count map to a reversed map. +- Add a function to get the ordered sequence that can form a key. + +## [0.9.8] - 2023-11-11 + +### Added +- Add functions to get possible keys. + +## [0.9.7] - 2023-11-03 + +### Added +- Add the default frequency factory. + +## [0.9.6] - 2023-11-01 + +### Added +- Add int-string? to check if a string is a number. +- Add key? to check if a string is a gamma key. +- Add key validation for Caesar and Gamma ciphers. + +### Changed +- Rename unsigned-int? to unsigned-int-string? + +## [0.9.5] - 2023-10-31 +## [0.9.5] - 2023-10-31 + +### Added +- Add the option for the gamma cipher. + +## [0.9.4] - 2023-10-31 + +### Fixed +- Fix the key parsing in the gamma actions. + +## [0.9.3] - 2023-10-31 + +### Added +- Add the action of encryption for the gamma cipher. +- Add the action of decryption for the gamma cipher. + +## [0.9.2] - 2023-10-29 + +### Added +- Add a function to encrypt a message with the gamma cipher. +- Add a function to decrypt a message with the gamma cipher. + +## [0.9.1] - 2023-10-28 + +### Changed +- Refactor the simple substitute to use the decrypt-by-table function. + +## [0.9.0] - 2023-10-27 + +### Added +- Add the gamma generator. +- Add the functions to encrypt and decrypt with the gamma cipher. + +## [0.8.5] - 2023-10-25 + +### Added +- Add the main for the cli module. + +## [0.8.4] - 2023-10-24 + +### Changed +- Update the simple substitution cipher to split the decrypt-message. + +## [0.8.3] - 2023-10-24 + +### Added +- Add functions to make a frequency map. + +## [0.8.2] - 2023-10-23 + +### Added +- Add a function to invert the table in a decoded json. +- Add the parser to parse the string as an integer if it's needed. +- Add the simple substitution cipher. + +## [0.8.1] - 2023-10-22 + +### Added +- Add a generator for the sorted substitution table. +- Add the parsers to parse keys. + +### Changed +- Update the simple substitution cipher to split the functions. + +## [0.8.0] - 2023-10-21 + +### Added +- Add the simple substitution cipher. + ## [0.7.2] - 2023-10-21 ### Changed @@ -177,7 +291,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Project initialization. -[Unreleased]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.7.2...main?from_project_id=50218541&straight=false +[Unreleased]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.12...main?from_project_id=50218541&straight=false +[0.9.12]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.11...0.0.9.12?from_project_id=50218541&straight=false +[0.9.11]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.10...0.0.9.11?from_project_id=50218541&straight=false +[0.9.10]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.9...0.9.10?from_project_id=50218541&straight=false +[0.9.9]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.8...0.9.9?from_project_id=50218541&straight=false +[0.9.8]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.7...0.9.8?from_project_id=50218541&straight=false +[0.9.7]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.6...0.9.7?from_project_id=50218541&straight=false +[0.9.6]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.5...0.9.6?from_project_id=50218541&straight=false +[0.9.5]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.4...0.9.5?from_project_id=50218541&straight=false +[0.9.4]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.3...0.9.4?from_project_id=50218541&straight=false +[0.9.3]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.2...0.9.3?from_project_id=50218541&straight=false +[0.9.2]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.1...0.9.2?from_project_id=50218541&straight=false +[0.9.1]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.9.0...0.9.1?from_project_id=50218541&straight=false +[0.9.0]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.8.5...0.9.0?from_project_id=50218541&straight=false +[0.8.5]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.8.4...0.8.5?from_project_id=50218541&straight=false +[0.8.4]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.8.3...0.8.4?from_project_id=50218541&straight=false +[0.8.3]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.8.2...0.8.3?from_project_id=50218541&straight=false +[0.8.2]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.8.1...0.8.2?from_project_id=50218541&straight=false +[0.8.1]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.8.0...0.8.1?from_project_id=50218541&straight=false +[0.8.0]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.7.2...0.8.0?from_project_id=50218541&straight=false [0.7.2]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.7.1...0.7.2?from_project_id=50218541&straight=false [0.7.1]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.7.0...0.7.1?from_project_id=50218541&straight=false [0.7.0]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.6.3...0.7.0?from_project_id=50218541&straight=false diff --git a/README.md b/README.md index 3f3f08b..4a7f018 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ You need install java, before run the application. Run the application with a command: $ java -jar cipher-analytical-machine-VERSION-standalone.jar [args] - $ java -jar cipher-analytical-machine-0.7.2-standalone.jar [args] + $ java -jar cipher-analytical-machine-0.9.12-standalone.jar [args] ## License diff --git a/project.clj b/project.clj index 116b5f3..45bb729 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject cipher-analytical-machine "0.7.2" +(defproject cipher-analytical-machine "0.9.12" :description "A program helps to learn how ciphers works." :url "https://gitlab.com/KKlochko/cipher-analytical-machine" :license {:name "LGPL" @@ -6,7 +6,9 @@ :dependencies [[org.clojure/clojure "1.11.1"] [org.clojure/tools.cli "1.0.219"] [org.apache.tika/tika-core "1.28.5"] - [org.apache.tika/tika-langdetect "1.28.5"]] + [org.apache.tika/tika-langdetect "1.28.5"] + [cheshire "5.12.0"] + [org.clojure/math.combinatorics "0.2.0"]] :main ^:skip-aot cipher-analytical-machine.core :java-source-paths ["src/main/java"] :target-path "target/%s" diff --git a/src/cipher_analytical_machine/analyzers/analyzers.clj b/src/cipher_analytical_machine/analyzers/analyzers.clj index d463afe..b648c1b 100644 --- a/src/cipher_analytical_machine/analyzers/analyzers.clj +++ b/src/cipher_analytical_machine/analyzers/analyzers.clj @@ -11,6 +11,16 @@ (inc (get acc val 0)))) {} text)) +(defn reverse-count-characters + "Convert a map of pairs to a map where the characters grouped by the count and a count is the key." + [count-map] + (reduce + (fn [acc [symbol count]] + (->> (conj (get acc count []) symbol) + (into []) + (assoc acc count))) + {} count-map)) + (defn chi-squared-letter-statistic "To calculate the frequency distribution of a letter." [letter-count letter-frequence length-text] @@ -36,3 +46,22 @@ 0 letter-counts))) +(defn sort-map-by-count + "Return a list of pairs from a map of pairs where a pair is a character and its count. The order is descending." + [count-map] + (->> count-map + (reduce (fn [acc [char count]] (conj acc [char count])) []) + (sort-by #(- (second %))))) + +(defn table-to-string + "Return the string from a frequency table." + [table] + (->> (map first table) + (apply str))) + +(defn symbol-frequency-table + "Return a table where pairs is a symbol from a cipher text and its value which is a symbol from frequency table." + [cipher-table frequency-table] + (zipmap (table-to-string (sort-map-by-count cipher-table)) + (table-to-string (sort-map-by-count frequency-table)))) + diff --git a/src/cipher_analytical_machine/analyzers/simple_substitution.clj b/src/cipher_analytical_machine/analyzers/simple_substitution.clj new file mode 100644 index 0000000..717b49b --- /dev/null +++ b/src/cipher_analytical_machine/analyzers/simple_substitution.clj @@ -0,0 +1,44 @@ +(ns cipher-analytical-machine.analyzers.simple-substitution + (:require [clojure.string :as cs] + [clojure.math.combinatorics :as comb] + [cipher-analytical-machine.ciphers.simple-substitution :as ss] + [cipher-analytical-machine.analyzers.analyzers :as analyzers] + [cipher-analytical-machine.analyzers.caesar :as caesar]) + (:gen-class)) + +(defn get-possible-key-vector + "Convert a possible key map to a vector of possible keys. For example, if you have a map: {\\a [\\a \\b \\c] \\b [\\b \\c] \\c [\\a \\c]}, then the vector is [[\\a \\b \\c] [\\b \\c] [\\a \\c]]." + [possible-key-map symbols] + (reduce + (fn [acc el] + (conj acc (get possible-key-map el))) + [] symbols)) + +(defn get-possible-key-vector-from-reversed-count-map + "Convert a reversed count map to a vector. Example, from {2 [\\a \\b] 1 [\\d] 3 [\\e]} to [[\\e] [\\a \\b] [\\d]])." + [reversed-count-map] + (->> reversed-count-map + (into []) + (sort-by #(- (first %))) + (map second) + (into []))) + +(defn get-all-permutation-for-block + "Return a vector for all possible blocks that can be made from a vector of symbols." + [symbols] + (->> (comb/permutations symbols) + (map cs/join) + (into []))) + +(defn get-all-combinations-for-blocks + "Return the all combination of blocks in a possible key vector. For example, if you have a map: {\\a [\\a \"bf\" \\c] \\b [\"bf\" \\c] \\c [\\a \\c]}, then the vector is [[\\a \"bf\" \\c] [\"bf\" \\c] [\\a \\c]] and keys (\"abfa\" \"abfc\" ...)." + [possible-key-vector] + (->> (apply comb/cartesian-product possible-key-vector) + (map cs/join))) + +(defn get-possible-key-combinations + "Return possible keys for a possible key vector. For example, if you have a map: {\\a [\\a \\b \\c] \\e [\\f \\g] \\c [\\a \\c]}, then the keys are (\"abcefg\" \"abcegf\" ...)." + [possible-key-vector] + (->> (map get-all-permutation-for-block possible-key-vector) + (get-all-combinations-for-blocks))) + diff --git a/src/cipher_analytical_machine/ciphers/gamma.clj b/src/cipher_analytical_machine/ciphers/gamma.clj new file mode 100644 index 0000000..53cf42c --- /dev/null +++ b/src/cipher_analytical_machine/ciphers/gamma.clj @@ -0,0 +1,78 @@ +(ns cipher-analytical-machine.ciphers.gamma + (:require + [clojure.set :as set] + [clojure.string :as cs] + [cipher-analytical-machine.ciphers.simple-substitution :as ss]) + (:gen-class)) + +(defn add-mod + [f s module] + (-> (+ f s) + (mod module))) + +(defn generate-seq + "Generate a lazy sequence for a key (a b c)." + [a b c module] + ((fn build-seq [a b c] + (lazy-seq (cons a (build-seq b c (add-mod a c module))))) a b c)) + +(defn generate-gamma-seq + "Generate the gamma seq from a sequence." + [acc seq module] + (if (= (second seq) nil) acc + (generate-gamma-seq + (conj acc (add-mod (first seq) (second seq) module)) + (rest seq) + module))) + +(defn generate-gamma + "Generate the gamma from a key (a b c)." + [a b c module size] + (generate-gamma-seq [] + (->> (generate-seq a b c module) + (take (inc size))) + module)) + +(defn encrypt-array + "Encrypt an array using the gamma cipher. The function is case-insensitive. The key is an array of three elements [a b c]. The array contains integer in range [0, module)." + [key module array] + (let [size (count array) + gamma (->> (apply conj [key module size]) + (apply generate-gamma))] + (->> gamma + (map vector array) + (map (fn [[a g]] (add-mod a g module)))))) + +(defn encrypt-message + "Encrypt a message using the gamma cipher. The function is case-insensitive. The key is an array of three elements [a b c]. The array contains integer in range [0, module)." + [key symbols message] + (let [module (count symbols) + table (ss/generate-sorted-substitution-table symbols)] + (->> message + (ss/decrypt-by-table (set/map-invert table)) + (encrypt-array key module) + (ss/decrypt-by-table table) + (cs/join)))) + +(defn decrypt-array + "Decrypt an array using the gamma cipher. The function is case-insensitive. The key is an array of three elements [a b c]. The array contains integer in range [0, module)." + [key module array] + (let [size (count array) + gamma (->> (apply conj [key module size]) + (apply generate-gamma))] + (->> gamma + (map -) + (map vector array) + (map (fn [[a g]] (add-mod a g module)))))) + +(defn decrypt-message + "Decrypt a message using the gamma cipher. The function is case-insensitive. The key is an array of three elements [a b c]. The array contains integer in range [0, module)." + [key symbols message] + (let [module (count symbols) + table (ss/generate-sorted-substitution-table symbols)] + (->> message + (ss/decrypt-by-table (set/map-invert table)) + (decrypt-array key module) + (ss/decrypt-by-table table) + (cs/join)))) + diff --git a/src/cipher_analytical_machine/ciphers/simple_substitution.clj b/src/cipher_analytical_machine/ciphers/simple_substitution.clj new file mode 100644 index 0000000..4514f0f --- /dev/null +++ b/src/cipher_analytical_machine/ciphers/simple_substitution.clj @@ -0,0 +1,74 @@ +(ns cipher-analytical-machine.ciphers.simple-substitution + (:require [clojure.string :as cs] + [cipher-analytical-machine.ciphers.caesar :as caesar]) + (:gen-class)) + +(defn shuffled-numbers + "Generate the shuffled order for integer list." + [size] + (-> size + (take (range)) + (shuffle))) + +(defn generate-substitution-table + "Generate the map (char, int) for the substittion." + [symbols] + (->> (count symbols) + (shuffled-numbers) + (zipmap symbols))) + +(defn generate-sorted-substitution-table + "Generate the map (char, int) for the substittion. BUT a value is the index of a symbol (from 0)." + [symbols] + (-> (count symbols) + (take (range)) + (zipmap symbols))) + +(defn find-value-in-table + "It uses the substitution table to find the value of a char or a number." + [char substitution-table] + (get substitution-table char)) + +(defn decrypt-by-table + "Decrypt a message by the substitution-table" + [substitution-table message] + (map (fn [char] (find-value-in-table char substitution-table)) message)) + +(defn encrypt-message + "Encrypt a message using the simple substitution cipher. The function is case-insensitive. If a symbol isn't in the symbols list, then it will be removed." + [message substitution-table] + (->> message + (decrypt-by-table substitution-table) + (remove nil?) + (cs/join \,))) + +(defn encrypt-message-with-caesar + "Encrypt a message using the simple substitution cipher and the Caesar cipher. The function is case-insensitive. If a symbol isn't in the symbols list, then it will be removed." + [message key substitution-table symbols] + (-> message + (caesar/encrypt-message key symbols) + (encrypt-message substitution-table))) + +(defn decrypt-message-by-symbol-table + "Decrypt a message using the simple substitution cipher. The function is case-insensitive. The substitution-table must be (symbols, symbols)." + [substitution-table message] + (->> message + (decrypt-by-table substitution-table) + (remove nil?) + (cs/join))) + +(defn decrypt-message + "Decrypt a message using the simple substitution cipher. The function is case-insensitive. The substitution-table must be (int, symbols)." + [message substitution-table] + (let [message (cs/split message #",")] + (->> message + (map #(Integer/parseInt %)) + (decrypt-message-by-symbol-table substitution-table)))) + +(defn decrypt-message-with-caesar + "Decrypt a message using the simple substitution cipher and the Caesar cipher. The function is case-insensitive." + [message key substitution-table symbols] + (-> message + (decrypt-message substitution-table) + (caesar/decrypt-message key symbols))) + diff --git a/src/cipher_analytical_machine/cli/actions/cracking.clj b/src/cipher_analytical_machine/cli/actions/cracking.clj index 5e68aa1..db4a8ab 100644 --- a/src/cipher_analytical_machine/cli/actions/cracking.clj +++ b/src/cipher_analytical_machine/cli/actions/cracking.clj @@ -9,7 +9,8 @@ (let [analyzer (:analyzer options) message (:message options) symbols (:symbols options) - frequencies symbol-frequencies/english-letter-frequencies] + frequencies (-> (get options :language "en") + (symbol-frequencies/default-frequency-factory))] (cond (= analyzer "Chi^2") (caesar-analyzers/get-plaintext message symbols frequencies) diff --git a/src/cipher_analytical_machine/cli/actions/cryptography.clj b/src/cipher_analytical_machine/cli/actions/cryptography.clj index cdddd12..2c45086 100644 --- a/src/cipher_analytical_machine/cli/actions/cryptography.clj +++ b/src/cipher_analytical_machine/cli/actions/cryptography.clj @@ -1,6 +1,11 @@ (ns cipher-analytical-machine.cli.actions.cryptography (:require - [cipher-analytical-machine.ciphers.caesar :as caesar]) + [clojure.string :as cs] + [cipher-analytical-machine.ciphers.caesar :as caesar] + [cipher-analytical-machine.ciphers.simple-substitution :as ss] + [cipher-analytical-machine.ciphers.gamma :as gamma] + [cipher-analytical-machine.parsers.parsers :as ps] + [cipher-analytical-machine.parsers.simple-substitution :as pss]) (:gen-class)) (defn caesar-actions @@ -15,10 +20,50 @@ (= action-type :decrypt) (caesar/decrypt-message message key symbols)))) +(defn simple-substitution-with-caesar-actions + [options arguments action-type] + (let [message (:message options) + key (:key options) + symbols (:symbols options)] + (cond + (= action-type :encrypt) + (let [data (pss/generate-table-or-decode-json key symbols) + key (ps/parse-unsigned-int (get data "key")) + substitution-table (get data "table")] + (println (pss/encode-key-and-substitution-table-to-json key substitution-table)) + (ss/encrypt-message-with-caesar message key substitution-table symbols)) + + (= action-type :decrypt) + (let [data (-> (pss/decode-key-and-substitution-table-from-json key) + (pss/invert-table-in-map)) + key (get data "key") + substitution-table (get data "table")] + (ss/decrypt-message-with-caesar message key substitution-table symbols))))) + +(defn gamma-actions + [options arguments action-type] + (let [message (:message options) + key (->> (cs/split (:key options) #",") + (map #(Integer/parseInt %)) + (into [])) + symbols (:symbols options)] + (cond + (= action-type :encrypt) + (gamma/encrypt-message key symbols message) + + (= action-type :decrypt) + (gamma/decrypt-message key symbols message)))) + (defn cryptography-actions [options arguments action-type] (let [cipher (:cipher options)] (cond (= cipher "Caesar") - (caesar-actions options arguments action-type)))) + (caesar-actions options arguments action-type) + + (= cipher "Simple substitution and Caesar") + (simple-substitution-with-caesar-actions options arguments action-type) + + (= cipher "Gamma") + (gamma-actions options arguments action-type)))) diff --git a/src/cipher_analytical_machine/cli/cli.clj b/src/cipher_analytical_machine/cli/cli.clj index c4f67bc..e2dd527 100644 --- a/src/cipher_analytical_machine/cli/cli.clj +++ b/src/cipher_analytical_machine/cli/cli.clj @@ -1,5 +1,7 @@ (ns cipher-analytical-machine.cli.cli (:require + [cipher-analytical-machine.parsers.parsers :as ps] + [cipher-analytical-machine.parsers.gamma :as gamma-ps] [cipher-analytical-machine.cli.file :as file] [cipher-analytical-machine.symbols.factories :as sf] [cipher-analytical-machine.cli.options :as options] @@ -77,6 +79,15 @@ (or (contains? options :encrypt) (contains? options :decrypt))) (cond + (and (= (:cipher options) "Caesar") + (false? (ps/int-string? (:key options)))) + {:exit-message (error-msg ["Please, use the key format: [+-](integer)!!! Example: 10, +9, -7."])} + + (and (= (:cipher options) "Gamma") + (false? (gamma-ps/key? (:key options)))) + {:exit-message (error-msg ["Please, use the key format: [+-](integer),[+-](integer),[+-](integer) !!! Example: 10,+9,-7."])} + + (contains? options :encrypt) {:options options :arguments arguments :action-type :encrypt} @@ -160,3 +171,10 @@ (show-and-save-output options)))) +(defn cli-main + [args] + (let [{:keys [options arguments action-type exit-message ok?]} (validate-args args)] + (if exit-message + (exit exit-message ok?) + (actions options arguments action-type)))) + diff --git a/src/cipher_analytical_machine/cli/options.clj b/src/cipher_analytical_machine/cli/options.clj index 0c9d968..dd93152 100644 --- a/src/cipher_analytical_machine/cli/options.clj +++ b/src/cipher_analytical_machine/cli/options.clj @@ -22,7 +22,7 @@ (option-values-as-list option values))) (def cipher-options - #{"Caesar"}) + #{"Caesar" "Simple substitution and Caesar" "Gamma"}) (def default-cipher "Caesar") diff --git a/src/cipher_analytical_machine/core.clj b/src/cipher_analytical_machine/core.clj index e5106b4..bd01aae 100644 --- a/src/cipher_analytical_machine/core.clj +++ b/src/cipher_analytical_machine/core.clj @@ -5,8 +5,5 @@ (defn -main [& args] - (let [{:keys [options arguments action-type exit-message ok?]} (cli/validate-args args)] - (if exit-message - (cli/exit exit-message ok?) - (cli/actions options arguments action-type)))) + (cli/cli-main args)) diff --git a/src/cipher_analytical_machine/parsers/gamma.clj b/src/cipher_analytical_machine/parsers/gamma.clj new file mode 100644 index 0000000..22e2ac3 --- /dev/null +++ b/src/cipher_analytical_machine/parsers/gamma.clj @@ -0,0 +1,9 @@ +(ns cipher-analytical-machine.parsers.gamma + (:gen-class)) + +(defn key? + "Return true if the string is an key of three integers." + [str] + (if (re-matches #"[+-]?\b\d+\b,[+-]?\b\d+\b,[+-]?\b\d+\b" str) + true false)) + diff --git a/src/cipher_analytical_machine/parsers/parsers.clj b/src/cipher_analytical_machine/parsers/parsers.clj new file mode 100644 index 0000000..0d5d454 --- /dev/null +++ b/src/cipher_analytical_machine/parsers/parsers.clj @@ -0,0 +1,29 @@ +(ns cipher-analytical-machine.parsers.parsers + (:require [cheshire.core :as cc]) + (:gen-class)) + +(defn unsigned-int-string? + "Return true if the string is an unsigned integer." + [str] + (if (re-matches #"\d+" str) + true false)) + +(defn int-string? + "Return true if the string is an signed integer." + [str] + (if (re-matches #"[+-]?\b\d+\b" str) + true false)) + +(defn parse-unsigned-int + "Return an integer if the argument is an integer." + [str-or-int] + (cond + (int? str-or-int) + str-or-int + + (unsigned-int-string? str-or-int) + (Integer/parseInt str-or-int) + + :else + nil)) + diff --git a/src/cipher_analytical_machine/parsers/simple_substitution.clj b/src/cipher_analytical_machine/parsers/simple_substitution.clj new file mode 100644 index 0000000..784b92e --- /dev/null +++ b/src/cipher_analytical_machine/parsers/simple_substitution.clj @@ -0,0 +1,54 @@ +(ns cipher-analytical-machine.parsers.simple-substitution + (:require + [cipher-analytical-machine.ciphers.simple-substitution :as ss] + [cipher-analytical-machine.parsers.parsers :as ps] + [clojure.set :as set] + [cheshire.core :as cc]) + (:gen-class)) + +(defn encode-key-and-substitution-table-to-json + [key substitution-table] + (-> {"key" key} + (assoc "table" substitution-table) + (cc/generate-string))) + +(defn decode-key + "Returns the first character if the string is a char. Return the number if the string is a number." + [str] + (if (ps/unsigned-int-string? str) + (Integer/parseInt str) + (first str))) + +(defn decode-pair + "Decode a numbers as a string and a char as a string" + [acc [key value]] + (if (ps/unsigned-int-string? key) + (assoc acc (decode-key key) (decode-key value)) + (assoc acc (decode-key key) value))) + +(defn decode-substitution-table + [substitution-table-map-from-json] + (reduce decode-pair {} substitution-table-map-from-json)) + +(defn decode-key-and-substitution-table-from-json + [json] + (let [data (cc/parse-string json)] + (assoc data "table" (decode-substitution-table (get data "table"))))) + +(defn generate-table-or-decode-json + "It generate the table and return map with the key and the table if the argument is a key. Else it decode the json." + [key-or-json symbols] + (if (ps/unsigned-int-string? key-or-json) + (-> {"key" key-or-json} + (assoc "table" (ss/generate-substitution-table symbols))) + (decode-key-and-substitution-table-from-json key-or-json))) + +(defn invert-table-in-map + "Invert the table in a decoded json (key, table)" + [decoded-json] + (->> + (-> decoded-json + (get "table") + (set/map-invert)) + (assoc decoded-json "table"))) + diff --git a/src/cipher_analytical_machine/symbols/frequencies.clj b/src/cipher_analytical_machine/symbols/frequencies.clj index dad6368..ab8d94a 100644 --- a/src/cipher_analytical_machine/symbols/frequencies.clj +++ b/src/cipher_analytical_machine/symbols/frequencies.clj @@ -77,3 +77,16 @@ \ 0.138 }) +(defn default-frequency-factory + "Returns a default map of letter frequencies for a language" + [language-code] + (cond + (= language-code "en") + english-letter-frequencies + + (= language-code "uk") + ukrainian-letter-frequencies + + :else + english-letter-frequencies)) + diff --git a/test/cipher_analytical_machine/analyzers/analyzers_test.clj b/test/cipher_analytical_machine/analyzers/analyzers_test.clj new file mode 100644 index 0000000..f923a48 --- /dev/null +++ b/test/cipher_analytical_machine/analyzers/analyzers_test.clj @@ -0,0 +1,47 @@ +(ns cipher-analytical-machine.analyzers.analyzers-test + (:require + [clojure.test :refer :all] + [cipher-analytical-machine.analyzers.analyzers :refer :all])) + +(deftest count-characters-test + (testing "Count two dublicates and one uniq character from a string." + (is (= {\a 2 \b 1} + (count-characters "aba"))))) + +(deftest reverse-count-characters-test + (testing "The symbols must be grouped as a vector and the count must be a key." + (are [count-map expected-map] + (= expected-map (reverse-count-characters count-map)) + {\a 1 \b 2 \c 1} {1 [\a \c] 2 [\b]} + {\a 1 \b 2 \c 3} {1 [\a] 2 [\b] 3 [\c]}))) + +(deftest chi-squared-letter-statistic-test + (testing "If the frequence of A is 0.1, the text length is 100 and contains 20 times, then the chi-squared is 10." + (is (= 10.0 + (chi-squared-letter-statistic 20 0.1 100)))) + + (testing "If the frequence of A is 0.1, the text length is 10 and contains 5 times, then the chi-squared is 16.0." + (is (= 16.0 + (chi-squared-letter-statistic 5 0.1 10))))) + +(deftest chi-squared-statistic-test + (let [text "aaaaabbbbb"] + (testing "If the frequencies of a and b is 0.1, the text length is 10 and contains them 5 times, then score is 32." + (is (= 32.0 + (chi-squared-statistic text {\a 0.1 \b 0.1})))))) + +(deftest sort-map-by-count-test + (testing "Test the sort in descending order." + (is (= [[\b 3] [\a 2] [\c 1]] + (sort-map-by-count {\a 2 \b 3 \c 1} ))))) + +(deftest table-to-string-test + (testing "Test the sort in descending order." + (is (= "bac" + (table-to-string [[\b 3] [\a 2] [\c 1]]))))) + +(deftest symbol-frequency-table-test + (testing "Test the sort in descending order." + (is (= {\d \b, \e \a, \f \c} + (symbol-frequency-table {\d 3 \e 2 \f 1} {\b 3 \a 2 \c 1}))))) + diff --git a/test/cipher_analytical_machine/analyzers/simple_substitution_test.clj b/test/cipher_analytical_machine/analyzers/simple_substitution_test.clj new file mode 100644 index 0000000..c0f2c09 --- /dev/null +++ b/test/cipher_analytical_machine/analyzers/simple_substitution_test.clj @@ -0,0 +1,50 @@ +(ns cipher-analytical-machine.analyzers.simple-substitution-test + (:require + [clojure.test :refer :all] + [cipher-analytical-machine.symbols.frequencies :as sf] + [cipher-analytical-machine.ciphers.simple-substitution :as ss] + [cipher-analytical-machine.analyzers.simple-substitution :refer :all]) + (:gen-class)) + +(deftest get-possible-keys-vector-test + (testing "The vector must be ordered by symbols" + (are [possible-keys-map symbols expected-key-vector] + (= expected-key-vector (get-possible-key-vector possible-keys-map symbols)) + {\a [\a \b] \b [\b \a] \c [\a \c]} "abc" [[\a \b] [\b \a] [\a \c]] + {\a [\a \b] \c [\b \a] \b [\a \c]} "abc" [[\a \b] [\a \c] [\b \a]]))) + +(deftest get-possible-key-vector-from-reversed-count-map-test + (testing "The block order must be in descending order" + (are [reversed-count-map expected-key-vector] + (= expected-key-vector (get-possible-key-vector-from-reversed-count-map reversed-count-map)) + {1 [\a \c] 2 [\b]} [[\b] [\a \c]] + {2 [\a] 1 [\b] 3 [\c]} [[\c] [\a] [\b]]))) + +(deftest get-all-permutation-for-block-test + (testing "It must return all permutations" + (are [symbols expected-permutations] + (= expected-permutations (get-all-permutation-for-block symbols)) + [\a \b] ["ab" "ba"] + [\a \b \c] ["abc" "acb" "bac" "bca" "cab" "cba"]))) + +(deftest get-all-combinations-for-blocks-test + (testing "The keys must be the all combinations of blocks" + (are [possible-keys-vector expected-keys] + (= expected-keys (get-all-combinations-for-blocks possible-keys-vector)) + [[\a \b] [\f] [\a \c] [\g]] '("afag" "afcg" "bfag" "bfcg") + [[\a \b] [\a \c]] '("aa" "ac" "ba" "bc") + [[\a \b \c] [\a \c] [\b \c]] '("aab" "aac" "acb" "acc" "bab" "bac" "bcb" "bcc" "cab" "cac" "ccb" "ccc")))) + +(deftest get-possible-key-combinations-test + (testing "The keys must be the all combinations, but the blocks must create a permutations which will be joined as a combination" + (are [possible-keys-vector expected-keys] + (= expected-keys (get-possible-key-combinations possible-keys-vector)) + [[\a \b] [\f] [\e \c] [\g]] '("abfecg" "abfceg" "bafecg" "bafceg") + [[\a \b] [\a \c]] '("abac" "abca" "baac" "baca") + [[\a \b \c] [\a \c] [\b \c]] '("abcacbc" "abcaccb" "abccabc" "abccacb" + "acbacbc" "acbaccb" "acbcabc" "acbcacb" + "bacacbc" "bacaccb" "baccabc" "baccacb" + "bcaacbc" "bcaaccb" "bcacabc" "bcacacb" + "cabacbc" "cabaccb" "cabcabc" "cabcacb" + "cbaacbc" "cbaaccb" "cbacabc" "cbacacb")))) + diff --git a/test/cipher_analytical_machine/ciphers/gamma_test.clj b/test/cipher_analytical_machine/ciphers/gamma_test.clj new file mode 100644 index 0000000..0aaa809 --- /dev/null +++ b/test/cipher_analytical_machine/ciphers/gamma_test.clj @@ -0,0 +1,62 @@ +(ns cipher-analytical-machine.ciphers.gamma-test + (:require + [clojure.test :refer :all] + [cipher-analytical-machine.ciphers.gamma :refer :all])) + +(deftest add-mod-test + (testing "The remainder must be as expected." + (are [a b module expected] + (= expected (add-mod a b module)) + 5 2 4 3 + 1 2 3 0 + 2 2 3 1))) + +(deftest generate-seq-test + (testing "Checking that the next element of a sequence is calculated by (e[i-1] + e[i-3] % mod)." + (are [a b c module size expected] + (= expected (take size (generate-seq a b c module))) + 4 32 15 33 10 [4 32 15 19 18 0 19 4 4 23] + 1 2 3 3 9 [1 2 3 1 0 0 1 1 1]))) + +(deftest generate-gamma-seq-test + (testing "Checking that the next element of a sequence is calculated by (s[i] + s[i+1] % mod)." + (are [acc seq module expected] + (= expected (generate-gamma-seq acc seq module)) + [] [4 32 15 19 18 0 19 4 4 23] 33 [3 14 1 4 18 19 23 8 27] + [] [1 2 3 1 0 0 1 1 1] 3 [0 2 1 1 0 1 2 2]))) + +(deftest generate-gamma-test + (testing "Checking that the gamma is generated as expected." + (are [a b c module size expected] + (= expected (generate-gamma a b c module size)) + 4 32 15 33 10 [3 14 1 4 18 19 23 8 27 17] + 1 2 3 3 9 [0 2 1 1 0 1 2 2 0]))) + +(deftest encrypt-array-test + (testing "Checking that the message is encrypted as expected: (msg+gamma)%module." + (are [key module array expected] + (= expected (encrypt-array key module array)) + [4 32 15] 33 [3 14 1 4 18 19 23 8 27 17] [6 28 2 8 3 5 13 16 21 1] + [1 2 3 ] 3 [0 2 1 1 0 1 2 2 0] [0 1 2 2 0 2 1 1 0]))) + +(deftest encrypt-message-test + (testing "Checking that the message is encrypted as expected" + (are [key symbols message expected] + (= expected (encrypt-message key symbols message)) + [4 32 15] "абвгґдеєжзиіїйклмнопрстуфхцчшщьюя" "гкбґопужчн" "ешвжгдймсб" + [1 2 3 ] "абв" "авббабвва" "абввавбба"))) + +(deftest decrypt-array-test + (testing "Checking that the message is decrypted as expected." + (are [key module array expected] + (= expected (decrypt-array key module array)) + [4 32 15] 33 [6 28 2 8 3 5 13 16 21 1] [3 14 1 4 18 19 23 8 27 17] + [1 2 3 ] 3 [0 1 2 2 0 2 1 1 0] [0 2 1 1 0 1 2 2 0]))) + +(deftest decrypt-message-test + (testing "Checking that the message is decrypted as expected" + (are [key symbols message expected] + (= expected (decrypt-message key symbols message)) + [4 32 15] "абвгґдеєжзиіїйклмнопрстуфхцчшщьюя" "ешвжгдймсб" "гкбґопужчн" + [1 2 3 ] "абв" "абввавбба" "авббабвва"))) + diff --git a/test/cipher_analytical_machine/ciphers/simple_substitution_test.clj b/test/cipher_analytical_machine/ciphers/simple_substitution_test.clj new file mode 100644 index 0000000..5eac042 --- /dev/null +++ b/test/cipher_analytical_machine/ciphers/simple_substitution_test.clj @@ -0,0 +1,85 @@ +(ns cipher-analytical-machine.ciphers.simple-substitution-test + (:require + [clojure.test :refer :all] + [cipher-analytical-machine.ciphers.simple-substitution :refer :all])) + +(deftest find-value-in-table-test + (let [table {\a 1 \b 2 \c 3} + rtable {1 \a 2 \b 3 \c}] + (testing "If the symbol is in the table as a key, then the result won't nil." + (are [key expected] + (= expected (find-value-in-table key table)) + \a 1 + \d nil + 1 nil + 5 nil)) + + (testing "If the digit is in the reversed table as a key, then the result won't nil." + (are [key expected] + (= expected (find-value-in-table key rtable)) + \a nil + \d nil + 1 \a + 5 nil)))) + +(deftest generate-sorted-substitution-table-test + (testing "If the symbol is in the table as a key, then the result won't nil." + (are [symbols expected-table] + (= expected-table (generate-sorted-substitution-table symbols)) + "abc" {0 \a 1 \b 2 \c} + "a" {0 \a}))) + +(deftest encrypt-message-test + (let [symbols "abc" + table {\a 1 \b 2 \c 3}] + (testing "The function must encrypt the message and remove unknown symbols." + (are [message expected] + (= expected (encrypt-message message table)) + "abc" "1,2,3" + "aDbdc" "1,2,3")))) + +(deftest encrypt-message-with-caesar-test + (let [symbols "abc" + table {\a 1 \b 2 \c 3}] + (testing "The function must encrypt the message and remove unknown symbols." + (are [message key expected] + (= expected (encrypt-message-with-caesar message key table symbols)) + "abc" 0 "1,2,3" + "abc" 1 "2,3,1" + "aDbdc" 0 "1,2,3")))) + +(deftest decrypt-by-table-test + (let [table {\d \a \e \b \f \c}] + (testing "The function must decrypt the message by the table." + (are [message expected] + (= expected (decrypt-by-table table message)) + "def" '(\a \b \c) + "abf" '(nil nil \c) + "fed" '(\c \b \a))))) + +(deftest decrypt-message-by-symbol-table-test + (let [table {\d \a \e \b \f \c}] + (testing "The function must decrypt the message and remove unknown numbers." + (are [message expected] + (= expected (decrypt-message-by-symbol-table table message)) + "def" "abc" + "daef" "abc")))) + +(deftest decrypt-message-test + (let [table {1 \a 2 \b 3 \c}] + (testing "The function must decrypt the message and remove unknown numbers." + (are [message expected] + (= expected (decrypt-message message table)) + "1,2,3" "abc" + "1,12,2,3" "abc")))) + +(deftest decrypt-message-with-caesar-test + (let [symbols "abc" + table {1 \a 2 \b 3 \c}] + (testing "The function must decrypt the message and remove unknown numbers." + (are [message key expected] + (= expected (decrypt-message-with-caesar message key table symbols)) + "1,2,3" 0 "abc" + "2,3,1" 1 "abc" + "1,12,2,3" 0 "abc")))) + diff --git a/test/cipher_analytical_machine/parsers/gamma_test.clj b/test/cipher_analytical_machine/parsers/gamma_test.clj new file mode 100644 index 0000000..11992b2 --- /dev/null +++ b/test/cipher_analytical_machine/parsers/gamma_test.clj @@ -0,0 +1,20 @@ +(ns cipher-analytical-machine.parsers.gamma-test + (:require + [clojure.test :refer :all] + [cipher-analytical-machine.parsers.gamma :refer :all])) + +(deftest key?-test + (testing "The function return true only if the key in the format '%d,%d,%d'." + (are [str expected] + (= expected + (key? str)) + "9" false + "9,10" false + "10,9,8" true + "-10,9,-8" true + "-10,-9,-8" true + "asd 10 " false + " 10 " false + "-10" false + "abc" false))) + diff --git a/test/cipher_analytical_machine/parsers/parsers_test.clj b/test/cipher_analytical_machine/parsers/parsers_test.clj new file mode 100644 index 0000000..1d027f3 --- /dev/null +++ b/test/cipher_analytical_machine/parsers/parsers_test.clj @@ -0,0 +1,40 @@ +(ns cipher-analytical-machine.parsers.parsers-test + (:require + [clojure.test :refer :all] + [cipher-analytical-machine.parsers.parsers :refer :all])) + +(deftest unsigned-int-string?-test + (testing "The function return true only for an unsingned integer as a string." + (are [str expected] + (= expected + (unsigned-int-string? str)) + "9" true + "9" true + "10" true + "asd 10 " false + " 10 " false + "-10" false + "abc" false))) + +(deftest int-string?-test + (testing "The function return true only for an singned integer as a string." + (are [str expected] + (= expected + (int-string? str)) + "9" true + "10" true + "-10" true + "asd -10 " false + " -10 " false + "abc" false))) + +(deftest parse-unsigned-int-test + (testing "The function parse the integer if the string is an integer." + (are [str-or-int expected] + (= expected + (parse-unsigned-int str-or-int)) + "9" 9 + 9 9 + "10" 10 + "-10" nil + "abc" nil))) diff --git a/test/cipher_analytical_machine/parsers/simple_substitution_test.clj b/test/cipher_analytical_machine/parsers/simple_substitution_test.clj new file mode 100644 index 0000000..0300066 --- /dev/null +++ b/test/cipher_analytical_machine/parsers/simple_substitution_test.clj @@ -0,0 +1,49 @@ +(ns cipher-analytical-machine.parsers.simple-substitution-test + (:require + [clojure.test :refer :all] + [cipher-analytical-machine.parsers.simple-substitution :refer :all])) + +(deftest encode-key-and-substitution-table-to-json-test + (testing "The encoder must parse the map and create a json." + (are [key table expected-json] + (= expected-json + (encode-key-and-substitution-table-to-json key table)) + 1 {0 \a 1 \b 2 \c} "{\"key\":1,\"table\":{\"0\":\"a\",\"1\":\"b\",\"2\":\"c\"}}" + 2 {\a 0 \b 1 \c 2} "{\"key\":2,\"table\":{\"a\":0,\"b\":1,\"c\":2}}"))) + +(deftest decode-key-and-substitution-table-from-json-test + (testing "The decoder must parse the json and create a map." + (are [json expected-data] + (= expected-data + (decode-key-and-substitution-table-from-json json)) + "{\"key\":1, \"table\":{\"0\":\"a\",\"1\":\"b\",\"2\":\"c\"}}" {"key" 1 "table" {0 \a 1 \b 2 \c}} + "{\"key\":2, \"table\":{\"a\":0,\"b\":1,\"c\":2}}" {"key" 2 "table" {\a 0 \b 1 \c 2}}))) + +(deftest generate-table-or-decode-json-test + (let [symbols "abc"] + (testing "The function decodes the json." + (are [json expected-data] + (= expected-data + (generate-table-or-decode-json json symbols)) + "{\"key\":1, \"table\":{\"0\":\"a\",\"1\":\"b\",\"2\":\"c\"}}" {"key" 1 "table" {0 \a 1 \b 2 \c}} + "{\"key\":2, \"table\":{\"a\":0,\"b\":1,\"c\":2}}" {"key" 2 "table" {\a 0 \b 1 \c 2}})) + + (testing "The function generates the table, because the argument is a key." + (are [json expected-data] + (let [data (generate-table-or-decode-json json symbols)] + (and (= (get expected-data "key") + (get data "key")) + (= (keys (get expected-data "table")) + (keys (get data "table")))) + (generate-table-or-decode-json json symbols)) + "1" {"key" 1 "table" {0 \a 1 \b 2 \c}} + "2" {"key" 2 "table" {\a 0 \b 1 \c 2}})))) + +(deftest invert-table-in-map-test + (testing "The function invert the table in a decoded json." + (are [decoded-json expected-data] + (= expected-data + (invert-table-in-map decoded-json)) + {"key" 1 "table" {0 \a 1 \b 2 \c}} {"key" 1 "table" {\a 0 \b 1 \c 2}} + {"key" 2 "table" {\a 0 \b 1 \c 2}} {"key" 2 "table" {0 \a 1 \b 2 \c}}))) + diff --git a/test/cipher_analytical_machine/symbols/frequencies_test.clj b/test/cipher_analytical_machine/symbols/frequencies_test.clj index 1db5bd0..6f6e098 100644 --- a/test/cipher_analytical_machine/symbols/frequencies_test.clj +++ b/test/cipher_analytical_machine/symbols/frequencies_test.clj @@ -9,3 +9,13 @@ (is (= "etaoinsrhldcumfpgwybvkxjqz" (map-to-string symbol-map)))))) +(deftest default-frequency-factory-test + (testing "Factory supports languages: English (en), Ukrainian (uk). If language code is unidentified, then it must have the 'en'." + (are [language-code expected-map-size] + (->> (default-frequency-factory language-code) + count + (= expected-map-size)) + "en" 26 + "uk" 34 + "Maybe a code" 26))) +