Files
odinsea-elixir/lib/odinsea/net/cipher/aes_cipher.ex
2026-02-25 12:26:26 -07:00

107 lines
3.4 KiB
Elixir

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