project_ares 0.12.0

Automated decoding tool, Ciphey but in Rust
Documentation
//! Decode both standard and URL-safe base64 strings
//! Performs error handling and returns a string
//! Call base64_decoder.crack to use. It returns option<String> and check with
//! `result.is_some()` to see if it returned okay.

use crate::checkers::CheckerTypes;
use crate::decoders::interface::check_string_success;
use base64::{engine::general_purpose, Engine as _};

use super::crack_results::CrackResult;
use super::interface::Crack;
use super::interface::Decoder;

use log::{debug, info, trace};

/// The Base64 decoder, call:
/// `let base64_decoder = Decoder::<Base64Decoder>::new()` to create a new instance
/// And then call:
/// `result = base64_decoder.crack(input)` to decode a base64 string
/// The struct generated by new() comes from interface.rs
/// ```
/// use ares::decoders::base64_decoder::{Base64Decoder};
/// use ares::decoders::interface::{Crack, Decoder};
/// use ares::checkers::{athena::Athena, CheckerTypes, checker_type::{Check, Checker}};
///
/// let decode_base64 = Decoder::<Base64Decoder>::new();
/// let athena_checker = Checker::<Athena>::new();
/// let checker = CheckerTypes::CheckAthena(athena_checker);
///
/// let result = decode_base64.crack("aGVsbG8gd29ybGQ=", &checker).unencrypted_text;
/// assert!(result.is_some());
/// assert_eq!(result.unwrap()[0], "hello world");
/// ```
pub struct Base64Decoder;

impl Crack for Decoder<Base64Decoder> {
    fn new() -> Decoder<Base64Decoder> {
        Decoder {
            name: "Base64",
            description: "Base64 is a group of binary-to-text encoding schemes that represent binary data in ASCII string format. Supports both standard Base64 (with +/) and URL-safe Base64 (with -_) variants.",
            link: "https://en.wikipedia.org/wiki/Base64",
            tags: vec!["base64", "base64_url", "url", "decoder", "base"],
            popularity: 1.0,
            phantom: std::marker::PhantomData,
        }
    }

    /// This function does the actual decoding
    /// It returns an Option<string> if it was successful
    /// Else the Option returns nothing and the error is logged in Trace
    fn crack(&self, text: &str, checker: &CheckerTypes) -> CrackResult {
        trace!("Trying Base64 with text {:?}", text);

        let mut results = CrackResult::new(self, text.to_string());

        // Determine which decoder to use based on the characters present
        let uses_standard_chars = text.contains('+') || text.contains('=') || text.contains('/');

        let decoded_text = if uses_standard_chars {
            debug!("Using standard Base64 decoder");
            decode_base64_no_error_handling(text)
        } else {
            debug!("Using URL-safe Base64 decoder");
            decode_base64_url_no_error_handling(text)
        };

        if decoded_text.is_none() {
            debug!("Base64 decode failed");
            return results;
        }

        let decoded_text = decoded_text.unwrap();
        if !check_string_success(&decoded_text, text) {
            info!(
                "Failed to decode base64 because check_string_success returned false on string {}",
                decoded_text
            );
            return results;
        }

        let checker_result = checker.check(&decoded_text);
        results.unencrypted_text = Some(vec![decoded_text]);
        results.update_checker(&checker_result);

        results
    }

    /// Gets all tags for this decoder
    fn get_tags(&self) -> &Vec<&str> {
        &self.tags
    }
    /// Gets the name for the current decoder
    fn get_name(&self) -> &str {
        self.name
    }
}

/// helper function for standard base64
fn decode_base64_no_error_handling(text: &str) -> Option<String> {
    // Strip all padding
    let text = text.replace('=', "");
    // Runs the code to decode base64
    // Doesn't perform error handling, call from_base64
    general_purpose::STANDARD_NO_PAD
        .decode(text.as_bytes())
        .ok()
        .map(|inner| String::from_utf8(inner).ok())?
}

/// helper function for url-safe base64
fn decode_base64_url_no_error_handling(text: &str) -> Option<String> {
    // Strip all padding
    let text = text.replace('=', "");

    // Use URL_SAFE_NO_PAD engine to decode URL-safe Base64
    general_purpose::URL_SAFE_NO_PAD
        .decode(text.as_bytes())
        .ok()
        .map(|inner| String::from_utf8(inner).ok())?
}

