defmodule Odinsea.Net.Cipher.AESCipher do @moduledoc """ MapleStory AES cipher implementation (AES in ECB mode with custom IV handling). This is used for encrypting/decrypting packet data. Ported from: src/handling/netty/cipher/AESCipher.java """ import Bitwise @block_size 1460 # MapleStory AES key (32 bytes = AES-256) # Must match the Java AES_KEY exactly @aes_key << 0x13, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0xB4, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00 >> @doc """ Encrypts or decrypts packet data using AES-ECB with IV. The algorithm (per block of 1456/1460 bytes): 1. Expand the 4-byte IV to 16 bytes by repeating it 4 times 2. Use AES-ECB to encrypt the expanded IV, producing a 16-byte keystream 3. XOR the next 16 bytes of data with the keystream 4. The encrypted IV becomes the new IV for the next 16-byte chunk 5. Repeat until the block is processed ## Parameters - data: Binary data to encrypt/decrypt - iv: 4-byte IV binary ## Returns - Encrypted/decrypted binary data """ @spec crypt(binary(), binary()) :: binary() def crypt(data, <<_::binary-size(4)>> = iv) when is_binary(data) do data_list = :binary.bin_to_list(data) result = crypt_blocks(data_list, 0, @block_size - 4, iv) :binary.list_to_bin(result) end # Process data in blocks (first block: 1456 bytes, subsequent: 1460 bytes) defp crypt_blocks([], _start, _length, _iv), do: [] defp crypt_blocks(data, start, length, iv) do remaining = Kernel.length(data) actual_length = min(remaining, length) # Expand 4-byte IV to 16 bytes (repeat 4 times) seq_iv = :binary.copy(iv, 4) seq_iv_list = :binary.bin_to_list(seq_iv) # Process this block's bytes, re-encrypting keystream every 16 bytes {block, rest} = Enum.split(data, actual_length) {processed, _final_iv} = process_block_bytes(block, seq_iv_list, 0) # Continue with next block using fresh IV expansion processed ++ crypt_blocks(rest, start + actual_length, @block_size, iv) end # Process bytes within a single block, re-encrypting keystream every 16 bytes defp process_block_bytes([], iv_list, _offset), do: {[], iv_list} defp process_block_bytes(data, iv_list, offset) do # Re-encrypt keystream at every 16-byte boundary iv_list = if rem(offset, 16) == 0 do new_iv = aes_encrypt_block(:binary.list_to_bin(iv_list)) :binary.bin_to_list(new_iv) else iv_list end [byte | rest] = data iv_byte = Enum.at(iv_list, rem(offset, 16)) xored = bxor(byte, iv_byte) {rest_result, final_iv} = process_block_bytes(rest, iv_list, offset + 1) {[xored | rest_result], final_iv} end # Encrypt a single 16-byte block using AES-256-ECB @spec aes_encrypt_block(binary()) :: binary() defp aes_encrypt_block(block) do # Pad or truncate to 16 bytes for AES padded_block = case byte_size(block) do 16 -> block size when size < 16 -> block <> :binary.copy(<<0>>, 16 - size) size when size > 16 -> binary_part(block, 0, 16) end # Perform AES encryption in ECB mode (AES-256) result = :crypto.crypto_one_time(:aes_256_ecb, @aes_key, padded_block, true) # Take only first 16 bytes (OpenSSL may add PKCS padding) binary_part(result, 0, 16) end end