mirror of
https://github.com/ReVax-2007/cipher_master.git
synced 2026-09-17 21:32:27 +00:00
Compare commits
3 Commits
v1.0.0
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e24e2d989 | |||
|
|
98917ccbf9 | ||
| 5f91eb3542 |
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -39,14 +39,13 @@ jobs:
|
||||
patchelf
|
||||
|
||||
- name: Build Tauri App
|
||||
uses: tauri-apps/tauri-action@v1
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tagName: v__VERSION__
|
||||
releaseName: 'Cipher Master v__VERSION__'
|
||||
projectPath: src-tauri
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: 'Cipher Master ${{ github.ref_name }}'
|
||||
releaseBody: 'Download the compiled standalone application binaries below.'
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
includeRelease: true
|
||||
includeDebug: false
|
||||
prerelease: false
|
||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -1 +1,36 @@
|
||||
/target
|
||||
# Rust and Tauri build output
|
||||
/target/
|
||||
/src-tauri/target/
|
||||
|
||||
# Operating-system metadata
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
._*
|
||||
.Spotlight-V100/
|
||||
.Trashes/
|
||||
.fseventsd
|
||||
.VolumeIcon.icns
|
||||
Desktop.ini
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Editor and tool state
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*.swn
|
||||
|
||||
# Logs, temporary files, and local environment settings
|
||||
*.log
|
||||
*.tmp
|
||||
*.temp
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Files generated by the application when run from the repository
|
||||
cipher_challenge.txt
|
||||
cipher_hint.txt
|
||||
cipher_solution.txt
|
||||
|
||||
1638
Cargo.lock
generated
1638
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ An AI-powered desktop application built with **Rust** and **Tauri v2** that gene
|
||||
|
||||
## 🌟 Key Features
|
||||
|
||||
* **8 Cipher Engines**:
|
||||
* **9 Cipher Engines**:
|
||||
* **Elder Futhark Runes**: Custom handwritten runic character map.
|
||||
* **Caesar Shift**: Dynamic rotational shift ($1..25$).
|
||||
* **Atbash Cipher**: Reversed alphabet substitution.
|
||||
@@ -22,6 +22,7 @@ An AI-powered desktop application built with **Rust** and **Tauri v2** that gene
|
||||
* **Rail Fence (Zig-Zag)**: Multi-rail transposition matrix.
|
||||
* **Affine Cipher**: Mathematical linear function cipher ($E(x) = (ax + b) \pmod{26}$).
|
||||
* **A1Z26 Numeric**: Positional index substitution.
|
||||
* **Fill in the Blank**: Assigns every letter a unique number from `01` to `26`, reveals a few mappings as clues, and preserves repeated-letter patterns.
|
||||
* **Local AI Integration & Fallback Support**: Connects to a local [Ollama](https://ollama.com/) instance (`llama3.2:1b`) to generate dynamic quotes. If Ollama is not installed or running, the engine automatically uses internal text prompts.
|
||||
* **Difficulty Scaling**:
|
||||
* **Easy**: Preserves original word boundaries, spaces, and punctuation. Generates detailed letter frequency and vowel count hints.
|
||||
@@ -31,6 +32,10 @@ An AI-powered desktop application built with **Rust** and **Tauri v2** that gene
|
||||
* `cipher_challenge.txt`: Formatted ciphertext ready to solve.
|
||||
* `cipher_hint.txt`: Cryptanalytic structural metrics and riddle clues.
|
||||
* `cipher_solution.txt`: Full original plaintext and decryption parameters/keys.
|
||||
|
||||
### Fill in the Blank Mode
|
||||
|
||||
This mode creates a numeric substitution puzzle. Each letter is assigned a unique number from `01` to `26`; the same letter always produces the same number, while different letters never share a number. Word boundaries remain visible in the challenge, and `cipher_hint.txt` reveals a small number of starter mappings based on the selected difficulty. The complete alphabet mapping is available only in `cipher_solution.txt`.
|
||||
* **Cross-Platform Support**: Native desktop GUI built for macOS and Windows.
|
||||
|
||||
---
|
||||
|
||||
1
SoT.md
1
SoT.md
@@ -125,6 +125,7 @@ $$E(x) = (a \cdot x + b) \pmod{26}$$
|
||||
|
||||
|
||||
8. **A1Z26 Numeric**: Converts each letter to its 1-based numerical index separated by hyphens (e.g., $A=1, B=2, Z=26$).
|
||||
9. **Fill in the Blank**: Builds a randomized one-to-one mapping from every letter in $A..Z$ to a unique number in $01..26$. The challenge displays those numbers while preserving word boundaries, `cipher_hint.txt` reveals a small set of known letter mappings, and `cipher_solution.txt` stores the complete mapping.
|
||||
|
||||
### B. Difficulty Formatting Pipeline
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
<option value="RailFence">Rail Fence (Zig-Zag)</option>
|
||||
<option value="Affine">Affine Cipher</option>
|
||||
<option value="A1Z26">A1Z26 Numeric</option>
|
||||
<option value="FillInTheBlank">Fill in the Blank (Number Mapping)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ enum CipherType {
|
||||
RailFence,
|
||||
Affine,
|
||||
A1Z26,
|
||||
FillInTheBlank,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
@@ -52,6 +53,7 @@ fn generate_cipher_challenge(
|
||||
"Vigenere" => CipherType::Vigenere,
|
||||
"RailFence" => CipherType::RailFence,
|
||||
"Affine" => CipherType::Affine,
|
||||
"FillInTheBlank" => CipherType::FillInTheBlank,
|
||||
_ => CipherType::A1Z26,
|
||||
};
|
||||
|
||||
@@ -202,7 +204,7 @@ fn get_custom_futhark_map() -> HashMap<char, char> {
|
||||
fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String, String) {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let (ciphertext, key_info) = match cipher {
|
||||
let (ciphertext, key_info, custom_hint) = match cipher {
|
||||
CipherType::CustomFuthark => {
|
||||
let map = get_custom_futhark_map();
|
||||
let encrypted = text
|
||||
@@ -210,7 +212,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
.chars()
|
||||
.map(|c| *map.get(&c).unwrap_or(&c))
|
||||
.collect();
|
||||
(encrypted, "Elder Futhark Key Map".to_string())
|
||||
(encrypted, "Elder Futhark Key Map".to_string(), None)
|
||||
}
|
||||
CipherType::Caesar => {
|
||||
let shift = rng.gen_range(1..25) as u8;
|
||||
@@ -223,7 +225,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
} else { c }
|
||||
})
|
||||
.collect();
|
||||
(encrypted, format!("Caesar Shift = {}", shift))
|
||||
(encrypted, format!("Caesar Shift = {}", shift), None)
|
||||
}
|
||||
CipherType::Atbash => {
|
||||
let encrypted = text
|
||||
@@ -235,7 +237,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
} else { c }
|
||||
})
|
||||
.collect();
|
||||
(encrypted, "Atbash (Reversed Alphabet)".to_string())
|
||||
(encrypted, "Atbash (Reversed Alphabet)".to_string(), None)
|
||||
}
|
||||
CipherType::Substitution => {
|
||||
let mut alphabet: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().collect();
|
||||
@@ -253,7 +255,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
})
|
||||
.collect();
|
||||
let key_str = map.iter().map(|(k, v)| format!("{}->{}", k, v)).collect::<Vec<String>>().join(", ");
|
||||
(encrypted, format!("Substitution Table: [{}]", key_str))
|
||||
(encrypted, format!("Substitution Table: [{}]", key_str), None)
|
||||
}
|
||||
CipherType::Vigenere => {
|
||||
let keys = vec!["ODIN", "THOR", "VALHALLA", "RUNE", "RUST", "CRYPT"];
|
||||
@@ -272,7 +274,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
} else { c }
|
||||
})
|
||||
.collect();
|
||||
(encrypted, format!("Vigenere Passphrase: {}", key))
|
||||
(encrypted, format!("Vigenere Passphrase: {}", key), None)
|
||||
}
|
||||
CipherType::RailFence => {
|
||||
let rails = rng.gen_range(2..=4);
|
||||
@@ -288,7 +290,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
}
|
||||
|
||||
let encrypted = fence.into_iter().flatten().collect();
|
||||
(encrypted, format!("Rail Fence Depth = {} Rails", rails))
|
||||
(encrypted, format!("Rail Fence Depth = {} Rails", rails), None)
|
||||
}
|
||||
CipherType::Affine => {
|
||||
let a_options = vec![3, 5, 7, 9, 11, 15, 17, 19, 21, 23];
|
||||
@@ -306,7 +308,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
} else { c }
|
||||
})
|
||||
.collect();
|
||||
(encrypted, format!("Affine Cipher: E(x) = ({}x + {}) mod 26", a, b))
|
||||
(encrypted, format!("Affine Cipher: E(x) = ({}x + {}) mod 26", a, b), None)
|
||||
}
|
||||
CipherType::A1Z26 => {
|
||||
let encrypted = text
|
||||
@@ -322,14 +324,73 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
.collect::<String>()
|
||||
.trim_start_matches('-')
|
||||
.to_string();
|
||||
(encrypted, "A1Z26 Position Mapping".to_string())
|
||||
(encrypted, "A1Z26 Position Mapping".to_string(), None)
|
||||
}
|
||||
CipherType::FillInTheBlank => {
|
||||
let (challenge, key, hint) = generate_fill_in_the_blank(text, diff, &mut rng);
|
||||
(challenge, key, Some(hint))
|
||||
}
|
||||
};
|
||||
|
||||
let content_hint = generate_content_hints(text, diff);
|
||||
let content_hint = custom_hint.unwrap_or_else(|| generate_content_hints(text, diff));
|
||||
(ciphertext, key_info, content_hint)
|
||||
}
|
||||
|
||||
fn generate_fill_in_the_blank(
|
||||
text: &str,
|
||||
diff: Difficulty,
|
||||
rng: &mut impl Rng,
|
||||
) -> (String, String, String) {
|
||||
let alphabet: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().collect();
|
||||
let mut numbers: Vec<u8> = (1..=26).collect();
|
||||
numbers.shuffle(rng);
|
||||
let mapping: HashMap<char, u8> = alphabet.iter().copied().zip(numbers).collect();
|
||||
|
||||
let challenge = text
|
||||
.chars()
|
||||
.map(|character| {
|
||||
if character.is_ascii_alphabetic() {
|
||||
format!("{:02} ", mapping[&character.to_ascii_uppercase()])
|
||||
} else if character.is_whitespace() {
|
||||
" ".to_string()
|
||||
} else {
|
||||
character.to_string()
|
||||
}
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_string();
|
||||
|
||||
let used_letters: Vec<char> = alphabet
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|letter| text.to_ascii_uppercase().contains(*letter))
|
||||
.collect();
|
||||
let clue_count = match diff {
|
||||
Difficulty::Easy => 4,
|
||||
Difficulty::Medium => 3,
|
||||
Difficulty::Hard => 2,
|
||||
}
|
||||
.min(used_letters.len());
|
||||
let clues = used_letters
|
||||
.iter()
|
||||
.take(clue_count)
|
||||
.map(|letter| format!("{} = {:02}", letter, mapping[letter]))
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let key = alphabet
|
||||
.iter()
|
||||
.map(|letter| format!("{}={:02}", letter, mapping[letter]))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
let hint = format!(
|
||||
"FILL-IN-THE-BLANK RULES:\n- Each number represents one letter.\n- Every letter uses a unique number from 01 to 26.\n- Repeated numbers represent repeated letters.\n\nKNOWN LETTER MAPPINGS:\n{}",
|
||||
clues.join(", ")
|
||||
);
|
||||
|
||||
(challenge, format!("Fill-in-the-Blank Number Key: {}", key), hint)
|
||||
}
|
||||
|
||||
fn generate_content_hints(plaintext: &str, diff: Difficulty) -> String {
|
||||
let cleaned: Vec<char> = plaintext
|
||||
.chars()
|
||||
@@ -404,6 +465,31 @@ fn generate_content_hints(plaintext: &str, diff: Difficulty) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fill_in_the_blank_uses_unique_stable_numbers_and_clues() {
|
||||
let (challenge, key, hint) = generate_fill_in_the_blank(
|
||||
"ABBA CAB",
|
||||
Difficulty::Easy,
|
||||
&mut rand::thread_rng(),
|
||||
);
|
||||
|
||||
let numbers: Vec<&str> = challenge
|
||||
.split_whitespace()
|
||||
.filter(|token| token.chars().all(|character| character.is_ascii_digit()))
|
||||
.collect();
|
||||
assert_eq!(numbers.len(), 7);
|
||||
assert_eq!(numbers[1], numbers[2]);
|
||||
assert_eq!(numbers[0], numbers[3]);
|
||||
assert!(key.contains("A="));
|
||||
assert!(key.contains("Z="));
|
||||
assert!(hint.contains("KNOWN LETTER MAPPINGS:"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
|
||||
81
src/main.rs
81
src/main.rs
@@ -19,6 +19,7 @@ enum CipherType {
|
||||
RailFence,
|
||||
Affine,
|
||||
A1Z26,
|
||||
FillInTheBlank,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
@@ -54,6 +55,7 @@ fn generate_cipher_challenge(
|
||||
"Vigenere" => CipherType::Vigenere,
|
||||
"RailFence" => CipherType::RailFence,
|
||||
"Affine" => CipherType::Affine,
|
||||
"FillInTheBlank" => CipherType::FillInTheBlank,
|
||||
_ => CipherType::A1Z26,
|
||||
};
|
||||
|
||||
@@ -204,7 +206,7 @@ fn get_custom_futhark_map() -> HashMap<char, char> {
|
||||
fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String, String) {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let (ciphertext, key_info) = match cipher {
|
||||
let (ciphertext, key_info, custom_hint) = match cipher {
|
||||
CipherType::CustomFuthark => {
|
||||
let map = get_custom_futhark_map();
|
||||
let encrypted = text
|
||||
@@ -212,7 +214,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
.chars()
|
||||
.map(|c| *map.get(&c).unwrap_or(&c))
|
||||
.collect();
|
||||
(encrypted, "Elder Futhark Key Map".to_string())
|
||||
(encrypted, "Elder Futhark Key Map".to_string(), None)
|
||||
}
|
||||
CipherType::Caesar => {
|
||||
let shift = rng.gen_range(1..25) as u8;
|
||||
@@ -225,7 +227,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
} else { c }
|
||||
})
|
||||
.collect();
|
||||
(encrypted, format!("Caesar Shift = {}", shift))
|
||||
(encrypted, format!("Caesar Shift = {}", shift), None)
|
||||
}
|
||||
CipherType::Atbash => {
|
||||
let encrypted = text
|
||||
@@ -237,7 +239,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
} else { c }
|
||||
})
|
||||
.collect();
|
||||
(encrypted, "Atbash (Reversed Alphabet)".to_string())
|
||||
(encrypted, "Atbash (Reversed Alphabet)".to_string(), None)
|
||||
}
|
||||
CipherType::Substitution => {
|
||||
let mut alphabet: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().collect();
|
||||
@@ -255,7 +257,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
})
|
||||
.collect();
|
||||
let key_str = map.iter().map(|(k, v)| format!("{}->{}", k, v)).collect::<Vec<String>>().join(", ");
|
||||
(encrypted, format!("Substitution Table: [{}]", key_str))
|
||||
(encrypted, format!("Substitution Table: [{}]", key_str), None)
|
||||
}
|
||||
CipherType::Vigenere => {
|
||||
let keys = vec!["ODIN", "THOR", "VALHALLA", "RUNE", "RUST", "CRYPT"];
|
||||
@@ -274,7 +276,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
} else { c }
|
||||
})
|
||||
.collect();
|
||||
(encrypted, format!("Vigenere Passphrase: {}", key))
|
||||
(encrypted, format!("Vigenere Passphrase: {}", key), None)
|
||||
}
|
||||
CipherType::RailFence => {
|
||||
let rails = rng.gen_range(2..=4);
|
||||
@@ -290,7 +292,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
}
|
||||
|
||||
let encrypted = fence.into_iter().flatten().collect();
|
||||
(encrypted, format!("Rail Fence Depth = {} Rails", rails))
|
||||
(encrypted, format!("Rail Fence Depth = {} Rails", rails), None)
|
||||
}
|
||||
CipherType::Affine => {
|
||||
let a_options = vec![3, 5, 7, 9, 11, 15, 17, 19, 21, 23];
|
||||
@@ -308,7 +310,7 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
} else { c }
|
||||
})
|
||||
.collect();
|
||||
(encrypted, format!("Affine Cipher: E(x) = ({}x + {}) mod 26", a, b))
|
||||
(encrypted, format!("Affine Cipher: E(x) = ({}x + {}) mod 26", a, b), None)
|
||||
}
|
||||
CipherType::A1Z26 => {
|
||||
let encrypted = text
|
||||
@@ -324,14 +326,73 @@ fn encrypt(text: &str, cipher: CipherType, diff: Difficulty) -> (String, String,
|
||||
.collect::<String>()
|
||||
.trim_start_matches('-')
|
||||
.to_string();
|
||||
(encrypted, "A1Z26 Position Mapping".to_string())
|
||||
(encrypted, "A1Z26 Position Mapping".to_string(), None)
|
||||
}
|
||||
CipherType::FillInTheBlank => {
|
||||
let (challenge, key, hint) = generate_fill_in_the_blank(text, diff, &mut rng);
|
||||
(challenge, key, Some(hint))
|
||||
}
|
||||
};
|
||||
|
||||
let content_hint = generate_content_hints(text, diff);
|
||||
let content_hint = custom_hint.unwrap_or_else(|| generate_content_hints(text, diff));
|
||||
(ciphertext, key_info, content_hint)
|
||||
}
|
||||
|
||||
fn generate_fill_in_the_blank(
|
||||
text: &str,
|
||||
diff: Difficulty,
|
||||
rng: &mut impl Rng,
|
||||
) -> (String, String, String) {
|
||||
let alphabet: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().collect();
|
||||
let mut numbers: Vec<u8> = (1..=26).collect();
|
||||
numbers.shuffle(rng);
|
||||
let mapping: HashMap<char, u8> = alphabet.iter().copied().zip(numbers).collect();
|
||||
|
||||
let challenge = text
|
||||
.chars()
|
||||
.map(|character| {
|
||||
if character.is_ascii_alphabetic() {
|
||||
format!("{:02} ", mapping[&character.to_ascii_uppercase()])
|
||||
} else if character.is_whitespace() {
|
||||
" ".to_string()
|
||||
} else {
|
||||
character.to_string()
|
||||
}
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_string();
|
||||
|
||||
let used_letters: Vec<char> = alphabet
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|letter| text.to_ascii_uppercase().contains(*letter))
|
||||
.collect();
|
||||
let clue_count = match diff {
|
||||
Difficulty::Easy => 4,
|
||||
Difficulty::Medium => 3,
|
||||
Difficulty::Hard => 2,
|
||||
}
|
||||
.min(used_letters.len());
|
||||
let clues = used_letters
|
||||
.iter()
|
||||
.take(clue_count)
|
||||
.map(|letter| format!("{} = {:02}", letter, mapping[letter]))
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let key = alphabet
|
||||
.iter()
|
||||
.map(|letter| format!("{}={:02}", letter, mapping[letter]))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
let hint = format!(
|
||||
"FILL-IN-THE-BLANK RULES:\n- Each number represents one letter.\n- Every letter uses a unique number from 01 to 26.\n- Repeated numbers represent repeated letters.\n\nKNOWN LETTER MAPPINGS:\n{}",
|
||||
clues.join(", ")
|
||||
);
|
||||
|
||||
(challenge, format!("Fill-in-the-Blank Number Key: {}", key), hint)
|
||||
}
|
||||
|
||||
fn generate_content_hints(plaintext: &str, diff: Difficulty) -> String {
|
||||
let cleaned: Vec<char> = plaintext
|
||||
.chars()
|
||||
|
||||
Reference in New Issue
Block a user