User Tools

Site Tools


clubs:python_club:python_club_ex_crypt
Home | clubs :: cloud club :: python_club :: 3D-Printing | projects :: Proxmox | Kubernetes | scripting | utilities | games

About the Club

Python Club Topics - Exercise: Encryption & Decryption

Exercise: Encryption and Decryption

  1. Output: The user will request a cyper ( encryption/decryption method ) and input a string of text. For the encryption process, the user will enter the string in plain text. For the decryption process, the user will enter the encrypted text. The program will then supply the resulting string, opposite of what was entered.
  2. What you learn from the example:
    1. Cryptography cyphers ( encryption/decryption method )
    2. Create a choice menu
    3. Error handling
    4. Use a class / object
    5. Input validation

Solution

[1] code:python show
#!/usr/bin/env python3

'''
Encryption and Decryption Tool
===============================

A command-line tool that provides various encryption and decryption methods:
- Caesar cipher (shift cipher)
- Vigenère cipher
- Base64 encoding/decoding
- Fernet symmetric encryption (using cryptography library)

Features:
- Multiple encryption/decryption methods
- Input validation for messages and keys
- Error handling for invalid inputs
- File operations for saving/loading encrypted messages
- Secure key management for Fernet encryption
'''

import os
import sys
import base64
import json
import getpass
from pathlib import Path
from typing import Dict, Any, Tuple, Optional, Union, List

# Try to import cryptography, show helpful error if not installed
try:
    from cryptography.fernet import Fernet
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
    CRYPTOGRAPHY_AVAILABLE = True
except ImportError:
    CRYPTOGRAPHY_AVAILABLE = False


class TextColors:
    '''ANSI color codes for terminal output styling.'''
    HEADER = '\033[95m'
    BLUE = '\033[94m'
    CYAN = '\033[96m'
    GREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'


def clear_screen():
    '''Clear the terminal screen.'''
    os.system('cls' if os.name == 'nt' else 'clear')


def print_header(text: str):
    '''Print a formatted header.'''
    print(f'\n{TextColors.HEADER}{TextColors.BOLD}{text}{TextColors.ENDC}')


def print_success(text: str):
    '''Print a success message.'''
    print(f'{TextColors.GREEN}{text}{TextColors.ENDC}')


def print_warning(text: str):
    '''Print a warning message.'''
    print(f'{TextColors.WARNING}{text}{TextColors.ENDC}')


def print_error(text: str):
    '''Print an error message.'''
    print(f'{TextColors.FAIL}{text}{TextColors.ENDC}')


def input_with_validation(prompt: str, validator=None, error_msg: str = 'Invalid input') → str:
    '''
    Get user input with validation.
    
    Args:
        prompt: The input prompt to display
        validator: A function that returns True if input is valid
        error_msg: Message to display if validation fails
    
    Returns:
        The validated user input
    '''
    while True:
        user_input = input(prompt)
        if validator is None or validator(user_input):
            return user_input
        print_error(error_msg)


def caesar_cipher_encrypt(text: str, shift: int) → str:
    '''
    Encrypt text using Caesar cipher (shift cipher).
    
    Args:
        text: The plaintext to encrypt
        shift: The shift value (how many positions to shift each character)
    
    Returns:
        The encrypted text
    '''
    result = ''
    for char in text:
        if char.isalpha():
            ascii_offset = ord('A') if char.isupper() else ord('a')
            # Convert to 0-25, apply shift, mod 26, convert back to ASCII
            shifted = (ord(char) - ascii_offset + shift) % 26 + ascii_offset
            result += chr(shifted)
        else:
            result += char
    return result


def caesar_cipher_decrypt(text: str, shift: int) → str:
    '''
    Decrypt text using Caesar cipher (shift cipher).
    
    Args:
        text: The encrypted text
        shift: The shift value used for encryption
    
    Returns:
        The decrypted text
    '''
    return caesar_cipher_encrypt(text, -shift)


def vigenere_cipher_encrypt(text: str, key: str) → str:
    '''
    Encrypt text using Vigenère cipher.
    
    Args:
        text: The plaintext to encrypt
        key: The encryption key (a word or phrase)
    
    Returns:
        The encrypted text
    '''
    result = ''
    key = key.upper()
    key_length = len(key)
    key_as_int = [ord(k) - ord('A') for k in key]
    key_index = 0
    
    for char in text:
        if char.isalpha():
            # Convert to 0-25, apply key shift, mod 26, convert back to ASCII
            ascii_offset = ord('A') if char.isupper() else ord('a')
            key_shift = key_as_int[key_index % key_length]
            shifted = (ord(char) - ascii_offset + key_shift) % 26 + ascii_offset
            result += chr(shifted)
            key_index += 1
        else:
            result += char
    
    return result


def vigenere_cipher_decrypt(text: str, key: str) → str:
    '''
    Decrypt text using Vigenère cipher.
    
    Args:
        text: The encrypted text
        key: The encryption key used for encryption
    
    Returns:
        The decrypted text
    '''
    result = ''
    key = key.upper()
    key_length = len(key)
    key_as_int = [ord(k) - ord('A') for k in key]
    key_index = 0
    
    for char in text:
        if char.isalpha():
            # Convert to 0-25, apply negative key shift, mod 26, convert back to ASCII
            ascii_offset = ord('A') if char.isupper() else ord('a')
            key_shift = key_as_int[key_index % key_length]
            shifted = (ord(char) - ascii_offset - key_shift) % 26 + ascii_offset
            result += chr(shifted)
            key_index += 1
        else:
            result += char
    
    return result


def base64_encode(text: str) → str:
    '''
    Encode text using Base64 encoding.
    
    Args:
        text: The plaintext to encode
    
    Returns:
        The Base64 encoded text
    '''
    text_bytes = text.encode('utf-8')
    encoded_bytes = base64.b64encode(text_bytes)
    return encoded_bytes.decode('utf-8')


def base64_decode(encoded_text: str) → str:
    '''
    Decode Base64 encoded text.
    
    Args:
        encoded_text: The Base64 encoded text
    
    Returns:
        The decoded text
    '''
    try:
        encoded_bytes = encoded_text.encode('utf-8')
        decoded_bytes = base64.b64decode(encoded_bytes)
        return decoded_bytes.decode('utf-8')
    except Exception as e:
        raise ValueError(f'Invalid Base64 string: {str(e)}')


def derive_key_from_password(password: str, salt: Optional[bytes] = None) → Tuple[bytes, bytes]:
    '''
    Derive a secure key from a password using PBKDF2.
    
    Args:
        password: The password to derive the key from
        salt: Optional salt for key derivation, generated if not provided
    
    Returns:
        A tuple of (key, salt)
    '''
    if not salt:
        salt = os.urandom(16)
    
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=100000,
    )
    
    key = base64.urlsafe_b64encode(kdf.derive(password.encode('utf-8')))
    return key, salt


def fernet_encrypt(text: str, password: str) → Dict[str, Union[str, bytes]]:
    '''
    Encrypt text using Fernet symmetric encryption.
    
    Args:
        text: The plaintext to encrypt
        password: The password to derive the encryption key from
    
    Returns:
        A dictionary containing the encrypted text and salt
    '''
    # Derive a key from the password
    key, salt = derive_key_from_password(password)
    
    # Create a Fernet cipher with the derived key
    cipher = Fernet(key)
    
    # Encrypt the text
    encrypted_bytes = cipher.encrypt(text.encode('utf-8'))
    encrypted_text = encrypted_bytes.decode('utf-8')
    
    # Return the encrypted text and salt (needed for decryption)
    return {
        'encrypted_text': encrypted_text,
        'salt': salt
    }


