project_ares 0.12.0

Automated decoding tool, Ciphey but in Rust
Documentation
//! Decode a base64_url string
//! Performs error handling and returns a string
//! Call base64_url_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_url decoder, call:
/// `let base64_url_decoder = Decoder::<Base64URLDecoder>::new()` to create a new instance
/// And then call:
/// `result = base64_url_decoder.crack(input)` to decode a base64_url string
/// The struct generated by new() comes from interface.rs
/// ```
/// use ares::decoders::base64_url_decoder::{Base64URLDecoder};
/// use ares::decoders::interface::{Crack, Decoder};
/// use ares::checkers::{athena::Athena, CheckerTypes, checker_type::{Check, Checker}};
///
/// let decode_base64_url = Decoder::<Base64URLDecoder>::new();
/// let athena_checker = Checker::<Athena>::new();
/// let checker = CheckerTypes::CheckAthena(athena_checker);
///
/// let result = decode_base64_url.crack("aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS8_ZXhhbXBsZT10ZXN0", &checker).unencrypted_text;
/// assert!(result.is_some());
/// assert_eq!(result.unwrap()[0], "https://www.google.com/?example=test");
/// ```
pub struct Base64URLDecoder;

impl Crack for Decoder<Base64URLDecoder> {
    fn new() -> Decoder<Base64URLDecoder> {
        Decoder {
            name: "Base64 URL",
            description: "Modified Base64 for URL variants exist (such as base64url in RFC 4648), where the '+' and '/' characters of standard Base64 are respectively replaced by '-' and '_', so that using URL encoders/decoders is no longer necessary.",
            link: "https://en.wikipedia.org/wiki/Base64#URL_applications",
            tags: vec!["base64_url", "base64", "url", "decoder", "base"],
            popularity: 0.9,
            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_url with text {:?}", text);
        
        let mut results = CrackResult::new(self, text.to_string());
        
        // Skip if:
        // 1. String has no URL-safe chars (not for us)
        // 2. String has standard Base64 chars (let Base64 handle it)
        if (!text.contains('-') && !text.contains('_')) || text.contains('+') || text.contains('/') {
            debug!("Base64 URL decoder skipping string (no URL-safe chars or has standard Base64 chars)");
            return results;
        }
        
        let decoded_text = decode_base64_url_no_error_handling(text);

        if decoded_text.is_none() {
            debug!("Failed to decode base64_url because Base64URLDecoder::decode_base64_url_no_error_handling returned None");
            return results;
        }

        let decoded_text = decoded_text.unwrap();
        if !check_string_success(&decoded_text, text) {
            info!(
                "Failed to decode base64_url 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
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()) // Decode bytes
        .ok() // Handle decode errors
        .and_then(|bytes| String::from_utf8(bytes).ok()) // Handle UTF-8 errors
}

#[cfg(test)]
mod tests {
    use super::Base64URLDecoder;
    use super::super::base64_decoder::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 base64_url_handles_only_url_safe() {
        let base64_url_decoder = Decoder::<Base64URLDecoder>::new();
        let base64_decoder = Decoder::<Base64Decoder>::new();
        let checker = get_athena_checker();
        
        // Test cases for URL-safe strings (should only decode in Base64URL)
        let url_safe_cases = vec![
            // With hyphen (URL-safe variant of +)
            "SGVsbG8tV29ybGQ",
            // With underscore (URL-safe variant of /)
            "SGVsbG9fV29ybGQ", 
            // With both
            "SGVsbG8tV29ybGRfMjAyNA"
        ];
        
        // URL-safe strings should decode in Base64URL but not Base64
        for input in url_safe_cases {
            let url_result = base64_url_decoder.crack(input, &checker);
            assert!(url_result.unencrypted_text.is_some(), "Base64URL failed to decode URL-safe string: {}", input);
            
            let std_result = base64_decoder.crack(input, &checker);
            assert!(std_result.unencrypted_text.is_none(), "Standard Base64 should not decode URL-safe string: {}", input);
        }
        
        // Test cases that should be rejected by Base64URL
        let reject_cases = vec![
            // Standard Base64 (no special chars)
            "SGVsbG8gV29ybGQ",
            // With +
            "SGVsbG8rV29ybGQ",
            // With /
            "SGVsbG8vV29ybGQ",
            // With both + and /
            "SGVsbG8rV29ybGQv",
            // Empty string
            "",
            // Invalid content
            "😂",
            // Not Base64
            "hello world"
        ];
        
        // Non-URL-safe strings should decode in Base64 but not Base64URL
        for input in reject_cases {
            let url_result = base64_url_decoder.crack(input, &checker);
            assert!(url_result.unencrypted_text.is_none(), "Base64URL should reject non-URL-safe string: {}", input);
        }
    }
}