Skip to main content

ender_eye/
lib.rs

1mod crypto;
2mod error;
3mod payload;
4mod sga;
5
6use crate::error::Result;
7
8/// Encrypts a message using a password and returns a Base64-encoded payload.
9///
10/// Internally runs the message through three stages:
11/// 1. **SGA encoding** — substitutes each letter with its Standard Galactic Alphabet symbol.
12/// 2. **AES-256-GCM** — encrypts the encoded text. A random salt and nonce are generated on
13///    every call, so two calls with identical inputs will always produce different outputs.
14/// 3. **Base64** — packs `salt + nonce + ciphertext` into a single portable string.
15///
16/// # Errors
17///
18/// Returns an error if encryption fails.
19///
20/// # Example
21///
22/// ```
23/// let ciphertext = ender_eye::encrypt("hello world", "my-password").unwrap();
24/// ```
25pub fn encrypt(message: &str, password: &str) -> Result<String> {
26    if password.is_empty() {
27        return Err(error::ValidationErrors::EmptyPassword);
28    }
29    if message.is_empty() {
30        return Err(error::ValidationErrors::EmptyCharacters);
31    }
32
33    let sga_encoded = sga::encode(message);
34    let (ciphertext, salt, nonce) = crypto::encrypt(&sga_encoded, password)?;
35
36    Ok(payload::encode_payload(&ciphertext, &salt, &nonce))
37}
38
39/// Decrypts a Base64-encoded payload produced by [`encrypt`] and returns the original message.
40///
41/// Reverses the three stages of [`encrypt`]:
42/// 1. **Base64 decode** — unpacks the payload back into `salt`, `nonce`, and `ciphertext`.
43/// 2. **AES-256-GCM** — decrypts and authenticates the ciphertext. Fails if the payload was
44///    tampered with or if the wrong password is provided.
45/// 3. **SGA decode** — converts SGA symbols back to plain ASCII letters.
46///
47/// # Errors
48///
49/// Returns an error if the payload is malformed, authentication fails, or decoding fails.
50///
51/// # Example
52///
53/// ```
54/// # let ciphertext = ender_eye::encrypt("hello world", "my-password").unwrap();
55/// let plaintext = ender_eye::decrypt(&ciphertext, "my-password").unwrap();
56/// assert_eq!(plaintext, "hello world");
57/// ```
58pub fn decrypt(message: &str, password: &str) -> Result<String> {
59    let (ciphertext, salt, nonce) = payload::decode_payload(message)?;
60    let decrypted = crypto::decrypt(&ciphertext, password, &salt, &nonce)?;
61
62    let decoded = sga::decode(&decrypted)?;
63
64    Ok(decoded)
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use base64::Engine;
71
72    #[test]
73    fn roundtrip_default() {
74        let encrypted = encrypt("hello world", "ultra_super_secret_password").unwrap();
75        let decrypted = decrypt(&encrypted, "ultra_super_secret_password").unwrap();
76        assert_eq!(decrypted, "hello world");
77    }
78
79    #[test]
80    fn roundtrip_special_characters() {
81        let message = "hello world 123";
82        let encrypted = encrypt(message, "password").unwrap();
83        let decrypted = decrypt(&encrypted, "password").unwrap();
84        assert_eq!(decrypted, message);
85    }
86
87    #[test]
88    fn roundtrip_long_message() {
89        let message = "a".repeat(500);
90        let encrypted = encrypt(&message, "password").unwrap();
91        let decrypted = decrypt(&encrypted, "password").unwrap();
92        assert_eq!(decrypted, message);
93    }
94
95    #[test]
96    fn encrypt_empty_password_returns_err() {
97        assert!(encrypt("hello world", "").is_err());
98    }
99
100    #[test]
101    fn encrypt_empty_message_returns_err() {
102        assert!(encrypt("", "password").is_err());
103    }
104
105    #[test]
106    fn decrypt_wrong_password_returns_err() {
107        let encrypted = encrypt("hello world", "correct-password").unwrap();
108        assert!(decrypt(&encrypted, "wrong-password").is_err());
109    }
110
111    #[test]
112    fn decrypt_corrupted_payload_returns_err() {
113        assert!(decrypt("this-is-not-a-valid-payload", "password").is_err());
114    }
115
116    #[test]
117    fn decrypt_payload_too_short_returns_err() {
118        // Valid Base64 but fewer than 29 bytes when decoded
119        let short = base64::engine::general_purpose::STANDARD.encode([0u8; 10]);
120        assert!(decrypt(&short, "password").is_err());
121    }
122}