def fernet_decrypt(encrypted_data: Dict[str, Union[str, bytes]], password: str) → str:
    '''
    Decrypt text that was encrypted using Fernet.
    
    Args:
        encrypted_data: Dictionary containing the encrypted text and salt
        password: The password used for encryption
    
    Returns:
        The decrypted text
    '''
    encrypted_text = encrypted_data['encrypted_text']
    salt = encrypted_data['salt']
    
    # Derive the same key using the password and stored salt
    key, _ = derive_key_from_password(password, salt)
    
    # Create a Fernet cipher with the derived key
    cipher = Fernet(key)
    
    try:
        # Decrypt the text
        decrypted_bytes = cipher.decrypt(encrypted_text.encode('utf-8'))
        return decrypted_bytes.decode('utf-8')
    except Exception as e:
        raise ValueError(f'Decryption failed: {str(e)}. This could be due to an incorrect password or corrupted data.')


def save_to_file(data: Dict[str, Any], filename: str):
    '''
    Save encrypted data to a file.
    
    Args:
        data: The data to save
        filename: The file to save to
    '''
    # Convert binary salt to hex string for JSON serialization
    if 'salt' in data and isinstance(data['salt'], bytes):
        data = data.copy()  # Create a copy to avoid modifying the original
        data['salt'] = data['salt'].hex()
    
    with open(filename, 'w') as f:
        json.dump(data, f)


def load_from_file(filename: str) → Dict[str, Any]:
    '''
    Load encrypted data from a file.
    
    Args:
        filename: The file to load from
    
    Returns:
        The loaded data
    '''
    with open(filename, 'r') as f:
        data = json.load(f)
    
    # Convert hex string back to bytes
    if 'salt' in data and isinstance(data['salt'], str):
        data['salt'] = bytes.fromhex(data['salt'])
    
    return data


