├── .gitignore ├── purrcrypt-demo.gif ├── src ├── lib.rs ├── debug.rs ├── config.rs ├── crypto.rs ├── keystore.rs ├── cipher │ ├── patterns.rs │ └── mod.rs ├── keys.rs └── main.rs ├── .github └── FUNDING.yml ├── Cargo.toml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /purrcrypt-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vxfemboy/purrcrypt/HEAD/purrcrypt-demo.gif -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // src/lib.rs 2 | pub mod cipher; 3 | pub mod config; 4 | pub mod crypto; 5 | pub mod debug; 6 | pub mod keys; 7 | pub mod keystore; 8 | -------------------------------------------------------------------------------- /src/debug.rs: -------------------------------------------------------------------------------- 1 | // src/debug.rs 2 | use std::sync::atomic::{AtomicBool, Ordering}; 3 | 4 | static VERBOSE: AtomicBool = AtomicBool::new(false); 5 | 6 | pub fn set_verbose(enabled: bool) { 7 | VERBOSE.store(enabled, Ordering::SeqCst); 8 | } 9 | 10 | pub fn is_verbose() -> bool { 11 | VERBOSE.load(Ordering::SeqCst) 12 | } 13 | 14 | #[macro_export] 15 | macro_rules! debug { 16 | ($($arg:tt)*) => { 17 | if $crate::debug::is_verbose() { 18 | eprintln!($($arg)*); 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: vxfemboy 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "purrcrypt" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["sad "] 6 | description = "A cat-and-dog-themed pgp-like encryption tool" 7 | readme = "README.md" 8 | homepage = "https://github.com/vxfemboy/purrcrypt" 9 | repository = "https://github.com/vxfemboy/purrcrypt" 10 | license = "MIT" 11 | keywords = ["encryption", "security", "cryptography", "cli", "cat"] 12 | categories = ["command-line-utilities", "cryptography"] 13 | 14 | [[bin]] 15 | name = "purr" 16 | path = "src/main.rs" 17 | 18 | [dependencies] 19 | flate2 = "1.0.35" 20 | regex = "1.11.1" 21 | elliptic-curve = "0.13.8" 22 | rand_core = "0.6.4" 23 | rand = "0.9.0" 24 | k256 = { version = "0.13.3", features = ["ecdh"] } 25 | aes-gcm = "0.10.3" 26 | base64 = "0.22.1" 27 | thiserror = "2.0.12" 28 | dirs = "6.0.0" 29 | serde = { version = "1.0.130", features = ["derive"] } 30 | serde_json = "1.0.68" 31 | toml = "0.8.20" 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 sad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // src/config.rs 2 | use serde::{Deserialize, Serialize}; 3 | use std::io::{self, Write}; 4 | use std::path::PathBuf; 5 | use std::{fs, path::Path}; 6 | use thiserror::Error; 7 | 8 | #[derive(Error, Debug)] 9 | pub enum ConfigError { 10 | #[error("IO error: {0}")] 11 | Io(#[from] io::Error), 12 | #[error("TOML error: {0}")] 13 | Toml(#[from] toml::ser::Error), 14 | #[error("TOML de error: {0}")] 15 | TomlDe(#[from] toml::de::Error), 16 | } 17 | 18 | #[derive(Debug, Serialize, Deserialize)] 19 | pub enum PreferredDialect { 20 | #[serde(rename = "cat")] 21 | Cat, 22 | #[serde(rename = "dog")] 23 | Dog, 24 | } 25 | 26 | #[derive(Debug, Serialize, Deserialize)] 27 | pub struct Config { 28 | pub dialect: PreferredDialect, 29 | } 30 | 31 | impl Default for Config { 32 | fn default() -> Self { 33 | Self { 34 | dialect: PreferredDialect::Cat, 35 | } 36 | } 37 | } 38 | 39 | impl Config { 40 | pub fn load(config_path: &Path) -> Result { 41 | if config_path.exists() { 42 | let contents = fs::read_to_string(config_path)?; 43 | Ok(toml::from_str(&contents)?) 44 | } else { 45 | Ok(Self::default()) 46 | } 47 | } 48 | 49 | pub fn save(&self, config_path: &Path) -> Result<(), ConfigError> { 50 | let contents = toml::to_string_pretty(self)?; 51 | fs::write(config_path, contents)?; 52 | Ok(()) 53 | } 54 | 55 | pub fn initialize(config_dir: &Path) -> Result { 56 | let config_path = config_dir.join("config.toml"); 57 | 58 | if config_path.exists() { 59 | return Self::load(&config_path); 60 | } 61 | 62 | // Create config directory if it doesn't exist 63 | fs::create_dir_all(config_dir)?; 64 | 65 | print!("🐱 Welcome to purrcrypt! Do you prefer cat or dog mode? [cat/dog]: "); 66 | io::stdout().flush()?; 67 | 68 | let mut input = String::new(); 69 | io::stdin().read_line(&mut input)?; 70 | 71 | let config = Config { 72 | dialect: match input.trim().to_lowercase().as_str() { 73 | "dog" => PreferredDialect::Dog, 74 | _ => PreferredDialect::Cat, 75 | }, 76 | }; 77 | 78 | config.save(&config_path)?; 79 | 80 | match config.dialect { 81 | PreferredDialect::Cat => println!("😺 Meow! Cat mode activated!"), 82 | PreferredDialect::Dog => println!("🐕 Woof! Dog mode activated!"), 83 | } 84 | 85 | Ok(config) 86 | } 87 | } 88 | 89 | pub struct ConfigManager { 90 | config: Config, 91 | config_path: PathBuf, 92 | } 93 | 94 | impl ConfigManager { 95 | pub fn new(config_dir: &Path) -> Result { 96 | let config_path = config_dir.join("config.toml"); 97 | let config = if config_path.exists() { 98 | Config::load(&config_path)? 99 | } else { 100 | Config::initialize(config_dir)? 101 | }; 102 | 103 | Ok(Self { 104 | config, 105 | config_path, 106 | }) 107 | } 108 | 109 | pub fn get_dialect(&self) -> &PreferredDialect { 110 | &self.config.dialect 111 | } 112 | 113 | pub fn set_dialect(&mut self, dialect: PreferredDialect) -> Result<(), ConfigError> { 114 | self.config.dialect = dialect; 115 | self.config.save(&self.config_path)?; 116 | Ok(()) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/crypto.rs: -------------------------------------------------------------------------------- 1 | // src/crypto.rs 2 | use crate::{ 3 | cipher::{CatCipher, CipherDialect, CipherMode}, 4 | debug, 5 | keys::{decrypt_data, encrypt_data, KeyError, KeyPair}, 6 | }; 7 | use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; 8 | use flate2::{ 9 | read::{ZlibDecoder, ZlibEncoder}, 10 | Compression, 11 | }; 12 | use k256::PublicKey; 13 | use std::{ 14 | fs::File, 15 | io::{self, BufReader, BufWriter, Read}, 16 | path::Path, 17 | }; 18 | use thiserror::Error; 19 | 20 | 21 | 22 | #[derive(Error, Debug)] 23 | pub enum CryptoError { 24 | #[error("IO error: {0}")] 25 | Io(#[from] io::Error), 26 | #[error("Key error: {0}")] 27 | Key(#[from] KeyError), 28 | #[error("Base64 error: {0}")] 29 | Base64(String), 30 | } 31 | 32 | // Debug function to print bytes in hex 33 | fn debug_hex(label: &str, data: &[u8]) { 34 | let hex: Vec = data.iter().map(|b| format!("{:02x}", b)).collect(); 35 | debug!("{}: {} bytes", label, data.len()); 36 | for chunk in hex.chunks(16) { 37 | debug!(" {}", chunk.join(" ")); 38 | } 39 | } 40 | 41 | pub fn encrypt_file( 42 | input_filename: &str, 43 | output_filename: &str, 44 | recipient_public_key: &PublicKey, 45 | dialect: CipherDialect, 46 | ) -> Result<(), CryptoError> { 47 | let mut input_file = BufReader::new(File::open(input_filename)?); 48 | let mut output_file = BufWriter::new(File::create(output_filename)?); 49 | let cipher = CatCipher::new(dialect); 50 | 51 | // Read input data 52 | let mut input_data = Vec::new(); 53 | input_file.read_to_end(&mut input_data)?; 54 | debug_hex("Input data", &input_data); 55 | 56 | // Compress the input data 57 | let mut compressor = ZlibEncoder::new(&input_data[..], Compression::default()); 58 | let mut compressed_data = Vec::new(); 59 | compressor.read_to_end(&mut compressed_data)?; 60 | debug_hex("Compressed data", &compressed_data); 61 | 62 | // Encrypt the compressed data using elliptic curve 63 | let encrypted_data = encrypt_data(&compressed_data, recipient_public_key)?; 64 | debug_hex("Encrypted data", &encrypted_data); 65 | 66 | // First base64 encode the binary data 67 | let encoded_data = BASE64.encode(&encrypted_data); 68 | debug!("Base64 encoded: {}", encoded_data); 69 | 70 | // Then process it with the animal cipher 71 | cipher.process_data( 72 | encoded_data.as_bytes(), 73 | &mut output_file, 74 | CipherMode::Encrypt, 75 | )?; 76 | 77 | Ok(()) 78 | } 79 | 80 | pub fn decrypt_file( 81 | input_filename: &str, 82 | output_filename: &str, 83 | keypair: &KeyPair, 84 | ) -> Result<(), CryptoError> { 85 | let mut input_file = BufReader::new(File::open(input_filename)?); 86 | let mut output_file = BufWriter::new(File::create(output_filename)?); 87 | // Use cat dialect for decryption - it will try both patterns automatically 88 | let cipher = CatCipher::new(CipherDialect::Cat); 89 | 90 | // Read the encoded data 91 | let mut content = String::new(); 92 | input_file.read_to_string(&mut content)?; 93 | debug!("Encoded content: {}", content); 94 | 95 | // Decode cipher 96 | let decoded = cipher.process_string(&content, CipherMode::Decrypt)?; 97 | debug!( 98 | "Decoded content (base64): {}", 99 | String::from_utf8_lossy(&decoded) 100 | ); 101 | 102 | // Decode base64 103 | let encrypted_data = BASE64 104 | .decode(&decoded) 105 | .map_err(|e| CryptoError::Base64(e.to_string()))?; 106 | debug_hex("Decoded encrypted data", &encrypted_data); 107 | 108 | // Split and show ephemeral key 109 | if encrypted_data.len() > 65 { 110 | debug_hex("Ephemeral public key", &encrypted_data[..65]); 111 | debug_hex("Encrypted content", &encrypted_data[65..]); 112 | } 113 | 114 | // Decrypt using elliptic curve 115 | let compressed_data = decrypt_data(&encrypted_data, &keypair.secret_key)?; 116 | debug_hex("Decrypted data", &compressed_data); 117 | 118 | // Decompress the data 119 | let mut decompressor = ZlibDecoder::new(&compressed_data[..]); 120 | io::copy(&mut decompressor, &mut output_file)?; 121 | 122 | Ok(()) 123 | } 124 | 125 | pub fn generate_keypair(pub_path: &Path, secret_path: &Path) -> Result<(), KeyError> { 126 | let keypair = KeyPair::new(); 127 | keypair.save_keys(pub_path, secret_path) 128 | } 129 | 130 | 131 | pub fn load_keypair(pub_path: &Path, secret_path: &Path) -> Result { 132 | KeyPair::load_keypair(pub_path, secret_path) 133 | } -------------------------------------------------------------------------------- /src/keystore.rs: -------------------------------------------------------------------------------- 1 | // src/keystore.rs 2 | use crate::debug; 3 | use std::{ 4 | fs, io, 5 | path::{Path, PathBuf}, 6 | }; 7 | use thiserror::Error; 8 | 9 | #[cfg(unix)] 10 | use std::os::unix::fs::PermissionsExt; 11 | 12 | #[derive(Error, Debug)] 13 | pub enum KeystoreError { 14 | #[error("IO error: {0}")] 15 | Io(#[from] io::Error), 16 | #[error("Home directory not found")] 17 | NoHomeDir, 18 | #[error("Invalid key permissions: {0}")] 19 | InvalidPermissions(String), 20 | #[error("Key not found: {0}")] 21 | KeyNotFound(String), 22 | } 23 | 24 | pub struct Keystore { 25 | pub home_dir: PathBuf, 26 | pub keys_dir: PathBuf, 27 | } 28 | 29 | impl Keystore { 30 | pub fn new() -> Result { 31 | let home = dirs::home_dir().ok_or(KeystoreError::NoHomeDir)?; 32 | let purr_dir = home.join(".purr"); 33 | let keys_dir = purr_dir.join("keys"); 34 | 35 | // Create required directories 36 | fs::create_dir_all(&keys_dir)?; 37 | fs::create_dir_all(keys_dir.join("public"))?; 38 | fs::create_dir_all(keys_dir.join("private"))?; 39 | 40 | #[cfg(unix)] 41 | { 42 | // ~/.purr and keys directories should be private (700) 43 | fs::set_permissions(&purr_dir, fs::Permissions::from_mode(0o700))?; 44 | fs::set_permissions(&keys_dir, fs::Permissions::from_mode(0o700))?; 45 | 46 | // private key directory should be private (700) 47 | fs::set_permissions(&keys_dir.join("private"), fs::Permissions::from_mode(0o700))?; 48 | 49 | // public key directory can be readable (755) 50 | fs::set_permissions(&keys_dir.join("public"), fs::Permissions::from_mode(0o755))?; 51 | } 52 | 53 | Ok(Self { 54 | home_dir: purr_dir, 55 | keys_dir, 56 | }) 57 | } 58 | 59 | pub fn get_key_paths(&self, name: &str) -> (PathBuf, PathBuf) { 60 | // Strip any existing extensions 61 | let stem = Path::new(name) 62 | .file_stem() 63 | .and_then(|s| s.to_str()) 64 | .unwrap_or(name); 65 | 66 | let pub_path = self.keys_dir.join("public").join(format!("{}.pub", stem)); 67 | let priv_path = self.keys_dir.join("private").join(format!("{}.key", stem)); 68 | 69 | debug!("Looking for public key at: {}", pub_path.display()); 70 | debug!("Looking for private key at: {}", priv_path.display()); 71 | 72 | (pub_path, priv_path) 73 | } 74 | 75 | pub fn import_key(&self, key_path: &Path, is_public: bool) -> Result { 76 | let stem = key_path 77 | .file_stem() 78 | .and_then(|s| s.to_str()) 79 | .ok_or_else(|| { 80 | KeystoreError::Io(io::Error::new( 81 | io::ErrorKind::InvalidInput, 82 | "Invalid key filename", 83 | )) 84 | })?; 85 | 86 | let target_dir = if is_public { 87 | self.keys_dir.join("public") 88 | } else { 89 | self.keys_dir.join("private") 90 | }; 91 | 92 | let ext = if is_public { "pub" } else { "key" }; 93 | let target_path = target_dir.join(format!("{}.{}", stem, ext)); 94 | 95 | fs::copy(key_path, &target_path)?; 96 | 97 | if !is_public { 98 | #[cfg(unix)] 99 | { 100 | let perms = fs::Permissions::from_mode(0o600); 101 | fs::set_permissions(&target_path, perms)?; 102 | } 103 | 104 | #[cfg(windows)] 105 | { 106 | let mut perms = fs::metadata(&target_path)?.permissions(); 107 | perms.set_readonly(false); 108 | fs::set_permissions(&target_path, perms)?; 109 | } 110 | } 111 | 112 | Ok(target_path) 113 | } 114 | 115 | pub fn find_key(&self, name: &str, is_public: bool) -> Result { 116 | let (pub_path, priv_path) = self.get_key_paths(name); 117 | let path = if is_public { pub_path } else { priv_path }; 118 | 119 | if path.exists() { 120 | Ok(path) 121 | } else { 122 | // Try as direct path if not found in keystore 123 | let direct_path = Path::new(name); 124 | if direct_path.exists() { 125 | Ok(direct_path.to_path_buf()) 126 | } else { 127 | Err(KeystoreError::KeyNotFound(format!( 128 | "Key not found at {} or {}", 129 | path.display(), 130 | direct_path.display() 131 | ))) 132 | } 133 | } 134 | } 135 | 136 | pub fn list_keys(&self) -> Result<(Vec, Vec), KeystoreError> { 137 | let mut public_keys = Vec::new(); 138 | let mut private_keys = Vec::new(); 139 | 140 | for entry in fs::read_dir(self.keys_dir.join("public"))? { 141 | let entry = entry?; 142 | public_keys.push(entry.path()); 143 | } 144 | 145 | for entry in fs::read_dir(self.keys_dir.join("private"))? { 146 | let entry = entry?; 147 | private_keys.push(entry.path()); 148 | } 149 | 150 | Ok((public_keys, private_keys)) 151 | } 152 | 153 | pub fn verify_permissions(&self) -> Result<(), KeystoreError> { 154 | for entry in fs::read_dir(self.keys_dir.join("private"))? { 155 | let entry = entry?; 156 | let metadata = entry.metadata()?; 157 | 158 | #[cfg(unix)] 159 | { 160 | let mode = metadata.permissions().mode(); 161 | 162 | if mode & 0o077 != 0 { 163 | return Err(KeystoreError::InvalidPermissions(format!( 164 | "Private key {} has unsafe permissions: {:o}", 165 | entry.path().display(), 166 | mode 167 | ))); 168 | } 169 | } 170 | 171 | #[cfg(windows)] 172 | { 173 | let permissions = metadata.permissions(); 174 | 175 | if !permissions.readonly() { 176 | return Err(KeystoreError::InvalidPermissions(format!( 177 | "Private key {} is not read-only on Windows", 178 | entry.path().display() 179 | ))); 180 | } 181 | } 182 | } 183 | Ok(()) 184 | } 185 | pub fn delete_key(&self, name: &str, is_public: bool) -> Result<(), KeystoreError> { 186 | let (pub_path, priv_path) = self.get_key_paths(name); 187 | let target = if is_public { pub_path } else { priv_path }; 188 | 189 | if target.exists() { 190 | fs::remove_file(&target)?; 191 | Ok(()) 192 | } else { 193 | Err(KeystoreError::KeyNotFound(format!( 194 | "Cannot delete: key not found at {}", 195 | target.display() 196 | ))) 197 | } 198 | } 199 | 200 | } -------------------------------------------------------------------------------- /src/cipher/patterns.rs: -------------------------------------------------------------------------------- 1 | // src/cipher/patterns.rs 2 | use regex::Regex; 3 | 4 | #[derive(Copy, Clone, Debug)] 5 | pub enum PatternVariation { 6 | Complex, // For patterns with three parts 7 | Special, // For meow and bark patterns with four parts 8 | } 9 | 10 | pub struct CipherPattern { 11 | pub(crate) pattern_type: PatternVariation, 12 | pub(crate) prefix: String, 13 | #[allow(dead_code)] 14 | pub(crate) prefix_min: usize, 15 | #[allow(dead_code)] 16 | pub(crate) prefix_max: usize, 17 | #[allow(dead_code)] 18 | pub(crate) _middle_prefix: String, 19 | #[allow(dead_code)] 20 | pub(crate) _middle_min: usize, 21 | #[allow(dead_code)] 22 | pub(crate) _middle_max: usize, 23 | pub(crate) suffix: String, 24 | #[allow(dead_code)] 25 | pub(crate) _suffix_min: usize, 26 | #[allow(dead_code)] 27 | pub(crate) _suffix_max: usize, 28 | pub(crate) regex: Regex, 29 | } 30 | 31 | impl CipherPattern { 32 | pub fn new_complex( 33 | _base: &str, 34 | prefix: &str, 35 | prefix_min: usize, 36 | prefix_max: usize, 37 | middle: &str, 38 | middle_min: usize, 39 | middle_max: usize, 40 | suffix: &str, 41 | suffix_min: usize, 42 | suffix_max: usize, 43 | ) -> Self { 44 | // Create a regex pattern that allows for varying repetitions 45 | let pattern = format!( 46 | "^{prefix}{{1,{prefix_max}}}{middle}{{1,{middle_max}}}{suffix}{{1,{suffix_max}}}$", 47 | prefix = regex::escape(prefix), 48 | prefix_max = prefix_max, 49 | middle = regex::escape(middle), 50 | middle_max = middle_max, 51 | suffix = regex::escape(suffix), 52 | suffix_max = suffix_max 53 | ); 54 | 55 | Self { 56 | pattern_type: PatternVariation::Complex, 57 | prefix: prefix.to_string(), 58 | prefix_min, 59 | prefix_max, 60 | _middle_prefix: middle.to_string(), 61 | _middle_min: middle_min, 62 | _middle_max: middle_max, 63 | suffix: suffix.to_string(), 64 | _suffix_min: suffix_min, 65 | _suffix_max: suffix_max, 66 | regex: Regex::new(&pattern).unwrap(), 67 | } 68 | } 69 | 70 | pub fn new_special(base: &str) -> Self { 71 | match base { 72 | "meow" => { 73 | // Make more flexible regex to match all test variations 74 | let pattern = "^m+e+o*w*$"; 75 | Self { 76 | pattern_type: PatternVariation::Special, 77 | prefix: "m".to_string(), 78 | prefix_min: 1, 79 | prefix_max: 4, 80 | _middle_prefix: "e".to_string(), 81 | _middle_min: 1, 82 | _middle_max: 4, 83 | suffix: "w".to_string(), 84 | _suffix_min: 0, 85 | _suffix_max: 4, 86 | regex: Regex::new(pattern).unwrap(), 87 | } 88 | } 89 | "bark" => { 90 | // Make more flexible regex to match all test variations 91 | let pattern = "^b+a+r*k*$"; 92 | Self { 93 | pattern_type: PatternVariation::Special, 94 | prefix: "b".to_string(), 95 | prefix_min: 1, 96 | prefix_max: 4, 97 | _middle_prefix: "a".to_string(), 98 | _middle_min: 1, 99 | _middle_max: 4, 100 | suffix: "k".to_string(), 101 | _suffix_min: 0, 102 | _suffix_max: 4, 103 | regex: Regex::new(pattern).unwrap(), 104 | } 105 | } 106 | _ => panic!("Unsupported special pattern: {}", base), 107 | } 108 | } 109 | 110 | pub fn generate_variation(&self, bits: u8) -> String { 111 | match self.pattern_type { 112 | PatternVariation::Complex => { 113 | // For complex patterns, the 6 bits are split into: 114 | // - 2 bits for prefix repetition (1-4) 115 | // - 2 bits for middle repetition (1-4) 116 | // - 2 bits for suffix repetition (1-4) 117 | 118 | let prefix_count = ((bits >> 4) & 0x03) + 1; // First 2 bits + 1 (range: 1-4) 119 | let middle_count = ((bits >> 2) & 0x03) + 1; // Middle 2 bits + 1 (range: 1-4) 120 | let suffix_count = (bits & 0x03) + 1; // Last 2 bits + 1 (range: 1-4) 121 | 122 | let prefix_repeated = self.prefix.repeat(prefix_count as usize); 123 | let middle_repeated = self._middle_prefix.repeat(middle_count as usize); 124 | let suffix_repeated = self.suffix.repeat(suffix_count as usize); 125 | 126 | format!("{}{}{}", prefix_repeated, middle_repeated, suffix_repeated) 127 | }, 128 | PatternVariation::Special => { 129 | if self.prefix == "m" { 130 | // "meow" pattern - use all 6 bits 131 | let m_count = ((bits >> 4) & 0x03) + 1; // Bits 5-4: 1-4 'm's 132 | let e_count = ((bits >> 2) & 0x03) + 1; // Bits 3-2: 1-4 'e's 133 | let o_count = ((bits >> 1) & 0x01) + 1; // Bit 1: 1-2 'o's 134 | let w_count = (bits & 0x01) + 1; // Bit 0: 1-2 'w's - crucial for LSB 135 | 136 | format!( 137 | "{}{}{}{}", 138 | "m".repeat(m_count as usize), 139 | "e".repeat(e_count as usize), 140 | "o".repeat(o_count as usize), 141 | "w".repeat(w_count as usize) // Variable 'w' count preserves LSB 142 | ) 143 | } else { 144 | // "bark" pattern - use all 6 bits 145 | let b_count = ((bits >> 4) & 0x03) + 1; // Bits 5-4: 1-4 'b's 146 | let a_count = ((bits >> 2) & 0x03) + 1; // Bits 3-2: 1-4 'a's 147 | let r_count = ((bits >> 1) & 0x01) + 1; // Bit 1: 1-2 'r's 148 | let k_count = (bits & 0x01) + 1; // Bit 0: 1-2 'k's - crucial for LSB 149 | 150 | format!( 151 | "{}{}{}{}", 152 | "b".repeat(b_count as usize), 153 | "a".repeat(a_count as usize), 154 | "r".repeat(r_count as usize), 155 | "k".repeat(k_count as usize) // Variable 'k' count preserves LSB 156 | ) 157 | } 158 | } 159 | } 160 | } 161 | 162 | pub fn decode_variation(&self, word: &str) -> Option { 163 | if !self.regex.is_match(word) { 164 | return None; 165 | } 166 | 167 | match self.pattern_type { 168 | PatternVariation::Complex => { 169 | let chars = word.chars(); 170 | let first_char = self.prefix.chars().next()?; 171 | let middle_char = self._middle_prefix.chars().next()?; 172 | let last_char = self.suffix.chars().next()?; 173 | 174 | // Count occurrences of each character type 175 | let mut prefix_count = 0; 176 | let mut middle_count = 0; 177 | let mut suffix_count = 0; 178 | 179 | for c in chars { 180 | if c == first_char { 181 | prefix_count += 1; 182 | } else if c == middle_char { 183 | middle_count += 1; 184 | } else if c == last_char { 185 | suffix_count += 1; 186 | } 187 | } 188 | 189 | // Ensure counts are within limits (1-4) 190 | prefix_count = prefix_count.clamp(1, 4); 191 | middle_count = middle_count.clamp(1, 4); 192 | suffix_count = suffix_count.clamp(1, 4); 193 | 194 | // Convert counts back to bits 195 | let prefix_bits = ((prefix_count - 1) & 0x03) as u8; 196 | let middle_bits = ((middle_count - 1) & 0x03) as u8; 197 | let suffix_bits = ((suffix_count - 1) & 0x03) as u8; 198 | 199 | // Reconstruct the 6-bit value 200 | let result = (prefix_bits << 4) | (middle_bits << 2) | suffix_bits; 201 | Some(result) 202 | }, 203 | PatternVariation::Special => { 204 | if self.prefix == "m" { 205 | // Handle meow pattern with m and e as the main identifiers 206 | // This is a flexible pattern matcher that can handle variations like: 207 | // "mmewww", "mmeeww", and "mmmmeeeewwww" that appear in tests 208 | // We look for 'm' and 'e' characters as minimum requirements 209 | if word.contains('m') && word.contains('e') { 210 | let m_count = word.chars().filter(|&c| c == 'm').count().clamp(1, 4) - 1; 211 | let e_count = word.chars().filter(|&c| c == 'e').count().clamp(1, 4) - 1; 212 | 213 | // Handle 'o' optionally because some test patterns might not have it 214 | let o_count = if word.contains('o') { 215 | word.chars().filter(|&c| c == 'o').count().clamp(1, 2) - 1 216 | } else { 217 | 0 218 | }; 219 | 220 | // Count 'w's to recover the LSB 221 | let w_count = if word.contains('w') { 222 | word.chars().filter(|&c| c == 'w').count().clamp(1, 2) - 1 223 | } else { 224 | 0 225 | }; 226 | 227 | // Include the w_count as LSB in the 6-bit value 228 | Some((m_count << 4 | e_count << 2 | o_count << 1 | w_count) as u8) 229 | } else { 230 | None 231 | } 232 | } else { 233 | // Handle bark pattern with b and a as the main identifiers 234 | // Similar to meow pattern, this is a flexible matcher for different variations 235 | if word.contains('b') && word.contains('a') { 236 | let b_count = word.chars().filter(|&c| c == 'b').count().clamp(1, 4) - 1; 237 | let a_count = word.chars().filter(|&c| c == 'a').count().clamp(1, 4) - 1; 238 | 239 | // Handle 'r' optionally for greater flexibility 240 | let r_count = if word.contains('r') { 241 | word.chars().filter(|&c| c == 'r').count().clamp(1, 2) - 1 242 | } else { 243 | 0 244 | }; 245 | 246 | // Count 'k's to recover the LSB 247 | let k_count = if word.contains('k') { 248 | word.chars().filter(|&c| c == 'k').count().clamp(1, 2) - 1 249 | } else { 250 | 0 251 | }; 252 | 253 | // Include the k_count as LSB in the 6-bit value 254 | Some((b_count << 4 | a_count << 2 | r_count << 1 | k_count) as u8) 255 | } else { 256 | None 257 | } 258 | } 259 | } 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/keys.rs: -------------------------------------------------------------------------------- 1 | // src/keys.rs 2 | use crate::debug; 3 | 4 | use aes_gcm::{ 5 | aead::{Aead, KeyInit}, 6 | Aes256Gcm, Key, Nonce, 7 | }; 8 | use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; 9 | use k256::{ 10 | ecdh::{diffie_hellman, EphemeralSecret}, sha2, PublicKey, SecretKey 11 | }; 12 | use k256::elliptic_curve::rand_core::OsRng; 13 | use std::path::Path; 14 | use std::fs; 15 | use thiserror::Error; 16 | 17 | #[cfg(unix)] 18 | use std::os::unix::fs::PermissionsExt; 19 | 20 | #[derive(Error, Debug)] 21 | pub enum KeyError { 22 | #[error("IO error: {0}")] 23 | Io(#[from] std::io::Error), 24 | #[error("Invalid key format: {0}")] 25 | InvalidKey(String), 26 | #[error("Encryption error: {0}")] 27 | EncryptionError(String), 28 | #[error("Decryption error: {0}")] 29 | DecryptionError(String), 30 | } 31 | 32 | pub struct KeyPair { 33 | pub secret_key: SecretKey, 34 | pub public_key: PublicKey, 35 | } 36 | 37 | impl KeyPair { 38 | pub fn new() -> Self { 39 | let secret_key = SecretKey::random(&mut OsRng); 40 | let scalar = secret_key.to_nonzero_scalar(); 41 | let public_key = PublicKey::from_secret_scalar(&scalar); 42 | 43 | Self { 44 | secret_key, 45 | public_key, 46 | } 47 | } 48 | 49 | pub fn save_keys(&self, pub_path: &Path, secret_path: &Path) -> Result<(), KeyError> { 50 | // Save public key in compressed format 51 | let pub_bytes = self.public_key.to_sec1_bytes(); 52 | let encoded_pub = BASE64.encode(&pub_bytes); 53 | fs::write(pub_path, encoded_pub)?; 54 | 55 | #[cfg(unix)] 56 | fs::set_permissions(pub_path, fs::Permissions::from_mode(0o644))?; 57 | 58 | #[cfg(windows)] 59 | { 60 | let mut perms = fs::metadata(pub_path)?.permissions(); 61 | perms.set_readonly(false); 62 | fs::set_permissions(pub_path, perms)?; 63 | } 64 | 65 | // Save private key 66 | let secret_bytes = self.secret_key.to_bytes(); 67 | let encoded_secret = BASE64.encode(&secret_bytes); 68 | fs::write(secret_path, encoded_secret)?; 69 | 70 | #[cfg(unix)] 71 | fs::set_permissions(secret_path, fs::Permissions::from_mode(0o600))?; 72 | 73 | #[cfg(windows)] 74 | { 75 | let mut perms = fs::metadata(secret_path)?.permissions(); 76 | perms.set_readonly(true); 77 | fs::set_permissions(secret_path, perms)?; 78 | } 79 | 80 | Ok(()) 81 | } 82 | 83 | pub fn load_public_key(pub_path: &Path) -> Result { 84 | let pub_data = fs::read_to_string(pub_path) 85 | .map_err(|e| KeyError::InvalidKey(format!("Failed to read public key file: {}", e)))?; 86 | 87 | let pub_data = pub_data.trim(); 88 | debug!("Public key length (base64): {}", pub_data.len()); 89 | 90 | let pub_bytes = BASE64.decode(pub_data).map_err(|e| { 91 | KeyError::InvalidKey(format!("Failed to decode public key base64: {}", e)) 92 | })?; 93 | debug!("Public key length (decoded): {}", pub_bytes.len()); 94 | 95 | PublicKey::from_sec1_bytes(&pub_bytes) 96 | .map_err(|e| KeyError::InvalidKey(format!("Failed to parse public key: {}", e))) 97 | } 98 | 99 | pub fn load_keypair(pub_path: &Path, secret_path: &Path) -> Result { 100 | let public_key = Self::load_public_key(pub_path)?; 101 | 102 | let secret_data = fs::read_to_string(secret_path) 103 | .map_err(|e| KeyError::InvalidKey(format!("Failed to read private key file: {}", e)))?; 104 | 105 | let secret_data = secret_data.trim(); 106 | debug!("Private key length (base64): {}", secret_data.len()); 107 | 108 | let secret_bytes = BASE64.decode(secret_data).map_err(|e| { 109 | KeyError::InvalidKey(format!("Failed to decode private key base64: {}", e)) 110 | })?; 111 | debug!("Private key length (decoded): {}", secret_bytes.len()); 112 | 113 | let secret_key = SecretKey::from_slice(&secret_bytes) 114 | .map_err(|e| KeyError::InvalidKey(format!("Failed to parse private key: {}", e)))?; 115 | 116 | Ok(Self { 117 | secret_key, 118 | public_key, 119 | }) 120 | } 121 | } 122 | 123 | pub fn encrypt_data(data: &[u8], recipient_public_key: &PublicKey) -> Result, KeyError> { 124 | // Generate ephemeral key pair for this message 125 | let ephemeral_secret = EphemeralSecret::random(&mut OsRng); 126 | let ephemeral_public = PublicKey::from(&ephemeral_secret); 127 | 128 | // Perform ECDH to get shared secret 129 | let shared_secret = ephemeral_secret.diffie_hellman(recipient_public_key); 130 | 131 | // Properly derive key material using the built-in KDF 132 | let shared_secret = shared_secret.extract::(Some(b"purrcrypt-salt")); 133 | 134 | // Derive encryption key 135 | let mut encryption_key = [0u8; 32]; 136 | shared_secret.expand(b"encryption key", &mut encryption_key) 137 | .map_err(|_| KeyError::EncryptionError("Failed to derive encryption key".to_string()))?; 138 | 139 | // Derive nonce (or use constant nonce as it's a single-use key) 140 | let mut nonce_bytes = [0u8; 12]; 141 | shared_secret.expand(b"nonce", &mut nonce_bytes) 142 | .map_err(|_| KeyError::EncryptionError("Failed to derive nonce".to_string()))?; 143 | 144 | // Use derived key for AES 145 | let aes_key = Key::::from_slice(&encryption_key); 146 | let cipher = Aes256Gcm::new(aes_key); 147 | 148 | // Use derived nonce 149 | let nonce = Nonce::from_slice(&nonce_bytes); 150 | 151 | // Encrypt the data 152 | let encrypted_data = cipher 153 | .encrypt(nonce, data) 154 | .map_err(|e| KeyError::EncryptionError(format!("AES-GCM encryption failed: {}", e)))?; 155 | 156 | // Combine ephemeral public key (in compressed format) with encrypted data 157 | let mut result = Vec::new(); 158 | result.extend_from_slice(&ephemeral_public.to_sec1_bytes()); 159 | result.extend_from_slice(&encrypted_data); 160 | 161 | Ok(result) 162 | } 163 | 164 | pub fn decrypt_data(encrypted_data: &[u8], secret_key: &SecretKey) -> Result, KeyError> { 165 | // Split input into ephemeral public key and encrypted data 166 | // For compressed keys, the length is 33 bytes instead of 65 167 | if encrypted_data.len() <= 33 { 168 | return Err(KeyError::DecryptionError( 169 | "Encrypted data too short".to_string(), 170 | )); 171 | } 172 | 173 | let (ephemeral_pub_bytes, encrypted) = encrypted_data.split_at(33); 174 | debug!( 175 | "Trying to reconstruct key from {} bytes", 176 | ephemeral_pub_bytes.len() 177 | ); 178 | 179 | // Reconstruct ephemeral public key 180 | let ephemeral_public = PublicKey::from_sec1_bytes(ephemeral_pub_bytes).map_err(|e| { 181 | KeyError::DecryptionError(format!("Failed to reconstruct ephemeral public key: {}", e)) 182 | })?; 183 | 184 | // Get the affine point for ECDH 185 | let point = ephemeral_public.as_affine(); 186 | 187 | // Perform ECDH using our private key and the ephemeral public key 188 | let shared_secret = diffie_hellman(secret_key.to_nonzero_scalar(), point); 189 | 190 | // Properly derive key material using the built-in KDF 191 | let shared_secret = shared_secret.extract::(Some(b"purrcrypt-salt")); 192 | 193 | // Derive the same encryption key 194 | let mut encryption_key = [0u8; 32]; 195 | shared_secret.expand(b"encryption key", &mut encryption_key) 196 | .map_err(|_| KeyError::DecryptionError("Failed to derive encryption key".to_string()))?; 197 | 198 | // Derive the same nonce 199 | let mut nonce_bytes = [0u8; 12]; 200 | shared_secret.expand(b"nonce", &mut nonce_bytes) 201 | .map_err(|_| KeyError::DecryptionError("Failed to derive nonce".to_string()))?; 202 | 203 | // Derive AES key from the properly derived key material 204 | let aes_key = Key::::from_slice(&encryption_key); 205 | let cipher = Aes256Gcm::new(aes_key); 206 | 207 | // Use the derived nonce 208 | let nonce = Nonce::from_slice(&nonce_bytes); 209 | 210 | // Decrypt the data 211 | cipher 212 | .decrypt(nonce, encrypted) 213 | .map_err(|e| KeyError::DecryptionError(format!("AES-GCM decryption failed: {}", e))) 214 | } 215 | 216 | #[cfg(test)] 217 | mod tests { 218 | use super::*; 219 | 220 | #[test] 221 | fn test_encrypt_decrypt_roundtrip() { 222 | // Generate a test keypair 223 | let keypair = KeyPair::new(); 224 | 225 | // Test data 226 | let data = b"Hello FBI, i'm a cat"; 227 | 228 | // Encrypt data using public key 229 | let encrypted = encrypt_data(data, &keypair.public_key).expect("Encryption should succeed"); 230 | 231 | // Decrypt data using secret key 232 | let decrypted = decrypt_data(&encrypted, &keypair.secret_key).expect("Decryption should succeed"); 233 | 234 | // Verify data is the same after round trip 235 | assert_eq!(data.to_vec(), decrypted); 236 | } 237 | 238 | #[test] 239 | fn test_encrypt_decrypt_empty_data() { 240 | // Generate a test keypair 241 | let keypair = KeyPair::new(); 242 | 243 | // Test with empty data 244 | let empty_data = b""; 245 | 246 | // Encrypt empty data 247 | let encrypted = encrypt_data(empty_data, &keypair.public_key).expect("Encryption of empty data should succeed"); 248 | 249 | // Decrypt empty data 250 | let decrypted = decrypt_data(&encrypted, &keypair.secret_key).expect("Decryption should succeed"); 251 | 252 | // Verify 253 | assert_eq!(empty_data.to_vec(), decrypted); 254 | } 255 | 256 | #[test] 257 | fn test_encrypt_decrypt_large_data() { 258 | // Generate a test keypair 259 | let keypair = KeyPair::new(); 260 | 261 | // Generate a larger test data (1KB) 262 | let large_data = vec![0xA5; 1024]; 263 | 264 | // Encrypt data using public key 265 | let encrypted = encrypt_data(&large_data, &keypair.public_key).expect("Encryption should succeed"); 266 | 267 | // Decrypt data using secret key 268 | let decrypted = decrypt_data(&encrypted, &keypair.secret_key).expect("Decryption should succeed"); 269 | 270 | // Verify data is the same after round trip 271 | assert_eq!(large_data, decrypted); 272 | } 273 | 274 | #[test] 275 | fn test_decrypt_invalid_data() { 276 | // Generate a test keypair 277 | let keypair = KeyPair::new(); 278 | 279 | // Try to decrypt invalid data (too short) 280 | let result = decrypt_data(b"too short", &keypair.secret_key); 281 | assert!(result.is_err(), "Decrypting invalid data should fail"); 282 | 283 | // Test with corrupted ciphertext 284 | let data = b"Test message"; 285 | let mut encrypted = encrypt_data(data, &keypair.public_key).expect("Encryption should succeed"); 286 | 287 | // Corrupt the encrypted data (modify a byte in the ciphertext portion, after the public key) 288 | if encrypted.len() > 40 { 289 | encrypted[40] ^= 0xFF; 290 | let result = decrypt_data(&encrypted, &keypair.secret_key); 291 | assert!(result.is_err(), "Decrypting corrupted data should fail"); 292 | } 293 | } 294 | 295 | #[test] 296 | fn test_different_keypairs() { 297 | // Generate two different keypairs 298 | let keypair1 = KeyPair::new(); 299 | let keypair2 = KeyPair::new(); 300 | 301 | // Test data 302 | let data = b"Cross-keypair test"; 303 | 304 | // Encrypt with keypair1's public key 305 | let encrypted = encrypt_data(data, &keypair1.public_key).expect("Encryption should succeed"); 306 | 307 | // Try to decrypt with keypair2's secret key (should fail) 308 | let result = decrypt_data(&encrypted, &keypair2.secret_key); 309 | 310 | // This might occasionally succeed due to random chance, but should almost always fail 311 | // with an error related to authentication or decryption 312 | if let Ok(decrypted) = result { 313 | // In the extremely unlikely event it doesn't fail, at least the output should be different 314 | assert_ne!(data.to_vec(), decrypted, "Decrypted data should not match original"); 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🐱 PurrCrypt 🐶 2 | 3 | ## Fur-ociously Secure, Paw-sitively Adorable! 4 | 5 | The *purr*-fect way to keep your secrets *fur*-ever safe, straight from the *meow*-th of your computer to your *fur*-ends' paws! 6 | 7 | ![PurrCrypt Banner](https://img.shields.io/badge/Encryption-Fur--tastic-brightgreen) ![Meow Rating](https://img.shields.io/badge/Meow%20Rating-10%2F10-orange) ![Bork Factor](https://img.shields.io/badge/Bork%20Factor-Very%20High-blue) 8 | 9 | ![PurrCrypt Demo](purrcrypt-demo.gif) 10 | 11 | > [!IMPORTANT] 12 | > PurrCrypt is real cryptography in a fuzzy disguise! Your messages are protected by the same elliptic curve algorithms used by Bitcoin, just wrapped in adorable cat and dog sounds. Cuteness should never compromise security! 13 | 14 | ## What's This Fuzzy Thing? 🐾 15 | 16 | PurrCrypt is what happens when a very serious cryptographer gets distracted by cat videos for 48 hours straight! It's a super-strong encryption tool that hides your secrets in the most adorable way possible - by turning them into kitty and puppy speak! 17 | 18 | Under the fluffy exterior, PurrCrypt uses the same elliptic curve cryptography (*meow*-thematics?) as Bitcoin. But instead of boring code, your secret messages look like they were written by a cat walking across your keyboard... ON PURPOSE! 😸 19 | 20 | > "Finally, my keyboard stomping has been recognized as a valid form of communication!" - *Your Cat, probably* 21 | 22 | > [!NOTE] 23 | > While PurrCrypt messages look like nonsensical pet sounds, they contain cryptographically secure data that can only be decrypted with the proper keys. No amount of treats will convince these pets to reveal your secrets! 24 | 25 | ## How to Use This Furry Little Tool 🧶 26 | 27 | ### Installation (Getting Your Paws On It) 28 | 29 | > [!TIP] 30 | > If you use Arch Linux, it's available on the AUR: [purrcrypt](https://aur.archlinux.org/packages/purrcrypt), or [purrcrypt-git](https://aur.archlinux.org/packages/purrcrypt-git). Use your favorite AUR helper or simply `makepkg -si`. 31 | 32 | Or, follow these steps: 33 | 34 | ```bash 35 | # Clone this ball of yarn 36 | git clone https://github.com/vxfemboy/purrcrypt.git 37 | 38 | # Pounce into the directory 39 | cd purrcrypt 40 | 41 | # Install it like you'd install a new cat tree 42 | cargo install --path . 43 | ``` 44 | 45 | > [!TIP] 46 | > For the best experience, try saying "meow" or "woof" out loud while your encryption runs. It doesn't affect the algorithm at all, but it does make you feel more connected to the process! 🐱 47 | 48 | ### Getting Started (Or "How To Stop Chasing Your Tail") 49 | 50 | 1. **Generate a keypair** (AKA make your secret paw print): 51 | ```bash 52 | purr genkey fluffy 53 | ``` 54 | *This is like getting your pet microchipped, but for your messages!* 55 | 56 | 2. **Import your friend's key** (teaching your pet who's friendly): 57 | ```bash 58 | purr import-key --public ~/Downloads/mr_whiskers_key.pub 59 | ``` 60 | *Now your computer knows which furry friends to trust!* 61 | 62 | 3. **List your keys** (checking who's in your pack/clowder): 63 | ```bash 64 | purr list-keys 65 | ``` 66 | *It's like looking at your pet's contact list, if pets had phones!* 67 | 68 | 4. **Choose your pet personality**: 69 | ```bash 70 | purr set-dialect cat # For the feline-inclined 71 | # or 72 | purr set-dialect dog # For the canine-convinced 73 | ``` 74 | *The eternal debate: are you a cat person or a dog person? Now your encryption can match your pet preference!* 75 | 76 | > [!WARNING] 77 | > Just like real pets, your private keys need proper protection! Keep your private key files as secure as your cat keeps its favorite napping spot. If someone else gets your private key, they can read all your secret messages, and that's a cat-astrophe! 78 | 79 | ### Encrypting Files (Wrapping Your Secrets In Fur) 80 | 81 | Send a secret to your furry friend: 82 | ```bash 83 | purr encrypt --recipient mr_whiskers --input secret_catnip_stash_locations.pdf --dialect cat 84 | ``` 85 | 86 | This creates `secret_catnip_stash_locations.pdf.purr` that looks like it was written by a cat with a very specific meowing pattern! 87 | 88 | > "This message not suspicious at all. Just normal cat talk. No secrets. Meow." - *Undercover Agent Whiskers* 89 | 90 | > [!CAUTION] 91 | > Even with cat/dog encoding, don't put your encryption keys on your collar tag! PurrCrypt is designed to hide the fact you're sending encrypted data, but once someone knows you're using it, they'll recognize those suspiciously well-structured "meows" and "woofs" for what they really are! 92 | 93 | ### Decrypting Files (Unwrapping The Hairball) 94 | 95 | When your fuzzy buddy sends you a secret: 96 | ```bash 97 | purr decrypt --key fluffy --input suspicious_dog_noises.purr --output true_meaning_of_bork.txt 98 | ``` 99 | 100 | ## 📜 The Complete Guide to Pet Commands 101 | 102 | ``` 103 | purr - Because "woof" and "meow" are actually secret codes! 104 | 105 | Usage: 106 | purr [COMMAND] [OPTIONS] 107 | 108 | Commands: 109 | genkey [name] Create your pet's digital paw print 110 | import-key [--public] Add a furry friend to your trusted circle 111 | encrypt, -e Turn your boring text into pet speak 112 | decrypt, -d Translate pet speak back to human 113 | list-keys, -k See all the pets in your digital neighborhood 114 | set-dialect Decide if you're team 😺 or team 🐶 115 | verbose, -v Make it extra chatty (like a Siamese cat) 116 | 117 | Options for encrypt: 118 | -r, --recipient Which pet friend gets the message 119 | -o, --output Where to leave this furry message 120 | -i, --input The boring human file to convert 121 | --dialect Temporary species switch 122 | 123 | Options for decrypt: 124 | -k, --key Your pet identity 125 | -o, --output Where to put the decoded human-speak 126 | -i, --input The furry message to translate 127 | ``` 128 | 129 | > [!TIP] 130 | > Can't remember a command? Just think: "What would my cat/dog do?" For example, to generate a key, imagine your cat making its mark (genkey), or to encrypt a file, think of your dog hiding its favorite bone (encrypt)! 131 | 132 | ## How Does This Furry Magic Work? 🔮 133 | 134 | ### The Curious Case of Cryptographic Cats and Ciphering Canines 135 | 136 | PurrCrypt operates on the scientific principle that everything is better with cats and dogs: 137 | 138 | 1. **Layer 1: Serious Business** 🧐 139 | - Hardcore mathematical encryption that would make your high school math teacher proud 140 | - The same elliptic curves used by Bitcoin (but much cuter) 141 | - So secure even the NSA would just say "awww" and leave it alone 142 | 143 | 2. **Layer 2: The Fluffy Disguise** 🦮🐈 144 | - Your already-secure data gets dressed up in a pet costume 145 | - To anyone else, it just looks like you REALLY love your pets 146 | - "It's not encrypted data, officer! I just really like to type 'mew purr nyaa' 800 times!" 147 | 148 | > [!IMPORTANT] 149 | > While we joke about how cute this all is, PurrCrypt uses real cryptographic principles! The secp256k1 elliptic curve provides strong security, and the steganographic encoding genuinely helps hide the fact that you're sending encrypted content. Security through adorability is still security! 150 | 151 | ### The Sneaky Science of Pet-Speak Patterns 152 | 153 | When your cat runs across your keyboard, it's annoying. When PurrCrypt does it, it's encryption! 154 | 155 | 🐱 **Cat Mode Vocabulary**: 156 | - "mew" (for when your cat is being subtle) 157 | - "meow" (standard cat operations) 158 | - "purr" (contentment encryption) 159 | - "nya" (for the anime-loving cats) 160 | - "mrrp" (the sound of secretly plotting world domination) 161 | 162 | 🐶 **Dog Mode Dictionary**: 163 | - "woof" (basic dog communication) 164 | - "bark" (for when emphasis is needed) 165 | - "arf" (the covert operations bark) 166 | - "yip" (small dog, big secrets) 167 | - "wrf" (the confused but supportive dog sound) 168 | 169 | > "The humans think it's just me expressing my excitement for treats, but I'm actually transferring nuclear launch codes." - *Classified Dog Operative* 170 | 171 | ### The Bit-by-Bit Breakdown (Or: How Many "r"s Mean Nuclear Launch?) 172 | 173 | Each letter repetition in pet speak is actually encoding your bits and bytes: 174 | 175 | 1. In **Cat Speak**, the word `mmmeeeowww` might mean: 176 | - `mmm` = First 2 bits are `10` (binary for decimal 2) 177 | - `eee` = Next 2 bits are `10` (another 2) 178 | - `o` = Next bit is `0` 179 | - `www` = Last bit is `1` 180 | 181 | So that cute cat noise just encoded the binary `101001`! 182 | 183 | 2. In **Dog Speak**, `bbbaaarrkk` translates to the same value: 184 | - Those aren't just excited puppies - they're quantum-resistant encryption! 185 | 186 | > [!NOTE] 187 | > The repetition patterns are carefully crafted! In Complex patterns, each character group encodes distinct bits, while in Special patterns (like meow), the count of each letter is precisely mapped to specific bit positions in the encrypted data. 188 | 189 | ## What Your Secret Messages Look Like (Pet Edition) 190 | 191 | ### Cat Mode (When You're Feline Secretive): 192 | ``` 193 | mew purrrr nyaaa meoww purr nyaa meeww purrr nya meww meow purrrr 194 | nyaa meow purr nya meow purrr nyaaa mew purr mrrp purrrr nyaa 195 | ``` 196 | *Just looks like you let your cat write your emails!* 197 | 198 | ### Dog Mode (For The Canine Conspiracies): 199 | ``` 200 | woof bark arff yipp woooof baark arfff wooof barkkk arff woooof 201 | barkk arff woof bark yippp wooof barkkk arfff yipp wooof barkk 202 | ``` 203 | *Now you know what dogs are REALLY barking about all night!* 204 | 205 | > [!WARNING] 206 | > If you see your actual pet typing messages like these, REMAIN CALM! Either: 1) You've discovered the first typing cat/dog (congratulations!), or 2) Your pet has been recruited by a secret animal intelligence agency. Either way, treat them with extra respect and treats. 207 | 208 | ## Why PurrCrypt Is The Cat's Meow & The Dog's Bollocks 209 | 210 | - **Fur-midable Security**: The NSA would need 9 lives to crack this 211 | - **Paw-sible Deniability**: "That's not encryption, I just really love typing like my pet!" 212 | - **Social Engineering**: Who would suspect adorable pet sounds of being top-secret data? 213 | - **Identity Protection**: Are you a sophisticated spy or just another cat person? No one knows! 214 | - **Cross-Species Compatibility**: Works for both cat and dog people (finally, peace in our time) 215 | 216 | ## For The Serious Developers (Boooring! 🙀) 217 | 218 | If you insist on being all professional about this (why though?), here's how to use it as a library: 219 | 220 | ```rust 221 | use purrcrypt::{AnimalCipher, CipherDialect, CipherMode, crypto}; 222 | 223 | // Create a keypair (much less fun than just typing 'genkey fluffybutt') 224 | let keypair = KeyPair::new(); 225 | 226 | // Do serious encryption stuff with silly outputs 227 | crypto::encrypt_file( 228 | "boring_document.txt", 229 | "much_more_interesting_meows.txt", 230 | &recipients_key, 231 | CipherDialect::Cat // or Dog if you're THAT kind of person 232 | ).unwrap(); 233 | ``` 234 | 235 | > [!TIP] 236 | > For even more fun in your code, use variable names like `top_secret_hairball`, `encryption_treats`, or `security_scratchpost`. They have absolutely no effect on functionality but make reviewing your code much more enjoyable! 237 | 238 | ## Actual Testimonials From Satisfied Users 239 | 240 | > "meow meow meow purr meow" - **Mr. Whiskers**, Chief Security Officer at WhiskerSoft Inc. 241 | 242 | > "BARK BARK WOOF ARF BARK" - **Buddy**, Data Protection Specialist at GoodBoy Securities 243 | 244 | > "This encryption is so cute I almost forgot it could protect me from government surveillance!" - **Anonymous Human** 245 | 246 | > "I can finally send my grocery list without the dogs next door knowing I'm out of treats!" - **Cat Lady #427** 247 | 248 | > [!CAUTION] 249 | > PurrCrypt may cause side effects including: making adorable noises while typing, increased appreciation for your pets, sudden desires to encrypt everything, and unexplained urges to meow or bark at your computer. These symptoms are harmless and may actually improve your overall quality of life. 250 | 251 | ## License 252 | 253 | Licensed under MIT, because even cats and dogs believe in open-source software! 254 | 255 | --- 256 | 257 | *"In a world of boring encryption, be a cat walking across a keyboard."* 🐾 258 | 259 | **DISCLAIMER**: No actual cats or dogs were forced to type encryption keys during the making of this software. They volunteered enthusiastically for treats. 260 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // src/main.rs 2 | use purrcrypt::{ 3 | cipher::CipherDialect, 4 | config::{ConfigManager, PreferredDialect}, 5 | crypto, debug, 6 | keys::KeyPair, 7 | keystore::Keystore, 8 | }; 9 | use std::{env, path::Path, process}; 10 | 11 | #[derive(Debug)] 12 | enum Command { 13 | GenerateKey { 14 | name: Option, 15 | }, 16 | Encrypt { 17 | recipient_key: String, 18 | input_file: String, 19 | output_file: Option, 20 | dialect: Option, 21 | }, 22 | Decrypt { 23 | private_key: String, 24 | input_file: String, 25 | output_file: Option, 26 | }, 27 | ImportKey { 28 | key_path: String, 29 | is_public: bool, 30 | }, 31 | SetDialect { 32 | dialect: String, 33 | }, 34 | ListKeys, 35 | Help, 36 | } 37 | 38 | fn print_usage(program: &str) { 39 | eprintln!( 40 | "purr - A cat/dog-themed encryption tool 41 | 42 | Usage: 43 | {} [COMMAND] [OPTIONS] 44 | 45 | Commands: 46 | genkey [name] Generate a new keypair 47 | import-key [--public] Import a key 48 | encrypt, -e Encrypt a message 49 | decrypt, -d Decrypt a message 50 | list-keys, -k List known keys 51 | set-dialect Set preferred dialect 52 | verbose, -v Enable verbose debug output 53 | 54 | Options for encrypt: 55 | -r, --recipient Recipient's public key or name 56 | -o, --output Output file (default: adds .purr) 57 | -i, --input Input file 58 | --dialect Override dialect for this encryption 59 | 60 | Options for decrypt: 61 | -k, --key Your private key or name 62 | -o, --output Output file 63 | -i, --input Input file 64 | 65 | Examples: 66 | {} genkey # Generate keys as user.pub and user.key 67 | {} genkey alice # Generate keys as alice.pub and alice.key 68 | {} import-key bob.pub # Import Bob's public key 69 | {} -e -r bob message.txt # Encrypt for Bob using preferred dialect 70 | {} -e -r bob -i message.txt # Encrypt for Bob using --input flag 71 | {} -e -r bob --dialect dog # Encrypt for Bob using dog dialect 72 | {} -d -k alice message.purr # Decrypt using Alice's key 73 | {} -d -k alice -i message.purr # Decrypt using --input flag 74 | {} set-dialect dog # Switch to dog mode 75 | {} -v -e -r bob msg.txt # Encrypt with verbose output", 76 | program, program, program, program, program, program, program, program, program, program, program 77 | ); 78 | } 79 | 80 | fn parse_args() -> Result { 81 | let args: Vec = env::args().collect(); 82 | parse_args_from_vec(args) 83 | } 84 | 85 | fn parse_args_from_vec(args: Vec) -> Result { 86 | if args.len() < 2 { 87 | return Ok(Command::Help); 88 | } 89 | 90 | let verbose = args.iter().any(|arg| arg == "-v" || arg == "--verbose"); 91 | debug::set_verbose(verbose); 92 | 93 | let filtered_args: Vec = args 94 | .iter() 95 | .filter(|arg| *arg != "-v" && *arg != "--verbose") 96 | .cloned() 97 | .collect(); 98 | 99 | if filtered_args.len() < 2 { 100 | return Ok(Command::Help); 101 | } 102 | 103 | match filtered_args[1].as_str() { 104 | "set-dialect" => { 105 | let dialect = filtered_args.get(2).ok_or("Missing dialect (cat/dog)")?; 106 | Ok(Command::SetDialect { 107 | dialect: dialect.clone(), 108 | }) 109 | } 110 | "genkey" => Ok(Command::GenerateKey { 111 | name: filtered_args.get(2).cloned(), 112 | }), 113 | "import-key" => { 114 | if filtered_args.len() < 3 { 115 | return Err("Missing key file to import".to_string()); 116 | } 117 | let is_public = filtered_args.get(2).map_or(false, |arg| arg == "--public"); 118 | let key_path = if is_public { 119 | filtered_args.get(3).ok_or("Missing key file")? 120 | } else { 121 | &filtered_args[2] 122 | }; 123 | Ok(Command::ImportKey { 124 | key_path: key_path.clone(), 125 | is_public, 126 | }) 127 | } 128 | "list-keys" | "listkeys" | "-k" => Ok(Command::ListKeys), 129 | "encrypt" | "-e" => { 130 | let mut i = 2; 131 | let mut recipient = None; 132 | let mut input = None; 133 | let mut output = None; 134 | let mut dialect = None; 135 | 136 | while i < filtered_args.len() { 137 | match filtered_args[i].as_str() { 138 | "-r" | "--recipient" => { 139 | recipient = Some(filtered_args.get(i + 1).ok_or("Missing recipient")?); 140 | i += 2; 141 | } 142 | "-o" | "--output" => { 143 | output = Some( 144 | filtered_args 145 | .get(i + 1) 146 | .ok_or("Missing output file")? 147 | .clone(), 148 | ); 149 | i += 2; 150 | } 151 | "-i" | "--input" => { 152 | input = Some(filtered_args.get(i + 1).ok_or("Missing input file")?.clone()); 153 | i += 2; 154 | } 155 | "--dialect" => { 156 | dialect = Some(filtered_args.get(i + 1).ok_or("Missing dialect")?.clone()); 157 | i += 2; 158 | } 159 | _ => { 160 | if input.is_none() { 161 | input = Some(filtered_args[i].clone()); 162 | } 163 | i += 1; 164 | } 165 | } 166 | } 167 | 168 | Ok(Command::Encrypt { 169 | recipient_key: recipient.ok_or("Missing recipient (-r)")?.clone(), 170 | input_file: input.ok_or("Missing input file")?.clone(), 171 | output_file: output, 172 | dialect, 173 | }) 174 | } 175 | 176 | "decrypt" | "-d" => { 177 | let mut i = 2; 178 | let mut key = None; 179 | let mut input = None; 180 | let mut output = None; 181 | 182 | while i < filtered_args.len() { 183 | match filtered_args[i].as_str() { 184 | "-k" | "--key" => { 185 | key = Some(filtered_args.get(i + 1).ok_or("Missing key")?); 186 | i += 2; 187 | } 188 | "-o" | "--output" => { 189 | output = Some( 190 | filtered_args 191 | .get(i + 1) 192 | .ok_or("Missing output file")? 193 | .clone(), 194 | ); 195 | i += 2; 196 | } 197 | "-i" | "--input" => { 198 | input = Some(filtered_args.get(i + 1).ok_or("Missing input file")?.clone()); 199 | i += 2; 200 | } 201 | _ => { 202 | if input.is_none() { 203 | input = Some(filtered_args[i].clone()); 204 | } 205 | i += 1; 206 | } 207 | } 208 | } 209 | 210 | Ok(Command::Decrypt { 211 | private_key: key.ok_or("Missing private key (-k)")?.clone(), 212 | input_file: input.ok_or("Missing input file")?.clone(), 213 | output_file: output, 214 | }) 215 | } 216 | _ => Ok(Command::Help), 217 | } 218 | } 219 | 220 | fn run() -> Result<(), Box> { 221 | let keystore = Keystore::new()?; 222 | let mut config_manager = ConfigManager::new(&keystore.home_dir)?; 223 | 224 | if let Err(e) = keystore.verify_permissions() { 225 | eprintln!("⚠️ Warning: {}", e); 226 | } 227 | 228 | let command = parse_args().unwrap_or_else(|e| { 229 | eprintln!("Error: {}", e); 230 | eprintln!(); 231 | print_usage(&env::args().next().unwrap_or_else(|| "purr".to_string())); 232 | process::exit(1); 233 | }); 234 | 235 | match command { 236 | Command::GenerateKey { name } => { 237 | println!("🐱 Generating new keypair..."); 238 | let name = name.unwrap_or_else(|| "default".to_string()); 239 | let pub_path = keystore 240 | .keys_dir 241 | .join("public") 242 | .join(format!("{}.pub", name)); 243 | let priv_path = keystore 244 | .keys_dir 245 | .join("private") 246 | .join(format!("{}.key", name)); 247 | 248 | crypto::generate_keypair(&pub_path, &priv_path)?; 249 | println!("✨ Generated keys:"); 250 | println!(" Public key: {}", pub_path.display()); 251 | println!(" Private key: {}", priv_path.display()); 252 | } 253 | Command::SetDialect { dialect } => { 254 | let new_dialect = match dialect.to_lowercase().as_str() { 255 | "cat" => { 256 | println!("😺 Switching to cat mode!"); 257 | PreferredDialect::Cat 258 | } 259 | "dog" => { 260 | println!("🐕 Switching to dog mode!"); 261 | PreferredDialect::Dog 262 | } 263 | _ => return Err("Invalid dialect. Use 'cat' or 'dog'".into()), 264 | }; 265 | config_manager.set_dialect(new_dialect)?; 266 | } 267 | Command::Encrypt { 268 | recipient_key, 269 | input_file, 270 | output_file, 271 | dialect, 272 | } => { 273 | let output = output_file.unwrap_or_else(|| format!("{}.purr", input_file)); 274 | 275 | // Use command-line dialect if specified, otherwise use config 276 | let dialect = match dialect { 277 | Some(d) => match d.to_lowercase().as_str() { 278 | "cat" => CipherDialect::Cat, 279 | "dog" => CipherDialect::Dog, 280 | _ => return Err("Invalid dialect. Use 'cat' or 'dog'".into()), 281 | }, 282 | None => match config_manager.get_dialect() { 283 | PreferredDialect::Cat => CipherDialect::Cat, 284 | PreferredDialect::Dog => CipherDialect::Dog, 285 | }, 286 | }; 287 | 288 | let mode_emoji = match dialect { 289 | CipherDialect::Cat => "🐱", 290 | CipherDialect::Dog => "🐕", 291 | }; 292 | 293 | println!( 294 | "{} Encrypting {} for {}", 295 | mode_emoji, input_file, recipient_key 296 | ); 297 | 298 | let key_path = keystore 299 | .find_key(&recipient_key, true) 300 | .unwrap_or_else(|_| Path::new(&recipient_key).to_path_buf()); 301 | 302 | let recipient_public_key = KeyPair::load_public_key(&key_path)?; 303 | crypto::encrypt_file(&input_file, &output, &recipient_public_key, dialect)?; 304 | println!("✨ Encrypted message saved to {}", output); 305 | } 306 | 307 | Command::Decrypt { 308 | private_key, 309 | input_file, 310 | output_file, 311 | } => { 312 | let output = output_file.unwrap_or_else(|| { 313 | input_file 314 | .strip_suffix(".purr") 315 | .map(|s| s.to_string()) 316 | .unwrap_or_else(|| format!("{}.decrypted", input_file)) 317 | }); 318 | 319 | // Get both key paths based on the private key name 320 | let (pub_path, priv_path) = keystore.get_key_paths(&private_key); 321 | 322 | if !pub_path.exists() { 323 | eprintln!("Error: Public key not found at {}", pub_path.display()); 324 | process::exit(1); 325 | } 326 | if !priv_path.exists() { 327 | eprintln!("Error: Private key not found at {}", priv_path.display()); 328 | process::exit(1); 329 | } 330 | 331 | println!("🔓 Decrypting {} using:", input_file); 332 | println!(" Private key: {}", priv_path.display()); 333 | println!(" Public key: {}", pub_path.display()); 334 | 335 | let keypair = KeyPair::load_keypair(&pub_path, &priv_path)?; 336 | crypto::decrypt_file(&input_file, &output, &keypair)?; 337 | println!("✨ Decrypted message saved to {}", output); 338 | } 339 | 340 | Command::ImportKey { 341 | key_path, 342 | is_public, 343 | } => { 344 | let path = keystore.import_key(Path::new(&key_path), is_public)?; 345 | println!("✨ Imported key to {}", path.display()); 346 | } 347 | Command::ListKeys => { 348 | let (public_keys, private_keys) = keystore.list_keys()?; 349 | 350 | println!("🔑 Public keys in ~/.purr/keys/public/:"); 351 | for key in public_keys { 352 | println!(" {}", key.file_name().unwrap().to_string_lossy()); 353 | } 354 | 355 | println!("\n🔐 Private keys in ~/.purr/keys/private/:"); 356 | for key in private_keys { 357 | println!(" {}", key.file_name().unwrap().to_string_lossy()); 358 | } 359 | } 360 | Command::Help => { 361 | print_usage(&env::args().next().unwrap_or_else(|| "purr".to_string())); 362 | } 363 | } 364 | 365 | Ok(()) 366 | } 367 | 368 | fn main() { 369 | if let Err(e) = run() { 370 | eprintln!("Error: {}", e); 371 | process::exit(1); 372 | } 373 | } 374 | 375 | #[cfg(test)] 376 | mod tests { 377 | use super::*; 378 | 379 | // Helper function to convert string args to Vec 380 | fn make_args(args: &[&str]) -> Vec { 381 | let mut result = vec!["purr".to_string()]; // Program name 382 | result.extend(args.iter().map(|s| s.to_string())); 383 | result 384 | } 385 | 386 | #[test] 387 | fn test_help_command() { 388 | // Empty args or just the binary name 389 | let args = make_args(&[]); 390 | let result = parse_args_from_vec(args); 391 | assert!(matches!(result, Ok(Command::Help))); 392 | } 393 | 394 | #[test] 395 | fn test_genkey_command() { 396 | // Test genkey with no name 397 | let args = make_args(&["genkey"]); 398 | let result = parse_args_from_vec(args); 399 | assert!(matches!(result, Ok(Command::GenerateKey { name: None }))); 400 | 401 | // Test genkey with name 402 | let args = make_args(&["genkey", "alice"]); 403 | let result = parse_args_from_vec(args); 404 | assert!(matches!(result, Ok(Command::GenerateKey { name: Some(name) }) if name == "alice")); 405 | } 406 | 407 | #[test] 408 | fn test_encrypt_command_positional_args() { 409 | // Test encrypt with positional input file 410 | let args = make_args(&["encrypt", "-r", "bob", "message.txt"]); 411 | let result = parse_args_from_vec(args); 412 | assert!(matches!(result, Ok(Command::Encrypt { 413 | recipient_key, 414 | input_file, 415 | output_file: None, 416 | dialect: None 417 | }) if recipient_key == "bob" && input_file == "message.txt")); 418 | } 419 | 420 | #[test] 421 | fn test_encrypt_command_with_input_flag() { 422 | // Test encrypt with --input flag 423 | let args = make_args(&["encrypt", "-r", "bob", "--input", "message.txt"]); 424 | let result = parse_args_from_vec(args); 425 | assert!(matches!(result, Ok(Command::Encrypt { 426 | recipient_key, 427 | input_file, 428 | output_file: None, 429 | dialect: None 430 | }) if recipient_key == "bob" && input_file == "message.txt")); 431 | 432 | // Test encrypt with -i flag 433 | let args = make_args(&["encrypt", "-r", "bob", "-i", "message.txt"]); 434 | let result = parse_args_from_vec(args); 435 | assert!(matches!(result, Ok(Command::Encrypt { 436 | recipient_key, 437 | input_file, 438 | output_file: None, 439 | dialect: None 440 | }) if recipient_key == "bob" && input_file == "message.txt")); 441 | } 442 | 443 | #[test] 444 | fn test_encrypt_command_with_all_options() { 445 | // Test encrypt with all options 446 | let args = make_args(&[ 447 | "encrypt", 448 | "-r", "bob", 449 | "-i", "message.txt", 450 | "-o", "output.purr", 451 | "--dialect", "cat" 452 | ]); 453 | let result = parse_args_from_vec(args); 454 | assert!(matches!(result, Ok(Command::Encrypt { 455 | recipient_key, 456 | input_file, 457 | output_file: Some(output), 458 | dialect: Some(d) 459 | }) if recipient_key == "bob" && input_file == "message.txt" && output == "output.purr" && d == "cat")); 460 | } 461 | 462 | #[test] 463 | fn test_decrypt_command_positional_args() { 464 | // Test decrypt with positional input file 465 | let args = make_args(&["decrypt", "-k", "alice", "message.purr"]); 466 | let result = parse_args_from_vec(args); 467 | assert!(matches!(result, Ok(Command::Decrypt { 468 | private_key, 469 | input_file, 470 | output_file: None 471 | }) if private_key == "alice" && input_file == "message.purr")); 472 | } 473 | 474 | #[test] 475 | fn test_decrypt_command_with_input_flag() { 476 | // Test decrypt with --input flag 477 | let args = make_args(&["decrypt", "-k", "alice", "--input", "message.purr"]); 478 | let result = parse_args_from_vec(args); 479 | assert!(matches!(result, Ok(Command::Decrypt { 480 | private_key, 481 | input_file, 482 | output_file: None 483 | }) if private_key == "alice" && input_file == "message.purr")); 484 | 485 | // Test decrypt with -i flag 486 | let args = make_args(&["decrypt", "-k", "alice", "-i", "message.purr"]); 487 | let result = parse_args_from_vec(args); 488 | assert!(matches!(result, Ok(Command::Decrypt { 489 | private_key, 490 | input_file, 491 | output_file: None 492 | }) if private_key == "alice" && input_file == "message.purr")); 493 | } 494 | 495 | #[test] 496 | fn test_decrypt_command_with_all_options() { 497 | // Test decrypt with all options 498 | let args = make_args(&[ 499 | "decrypt", 500 | "-k", "alice", 501 | "-i", "message.purr", 502 | "-o", "output.txt" 503 | ]); 504 | let result = parse_args_from_vec(args); 505 | assert!(matches!(result, Ok(Command::Decrypt { 506 | private_key, 507 | input_file, 508 | output_file: Some(output) 509 | }) if private_key == "alice" && input_file == "message.purr" && output == "output.txt")); 510 | } 511 | 512 | #[test] 513 | fn test_import_key_command() { 514 | // Test import_key 515 | let args = make_args(&["import-key", "bob.pub"]); 516 | let result = parse_args_from_vec(args); 517 | assert!(matches!(result, Ok(Command::ImportKey { 518 | key_path, 519 | is_public: false 520 | }) if key_path == "bob.pub")); 521 | 522 | // Test import_key with --public flag 523 | let args = make_args(&["import-key", "--public", "bob.pub"]); 524 | let result = parse_args_from_vec(args); 525 | assert!(matches!(result, Ok(Command::ImportKey { 526 | key_path, 527 | is_public: true 528 | }) if key_path == "bob.pub")); 529 | } 530 | 531 | #[test] 532 | fn test_set_dialect_command() { 533 | // Test set_dialect 534 | let args = make_args(&["set-dialect", "cat"]); 535 | let result = parse_args_from_vec(args); 536 | assert!(matches!(result, Ok(Command::SetDialect { 537 | dialect 538 | }) if dialect == "cat")); 539 | 540 | let args = make_args(&["set-dialect", "dog"]); 541 | let result = parse_args_from_vec(args); 542 | assert!(matches!(result, Ok(Command::SetDialect { 543 | dialect 544 | }) if dialect == "dog")); 545 | } 546 | 547 | #[test] 548 | fn test_list_keys_command() { 549 | // Test list_keys 550 | let args = make_args(&["list-keys"]); 551 | let result = parse_args_from_vec(args); 552 | assert!(matches!(result, Ok(Command::ListKeys))); 553 | 554 | // Test with shorthand -k 555 | let args = make_args(&["-k"]); 556 | let result = parse_args_from_vec(args); 557 | assert!(matches!(result, Ok(Command::ListKeys))); 558 | } 559 | 560 | #[test] 561 | fn test_verbose_flag() { 562 | // Test verbose flag is filtered correctly 563 | let args = make_args(&["-v", "encrypt", "-r", "bob", "message.txt"]); 564 | let result = parse_args_from_vec(args); 565 | assert!(matches!(result, Ok(Command::Encrypt { 566 | recipient_key, 567 | input_file, 568 | output_file: None, 569 | dialect: None 570 | }) if recipient_key == "bob" && input_file == "message.txt")); 571 | 572 | // Test verbose flag in different position 573 | let args = make_args(&["encrypt", "-r", "bob", "message.txt", "-v"]); 574 | let result = parse_args_from_vec(args); 575 | assert!(matches!(result, Ok(Command::Encrypt { 576 | recipient_key, 577 | input_file, 578 | output_file: None, 579 | dialect: None 580 | }) if recipient_key == "bob" && input_file == "message.txt")); 581 | } 582 | } 583 | -------------------------------------------------------------------------------- /src/cipher/mod.rs: -------------------------------------------------------------------------------- 1 | // src/cipher/mod.rs 2 | mod patterns; 3 | pub use patterns::{CipherPattern, PatternVariation}; 4 | use std::io::{self, Write}; 5 | 6 | pub enum CipherMode { 7 | Encrypt, 8 | Decrypt, 9 | } 10 | 11 | pub enum CipherDialect { 12 | Cat, 13 | Dog, 14 | } 15 | 16 | pub struct AnimalCipher { 17 | cat_patterns: Vec, 18 | dog_patterns: Vec, 19 | current_dialect: CipherDialect, 20 | } 21 | 22 | impl AnimalCipher { 23 | pub fn new(dialect: CipherDialect) -> Self { 24 | match dialect { 25 | CipherDialect::Cat => Self { 26 | cat_patterns: vec![ 27 | // These patterns have been updated to support variable repetitions (1-4) 28 | // to make them more flexible for tests and edge cases 29 | CipherPattern::new_complex("mew", "m", 1, 4, "e", 1, 4, "w", 1, 4), 30 | CipherPattern::new_complex("purr", "p", 1, 4, "u", 1, 4, "r", 1, 4), 31 | CipherPattern::new_complex("nya", "n", 1, 4, "y", 1, 4, "a", 1, 4), 32 | // Special pattern with more complex handling for "meow" 33 | CipherPattern::new_special("meow"), 34 | CipherPattern::new_complex("mrrp", "m", 1, 4, "r", 1, 4, "p", 1, 4), 35 | ], 36 | dog_patterns: vec![ 37 | // Dog dialect patterns with the same flexibility 38 | CipherPattern::new_complex("woof", "w", 1, 4, "o", 1, 4, "f", 1, 4), 39 | // Special pattern with more complex handling for "bark" 40 | CipherPattern::new_special("bark"), 41 | CipherPattern::new_complex("arf", "a", 1, 4, "r", 1, 4, "f", 1, 4), 42 | CipherPattern::new_complex("yip", "y", 1, 4, "i", 1, 4, "p", 1, 4), 43 | CipherPattern::new_complex("wrf", "w", 1, 4, "r", 1, 4, "f", 1, 4), 44 | ], 45 | current_dialect: CipherDialect::Cat, 46 | }, 47 | CipherDialect::Dog => Self { 48 | // We maintain the same patterns for both dialects, but change the default 49 | cat_patterns: vec![ 50 | CipherPattern::new_complex("mew", "m", 1, 4, "e", 1, 4, "w", 1, 4), 51 | CipherPattern::new_complex("purr", "p", 1, 4, "u", 1, 4, "r", 1, 4), 52 | CipherPattern::new_complex("nya", "n", 1, 4, "y", 1, 4, "a", 1, 4), 53 | CipherPattern::new_special("meow"), 54 | CipherPattern::new_complex("mrrp", "m", 1, 4, "r", 1, 4, "p", 1, 4), 55 | ], 56 | dog_patterns: vec![ 57 | CipherPattern::new_complex("woof", "w", 1, 4, "o", 1, 4, "f", 1, 4), 58 | CipherPattern::new_special("bark"), 59 | CipherPattern::new_complex("arf", "a", 1, 4, "r", 1, 4, "f", 1, 4), 60 | CipherPattern::new_complex("yip", "y", 1, 4, "i", 1, 4, "p", 1, 4), 61 | CipherPattern::new_complex("wrf", "w", 1, 4, "r", 1, 4, "f", 1, 4), 62 | ], 63 | current_dialect: CipherDialect::Dog, 64 | }, 65 | } 66 | } 67 | 68 | pub fn process_data( 69 | &self, 70 | data: &[u8], 71 | writer: &mut W, 72 | _mode: CipherMode, 73 | ) -> io::Result<()> { 74 | let _patterns = match self.current_dialect { 75 | CipherDialect::Cat => &self.cat_patterns, 76 | CipherDialect::Dog => &self.dog_patterns, 77 | }; 78 | 79 | let mut i = 0; 80 | while i < data.len() { 81 | let remaining = data.len() - i; 82 | 83 | if remaining >= 3 { 84 | // Process 3 bytes (24 bits) at a time, producing 4 words 85 | let byte1 = data[i]; 86 | let byte2 = data[i + 1]; 87 | let byte3 = data[i + 2]; 88 | 89 | // Pack the 3 bytes into a 24-bit value - ensure each byte is in the correct position 90 | let packed_value = ((byte1 as u32) << 16) | ((byte2 as u32) << 8) | (byte3 as u32); 91 | 92 | if cfg!(test) { 93 | println!("DEBUG ENCODE: Chunk [{}, {}, {}], packed value=0x{:x}", 94 | byte1, byte2, byte3, packed_value); 95 | } 96 | 97 | // Extract 4 groups of 6 bits each 98 | let group1 = ((packed_value >> 18) & 0x3F) as u8; // Bits 23-18 99 | let group2 = ((packed_value >> 12) & 0x3F) as u8; // Bits 17-12 100 | let group3 = ((packed_value >> 6) & 0x3F) as u8; // Bits 11-6 101 | let group4 = (packed_value & 0x3F) as u8; // Bits 5-0 102 | 103 | if cfg!(test) { 104 | println!("DEBUG ENCODE: Group 0, shift=18, six_bits={}", group1); 105 | println!("DEBUG ENCODE: Group 1, shift=12, six_bits={}", group2); 106 | println!("DEBUG ENCODE: Group 2, shift=6, six_bits={}", group3); 107 | println!("DEBUG ENCODE: Group 3, shift=0, six_bits={}", group4); 108 | } 109 | 110 | // Encode each group as a word 111 | let word1 = self.encode_word(group1, 0)?; 112 | writer.write_all(word1.as_bytes())?; 113 | writer.write_all(b" ")?; 114 | 115 | let word2 = self.encode_word(group2, 1)?; 116 | writer.write_all(word2.as_bytes())?; 117 | writer.write_all(b" ")?; 118 | 119 | let word3 = self.encode_word(group3, 2)?; 120 | writer.write_all(word3.as_bytes())?; 121 | writer.write_all(b" ")?; 122 | 123 | let word4 = self.encode_word(group4, 3)?; 124 | writer.write_all(word4.as_bytes())?; 125 | writer.write_all(b" ")?; 126 | 127 | i += 3; 128 | } else if remaining == 2 { 129 | // Process 2 bytes (16 bits), producing 3 words 130 | let byte1 = data[i]; 131 | let byte2 = data[i + 1]; 132 | 133 | // Pack the 2 bytes into a 16-bit value 134 | let packed_value = ((byte1 as u16) << 8) | (byte2 as u16); 135 | 136 | if cfg!(test) { 137 | println!("DEBUG ENCODE: Chunk [{}, {}], packed value=0x{:x}", 138 | byte1, byte2, packed_value); 139 | } 140 | 141 | // Split into 3 groups - 5 bits, 6 bits, 5 bits 142 | let group1 = ((packed_value >> 11) & 0x1F) as u8; // Bits 15-11 143 | let group2 = ((packed_value >> 5) & 0x3F) as u8; // Bits 10-5 144 | let group3 = (packed_value & 0x1F) as u8; // Bits 4-0 145 | 146 | if cfg!(test) { 147 | println!("DEBUG ENCODE: Group 0, shift=11, six_bits={}", group1); 148 | println!("DEBUG ENCODE: Group 1, shift=6, six_bits={}", group2); 149 | println!("DEBUG ENCODE: Group 2, shift=0, six_bits={}", group3); 150 | } 151 | 152 | // Encode each group as a word, including position info 153 | let word1 = self.encode_word(group1, 0)?; 154 | writer.write_all(word1.as_bytes())?; 155 | writer.write_all(b" ")?; 156 | 157 | let word2 = self.encode_word(group2, 1)?; 158 | writer.write_all(word2.as_bytes())?; 159 | writer.write_all(b" ")?; 160 | 161 | let word3 = self.encode_word(group3, 2)?; 162 | writer.write_all(word3.as_bytes())?; 163 | writer.write_all(b" ")?; 164 | 165 | i += 2; 166 | } else { 167 | // Process 1 byte (8 bits), producing 2 words 168 | let byte = data[i]; 169 | 170 | if cfg!(test) { 171 | println!("DEBUG ENCODE: Chunk [{}], packed value=0x{:x}", 172 | byte, byte); 173 | } 174 | 175 | // Split the byte into 2 nibbles of 4 bits each 176 | let high_bits = (byte >> 4) & 0x0F; // Higher 4 bits 177 | let low_bits = byte & 0x0F; // Lower 4 bits 178 | 179 | if cfg!(test) { 180 | println!("DEBUG ENCODE: Group 0, shift=4, six_bits={}", high_bits); 181 | println!("DEBUG ENCODE: Group 1, shift=0, six_bits={}", low_bits); 182 | } 183 | 184 | // Encode each nibble as a word 185 | let word1 = self.encode_word(high_bits, 0)?; 186 | writer.write_all(word1.as_bytes())?; 187 | writer.write_all(b" ")?; 188 | 189 | let word2 = self.encode_word(low_bits, 1)?; 190 | writer.write_all(word2.as_bytes())?; 191 | writer.write_all(b" ")?; 192 | 193 | i += 1; 194 | } 195 | } 196 | 197 | Ok(()) 198 | } 199 | 200 | fn decode_word_cat(&self, word: &str) -> Option<(usize, u8)> { 201 | for (index, pattern) in self.cat_patterns.iter().enumerate() { 202 | if let Some(variation) = pattern.decode_variation(word) { 203 | return Some((index, variation)); 204 | } 205 | } 206 | None 207 | } 208 | 209 | fn decode_word_dog(&self, word: &str) -> Option<(usize, u8)> { 210 | for (index, pattern) in self.dog_patterns.iter().enumerate() { 211 | if let Some(variation) = pattern.decode_variation(word) { 212 | return Some((index, variation)); 213 | } 214 | } 215 | None 216 | } 217 | 218 | pub fn process_string(&self, content: &str, mode: CipherMode) -> io::Result> { 219 | let mut result = Vec::new(); 220 | 221 | // For decryption, trim trailing spaces and split by spaces to get words 222 | match mode { 223 | CipherMode::Decrypt => { 224 | let words: Vec<&str> = content.trim_end().split(' ').collect(); 225 | let mut i = 0; 226 | 227 | while i < words.len() { 228 | let remaining = words.len() - i; 229 | 230 | // Check if we have 4 words - handle as 3 bytes (24 bits) 231 | if remaining >= 4 { 232 | if cfg!(test) { 233 | println!("DEBUG DECODE: Starting chunk with 4 words, 3 bytes"); 234 | } 235 | 236 | let word1 = words[i]; 237 | let decoded1 = self.decode_word_with_fallback(word1, 0)?; 238 | let bits1 = decoded1.1 & 0x3F; // First 6 bits 239 | 240 | if cfg!(test) { 241 | println!("DEBUG DECODE: Word {}, pattern_index={}, six_bits={}", 242 | word1, decoded1.0, bits1); 243 | } 244 | 245 | let word2 = words[i + 1]; 246 | let decoded2 = self.decode_word_with_fallback(word2, 1)?; 247 | let bits2 = decoded2.1 & 0x3F; // Second 6 bits 248 | 249 | if cfg!(test) { 250 | println!("DEBUG DECODE: Word {}, pattern_index={}, six_bits={}", 251 | word2, decoded2.0, bits2); 252 | } 253 | 254 | let word3 = words[i + 2]; 255 | let decoded3 = self.decode_word_with_fallback(word3, 2)?; 256 | let bits3 = decoded3.1 & 0x3F; // Third 6 bits 257 | 258 | if cfg!(test) { 259 | println!("DEBUG DECODE: Word {}, pattern_index={}, six_bits={}", 260 | word3, decoded3.0, bits3); 261 | } 262 | 263 | let word4 = words[i + 3]; 264 | let decoded4 = self.decode_word_with_fallback(word4, 3)?; 265 | let bits4 = decoded4.1 & 0x3F; // Fourth 6 bits 266 | 267 | if cfg!(test) { 268 | println!("DEBUG DECODE: Word {}, pattern_index={}, six_bits={}", 269 | word4, decoded4.0, bits4); 270 | } 271 | 272 | // Assemble the full 24-bit value 273 | let mut value: u32 = 0; 274 | value |= (bits1 as u32) << 18; 275 | value |= (bits2 as u32) << 12; 276 | value |= (bits3 as u32) << 6; 277 | value |= bits4 as u32; 278 | 279 | if cfg!(test) { 280 | println!("DEBUG DECODE: Assembled value=0x{:x}", value); 281 | } 282 | 283 | // Extract the 3 bytes 284 | let byte1 = ((value >> 16) & 0xFF) as u8; 285 | let byte2 = ((value >> 8) & 0xFF) as u8; 286 | let byte3 = (value & 0xFF) as u8; 287 | 288 | if cfg!(test) { 289 | println!("DEBUG DECODE: Extracted byte 0 at shift=16: 0x{:x}", byte1); 290 | println!("DEBUG DECODE: Extracted byte 1 at shift=8: 0x{:x}", byte2); 291 | println!("DEBUG DECODE: Extracted byte 2 at shift=0: 0x{:x}", byte3); 292 | } 293 | 294 | result.push(byte1); 295 | result.push(byte2); 296 | result.push(byte3); 297 | 298 | i += 4; 299 | } else if remaining >= 3 { 300 | // Handle 2 bytes (16 bits) from 3 words 301 | if cfg!(test) { 302 | println!("DEBUG DECODE: Starting chunk with 3 words, 2 bytes"); 303 | } 304 | 305 | let word1 = words[i]; 306 | let decoded1 = self.decode_word_with_fallback(word1, 0)?; 307 | let bits1 = decoded1.1 & 0x1F; // First 5 bits 308 | 309 | if cfg!(test) { 310 | println!("DEBUG DECODE: Word {}, pattern_index={}, six_bits={}", 311 | word1, decoded1.0, bits1); 312 | } 313 | 314 | let word2 = words[i + 1]; 315 | let decoded2 = self.decode_word_with_fallback(word2, 1)?; 316 | let bits2 = decoded2.1 & 0x3F; // Middle 6 bits 317 | 318 | if cfg!(test) { 319 | println!("DEBUG DECODE: Word {}, pattern_index={}, six_bits={}", 320 | word2, decoded2.0, bits2); 321 | } 322 | 323 | let word3 = words[i + 2]; 324 | let decoded3 = self.decode_word_with_fallback(word3, 2)?; 325 | let bits3 = decoded3.1 & 0x1F; // Last 5 bits 326 | 327 | if cfg!(test) { 328 | println!("DEBUG DECODE: Word {}, pattern_index={}, six_bits={}", 329 | word3, decoded3.0, bits3); 330 | } 331 | 332 | // Assemble the full 16-bit value 333 | let mut value: u16 = 0; 334 | value |= (bits1 as u16) << 11; 335 | value |= (bits2 as u16) << 5; 336 | value |= bits3 as u16; 337 | 338 | if cfg!(test) { 339 | println!("DEBUG DECODE: Assembled value=0x{:x}", value); 340 | } 341 | 342 | // Extract the 2 bytes 343 | let byte1 = ((value >> 8) & 0xFF) as u8; 344 | let byte2 = (value & 0xFF) as u8; 345 | 346 | if cfg!(test) { 347 | println!("DEBUG DECODE: Extracted byte 0 at shift=8: 0x{:x}", byte1); 348 | println!("DEBUG DECODE: Extracted byte 1 at shift=0: 0x{:x}", byte2); 349 | } 350 | 351 | result.push(byte1); 352 | result.push(byte2); 353 | 354 | i += 3; 355 | } else if remaining >= 2 { 356 | // Handle 1 byte (8 bits) from 2 words 357 | if cfg!(test) { 358 | println!("DEBUG DECODE: Starting chunk with 2 words, 1 bytes"); 359 | } 360 | 361 | let word1 = words[i]; 362 | let decoded1 = self.decode_word_with_fallback(word1, 0)?; 363 | let high_bits = decoded1.1 & 0x0F; // Higher 4 bits 364 | 365 | if cfg!(test) { 366 | println!("DEBUG DECODE: Word {}, pattern_index={}, six_bits={}", 367 | word1, decoded1.0, high_bits); 368 | } 369 | 370 | let word2 = words[i + 1]; 371 | let decoded2 = self.decode_word_with_fallback(word2, 1)?; 372 | let low_bits = decoded2.1 & 0x0F; // Lower 4 bits 373 | 374 | if cfg!(test) { 375 | println!("DEBUG DECODE: Word {}, pattern_index={}, six_bits={}", 376 | word2, decoded2.0, low_bits); 377 | } 378 | 379 | // Combine into a single byte 380 | let value = (high_bits << 4) | low_bits; 381 | 382 | if cfg!(test) { 383 | println!("DEBUG DECODE: Assembled value=0x{:x}", value); 384 | println!("DEBUG DECODE: Extracted single byte: 0x{:x}", value); 385 | } 386 | 387 | result.push(value); 388 | i += 2; 389 | } else { 390 | break; 391 | } 392 | } 393 | }, 394 | _ => { 395 | return Err(io::Error::new( 396 | io::ErrorKind::InvalidInput, 397 | "Only decryption is supported for process_string", 398 | )); 399 | } 400 | } 401 | 402 | Ok(result) 403 | } 404 | 405 | fn decode_word_with_fallback(&self, word: &str, expected_position: usize) -> io::Result<(usize, u8)> { 406 | // Try to decode with current dialect first 407 | let decoded = match self.current_dialect { 408 | CipherDialect::Cat => self.decode_word_cat(word), 409 | CipherDialect::Dog => self.decode_word_dog(word), 410 | }; 411 | 412 | if let Some(decoded_value) = decoded { 413 | // Calculate expected pattern index 414 | let expected_pattern_index = expected_position % match self.current_dialect { 415 | CipherDialect::Cat => self.cat_patterns.len(), 416 | CipherDialect::Dog => self.dog_patterns.len(), 417 | }; 418 | 419 | // If the pattern matches expected position, use it directly 420 | if decoded_value.0 == expected_pattern_index { 421 | return Ok(decoded_value); 422 | } 423 | } 424 | 425 | // Try the other dialect as fallback if current didn't match 426 | let fallback = match self.current_dialect { 427 | CipherDialect::Cat => self.decode_word_dog(word), 428 | CipherDialect::Dog => self.decode_word_cat(word), 429 | }; 430 | 431 | if let Some(fallback_value) = fallback { 432 | return Ok(fallback_value); 433 | } 434 | 435 | // If we get here, we couldn't decode the word properly 436 | Err(io::Error::new( 437 | io::ErrorKind::InvalidData, 438 | format!("Pattern mismatch for word: {}", word) 439 | )) 440 | } 441 | 442 | /// Encodes a bit value into a word using the pattern at the specified position 443 | fn encode_word(&self, bits: u8, position: usize) -> io::Result { 444 | let patterns = match self.current_dialect { 445 | CipherDialect::Cat => &self.cat_patterns, 446 | CipherDialect::Dog => &self.dog_patterns, 447 | }; 448 | 449 | if position >= patterns.len() { 450 | return Err(io::Error::new( 451 | io::ErrorKind::InvalidInput, 452 | format!("Invalid pattern position: {}", position), 453 | )); 454 | } 455 | 456 | Ok(patterns[position].generate_variation(bits)) 457 | } 458 | } 459 | 460 | // For backwards compatibility 461 | pub type CatCipher = AnimalCipher; 462 | 463 | #[cfg(test)] 464 | mod tests { 465 | use super::*; 466 | 467 | 468 | #[test] 469 | fn test_basic_encryption_decryption() { 470 | let cipher = AnimalCipher::new(CipherDialect::Cat); 471 | let test_data = b"Hello, World!"; 472 | 473 | // Encrypt 474 | let mut encrypted = Vec::new(); 475 | cipher.process_data(test_data, &mut encrypted, CipherMode::Encrypt).unwrap(); 476 | let encrypted_str = String::from_utf8(encrypted.clone()).unwrap(); 477 | 478 | // Decrypt 479 | let decrypted = cipher.process_string(&encrypted_str, CipherMode::Decrypt).unwrap(); 480 | println!("Original: {:?}, Encrypted: {:?}, Decrypted: {:?}", test_data, encrypted, decrypted); 481 | assert_eq!(test_data.as_slice(), decrypted.as_slice()); 482 | } 483 | 484 | #[test] 485 | fn test_pattern_consistency() { 486 | let cipher = AnimalCipher::new(CipherDialect::Cat); 487 | 488 | // Test all possible 6-bit values 489 | for value in 0..64u8 { 490 | let mut encrypted = Vec::new(); 491 | cipher.process_data(&[value], &mut encrypted, CipherMode::Encrypt).unwrap(); 492 | let encrypted_str = String::from_utf8(encrypted.clone()).unwrap(); 493 | 494 | let decrypted = cipher.process_string(&encrypted_str, CipherMode::Decrypt).unwrap(); 495 | println!("Original: {:?}, Encrypted: {:?}, Decrypted: {:?}", &[value], &encrypted, &decrypted); 496 | assert_eq!(&[value], decrypted.as_slice(), 497 | "Failed for value {}: encrypted as '{}'", value, encrypted_str.trim()); 498 | } 499 | } 500 | 501 | #[test] 502 | fn test_multi_byte_encryption() { 503 | let cipher = AnimalCipher::new(CipherDialect::Cat); 504 | let test_cases = vec![ 505 | vec![0u8, 1u8, 2u8], 506 | vec![255u8, 254u8, 253u8], 507 | vec![128u8, 128u8, 128u8], 508 | vec![0u8, 255u8, 0u8], 509 | ]; 510 | 511 | for test_case in test_cases { 512 | let mut encrypted = Vec::new(); 513 | cipher.process_data(&test_case, &mut encrypted, CipherMode::Encrypt).unwrap(); 514 | let encrypted_str = String::from_utf8(encrypted.clone()).unwrap(); 515 | 516 | let decrypted = cipher.process_string(&encrypted_str, CipherMode::Decrypt).unwrap(); 517 | println!("Original: {:?}, Encrypted: {:?}, Decrypted: {:?}", test_case, &encrypted, &decrypted); 518 | assert_eq!(test_case.as_slice(), decrypted.as_slice(), 519 | "Failed for case {:?}: encrypted as '{}'", test_case, encrypted_str.trim()); 520 | } 521 | } 522 | 523 | #[test] 524 | fn test_dialect_switching() { 525 | let cat_cipher = AnimalCipher::new(CipherDialect::Cat); 526 | let dog_cipher = AnimalCipher::new(CipherDialect::Dog); 527 | let test_data = b"Test message"; 528 | 529 | // Encrypt with cat dialect 530 | let mut cat_encrypted = Vec::new(); 531 | cat_cipher.process_data(test_data, &mut cat_encrypted, CipherMode::Encrypt).unwrap(); 532 | let cat_str = String::from_utf8(cat_encrypted.clone()).unwrap(); 533 | 534 | // Encrypt with dog dialect 535 | let mut dog_encrypted = Vec::new(); 536 | dog_cipher.process_data(test_data, &mut dog_encrypted, CipherMode::Encrypt).unwrap(); 537 | let dog_str = String::from_utf8(dog_encrypted.clone()).unwrap(); 538 | 539 | // They should encrypt to different strings 540 | assert_ne!(cat_str, dog_str, "Cat and dog dialects produced identical output"); 541 | 542 | // But both should decrypt correctly 543 | let cat_decrypted = cat_cipher.process_string(&cat_str, CipherMode::Decrypt).unwrap(); 544 | let dog_decrypted = dog_cipher.process_string(&dog_str, CipherMode::Decrypt).unwrap(); 545 | 546 | println!("Original: {:?}, Encrypted: {:?}, Decrypted: {:?}", test_data, &cat_encrypted, &cat_decrypted); 547 | println!("Original: {:?}, Encrypted: {:?}, Decrypted: {:?}", test_data, &dog_encrypted, &dog_decrypted); 548 | 549 | assert_eq!(test_data.as_slice(), cat_decrypted.as_slice()); 550 | assert_eq!(test_data.as_slice(), dog_decrypted.as_slice()); 551 | } 552 | 553 | #[test] 554 | fn test_edge_cases() { 555 | let cipher = AnimalCipher::new(CipherDialect::Cat); 556 | 557 | // Test empty input 558 | let mut encrypted = Vec::new(); 559 | cipher.process_data(&[], &mut encrypted, CipherMode::Encrypt).unwrap(); 560 | assert!(encrypted.is_empty()); 561 | 562 | // Test single byte 563 | let mut encrypted = Vec::new(); 564 | cipher.process_data(&[42], &mut encrypted, CipherMode::Encrypt).unwrap(); 565 | let encrypted_str = String::from_utf8(encrypted.clone()).unwrap(); 566 | let decrypted = cipher.process_string(&encrypted_str, CipherMode::Decrypt).unwrap(); 567 | println!("Original: {:?}, Encrypted: {:?}, Decrypted: {:?}", &[42], &encrypted, &decrypted); 568 | assert_eq!(&[42], decrypted.as_slice()); 569 | 570 | // Test two bytes 571 | let mut encrypted = Vec::new(); 572 | cipher.process_data(&[0xAA, 0x55], &mut encrypted, CipherMode::Encrypt).unwrap(); 573 | let encrypted_str = String::from_utf8(encrypted.clone()).unwrap(); 574 | let decrypted = cipher.process_string(&encrypted_str, CipherMode::Decrypt).unwrap(); 575 | println!("Original: {:?}, Encrypted: {:?}, Decrypted: {:?}", &[0xAA, 0x55], &encrypted, &decrypted); 576 | assert_eq!(&[0xAA, 0x55], decrypted.as_slice()); 577 | } 578 | } 579 | 580 | --------------------------------------------------------------------------------