125 lines
4.0 KiB
Elixir
125 lines
4.0 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
|
|
"""
|
|
|
|
@block_size 1460
|
|
|
|
# MapleStory AES key (32 bytes, expanded from the Java version)
|
|
@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 in place using AES-ECB with IV.
|
|
|
|
## 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
|
|
crypt_recursive(data, iv, 0, byte_size(data), @block_size - 4)
|
|
end
|
|
|
|
# Recursive encryption/decryption function
|
|
@spec crypt_recursive(binary(), binary(), non_neg_integer(), non_neg_integer(), non_neg_integer()) :: binary()
|
|
defp crypt_recursive(data, _iv, start, remaining, _length) when remaining <= 0 do
|
|
# Return the portion of data we've processed
|
|
binary_part(data, 0, start)
|
|
end
|
|
|
|
defp crypt_recursive(data, iv, start, remaining, length) do
|
|
# Multiply the IV by 4
|
|
seq_iv = multiply_bytes(iv, byte_size(iv), 4)
|
|
|
|
# Adjust length if remaining is smaller
|
|
actual_length = min(remaining, length)
|
|
|
|
# Extract the portion of data to process
|
|
data_bytes = :binary.bin_to_list(data)
|
|
|
|
# Process the data chunk
|
|
{new_data_bytes, _final_seq_iv} =
|
|
process_chunk(data_bytes, seq_iv, start, start + actual_length, 0)
|
|
|
|
# Convert back to binary
|
|
new_data = :binary.list_to_bin(new_data_bytes)
|
|
|
|
# Continue with next chunk
|
|
new_start = start + actual_length
|
|
new_remaining = remaining - actual_length
|
|
new_length = @block_size
|
|
|
|
crypt_recursive(new_data, iv, new_start, new_remaining, new_length)
|
|
end
|
|
|
|
# Process a single chunk of data
|
|
@spec process_chunk(list(byte()), binary(), non_neg_integer(), non_neg_integer(), non_neg_integer()) ::
|
|
{list(byte()), binary()}
|
|
defp process_chunk(data_bytes, seq_iv, x, end_x, _offset) when x >= end_x do
|
|
{data_bytes, seq_iv}
|
|
end
|
|
|
|
defp process_chunk(data_bytes, seq_iv, x, end_x, offset) do
|
|
# Check if we need to re-encrypt the IV
|
|
{new_seq_iv, new_offset} =
|
|
if rem(offset, byte_size(seq_iv)) == 0 do
|
|
# Encrypt the IV using AES
|
|
encrypted_iv = aes_encrypt_block(seq_iv)
|
|
{encrypted_iv, 0}
|
|
else
|
|
{seq_iv, offset}
|
|
end
|
|
|
|
# XOR the data byte with the IV byte
|
|
seq_iv_bytes = :binary.bin_to_list(new_seq_iv)
|
|
iv_index = rem(new_offset, length(seq_iv_bytes))
|
|
iv_byte = Enum.at(seq_iv_bytes, iv_index)
|
|
data_byte = Enum.at(data_bytes, x)
|
|
xor_byte = Bitwise.bxor(data_byte, iv_byte)
|
|
|
|
# Update the data
|
|
updated_data = List.replace_at(data_bytes, x, xor_byte)
|
|
|
|
# Continue processing
|
|
process_chunk(updated_data, new_seq_iv, x + 1, end_x, new_offset + 1)
|
|
end
|
|
|
|
# Encrypt a single 16-byte block using AES-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
|
|
:crypto.crypto_one_time(:aes_128_ecb, @aes_key, padded_block, true)
|
|
end
|
|
|
|
# Multiply bytes - repeats the first `count` bytes of `input` `mul` times
|
|
@spec multiply_bytes(binary(), non_neg_integer(), non_neg_integer()) :: binary()
|
|
defp multiply_bytes(input, count, mul) do
|
|
# Take first `count` bytes and repeat them `mul` times
|
|
chunk = binary_part(input, 0, min(count, byte_size(input)))
|
|
:binary.copy(chunk, mul)
|
|
end
|
|
end
|