def handle_caesar_cipher():
    '''Handle Caesar cipher encryption/decryption.'''
    print_header('Caesar Cipher (Shift Cipher)')
    print_warning('Note: Caesar cipher is not secure for sensitive information.')
    
    operation = input_with_validation(
        'Choose operation (1=Encrypt, 2=Decrypt): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    text = input('Enter the text: ')
    
    shift = input_with_validation(
        'Enter the shift value (1-25): ',
        lambda x: x.isdigit() and 1 ⇐ int(x) ⇐ 25,
        'Please enter a number between 1 and 25.'
    )
    shift = int(shift)
    
    if operation == '1':
        result = caesar_cipher_encrypt(text, shift)
        print_success(f'\nEncrypted text: {result}')
    else:
        result = caesar_cipher_decrypt(text, shift)
        print_success(f'\nDecrypted text: {result}')
    
    save_option = input_with_validation(
        '\nDo you want to save the result to a file? (y/n): ',
        lambda x: x.lower() in ['y', 'n'],
        'Please enter 'y' or 'n'.'
    )
    
    if save_option.lower() == 'y':
        filename = input('Enter filename to save: ')
        data = {
            'method': 'caesar',
            'result': result,
            'shift': shift,
            'operation': 'encrypt' if operation == '1' else 'decrypt'
        }
        save_to_file(data, filename)
        print_success(f'Result saved to {filename}')


def handle_vigenere_cipher():
    '''Handle Vigenère cipher encryption/decryption.'''
    print_header('Vigenère Cipher')
    print_warning('Note: Vigenère cipher is not secure for sensitive information.')
    
    operation = input_with_validation(
        'Choose operation (1=Encrypt, 2=Decrypt): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    text = input('Enter the text: ')
    
    key = input_with_validation(
        'Enter the key (letters only): ',
        lambda x: x.isalpha() and len(x) > 0,
        'Key must contain only letters and cannot be empty.'
    )
    
    if operation == '1':
        result = vigenere_cipher_encrypt(text, key)
        print_success(f'\nEncrypted text: {result}')
    else:
        result = vigenere_cipher_decrypt(text, key)
        print_success(f'\nDecrypted text: {result}')
    
    save_option = input_with_validation(
        '\nDo you want to save the result to a file? (y/n): ',
        lambda x: x.lower() in ['y', 'n'],
        'Please enter 'y' or 'n'.'
    )
    
    if save_option.lower() == 'y':
        filename = input('Enter filename to save: ')
        data = {
            'method': 'vigenere',
            'result': result,
            'key': key,
            'operation': 'encrypt' if operation == '1' else 'decrypt'
        }
        save_to_file(data, filename)
        print_success(f'Result saved to {filename}')


def handle_base64():
    '''Handle Base64 encoding/decoding.'''
    print_header('Base64 Encoding/Decoding')
    print_warning('Note: Base64 is an encoding, not encryption. It provides no security.')
    
    operation = input_with_validation(
        'Choose operation (1=Encode, 2=Decode): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    if operation == '1':
        text = input('Enter the text to encode: ')
        try:
            result = base64_encode(text)
            print_success(f'\nBase64 encoded: {result}')
        except Exception as e:
            print_error(f'Encoding error: {str(e)}')
            return
    else:
        encoded_text = input('Enter the Base64 text to decode: ')
        try:
            result = base64_decode(encoded_text)
            print_success(f'\nDecoded text: {result}')
        except ValueError as e:
            print_error(f'{str(e)}')
            return
        except Exception as e:
            print_error(f'Decoding error: {str(e)}')
            return
    
    save_option = input_with_validation(
        '\nDo you want to save the result to a file? (y/n): ',
        lambda x: x.lower() in ['y', 'n'],
        'Please enter 'y' or 'n'.'
    )
    
    if save_option.lower() == 'y':
        filename = input('Enter filename to save: ')
        data = {
            'method': 'base64',
            'result': result,
            'operation': 'encode' if operation == '1' else 'decode'
        }
        save_to_file(data, filename)
        print_success(f'Result saved to {filename}')


def handle_fernet():
    '''Handle Fernet encryption/decryption.'''
    if not CRYPTOGRAPHY_AVAILABLE:
        print_error('Fernet encryption requires the 'cryptography' library.')
        print_error('Please install it with: pip install cryptography')
        return
    
    print_header('Fernet Symmetric Encryption')
    print_success('Note: Fernet provides strong encryption suitable for sensitive data.')
    
    operation = input_with_validation(
        'Choose operation (1=Encrypt, 2=Decrypt): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    if operation == '1':
        # Encryption
        text = input('Enter the text to encrypt: ')
        password = getpass.getpass('Enter a strong password: ')
        confirm_password = getpass.getpass('Confirm password: ')
        
        if password != confirm_password:
            print_error('Passwords do not match!')
            return
        
        if len(password) < 8:
            print_warning('Warning: Password is less than 8 characters. This may not be secure.')
            proceed = input_with_validation(
                'Continue anyway? (y/n): ',
                lambda x: x.lower() in ['y', 'n'],
                'Please enter 'y' or 'n'.'
            )
            if proceed.lower() != 'y':
                return
        
        try:
            encrypted_data = fernet_encrypt(text, password)
            print_success('\nText encrypted successfully!')
            
            # Ask to save to file
            save_option = input_with_validation(
                'Do you want to save the encrypted data to a file? (y/n): ',
                lambda x: x.lower() in ['y', 'n'],
                'Please enter 'y' or 'n'.'
            )
            
            if save_option.lower() == 'y':
                filename = input('Enter filename to save: ')
                save_to_file(encrypted_data, filename)
                print_success(f'Encrypted data saved to {filename}')
                print_warning(f'Keep this file and your password safe. You will need both to decrypt.')
        
        except Exception as e:
            print_error(f'Encryption error: {str(e)}')
    
    else:
        # Decryption
        load_option = input_with_validation(
            'Load encrypted data from file? (y/n): ',
            lambda x: x.lower() in ['y', 'n'],
            'Please enter 'y' or 'n'.'
        )
        
        if load_option.lower() == 'y':
            try:
                filename = input('Enter filename to load: ')
                encrypted_data = load_from_file(filename)
                if 'encrypted_text' not in encrypted_data or 'salt' not in encrypted_data:
                    print_error('Invalid file format. File does not contain required encryption data.')
                    return
            except FileNotFoundError:
                print_error(f'File not found: {filename}')
                return
            except json.JSONDecodeError:
                print_error(f'Invalid JSON in file: {filename}')
                return
            except Exception as e:
                print_error(f'Error loading file: {str(e)}')
                return
        else:
            encrypted_text = input('Enter the encrypted text: ')
            salt_hex = input('Enter the salt (hex format): ')
            try:
                salt = bytes.fromhex(salt_hex)
                encrypted_data = {
                    'encrypted_text': encrypted_text,
                    'salt': salt
                }
            except ValueError:
                print_error('Invalid salt format. Must be a hexadecimal string.')
                return
        
        password = getpass.getpass('Enter the password used for encryption: ')
        
        try:
            decrypted_text = fernet_decrypt(encrypted_data, password)
            print_success('\nDecryption successful!')
            print_success(f'Decrypted text: {decrypted_text}')
        
        except ValueError as e:
            print_error(f'{str(e)}')
        except Exception as e:
            print_error(f'Decryption error: {str(e)}')


def load_encrypted_file():
    '''Load an encrypted file and process it.'''
    print_header('Load Encrypted File')
    
    try:
        filename = input('Enter the filename to load: ')
        data = load_from_file(filename)
        
        if 'method' not in data:
            print_error('Invalid file format. Cannot determine encryption method.')
            return
        
        method = data['method']
        print_success(f'File loaded successfully. Method: {method}')
        
        if method == 'caesar':
            shift = data.get('shift')
            if shift is None:
                print_error('Invalid file: missing shift value for Caesar cipher.')
                return
                
            if data.get('operation') == 'encrypt':
                # If it was encrypted, we now decrypt
                text = data.get('result', '')
                result = caesar_cipher_decrypt(text, shift)
                print_success(f'Decrypted text: {result}')
            else:
                # If it was decrypted, we show it
                result = data.get('result', '')
                print_success(f'Decrypted text: {result}')
                
        elif method == 'vigenere':
            key = data.get('key')
            if key is None:
                print_error('Invalid file: missing key for Vigenère cipher.')
                return
                
            if data.get('operation') == 'encrypt':
                # If it was encrypted, we now decrypt
                text = data.get('result', '')
                result = vigenere_cipher_decrypt(text, key)
                print_success(f'Decrypted text: {result}')
            else:
                # If it was decrypted, we show it
                result = data.get('result', '')
                print_success(f'Decrypted text: {result}')
                
        elif method == 'base64':
            result = data.get('result', '')
            operation = data.get('operation', '')
            print_success(f'{'Encoded' if operation == 'encode' else 'Decoded'} text: {result}')
                
        else:
            print_warning('This file needs to be decrypted with the appropriate method.')
            print_warning('Please use the main menu to select the correct decryption option.')
    
    except FileNotFoundError:
        print_error(f'File not found.')
    except json.JSONDecodeError:
        print_error(f'Invalid JSON in file.')
    except Exception as e:
        print_error(f'Error loading file: {str(e)}')


def main_menu():
    '''Display the main menu and handle user choices.'''
    while True:
        clear_screen()
        print_header('Encryption and Decryption Tool')
        print('Select an option:')
        print(f'  1. {TextColors.CYAN}Caesar Cipher{TextColors.ENDC} (Shift Cipher)')
        print(f'  2. {TextColors.CYAN}Vigenère Cipher{TextColors.ENDC}')
        print(f'  3. {TextColors.CYAN}Base64{TextColors.ENDC} Encoding/Decoding')
        print(f'  4. {TextColors.CYAN}Fernet{TextColors.ENDC} Symmetric Encryption')
        print(f'  5. {TextColors.CYAN}Load{TextColors.ENDC} Encrypted File')
        print(f'  6. {TextColors.CYAN}Exit{TextColors.ENDC}')
        
        choice = input_with_validation(
            '\nEnter your choice (1-6): ',
            lambda x: x in ['1', '2', '3', '4', '5', '6'],
            'Please enter a number between 1 and 6.'
        )
        
        if choice == '1':
            handle_caesar_cipher()
        elif choice == '2':
            handle_vigenere_cipher()
        elif choice == '3':
            handle_base64()
        elif choice == '4':
            handle_fernet()
        elif choice == '5':
            load_encrypted_file()
        elif choice == '6':
            clear_screen()
            print_success('Thank you for using the Encryption and Decryption Tool!')
            print_success('Goodbye!')
            sys.exit(0)
        
        input('\nPress Enter to return to the main menu…')


def check_dependencies():
    '''Check if required dependencies are installed.'''
    missing_deps = []
    
    if not CRYPTOGRAPHY_AVAILABLE:
        missing_deps.append('cryptography')
    
    if missing_deps:
        print_warning('Some optional dependencies are missing:')
        for dep in missing_deps:
            print(f'  - {dep}')
        print('\nYou can install them with:')
        print(f'  pip install {' '.join(missing_deps)}')
        print('\nNote: The program will still run, but some features may be unavailable.')
        input('\nPress Enter to continue…')


if __name__ == '__main__':
    try:
        clear_screen()
        print_header('Welcome to the Encryption and Decryption Tool')
        print('This program provides various methods to encrypt and decrypt messages.')
        print('Some methods are for educational purposes only and are not secure.')
        
        check_dependencies()
        
        print('\nReady to begin!')
        input('Press Enter to continue…')
        
        main_menu()
    except KeyboardInterrupt:
        print('\n\nProgram interrupted. Exiting…')
        sys.exit(0)
#!/usr/bin/env python3

'''
Encryption and Decryption Tool
===============================

A command-line tool that provides various encryption and decryption methods:
- Caesar cipher (shift cipher)
- Vigenère cipher
- Base64 encoding/decoding
- Fernet symmetric encryption (using cryptography library)

Features:
- Multiple encryption/decryption methods
- Input validation for messages and keys
- Error handling for invalid inputs
- File operations for saving/loading encrypted messages
- Secure key management for Fernet encryption
'''

import os
import sys
import base64
import json
import getpass
from pathlib import Path
from typing import Dict, Any, Tuple, Optional, Union, List

# Try to import cryptography, show helpful error if not installed
try:
    from cryptography.fernet import Fernet
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
    CRYPTOGRAPHY_AVAILABLE = True
except ImportError:
    CRYPTOGRAPHY_AVAILABLE = False


class TextColors:
    '''ANSI color codes for terminal output styling.'''
    HEADER = '\033[95m'
    BLUE = '\033[94m'
    CYAN = '\033[96m'
    GREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'


def clear_screen():
    '''Clear the terminal screen.'''
    os.system('cls' if os.name == 'nt' else 'clear')


def print_header(text: str):
    '''Print a formatted header.'''
    print(f'\n{TextColors.HEADER}{TextColors.BOLD}{text}{TextColors.ENDC}')


def print_success(text: str):
    '''Print a success message.'''
    print(f'{TextColors.GREEN}{text}{TextColors.ENDC}')


def print_warning(text: str):
    '''Print a warning message.'''
    print(f'{TextColors.WARNING}{text}{TextColors.ENDC}')


def print_error(text: str):
    '''Print an error message.'''
    print(f'{TextColors.FAIL}{text}{TextColors.ENDC}')


def input_with_validation(prompt: str, validator=None, error_msg: str = 'Invalid input') → str:
    '''
    Get user input with validation.
    
    Args:
        prompt: The input prompt to display
        validator: A function that returns True if input is valid
        error_msg: Message to display if validation fails
    
    Returns:
        The validated user input
    '''
    while True:
        user_input = input(prompt)
        if validator is None or validator(user_input):
            return user_input
        print_error(error_msg)


def caesar_cipher_encrypt(text: str, shift: int) → str:
    '''
    Encrypt text using Caesar cipher (shift cipher).
    
    Args:
        text: The plaintext to encrypt
        shift: The shift value (how many positions to shift each character)
    
    Returns:
        The encrypted text
    '''
    result = ''
    for char in text:
        if char.isalpha():
            ascii_offset = ord('A') if char.isupper() else ord('a')
            # Convert to 0-25, apply shift, mod 26, convert back to ASCII
            shifted = (ord(char) - ascii_offset + shift) % 26 + ascii_offset
            result += chr(shifted)
        else:
            result += char
    return result


def caesar_cipher_decrypt(text: str, shift: int) → str:
    '''
    Decrypt text using Caesar cipher (shift cipher).
    
    Args:
        text: The encrypted text
        shift: The shift value used for encryption
    
    Returns:
        The decrypted text
    '''
    return caesar_cipher_encrypt(text, -shift)


def vigenere_cipher_encrypt(text: str, key: str) → str:
    '''
    Encrypt text using Vigenère cipher.
    
    Args:
        text: The plaintext to encrypt
        key: The encryption key (a word or phrase)
    
    Returns:
        The encrypted text
    '''
    result = ''
    key = key.upper()
    key_length = len(key)
    key_as_int = [ord(k) - ord('A') for k in key]
    key_index = 0
    
    for char in text:
        if char.isalpha():
            # Convert to 0-25, apply key shift, mod 26, convert back to ASCII
            ascii_offset = ord('A') if char.isupper() else ord('a')
            key_shift = key_as_int[key_index % key_length]
            shifted = (ord(char) - ascii_offset + key_shift) % 26 + ascii_offset
            result += chr(shifted)
            key_index += 1
        else:
            result += char
    
    return result


def vigenere_cipher_decrypt(text: str, key: str) → str:
    '''
    Decrypt text using Vigenère cipher.
    
    Args:
        text: The encrypted text
        key: The encryption key used for encryption
    
    Returns:
        The decrypted text
    '''
    result = ''
    key = key.upper()
    key_length = len(key)
    key_as_int = [ord(k) - ord('A') for k in key]
    key_index = 0
    
    for char in text:
        if char.isalpha():
            # Convert to 0-25, apply negative key shift, mod 26, convert back to ASCII
            ascii_offset = ord('A') if char.isupper() else ord('a')
            key_shift = key_as_int[key_index % key_length]
            shifted = (ord(char) - ascii_offset - key_shift) % 26 + ascii_offset
            result += chr(shifted)
            key_index += 1
        else:
            result += char
    
    return result


def base64_encode(text: str) → str:
    '''
    Encode text using Base64 encoding.
    
    Args:
        text: The plaintext to encode
    
    Returns:
        The Base64 encoded text
    '''
    text_bytes = text.encode('utf-8')
    encoded_bytes = base64.b64encode(text_bytes)
    return encoded_bytes.decode('utf-8')


def base64_decode(encoded_text: str) → str:
    '''
    Decode Base64 encoded text.
    
    Args:
        encoded_text: The Base64 encoded text
    
    Returns:
        The decoded text
    '''
    try:
        encoded_bytes = encoded_text.encode('utf-8')
        decoded_bytes = base64.b64decode(encoded_bytes)
        return decoded_bytes.decode('utf-8')
    except Exception as e:
        raise ValueError(f'Invalid Base64 string: {str(e)}')


def derive_key_from_password(password: str, salt: Optional[bytes] = None) → Tuple[bytes, bytes]:
    '''
    Derive a secure key from a password using PBKDF2.
    
    Args:
        password: The password to derive the key from
        salt: Optional salt for key derivation, generated if not provided
    
    Returns:
        A tuple of (key, salt)
    '''
    if not salt:
        salt = os.urandom(16)
    
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=100000,
    )
    
    key = base64.urlsafe_b64encode(kdf.derive(password.encode('utf-8')))
    return key, salt


def fernet_encrypt(text: str, password: str) → Dict[str, Union[str, bytes]]:
    '''
    Encrypt text using Fernet symmetric encryption.
    
    Args:
        text: The plaintext to encrypt
        password: The password to derive the encryption key from
    
    Returns:
        A dictionary containing the encrypted text and salt
    '''
    # Derive a key from the password
    key, salt = derive_key_from_password(password)
    
    # Create a Fernet cipher with the derived key
    cipher = Fernet(key)
    
    # Encrypt the text
    encrypted_bytes = cipher.encrypt(text.encode('utf-8'))
    encrypted_text = encrypted_bytes.decode('utf-8')
    
    # Return the encrypted text and salt (needed for decryption)
    return {
        'encrypted_text': encrypted_text,
        'salt': salt
    }


def fernet_decrypt(encrypted_data: Dict[str, Union[str, bytes]], password: str) → str:
    '''
    Decrypt text that was encrypted using Fernet.
    
    Args:
        encrypted_data: Dictionary containing the encrypted text and salt
        password: The password used for encryption
    
    Returns:
        The decrypted text
    '''
    encrypted_text = encrypted_data['encrypted_text']
    salt = encrypted_data['salt']
    
    # Derive the same key using the password and stored salt
    key, _ = derive_key_from_password(password, salt)
    
    # Create a Fernet cipher with the derived key
    cipher = Fernet(key)
    
    try:
        # Decrypt the text
        decrypted_bytes = cipher.decrypt(encrypted_text.encode('utf-8'))
        return decrypted_bytes.decode('utf-8')
    except Exception as e:
        raise ValueError(f'Decryption failed: {str(e)}. This could be due to an incorrect password or corrupted data.')


def save_to_file(data: Dict[str, Any], filename: str):
    '''
    Save encrypted data to a file.
    
    Args:
        data: The data to save
        filename: The file to save to
    '''
    # Convert binary salt to hex string for JSON serialization
    if 'salt' in data and isinstance(data['salt'], bytes):
        data = data.copy()  # Create a copy to avoid modifying the original
        data['salt'] = data['salt'].hex()
    
    with open(filename, 'w') as f:
        json.dump(data, f)


def load_from_file(filename: str) → Dict[str, Any]:
    '''
    Load encrypted data from a file.
    
    Args:
        filename: The file to load from
    
    Returns:
        The loaded data
    '''
    with open(filename, 'r') as f:
        data = json.load(f)
    
    # Convert hex string back to bytes
    if 'salt' in data and isinstance(data['salt'], str):
        data['salt'] = bytes.fromhex(data['salt'])
    
    return data


def handle_caesar_cipher():
    '''Handle Caesar cipher encryption/decryption.'''
    print_header('Caesar Cipher (Shift Cipher)')
    print_warning('Note: Caesar cipher is not secure for sensitive information.')
    
    operation = input_with_validation(
        'Choose operation (1=Encrypt, 2=Decrypt): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    text = input('Enter the text: ')
    
    shift = input_with_validation(
        'Enter the shift value (1-25): ',
        lambda x: x.isdigit() and 1 ⇐ int(x) ⇐ 25,
        'Please enter a number between 1 and 25.'
    )
    shift = int(shift)
    
    if operation == '1':
        result = caesar_cipher_encrypt(text, shift)
        print_success(f'\nEncrypted text: {result}')
    else:
        result = caesar_cipher_decrypt(text, shift)
        print_success(f'\nDecrypted text: {result}')
    
    save_option = input_with_validation(
        '\nDo you want to save the result to a file? (y/n): ',
        lambda x: x.lower() in ['y', 'n'],
        'Please enter 'y' or 'n'.'
    )
    
    if save_option.lower() == 'y':
        filename = input('Enter filename to save: ')
        data = {
            'method': 'caesar',
            'result': result,
            'shift': shift,
            'operation': 'encrypt' if operation == '1' else 'decrypt'
        }
        save_to_file(data, filename)
        print_success(f'Result saved to {filename}')


def handle_vigenere_cipher():
    '''Handle Vigenère cipher encryption/decryption.'''
    print_header('Vigenère Cipher')
    print_warning('Note: Vigenère cipher is not secure for sensitive information.')
    
    operation = input_with_validation(
        'Choose operation (1=Encrypt, 2=Decrypt): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    text = input('Enter the text: ')
    
    key = input_with_validation(
        'Enter the key (letters only): ',
        lambda x: x.isalpha() and len(x) > 0,
        'Key must contain only letters and cannot be empty.'
    )
    
    if operation == '1':
        result = vigenere_cipher_encrypt(text, key)
        print_success(f'\nEncrypted text: {result}')
    else:
        result = vigenere_cipher_decrypt(text, key)
        print_success(f'\nDecrypted text: {result}')
    
    save_option = input_with_validation(
        '\nDo you want to save the result to a file? (y/n): ',
        lambda x: x.lower() in ['y', 'n'],
        'Please enter 'y' or 'n'.'
    )
    
    if save_option.lower() == 'y':
        filename = input('Enter filename to save: ')
        data = {
            'method': 'vigenere',
            'result': result,
            'key': key,
            'operation': 'encrypt' if operation == '1' else 'decrypt'
        }
        save_to_file(data, filename)
        print_success(f'Result saved to {filename}')


def handle_base64():
    '''Handle Base64 encoding/decoding.'''
    print_header('Base64 Encoding/Decoding')
    print_warning('Note: Base64 is an encoding, not encryption. It provides no security.')
    
    operation = input_with_validation(
        'Choose operation (1=Encode, 2=Decode): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    if operation == '1':
        text = input('Enter the text to encode: ')
        try:
            result = base64_encode(text)
            print_success(f'\nBase64 encoded: {result}')
        except Exception as e:
            print_error(f'Encoding error: {str(e)}')
            return
    else:
        encoded_text = input('Enter the Base64 text to decode: ')
        try:
            result = base64_decode(encoded_text)
            print_success(f'\nDecoded text: {result}')
        except ValueError as e:
            print_error(f'{str(e)}')
            return
        except Exception as e:
            print_error(f'Decoding error: {str(e)}')
            return
    
    save_option = input_with_validation(
        '\nDo you want to save the result to a file? (y/n): ',
        lambda x: x.lower() in ['y', 'n'],
        'Please enter 'y' or 'n'.'
    )
    
    if save_option.lower() == 'y':
        filename = input('Enter filename to save: ')
        data = {
            'method': 'base64',
            'result': result,
            'operation': 'encode' if operation == '1' else 'decode'
        }
        save_to_file(data, filename)
        print_success(f'Result saved to {filename}')


def handle_fernet():
    '''Handle Fernet encryption/decryption.'''
    if not CRYPTOGRAPHY_AVAILABLE:
        print_error('Fernet encryption requires the 'cryptography' library.')
        print_error('Please install it with: pip install cryptography')
        return
    
    print_header('Fernet Symmetric Encryption')
    print_success('Note: Fernet provides strong encryption suitable for sensitive data.')
    
    operation = input_with_validation(
        'Choose operation (1=Encrypt, 2=Decrypt): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    if operation == '1':
        # Encryption
        text = input('Enter the text to encrypt: ')
        password = getpass.getpass('Enter a strong password: ')
        confirm_password = getpass.getpass('Confirm password: ')
        
        if password != confirm_password:
            print_error('Passwords do not match!')
            return
        
        if len(password) < 8:
            print_warning('Warning: Password is less than 8 characters. This may not be secure.')
            proceed = input_with_validation(
                'Continue anyway? (y/n): ',
                lambda x: x.lower() in ['y', 'n'],
                'Please enter 'y' or 'n'.'
            )
            if proceed.lower() != 'y':
                return
        
        try:
            encrypted_data = fernet_encrypt(text, password)
            print_success('\nText encrypted successfully!')
            
            # Ask to save to file
            save_option = input_with_validation(
                'Do you want to save the encrypted data to a file? (y/n): ',
                lambda x: x.lower() in ['y', 'n'],
                'Please enter 'y' or 'n'.'
            )
            
            if save_option.lower() == 'y':
                filename = input('Enter filename to save: ')
                save_to_file(encrypted_data, filename)
                print_success(f'Encrypted data saved to {filename}')
                print_warning(f'Keep this file and your password safe. You will need both to decrypt.')
        
        except Exception as e:
            print_error(f'Encryption error: {str(e)}')
    
    else:
        # Decryption
        load_option = input_with_validation(
            'Load encrypted data from file? (y/n): ',
            lambda x: x.lower() in ['y', 'n'],
            'Please enter 'y' or 'n'.'
        )
        
        if load_option.lower() == 'y':
            try:
                filename = input('Enter filename to load: ')
                encrypted_data = load_from_file(filename)
                if 'encrypted_text' not in encrypted_data or 'salt' not in encrypted_data:
                    print_error('Invalid file format. File does not contain required encryption data.')
                    return
            except FileNotFoundError:
                print_error(f'File not found: {filename}')
                return
            except json.JSONDecodeError:
                print_error(f'Invalid JSON in file: {filename}')
                return
            except Exception as e:
                print_error(f'Error loading file: {str(e)}')
                return
        else:
            encrypted_text = input('Enter the encrypted text: ')
            salt_hex = input('Enter the salt (hex format): ')
            try:
                salt = bytes.fromhex(salt_hex)
                encrypted_data = {
                    'encrypted_text': encrypted_text,
                    'salt': salt
                }
            except ValueError:
                print_error('Invalid salt format. Must be a hexadecimal string.')
                return
        
        password = getpass.getpass('Enter the password used for encryption: ')
        
        try:
            decrypted_text = fernet_decrypt(encrypted_data, password)
            print_success('\nDecryption successful!')
            print_success(f'Decrypted text: {decrypted_text}')
        
        except ValueError as e:
            print_error(f'{str(e)}')
        except Exception as e:
            print_error(f'Decryption error: {str(e)}')


def load_encrypted_file():
    '''Load an encrypted file and process it.'''
    print_header('Load Encrypted File')
    
    try:
        filename = input('Enter the filename to load: ')
        data = load_from_file(filename)
        
        if 'method' not in data:
            print_error('Invalid file format. Cannot determine encryption method.')
            return
        
        method = data['method']
        print_success(f'File loaded successfully. Method: {method}')
        
        if method == 'caesar':
            shift = data.get('shift')
            if shift is None:
                print_error('Invalid file: missing shift value for Caesar cipher.')
                return
                
            if data.get('operation') == 'encrypt':
                # If it was encrypted, we now decrypt
                text = data.get('result', '')
                result = caesar_cipher_decrypt(text, shift)
                print_success(f'Decrypted text: {result}')
            else:
                # If it was decrypted, we show it
                result = data.get('result', '')
                print_success(f'Decrypted text: {result}')
                
        elif method == 'vigenere':
            key = data.get('key')
            if key is None:
                print_error('Invalid file: missing key for Vigenère cipher.')
                return
                
            if data.get('operation') == 'encrypt':
                # If it was encrypted, we now decrypt
                text = data.get('result', '')
                result = vigenere_cipher_decrypt(text, key)
                print_success(f'Decrypted text: {result}')
            else:
                # If it was decrypted, we show it
                result = data.get('result', '')
                print_success(f'Decrypted text: {result}')
                
        elif method == 'base64':
            result = data.get('result', '')
            operation = data.get('operation', '')
            print_success(f'{'Encoded' if operation == 'encode' else 'Decoded'} text: {result}')
                
        else:
            print_warning('This file needs to be decrypted with the appropriate method.')
            print_warning('Please use the main menu to select the correct decryption option.')
    
    except FileNotFoundError:
        print_error(f'File not found.')
    except json.JSONDecodeError:
        print_error(f'Invalid JSON in file.')
    except Exception as e:
        print_error(f'Error loading file: {str(e)}')


def main_menu():
    '''Display the main menu and handle user choices.'''
    while True:
        clear_screen()
        print_header('Encryption and Decryption Tool')
        print('Select an option:')
        print(f'  1. {TextColors.CYAN}Caesar Cipher{TextColors.ENDC} (Shift Cipher)')
        print(f'  2. {TextColors.CYAN}Vigenère Cipher{TextColors.ENDC}')
        print(f'  3. {TextColors.CYAN}Base64{TextColors.ENDC} Encoding/Decoding')
        print(f'  4. {TextColors.CYAN}Fernet{TextColors.ENDC} Symmetric Encryption')
        print(f'  5. {TextColors.CYAN}Load{TextColors.ENDC} Encrypted File')
        print(f'  6. {TextColors.CYAN}Exit{TextColors.ENDC}')
        
        choice = input_with_validation(
            '\nEnter your choice (1-6): ',
            lambda x: x in ['1', '2', '3', '4', '5', '6'],
            'Please enter a number between 1 and 6.'
        )
        
        if choice == '1':
            handle_caesar_cipher()
        elif choice == '2':
            handle_vigenere_cipher()
        elif choice == '3':
            handle_base64()
        elif choice == '4':
            handle_fernet()
        elif choice == '5':
            load_encrypted_file()
        elif choice == '6':
            clear_screen()
            print_success('Thank you for using the Encryption and Decryption Tool!')
            print_success('Goodbye!')
            sys.exit(0)
        
        input('\nPress Enter to return to the main menu…')


def check_dependencies():
    '''Check if required dependencies are installed.'''
    missing_deps = []
    
    if not CRYPTOGRAPHY_AVAILABLE:
        missing_deps.append('cryptography')
    
    if missing_deps:
        print_warning('Some optional dependencies are missing:')
        for dep in missing_deps:
            print(f'  - {dep}')
        print('\nYou can install them with:')
        print(f'  pip install {' '.join(missing_deps)}')
        print('\nNote: The program will still run, but some features may be unavailable.')
        input('\nPress Enter to continue…')


if __name__ == '__main__':
    try:
        clear_screen()
        print_header('Welcome to the Encryption and Decryption Tool')
        print('This program provides various methods to encrypt and decrypt messages.')
        print('Some methods are for educational purposes only and are not secure.')
        
        check_dependencies()
        
        print('\nReady to begin!')
        input('Press Enter to continue…')
        
        main_menu()
    except KeyboardInterrupt:
        print('\n\nProgram interrupted. Exiting…')
        sys.exit(0)
#!/usr/bin/env python3

'''
Encryption and Decryption Tool
===============================

A command-line tool that provides various encryption and decryption methods:
- Caesar cipher (shift cipher)
- Vigenère cipher
- Base64 encoding/decoding
- Fernet symmetric encryption (using cryptography library)

Features:
- Multiple encryption/decryption methods
- Input validation for messages and keys
- Error handling for invalid inputs
- File operations for saving/loading encrypted messages
- Secure key management for Fernet encryption
'''

import os
import sys
import base64
import json
import getpass
from pathlib import Path
from typing import Dict, Any, Tuple, Optional, Union, List

# Try to import cryptography, show helpful error if not installed
try:
    from cryptography.fernet import Fernet
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
    CRYPTOGRAPHY_AVAILABLE = True
except ImportError:
    CRYPTOGRAPHY_AVAILABLE = False


class TextColors:
    '''ANSI color codes for terminal output styling.'''
    HEADER = '\033[95m'
    BLUE = '\033[94m'
    CYAN = '\033[96m'
    GREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'


def clear_screen():
    '''Clear the terminal screen.'''
    os.system('cls' if os.name == 'nt' else 'clear')


def print_header(text: str):
    '''Print a formatted header.'''
    print(f'\n{TextColors.HEADER}{TextColors.BOLD}{text}{TextColors.ENDC}')


def print_success(text: str):
    '''Print a success message.'''
    print(f'{TextColors.GREEN}{text}{TextColors.ENDC}')


def print_warning(text: str):
    '''Print a warning message.'''
    print(f'{TextColors.WARNING}{text}{TextColors.ENDC}')


def print_error(text: str):
    '''Print an error message.'''
    print(f'{TextColors.FAIL}{text}{TextColors.ENDC}')


def input_with_validation(prompt: str, validator=None, error_msg: str = 'Invalid input') → str:
    '''
    Get user input with validation.
    
    Args:
        prompt: The input prompt to display
        validator: A function that returns True if input is valid
        error_msg: Message to display if validation fails
    
    Returns:
        The validated user input
    '''
    while True:
        user_input = input(prompt)
        if validator is None or validator(user_input):
            return user_input
        print_error(error_msg)


def caesar_cipher_encrypt(text: str, shift: int) → str:
    '''
    Encrypt text using Caesar cipher (shift cipher).
    
    Args:
        text: The plaintext to encrypt
        shift: The shift value (how many positions to shift each character)
    
    Returns:
        The encrypted text
    '''
    result = ''
    for char in text:
        if char.isalpha():
            ascii_offset = ord('A') if char.isupper() else ord('a')
            # Convert to 0-25, apply shift, mod 26, convert back to ASCII
            shifted = (ord(char) - ascii_offset + shift) % 26 + ascii_offset
            result += chr(shifted)
        else:
            result += char
    return result


def caesar_cipher_decrypt(text: str, shift: int) → str:
    '''
    Decrypt text using Caesar cipher (shift cipher).
    
    Args:
        text: The encrypted text
        shift: The shift value used for encryption
    
    Returns:
        The decrypted text
    '''
    return caesar_cipher_encrypt(text, -shift)


def vigenere_cipher_encrypt(text: str, key: str) → str:
    '''
    Encrypt text using Vigenère cipher.
    
    Args:
        text: The plaintext to encrypt
        key: The encryption key (a word or phrase)
    
    Returns:
        The encrypted text
    '''
    result = ''
    key = key.upper()
    key_length = len(key)
    key_as_int = [ord(k) - ord('A') for k in key]
    key_index = 0
    
    for char in text:
        if char.isalpha():
            # Convert to 0-25, apply key shift, mod 26, convert back to ASCII
            ascii_offset = ord('A') if char.isupper() else ord('a')
            key_shift = key_as_int[key_index % key_length]
            shifted = (ord(char) - ascii_offset + key_shift) % 26 + ascii_offset
            result += chr(shifted)
            key_index += 1
        else:
            result += char
    
    return result


def vigenere_cipher_decrypt(text: str, key: str) → str:
    '''
    Decrypt text using Vigenère cipher.
    
    Args:
        text: The encrypted text
        key: The encryption key used for encryption
    
    Returns:
        The decrypted text
    '''
    result = ''
    key = key.upper()
    key_length = len(key)
    key_as_int = [ord(k) - ord('A') for k in key]
    key_index = 0
    
    for char in text:
        if char.isalpha():
            # Convert to 0-25, apply negative key shift, mod 26, convert back to ASCII
            ascii_offset = ord('A') if char.isupper() else ord('a')
            key_shift = key_as_int[key_index % key_length]
            shifted = (ord(char) - ascii_offset - key_shift) % 26 + ascii_offset
            result += chr(shifted)
            key_index += 1
        else:
            result += char
    
    return result


def base64_encode(text: str) → str:
    '''
    Encode text using Base64 encoding.
    
    Args:
        text: The plaintext to encode
    
    Returns:
        The Base64 encoded text
    '''
    text_bytes = text.encode('utf-8')
    encoded_bytes = base64.b64encode(text_bytes)
    return encoded_bytes.decode('utf-8')


def base64_decode(encoded_text: str) → str:
    '''
    Decode Base64 encoded text.
    
    Args:
        encoded_text: The Base64 encoded text
    
    Returns:
        The decoded text
    '''
    try:
        encoded_bytes = encoded_text.encode('utf-8')
        decoded_bytes = base64.b64decode(encoded_bytes)
        return decoded_bytes.decode('utf-8')
    except Exception as e:
        raise ValueError(f'Invalid Base64 string: {str(e)}')


def derive_key_from_password(password: str, salt: Optional[bytes] = None) → Tuple[bytes, bytes]:
    '''
    Derive a secure key from a password using PBKDF2.
    
    Args:
        password: The password to derive the key from
        salt: Optional salt for key derivation, generated if not provided
    
    Returns:
        A tuple of (key, salt)
    '''
    if not salt:
        salt = os.urandom(16)
    
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=100000,
    )
    
    key = base64.urlsafe_b64encode(kdf.derive(password.encode('utf-8')))
    return key, salt


def fernet_encrypt(text: str, password: str) → Dict[str, Union[str, bytes]]:
    '''
    Encrypt text using Fernet symmetric encryption.
    
    Args:
        text: The plaintext to encrypt
        password: The password to derive the encryption key from
    
    Returns:
        A dictionary containing the encrypted text and salt
    '''
    # Derive a key from the password
    key, salt = derive_key_from_password(password)
    
    # Create a Fernet cipher with the derived key
    cipher = Fernet(key)
    
    # Encrypt the text
    encrypted_bytes = cipher.encrypt(text.encode('utf-8'))
    encrypted_text = encrypted_bytes.decode('utf-8')
    
    # Return the encrypted text and salt (needed for decryption)
    return {
        'encrypted_text': encrypted_text,
        'salt': salt
    }


def fernet_decrypt(encrypted_data: Dict[str, Union[str, bytes]], password: str) → str:
    '''
    Decrypt text that was encrypted using Fernet.
    
    Args:
        encrypted_data: Dictionary containing the encrypted text and salt
        password: The password used for encryption
    
    Returns:
        The decrypted text
    '''
    encrypted_text = encrypted_data['encrypted_text']
    salt = encrypted_data['salt']
    
    # Derive the same key using the password and stored salt
    key, _ = derive_key_from_password(password, salt)
    
    # Create a Fernet cipher with the derived key
    cipher = Fernet(key)
    
    try:
        # Decrypt the text
        decrypted_bytes = cipher.decrypt(encrypted_text.encode('utf-8'))
        return decrypted_bytes.decode('utf-8')
    except Exception as e:
        raise ValueError(f'Decryption failed: {str(e)}. This could be due to an incorrect password or corrupted data.')


def save_to_file(data: Dict[str, Any], filename: str):
    '''
    Save encrypted data to a file.
    
    Args:
        data: The data to save
        filename: The file to save to
    '''
    # Convert binary salt to hex string for JSON serialization
    if 'salt' in data and isinstance(data['salt'], bytes):
        data = data.copy()  # Create a copy to avoid modifying the original
        data['salt'] = data['salt'].hex()
    
    with open(filename, 'w') as f:
        json.dump(data, f)


def load_from_file(filename: str) → Dict[str, Any]:
    '''
    Load encrypted data from a file.
    
    Args:
        filename: The file to load from
    
    Returns:
        The loaded data
    '''
    with open(filename, 'r') as f:
        data = json.load(f)
    
    # Convert hex string back to bytes
    if 'salt' in data and isinstance(data['salt'], str):
        data['salt'] = bytes.fromhex(data['salt'])
    
    return data


def handle_caesar_cipher():
    '''Handle Caesar cipher encryption/decryption.'''
    print_header('Caesar Cipher (Shift Cipher)')
    print_warning('Note: Caesar cipher is not secure for sensitive information.')
    
    operation = input_with_validation(
        'Choose operation (1=Encrypt, 2=Decrypt): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    text = input('Enter the text: ')
    
    shift = input_with_validation(
        'Enter the shift value (1-25): ',
        lambda x: x.isdigit() and 1 ⇐ int(x) ⇐ 25,
        'Please enter a number between 1 and 25.'
    )
    shift = int(shift)
    
    if operation == '1':
        result = caesar_cipher_encrypt(text, shift)
        print_success(f'\nEncrypted text: {result}')
    else:
        result = caesar_cipher_decrypt(text, shift)
        print_success(f'\nDecrypted text: {result}')
    
    save_option = input_with_validation(
        '\nDo you want to save the result to a file? (y/n): ',
        lambda x: x.lower() in ['y', 'n'],
        'Please enter 'y' or 'n'.'
    )
    
    if save_option.lower() == 'y':
        filename = input('Enter filename to save: ')
        data = {
            'method': 'caesar',
            'result': result,
            'shift': shift,
            'operation': 'encrypt' if operation == '1' else 'decrypt'
        }
        save_to_file(data, filename)
        print_success(f'Result saved to {filename}')


def handle_vigenere_cipher():
    '''Handle Vigenère cipher encryption/decryption.'''
    print_header('Vigenère Cipher')
    print_warning('Note: Vigenère cipher is not secure for sensitive information.')
    
    operation = input_with_validation(
        'Choose operation (1=Encrypt, 2=Decrypt): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    text = input('Enter the text: ')
    
    key = input_with_validation(
        'Enter the key (letters only): ',
        lambda x: x.isalpha() and len(x) > 0,
        'Key must contain only letters and cannot be empty.'
    )
    
    if operation == '1':
        result = vigenere_cipher_encrypt(text, key)
        print_success(f'\nEncrypted text: {result}')
    else:
        result = vigenere_cipher_decrypt(text, key)
        print_success(f'\nDecrypted text: {result}')
    
    save_option = input_with_validation(
        '\nDo you want to save the result to a file? (y/n): ',
        lambda x: x.lower() in ['y', 'n'],
        'Please enter 'y' or 'n'.'
    )
    
    if save_option.lower() == 'y':
        filename = input('Enter filename to save: ')
        data = {
            'method': 'vigenere',
            'result': result,
            'key': key,
            'operation': 'encrypt' if operation == '1' else 'decrypt'
        }
        save_to_file(data, filename)
        print_success(f'Result saved to {filename}')


def handle_base64():
    '''Handle Base64 encoding/decoding.'''
    print_header('Base64 Encoding/Decoding')
    print_warning('Note: Base64 is an encoding, not encryption. It provides no security.')
    
    operation = input_with_validation(
        'Choose operation (1=Encode, 2=Decode): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    if operation == '1':
        text = input('Enter the text to encode: ')
        try:
            result = base64_encode(text)
            print_success(f'\nBase64 encoded: {result}')
        except Exception as e:
            print_error(f'Encoding error: {str(e)}')
            return
    else:
        encoded_text = input('Enter the Base64 text to decode: ')
        try:
            result = base64_decode(encoded_text)
            print_success(f'\nDecoded text: {result}')
        except ValueError as e:
            print_error(f'{str(e)}')
            return
        except Exception as e:
            print_error(f'Decoding error: {str(e)}')
            return
    
    save_option = input_with_validation(
        '\nDo you want to save the result to a file? (y/n): ',
        lambda x: x.lower() in ['y', 'n'],
        'Please enter 'y' or 'n'.'
    )
    
    if save_option.lower() == 'y':
        filename = input('Enter filename to save: ')
        data = {
            'method': 'base64',
            'result': result,
            'operation': 'encode' if operation == '1' else 'decode'
        }
        save_to_file(data, filename)
        print_success(f'Result saved to {filename}')


def handle_fernet():
    '''Handle Fernet encryption/decryption.'''
    if not CRYPTOGRAPHY_AVAILABLE:
        print_error('Fernet encryption requires the 'cryptography' library.')
        print_error('Please install it with: pip install cryptography')
        return
    
    print_header('Fernet Symmetric Encryption')
    print_success('Note: Fernet provides strong encryption suitable for sensitive data.')
    
    operation = input_with_validation(
        'Choose operation (1=Encrypt, 2=Decrypt): ',
        lambda x: x in ['1', '2'],
        'Please enter 1 or 2.'
    )
    
    if operation == '1':
        # Encryption
        text = input('Enter the text to encrypt: ')
        password = getpass.getpass('Enter a strong password: ')
        confirm_password = getpass.getpass('Confirm password: ')
        
        if password != confirm_password:
            print_error('Passwords do not match!')
            return
        
        if len(password) < 8:
            print_warning('Warning: Password is less than 8 characters. This may not be secure.')
            proceed = input_with_validation(
                'Continue anyway? (y/n): ',
                lambda x: x.lower() in ['y', 'n'],
                'Please enter 'y' or 'n'.'
            )
            if proceed.lower() != 'y':
                return
        
        try:
            encrypted_data = fernet_encrypt(text, password)
            print_success('\nText encrypted successfully!')
            
            # Ask to save to file
            save_option = input_with_validation(
                'Do you want to save the encrypted data to a file? (y/n): ',
                lambda x: x.lower() in ['y', 'n'],
                'Please enter 'y' or 'n'.'
            )
            
            if save_option.lower() == 'y':
                filename = input('Enter filename to save: ')
                save_to_file(encrypted_data, filename)
                print_success(f'Encrypted data saved to {filename}')
                print_warning(f'Keep this file and your password safe. You will need both to decrypt.')
        
        except Exception as e:
            print_error(f'Encryption error: {str(e)}')
    
    else:
        # Decryption
        load_option = input_with_validation(
            'Load encrypted data from file? (y/n): ',
            lambda x: x.lower() in ['y', 'n'],
            'Please enter 'y' or 'n'.'
        )
        
        if load_option.lower() == 'y':
            try:
                filename = input('Enter filename to load: ')
                encrypted_data = load_from_file(filename)
                if 'encrypted_text' not in encrypted_data or 'salt' not in encrypted_data:
                    print_error('Invalid file format. File does not contain required encryption data.')
                    return
            except FileNotFoundError:
                print_error(f'File not found: {filename}')
                return
            except json.JSONDecodeError:
                print_error(f'Invalid JSON in file: {filename}')
                return
            except Exception as e:
                print_error(f'Error loading file: {str(e)}')
                return
        else:
            encrypted_text = input('Enter the encrypted text: ')
            salt_hex = input('Enter the salt (hex format): ')
            try:
                salt = bytes.fromhex(salt_hex)
                encrypted_data = {
                    'encrypted_text': encrypted_text,
                    'salt': salt
                }
            except ValueError:
                print_error('Invalid salt format. Must be a hexadecimal string.')
                return
        
        password = getpass.getpass('Enter the password used for encryption: ')
        
        try:
            decrypted_text = fernet_decrypt(encrypted_data, password)
            print_success('\nDecryption successful!')
            print_success(f'Decrypted text: {decrypted_text}')
        
        except ValueError as e:
            print_error(f'{str(e)}')
        except Exception as e:
            print_error(f'Decryption error: {str(e)}')


def load_encrypted_file():
    '''Load an encrypted file and process it.'''
    print_header('Load Encrypted File')
    
    try:
        filename = input('Enter the filename to load: ')
        data = load_from_file(filename)
        
        if 'method' not in data:
            print_error('Invalid file format. Cannot determine encryption method.')
            return
        
        method = data['method']
        print_success(f'File loaded successfully. Method: {method}')
        
        if method == 'caesar':
            shift = data.get('shift')
            if shift is None:
                print_error('Invalid file: missing shift value for Caesar cipher.')
                return
                
            if data.get('operation') == 'encrypt':
                # If it was encrypted, we now decrypt
                text = data.get('result', '')
                result = caesar_cipher_decrypt(text, shift)
                print_success(f'Decrypted text: {result}')
            else:
                # If it was decrypted, we show it
                result = data.get('result', '')
                print_success(f'Decrypted text: {result}')
                
        elif method == 'vigenere':
            key = data.get('key')
            if key is None:
                print_error('Invalid file: missing key for Vigenère cipher.')
                return
                
            if data.get('operation') == 'encrypt':
                # If it was encrypted, we now decrypt
                text = data.get('result', '')
                result = vigenere_cipher_decrypt(text, key)
                print_success(f'Decrypted text: {result}')
            else:
                # If it was decrypted, we show it
                result = data.get('result', '')
                print_success(f'Decrypted text: {result}')
                
        elif method == 'base64':
            result = data.get('result', '')
            operation = data.get('operation', '')
            print_success(f'{'Encoded' if operation == 'encode' else 'Decoded'} text: {result}')
                
        else:
            print_warning('This file needs to be decrypted with the appropriate method.')
            print_warning('Please use the main menu to select the correct decryption option.')
    
    except FileNotFoundError:
        print_error(f'File not found.')
    except json.JSONDecodeError:
        print_error(f'Invalid JSON in file.')
    except Exception as e:
        print_error(f'Error loading file: {str(e)}')


def main_menu():
    '''Display the main menu and handle user choices.'''
    while True:
        clear_screen()
        print_header('Encryption and Decryption Tool')
        print('Select an option:')
        print(f'  1. {TextColors.CYAN}Caesar Cipher{TextColors.ENDC} (Shift Cipher)')
        print(f'  2. {TextColors.CYAN}Vigenère Cipher{TextColors.ENDC}')
        print(f'  3. {TextColors.CYAN}Base64{TextColors.ENDC} Encoding/Decoding')
        print(f'  4. {TextColors.CYAN}Fernet{TextColors.ENDC} Symmetric Encryption')
        print(f'  5. {TextColors.CYAN}Load{TextColors.ENDC} Encrypted File')
        print(f'  6. {TextColors.CYAN}Exit{TextColors.ENDC}')
        
        choice = input_with_validation(
            '\nEnter your choice (1-6): ',
            lambda x: x in ['1', '2', '3', '4', '5', '6'],
            'Please enter a number between 1 and 6.'
        )
        
        if choice == '1':
            handle_caesar_cipher()
        elif choice == '2':
            handle_vigenere_cipher()
        elif choice == '3':
            handle_base64()
        elif choice == '4':
            handle_fernet()
        elif choice == '5':
            load_encrypted_file()
        elif choice == '6':
            clear_screen()
            print_success('Thank you for using the Encryption and Decryption Tool!')
            print_success('Goodbye!')
            sys.exit(0)
        
        input('\nPress Enter to return to the main menu…')


def check_dependencies():
    '''Check if required dependencies are installed.'''
    missing_deps = []
    
    if not CRYPTOGRAPHY_AVAILABLE:
        missing_deps.append('cryptography')
    
    if missing_deps:
        print_warning('Some optional dependencies are missing:')
        for dep in missing_deps:
            print(f'  - {dep}')
        print('\nYou can install them with:')
        print(f'  pip install {' '.join(missing_deps)}')
        print('\nNote: The program will still run, but some features may be unavailable.')
        input('\nPress Enter to continue…')


if __name__ == '__main__':
    try:
        clear_screen()
        print_header('Welcome to the Encryption and Decryption Tool')
        print('This program provides various methods to encrypt and decrypt messages.')
        print('Some methods are for educational purposes only and are not secure.')
        
        check_dependencies()
        
        print('\nReady to begin!')
        input('Press Enter to continue…')
        
        main_menu()
    except KeyboardInterrupt:
        print('\n\nProgram interrupted. Exiting…')
        sys.exit(0)

clubs/python_club/python_club_ex_crypt.txt · Last modified: by 127.0.0.1