EonaCatCipher/Ruby/EonaCatCipher.ruby

225 lines
6.6 KiB
Plaintext
Raw Permalink Normal View History

2024-09-25 19:22:33 +02:00
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