#[cfg(test)]
mod tests {
    use super::Base64Decoder;
    use crate::{
        checkers::{
            athena::Athena,
            checker_type::{Check, Checker},
            CheckerTypes,
        },
        decoders::interface::{Crack, Decoder},
    };

    // helper for tests
    fn get_athena_checker() -> CheckerTypes {
        let athena_checker = Checker::<Athena>::new();
        CheckerTypes::CheckAthena(athena_checker)
    }

    #[test]
    fn successful_standard_decoding() {
        let base64_decoder = Decoder::<Base64Decoder>::new();

        let result = base64_decoder.crack("aGVsbG8gd29ybGQ=", &get_athena_checker());
        let decoded_str = &result
            .unencrypted_text
            .expect("No unencrypted text for base64");
        assert_eq!(decoded_str[0], "hello world");
    }

    #[test]
    fn successful_url_safe_decoding() {
        let base64_decoder = Decoder::<Base64Decoder>::new();

        // Test URL-safe encoded strings
        let test_cases = vec![
            ("SGVsbG8tV29ybGQ", "Hello-World"),
            ("SGVsbG9fV29ybGQ", "Hello_World"),
            (
                "aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS8_ZXhhbXBsZT10ZXN0",
                "https://www.google.com/?example=test",
            ),
        ];

        for (input, expected) in test_cases {
            let result = base64_decoder.crack(input, &get_athena_checker());
            let decoded_str = &result
                .unencrypted_text
                .unwrap_or_else(|| panic!("Failed to decode URL-safe string: {}", input));
            assert_eq!(decoded_str[0], expected);
        }
    }

    #[test]
    fn base64_decode_empty_string() {
        let base64_decoder = Decoder::<Base64Decoder>::new();
        let result = base64_decoder
            .crack("", &get_athena_checker())
            .unencrypted_text;
        assert!(result.is_none());
    }

    #[test]
    fn base64_decode_handles_panics() {
        let base64_decoder = Decoder::<Base64Decoder>::new();
        let result = base64_decoder
            .crack(
                "hello my name is panicky mc panic face!",
                &get_athena_checker(),
            )
            .unencrypted_text;
        assert!(
            result.is_none(),
            "Decode_base64 should return None for invalid input"
        );
    }

    #[test]
    fn base64_handle_panic_if_emoji() {
        let base64_decoder = Decoder::<Base64Decoder>::new();
        let result = base64_decoder
            .crack("😂", &get_athena_checker())
            .unencrypted_text;
        assert!(
            result.is_none(),
            "Decode_base64 should return None for emoji input"
        );
    }

    #[test]
    fn base64_decode_triple() {
        let base64_decoder = Decoder::<Base64Decoder>::new();
        let input =
            "VVRKc2QyRkhWalZKUjJ4NlNVaE9ka2xIV21oak0xRnpTVWhTYjJGWVRXZGhXRTFuV1ROS2FHVnVhMmc9";

        let mut current = input.to_string();
        for _ in 0..3 {
            let result = base64_decoder.crack(&current, &get_athena_checker());
            assert!(
                result.unencrypted_text.is_some(),
                "Failed to decode base64 layer"
            );
            current = result.unencrypted_text.unwrap()[0].clone();
            assert!(!current.is_empty(), "Decoded to empty string");
            assert!(current.is_ascii(), "Decoded to non-ASCII content");
        }

        assert!(!current.trim().is_empty(), "Final decoded text is empty");
        println!("Final decoded text: {:?}", current);
    }

    #[test]
    fn test_mixed_encoding_variants() {
        let base64_decoder = Decoder::<Base64Decoder>::new();
        let checker = get_athena_checker();

        // Test standard Base64 (with standard chars)
        let standard_result = base64_decoder.crack("SGVsbG8+Pz8/Cg==", &checker);
        assert!(
            standard_result.unencrypted_text.is_some(),
            "Failed to decode standard Base64"
        );

        // Test URL-safe Base64
        let url_safe_result = base64_decoder.crack("SGVsbG8-Pz8_Cg", &checker);
        assert!(
            url_safe_result.unencrypted_text.is_some(),
            "Failed to decode URL-safe Base64"
        );
    }
}