190 lines
7.0 KiB
Python
190 lines
7.0 KiB
Python
import os
|
|
import hashlib
|
|
import hmac
|
|
import struct
|
|
import base64
|
|
|
|
class EonaCatCipher:
|
|
DEFAULT_SALT_SIZE = 2048; # Salt size for key derivation
|
|
DEFAULT_IV_SIZE = 2048; # IV size (16384 bits)
|
|
DEFAULT_KEY_SIZE = 2048; # Key size (16384 bits)
|
|
DEFAULT_ROUNDS = 2048; # Rounds
|
|
DEFAULT_BLOCK_SIZE = 8192; # 8kb
|
|
HMAC_KEY_SIZE = 32; # Key size for HMAC (256 bits)
|
|
|
|
#/*
|
|
# * EonaCatCipher - Because security is key!
|
|
# *
|
|
# * Copyright (c) 2024 EonaCat (Jeroen Saey)
|
|
# *
|
|
# * https://eonacat.com/license
|
|
# *
|
|
# * TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
# * OF SOFTWARE BY EONACAT (JEROEN SAEY)
|
|
# *
|
|
# * This software is provided "as is", without any express or implied warranty.
|
|
# * In no event shall the authors or copyright holders be liable for any claim,
|
|
# * damages or other liability, whether in an action of contract, tort or otherwise,
|
|
# * arising from, out of or in connection with the software or the use or other
|
|
# * dealings in the software.
|
|
# *
|
|
# * You may use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# * copies of the Software, and permit persons to whom the Software is furnished
|
|
# * to do so, subject to the following conditions:
|
|
# *
|
|
# * 1. The above copyright notice and this permission notice shall be included in
|
|
# * all copies or substantial portions of the Software.
|
|
# *
|
|
# * 2. The software must not be used for any unlawful purpose.
|
|
# *
|
|
# * For any inquiries, please contact: eonacat@gmail.com
|
|
# */
|
|
|
|
def __init__(self, password, salt_size=None, iv_size=None, key_size=None, rounds=None, block_size=None):
|
|
if not password:
|
|
raise ValueError("EonaCatCipher: Password cannot be null or empty.")
|
|
|
|
self.salt_size = salt_size if salt_size is not None else self.DEFAULT_SALT_SIZE
|
|
self.iv_size = iv_size if iv_size is not None else self.DEFAULT_IV_SIZE // 8 # Convert bits to bytes
|
|
self.key_size = key_size if key_size is not None else self.DEFAULT_KEY_SIZE // 8 # Convert bits to bytes
|
|
self.rounds = rounds if rounds is not None else self.DEFAULT_ROUNDS
|
|
self.block_size = block_size if block_size is not None else self.DEFAULT_BLOCK_SIZE // 8 # Convert bits to bytes
|
|
|
|
# Derive encryption key and HMAC key
|
|
self.derived_key, self.hmac_key = self.derive_key_and_hmac(password)
|
|
|
|
@staticmethod
|
|
def generate_random_bytes(size):
|
|
return os.urandom(size)
|
|
|
|
def derive_key_and_hmac(self, password):
|
|
salt = self.generate_random_bytes(self.salt_size)
|
|
encryption_key = self.pbkdf2(password, salt, self.key_size, self.rounds)
|
|
|
|
# Derive separate key for HMAC
|
|
hmac_key = self.pbkdf2(password, salt, self.HMAC_KEY_SIZE, self.rounds)
|
|
|
|
key_with_salt = salt + encryption_key
|
|
|
|
return key_with_salt, hmac_key
|
|
|
|
@staticmethod
|
|
def pbkdf2(password, salt, key_length, iterations):
|
|
# PBKDF2 using HMAC-SHA512
|
|
hmac_sha512 = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, iterations, dklen=key_length)
|
|
return hmac_sha512
|
|
|
|
def encrypt(self, plaintext):
|
|
iv = self.generate_random_bytes(self.iv_size)
|
|
plaintext_bytes = plaintext.encode()
|
|
|
|
ciphertext = bytearray(len(plaintext_bytes))
|
|
|
|
# Generate cipher
|
|
cipher = EonaCatCrypto(self.derived_key, iv, self.block_size, self.rounds)
|
|
cipher.generate(plaintext_bytes, ciphertext, True)
|
|
|
|
# Combine IV and ciphertext
|
|
result = iv + ciphertext
|
|
|
|
# Generate HMAC for integrity check
|
|
hmac = self.generate_hmac(result)
|
|
|
|
# Combine result and HMAC
|
|
final_result = result + hmac
|
|
return final_result
|
|
|
|
def decrypt(self, ciphertext_with_hmac):
|
|
hmac_offset = len(ciphertext_with_hmac) - self.HMAC_KEY_SIZE
|
|
|
|
# Separate HMAC from the ciphertext
|
|
provided_hmac = ciphertext_with_hmac[hmac_offset:]
|
|
ciphertext = ciphertext_with_hmac[:hmac_offset]
|
|
|
|
# Verify HMAC before decrypting
|
|
calculated_hmac = self.generate_hmac(ciphertext)
|
|
if not self.are_equal(provided_hmac, calculated_hmac):
|
|
raise ValueError("EonaCatCipher: HMAC validation failed. Data may have been tampered with.")
|
|
|
|
# Extract IV
|
|
iv = ciphertext[:self.iv_size]
|
|
encrypted_data = ciphertext[self.iv_size:]
|
|
|
|
# Decrypt
|
|
decrypted_data = bytearray(len(encrypted_data))
|
|
cipher = EonaCatCrypto(self.derived_key, iv, self.block_size, self.rounds)
|
|
cipher.generate(encrypted_data, decrypted_data, False)
|
|
|
|
return decrypted_data.decode()
|
|
|
|
def generate_hmac(self, data):
|
|
return hmac.new(self.hmac_key, data, hashlib.sha256).digest()
|
|
|
|
@staticmethod
|
|
def are_equal(a, b):
|
|
return hmac.compare_digest(a, b)
|
|
|
|
class EonaCatCrypto:
|
|
SECRET_SAUCE = 0x5DEECE66D
|
|
|
|
def __init__(self, key_with_salt, nonce, block_size, rounds):
|
|
self.rounds = rounds
|
|
self.block_size = block_size // 4 > 0 and block_size // 4 or 128
|
|
|
|
self.key = list(struct.unpack(f'>{len(key_with_salt) // 4}I', key_with_salt))
|
|
self.nonce = list(struct.unpack(f'>{len(nonce) // 4}I', nonce))
|
|
self.state = [0] * (self.block_size // 4)
|
|
self.block_counter = 0
|
|
|
|
def generate_block(self, output):
|
|
# Initialize state using a combined operation
|
|
for i in range(len(self.state)):
|
|
self.state[i] = (self.key[i % len(self.key)] ^ self.nonce[i % len(self.nonce)]) + (i * self.SECRET_SAUCE)
|
|
|
|
# Mix the states according to the rounds
|
|
for round in range(self.rounds):
|
|
for i in range(len(self.state)):
|
|
self.state[i] = (self.state[i] + round) ^ (round * self.SECRET_SAUCE) + (i + self.block_counter)
|
|
|
|
# Output block
|
|
output.extend(self.state)
|
|
self.block_counter += 1
|
|
|
|
def generate(self, input_data, output, encrypt):
|
|
total_blocks = (len(input_data) + self.block_size - 1) // self.block_size
|
|
|
|
for block_index in range(total_blocks):
|
|
input_offset = block_index * self.block_size
|
|
block = bytearray(self.block_size)
|
|
|
|
# Generate a block based on the input
|
|
self.generate_block(block)
|
|
|
|
# Perform XOR for encryption or decryption
|
|
for i in range(len(block)):
|
|
if input_offset + i < len(input_data):
|
|
output[input_offset + i] = input_data[input_offset + i] ^ block[i]
|
|
|
|
def main():
|
|
password = "securePassword123!@#$"
|
|
plaintext = "Thank you for using EonaCatCipher!"
|
|
|
|
print(f"Encrypting '{plaintext}' with password '{password}' (we do this 5 times)")
|
|
print("================")
|
|
|
|
for i in range(5):
|
|
print(f"Encryption round {i + 1}: ")
|
|
print("================")
|
|
|
|
cipher = EonaCatCipher(password)
|
|
encrypted = cipher.encrypt(plaintext)
|
|
|
|
print("Encrypted (byte array):", [b for b in encrypted])
|
|
|
|
decrypted = cipher.decrypt(encrypted)
|
|
print("Decrypted:", decrypted)
|
|
print("================")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|