107 lines
3.4 KiB
Elixir
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
|