require 'openssl' require 'securerandom' 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 initialize(password, salt_size = DEFAULT_SALT_SIZE, iv_size = DEFAULT_IV_SIZE, key_size = DEFAULT_KEY_SIZE, rounds = DEFAULT_ROUNDS, block_size = DEFAULT_BLOCK_SIZE) raise ArgumentError, 'EonaCatCipher: Password cannot be null or empty.' if password.nil? || password.empty? @iv_size = iv_size @key_size = key_size @rounds = rounds @block_size = block_size # Derive encryption key and HMAC key @derived_key, @hmac_key = derive_key_and_hmac(password, salt_size) end def encrypt(plaintext) iv = generate_random_bytes(@iv_size) plaintext_bytes = plaintext.encode('UTF-8') ciphertext = Array.new(plaintext_bytes.bytesize, 0) cipher = EonaCatCrypto.new(@derived_key, iv, @block_size, @rounds) cipher.generate(plaintext_bytes.bytes, ciphertext, true) # Combine IV and ciphertext result = iv + ciphertext.pack('C*') # Generate HMAC for integrity check hmac = generate_hmac(result) # Combine result and HMAC final_result = result + hmac final_result end def decrypt(ciphertext_with_hmac) hmac_offset = ciphertext_with_hmac.bytesize - HMAC_KEY_SIZE # Separate HMAC from the ciphertext provided_hmac = ciphertext_with_hmac[hmac_offset, HMAC_KEY_SIZE] ciphertext = ciphertext_with_hmac[0, hmac_offset] # Verify HMAC before decrypting calculated_hmac = generate_hmac(ciphertext) raise 'EonaCatCipher: HMAC validation failed. Data may have been tampered with.' unless secure_compare(provided_hmac, calculated_hmac) # Extract IV iv = ciphertext[0, @iv_size] # Extract encrypted data encrypted_data = ciphertext[@iv_size, ciphertext.bytesize - @iv_size] # Decrypt decrypted_data = Array.new(encrypted_data.bytesize, 0) cipher = EonaCatCrypto.new(@derived_key, iv, @block_size, @rounds) cipher.generate(encrypted_data.bytes, decrypted_data, false) decrypted_data.pack('C*').force_encoding('UTF-8') end private def generate_random_bytes(size) SecureRandom.random_bytes(size) end def derive_key_and_hmac(password, salt_size) salt = generate_random_bytes(salt_size) encryption_key = pbkdf2(password, salt, @key_size, @rounds) # Derive separate key for HMAC hmac_key = pbkdf2(password, salt, HMAC_KEY_SIZE, @rounds) key_with_salt = salt + encryption_key [key_with_salt, hmac_key] end def pbkdf2(password, salt, key_length, iterations) hmac = OpenSSL::Digest::SHA512.new derived_key = [] block_size = hmac.digest_length blocks_needed = (key_length.to_f / block_size).ceil blocks_needed.times do |block_index| block_index += 1 # PBKDF2 block indexing starts at 1 u = OpenSSL::HMAC.digest(hmac, password, salt + [block_index].pack('N')) derived_key.concat(u.bytes) (iterations - 1).times do u = OpenSSL::HMAC.digest(hmac, password, u) derived_key[-block_size, block_size].each_index do |i| derived_key[-block_size + i] ^= u.bytes[i] end end end derived_key[0, key_length] end def generate_hmac(data) hmac = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @hmac_key, data) hmac.bytes end def secure_compare(a, b) return false if a.bytesize != b.bytesize # Use `each_byte` for a timing-safe comparison res = 0 a.each_byte.with_index do |byte, i| res |= byte ^ b.getbyte(i) end res == 0 end end class EonaCatCrypto SECRET_SAUCE = 0x5DEECE66D def initialize(key_with_salt, nonce, block_size, rounds) @rounds = rounds @block_size = block_size / 4 > 0 ? block_size : 128 @key = key_with_salt.unpack('N*') @nonce = nonce.unpack('N*') @state = Array.new(@block_size / 4, 0) @block_counter = 0 end def generate(input, output, encrypt) total_blocks = (input.length + @block_size - 1) / @block_size total_blocks.times do |block_index| input_offset = block_index * @block_size output_offset = block_index * @block_size block = Array.new(@block_size, 0) generate_block(block) (0...block.size).each do |i| if input_offset + i < input.size output[output_offset + i] = input[input_offset + i] ^ block[i] end end end end private def generate_block(output) @state.each_index do |i| @state[i] = (@key[i % @key.size] ^ @nonce[i % @nonce.size]) + (i * SECRET_SAUCE) end @rounds.times do |round| @state.each_index do |i| @state[i] = ((@state[i] + round) ^ (round * SECRET_SAUCE) + (i + @block_counter)).to_i end end output.replace(@state.pack('Q*')) @block_counter += 1 end end # Example Usage if __FILE__ == $0 password = "securePassword123!@#$" plaintext = "Thank you for using EonaCatCipher!" puts "Encrypting '#{plaintext}' with password '#{password}' (we do this 5 times)" puts "================" 5.times do |i| puts "Encryption round #{i + 1}: " puts "================" cipher = EonaCatCipher.new(password) encrypted = cipher.encrypt(plaintext) puts "Encrypted (byte array): #{encrypted.unpack1('H*')}" decrypted = cipher.decrypt(encrypted) puts "Decrypted: #{decrypted}" puts "================" end end