#!/usr/bin/env python3
"""
Roundcube Password Decryption Script
Decrypts passwords stored in Roundcube session data using DES3 encryption.

Usage:
    python decrypt_roundcube.py [encrypted_password] [des_key]
    
Examples:
    python decrypt_roundcube.py
    python decrypt_roundcube.py "L7Rv00A8TuwJAr67kITxxcSgnIk25Am/"
    python decrypt_roundcube.py "L7Rv00A8TuwJAr67kITxxcSgnIk25Am/" "rcmail-!24ByteDESkey*Str"
"""

import sys
import base64
import binascii
import argparse
from base64 import b64decode

def try_pycryptodome():
    """Try to import from pycryptodome"""
    try:
        from Crypto.Cipher import DES3
        return DES3
    except ImportError:
        return None

def try_pycrypto():
    """Try to import from pycrypto"""
    try:
        from Crypto.Cipher import DES3
        return DES3
    except ImportError:
        return None

def install_instructions():
    """Print installation instructions"""
    print("ERROR: Required cryptography library not found!")
    print("\nTo install the required dependency, run one of these commands:")
    print("  pip install pycryptodome")
    print("  pip install pycrypto")
    print("\nOr if you're using conda:")
    print("  conda install pycryptodome")
    print("\nNote: pycryptodome is recommended over pycrypto (which is deprecated)")

def is_printable(text):
    """Check if text contains only printable characters"""
    try:
        return text.isprintable()
    except:
        return False

def analyze_decrypted_data(data):
    """Analyze decrypted data to determine if it's valid"""
    try:
        # Try to decode as UTF-8
        utf8_text = data.decode('utf-8', errors='ignore')
        
        # Check if it's printable
        if is_printable(utf8_text):
            return utf8_text, "UTF-8 printable text"
        
        # Check if it looks like hex
        hex_str = data.hex()
        if all(c in '0123456789abcdefABCDEF' for c in hex_str):
            return hex_str, "Hexadecimal data"
        
        # Check if it's ASCII with some non-printable chars
        ascii_text = ''.join(chr(b) if 32 <= b <= 126 else f'\\x{b:02x}' for b in data)
        return ascii_text, "ASCII with escape sequences"
        
    except Exception as e:
        return str(data), f"Raw bytes (error: {e})"

def decrypt_roundcube_password(enc_password, des_key, mode='ECB', iv=None):
    """
    Decrypt Roundcube password using DES3
    
    Args:
        enc_password (str): Base64 encoded encrypted password
        des_key (bytes): DES3 key (24 bytes)
        mode (str): Encryption mode ('ECB' or 'CBC')
        iv (bytes): Initialization vector for CBC mode
    
    Returns:
        list: List of (decrypted_data, analysis_result) tuples
    """
    try:
        # Get DES3 module
        DES3 = try_pycryptodome()
        if DES3 is None:
            DES3 = try_pycrypto()
        if DES3 is None:
            return [(None, "Library not available")]
        
        # Decode base64
        try:
            data = b64decode(enc_password)
        except Exception as e:
            return [(None, f"Failed to decode base64: {e}")]
        
        # Create cipher based on mode
        if mode == 'ECB':
            cipher = DES3.new(des_key, DES3.MODE_ECB)
        elif mode == 'CBC':
            if iv is None:
                iv = b"\x00" * 8
            cipher = DES3.new(des_key, DES3.MODE_CBC, iv)
        else:
            return [(None, f"Unsupported mode: {mode}")]
        
        # Decrypt
        try:
            plain = cipher.decrypt(data)
        except Exception as e:
            return [(None, f"Decryption failed: {e}")]
        
        # Try different padding removal strategies
        results = []
        
        # Original padding removal
        try:
            pad_len = plain[-1]
            if 1 <= pad_len <= 8:
                # Check if padding is valid
                if all(plain[-i] == pad_len for i in range(1, pad_len + 1)):
                    unpadded = plain[:-pad_len]
                    result, analysis = analyze_decrypted_data(unpadded)
                    results.append((result, f"PKCS#5 padding removed - {analysis}"))
        except (IndexError, ValueError):
            pass
        
        # Try without padding removal
        result, analysis = analyze_decrypted_data(plain)
        results.append((result, f"No padding removal - {analysis}"))
        
        # Try removing common padding patterns
        for i in range(1, 9):
            if len(plain) >= i and plain[-i:] == b'\x00' * i:
                unpadded = plain[:-i]
                result, analysis = analyze_decrypted_data(unpadded)
                results.append((result, f"Removed {i} null bytes - {analysis}"))
        
        return results
        
    except Exception as e:
        return [(None, f"Unexpected error: {e}")]

def test_common_keys():
    """Test common Roundcube DES keys"""
    return [
        b"rcmail-!24ByteDESkey*Str",
        b"rcmail-!24ByteDESkey*Str\x00",
        b"rcmail-!24ByteDESkey*Str\x00\x00",
        b"rcmail-!24ByteDESkey*Str\x00\x00\x00",
        b"rcmail-!24ByteDESkey*Str\x00\x00\x00\x00",
        b"rcmail-!24ByteDESkey*Str\x00\x00\x00\x00\x00",
        b"rcmail-!24ByteDESkey*Str\x00\x00\x00\x00\x00\x00",
        b"rcmail-!24ByteDESkey*Str\x00\x00\x00\x00\x00\x00\x00",
    ]

def parse_arguments():
    """Parse command line arguments"""
    parser = argparse.ArgumentParser(
        description="Decrypt Roundcube passwords using DES3 encryption",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s
  %(prog)s "L7Rv00A8TuwJAr67kITxxcSgnIk25Am/"
  %(prog)s "L7Rv00A8TuwJAr67kITxxcSgnIk25Am/" "rcmail-!24ByteDESkey*Str"
  %(prog)s --encrypted "L7Rv00A8TuwJAr67kITxxcSgnIk25Am/" --key "rcmail-!24ByteDESkey*Str"
        """
    )
    
    parser.add_argument(
        'encrypted_password',
        nargs='?',
        default="L7Rv00A8TuwJAr67kITxxcSgnIk25Am/",
        help='Base64 encoded encrypted password (default: example password)'
    )
    
    parser.add_argument(
        'des_key',
        nargs='?',
        default="rcmail-!24ByteDESkey*Str",
        help='DES3 key (default: common Roundcube key)'
    )
    
    parser.add_argument(
        '--encrypted', '-e',
        dest='encrypted_password_alt',
        help='Alternative way to specify encrypted password'
    )
    
    parser.add_argument(
        '--key', '-k',
        dest='des_key_alt',
        help='Alternative way to specify DES key'
    )
    
    parser.add_argument(
        '--verbose', '-v',
        action='store_true',
        help='Show verbose output including failed attempts'
    )
    
    return parser.parse_args()

def main():
    """Main function"""
    args = parse_arguments()
    
    # Use command line arguments or defaults
    enc_password = args.encrypted_password_alt or args.encrypted_password
    des_key_str = args.des_key_alt or args.des_key
    
    print("=== Roundcube Password Decryption Tool ===\n")
    print(f"Encrypted password: {enc_password}")
    print(f"DES key: {des_key_str}")
    print()
    
    # Convert DES key to bytes
    try:
        des_key = des_key_str.encode('utf-8')
    except Exception as e:
        print(f"ERROR: Failed to encode DES key: {e}")
        return
    
    # Test different keys and modes
    keys = [des_key]  # Start with user-provided key
    if des_key_str == "rcmail-!24ByteDESkey*Str":
        # If using default key, also test common variations
        keys.extend(test_common_keys())
    
    modes = [
        ('ECB', 'DES3 ECB mode (most common)'),
        ('CBC', 'DES3 CBC mode with null IV')
    ]
    
    successful_results = []
    
    for key_idx, des_key in enumerate(keys):
        if args.verbose or key_idx == 0:
            print(f"Testing key {key_idx + 1}/{len(keys)}: {des_key.decode('utf-8', errors='replace')}")
        
        for mode, description in modes:
            if args.verbose:
                print(f"  Trying {description}...")
            
            results = decrypt_roundcube_password(enc_password, des_key, mode)
            
            if results:
                for result, analysis in results:
                    if result and is_printable(result) and len(result) > 0:
                        if args.verbose:
                            print(f"    SUCCESS! Decrypted password: {result}")
                            print(f"    Method: {description}")
                            print(f"    Analysis: {analysis}")
                        successful_results.append((result, description, analysis))
                        break
                else:
                    if args.verbose:
                        print(f"    Failed with {description}")
            else:
                if args.verbose:
                    print(f"    Failed with {description}")
        
        if args.verbose:
            print()
    
    if successful_results:
        print("=== SUCCESSFUL DECRYPTIONS ===")
        for result, method, analysis in successful_results:
            print(f"Password: {result}")
            print(f"Method: {method}")
            print(f"Analysis: {analysis}")
            print()
    else:
        print("All decryption methods failed!")
        print("\nPossible reasons:")
        print("1. The encrypted password format is different")
        print("2. The DES key is not in the common list")
        print("3. The encryption mode is different")
        print("4. The password uses a different encoding")
        print("5. The password might be encrypted with additional layers")
    
    # Provide debug information
    print("\nDebug information:")
    try:
        data = b64decode(enc_password)
        print(f"Decoded data length: {len(data)} bytes")
        print(f"Decoded data (hex): {data.hex()}")
        
        # Check if data length is valid for DES3
        if len(data) % 8 != 0:
            print("WARNING: Data length is not a multiple of 8 (DES3 block size)")
        else:
            print(f"Data length is valid for DES3 ({len(data)} bytes = {len(data)//8} blocks)")
            
    except Exception as e:
        print(f"Failed to decode base64: {e}")

if __name__ == "__main__":
    main()
