204 lines
5.5 KiB
Elixir
204 lines
5.5 KiB
Elixir
defmodule Odinsea.Net.Cipher.ClientCrypto do
|
|
@moduledoc """
|
|
Client cryptography coordinator for MapleStory packet encryption.
|
|
Manages send/recv IVs, packet encryption/decryption, and header encoding/decoding.
|
|
|
|
Ported from: src/handling/netty/ClientCrypto.java
|
|
"""
|
|
|
|
use Bitwise
|
|
|
|
alias Odinsea.Net.Cipher.{AESCipher, IGCipher}
|
|
|
|
defstruct [
|
|
:version,
|
|
:use_custom_crypt,
|
|
:send_iv,
|
|
:send_iv_old,
|
|
:recv_iv,
|
|
:recv_iv_old
|
|
]
|
|
|
|
@type t :: %__MODULE__{
|
|
version: integer(),
|
|
use_custom_crypt: boolean(),
|
|
send_iv: binary(),
|
|
send_iv_old: binary(),
|
|
recv_iv: binary(),
|
|
recv_iv_old: binary()
|
|
}
|
|
|
|
@doc """
|
|
Creates a new ClientCrypto instance with random IVs.
|
|
|
|
## Parameters
|
|
- version: MapleStory version number (e.g., 342)
|
|
- use_custom_crypt: If false, uses AES encryption. If true, uses basic XOR with 0x69
|
|
|
|
## Returns
|
|
- New ClientCrypto struct
|
|
"""
|
|
@spec new(integer(), boolean()) :: t()
|
|
def new(version, use_custom_crypt \\ false) do
|
|
%__MODULE__{
|
|
version: version,
|
|
use_custom_crypt: use_custom_crypt,
|
|
send_iv: :crypto.strong_rand_bytes(4),
|
|
send_iv_old: <<0, 0, 0, 0>>,
|
|
recv_iv: :crypto.strong_rand_bytes(4),
|
|
recv_iv_old: <<0, 0, 0, 0>>
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Encrypts outgoing packet data and updates the send IV.
|
|
|
|
## Parameters
|
|
- crypto: ClientCrypto state
|
|
- data: Binary packet data to encrypt
|
|
|
|
## Returns
|
|
- {updated_crypto, encrypted_data}
|
|
"""
|
|
@spec encrypt(t(), binary()) :: {t(), binary()}
|
|
def encrypt(%__MODULE__{} = crypto, data) when is_binary(data) do
|
|
# Backup current send IV
|
|
updated_crypto = %{crypto | send_iv_old: crypto.send_iv}
|
|
|
|
# Encrypt the data
|
|
encrypted_data =
|
|
if crypto.use_custom_crypt do
|
|
basic_cipher(data)
|
|
else
|
|
AESCipher.crypt(data, crypto.send_iv)
|
|
end
|
|
|
|
# Update the send IV using InnoGames hash
|
|
new_send_iv = IGCipher.inno_hash(crypto.send_iv)
|
|
final_crypto = %{updated_crypto | send_iv: new_send_iv}
|
|
|
|
{final_crypto, encrypted_data}
|
|
end
|
|
|
|
@doc """
|
|
Decrypts incoming packet data and updates the recv IV.
|
|
|
|
## Parameters
|
|
- crypto: ClientCrypto state
|
|
- data: Binary packet data to decrypt
|
|
|
|
## Returns
|
|
- {updated_crypto, decrypted_data}
|
|
"""
|
|
@spec decrypt(t(), binary()) :: {t(), binary()}
|
|
def decrypt(%__MODULE__{} = crypto, data) when is_binary(data) do
|
|
# Backup current recv IV
|
|
updated_crypto = %{crypto | recv_iv_old: crypto.recv_iv}
|
|
|
|
# Decrypt the data
|
|
decrypted_data =
|
|
if crypto.use_custom_crypt do
|
|
basic_cipher(data)
|
|
else
|
|
AESCipher.crypt(data, crypto.recv_iv)
|
|
end
|
|
|
|
# Update the recv IV using InnoGames hash
|
|
new_recv_iv = IGCipher.inno_hash(crypto.recv_iv)
|
|
final_crypto = %{updated_crypto | recv_iv: new_recv_iv}
|
|
|
|
{final_crypto, decrypted_data}
|
|
end
|
|
|
|
@doc """
|
|
Encodes the packet header (4 bytes) for outgoing packets.
|
|
Returns the raw header bytes that prefix the encrypted packet.
|
|
|
|
## Parameters
|
|
- crypto: ClientCrypto state
|
|
- data_len: Length of the packet data (excluding header)
|
|
|
|
## Returns
|
|
- 4-byte binary header
|
|
"""
|
|
@spec encode_header_len(t(), non_neg_integer()) :: binary()
|
|
def encode_header_len(%__MODULE__{} = crypto, data_len) do
|
|
<<s0, s1, s2, s3>> = crypto.send_iv
|
|
|
|
# Calculate the encoded version
|
|
new_version = -(crypto.version + 1) &&& 0xFFFF
|
|
enc_version = (((new_version >>> 8) &&& 0xFF) ||| ((new_version <<< 8) &&& 0xFF00)) &&& 0xFFFF
|
|
|
|
# Calculate raw sequence from send IV
|
|
raw_seq = bxor((((s3 &&& 0xFF) ||| ((s2 <<< 8) &&& 0xFF00)) &&& 0xFFFF), enc_version)
|
|
|
|
# Calculate raw length
|
|
raw_len = (((data_len <<< 8) &&& 0xFF00) ||| (data_len >>> 8)) &&& 0xFFFF
|
|
raw_len_encoded = bxor(raw_len, raw_seq)
|
|
|
|
# Encode as 4 bytes
|
|
<<
|
|
(raw_seq >>> 8) &&& 0xFF,
|
|
raw_seq &&& 0xFF,
|
|
(raw_len_encoded >>> 8) &&& 0xFF,
|
|
raw_len_encoded &&& 0xFF
|
|
>>
|
|
end
|
|
|
|
@doc """
|
|
Decodes the packet header to extract the data length.
|
|
|
|
## Parameters
|
|
- raw_seq: 16-bit sequence number from header
|
|
- raw_len: 16-bit length field from header
|
|
|
|
## Returns
|
|
- Decoded packet length
|
|
"""
|
|
@spec decode_header_len(integer(), integer()) :: integer()
|
|
def decode_header_len(raw_seq, raw_len) do
|
|
bxor(raw_seq, raw_len) &&& 0xFFFF
|
|
end
|
|
|
|
@doc """
|
|
Validates that the incoming packet header is correct for this connection.
|
|
|
|
## Parameters
|
|
- crypto: ClientCrypto state
|
|
- raw_seq: 16-bit sequence number from header
|
|
|
|
## Returns
|
|
- true if valid, false otherwise
|
|
"""
|
|
@spec decode_header_valid?(t(), integer()) :: boolean()
|
|
def decode_header_valid?(%__MODULE__{} = crypto, raw_seq) do
|
|
<<_r0, _r1, r2, r3>> = crypto.recv_iv
|
|
|
|
enc_version = crypto.version &&& 0xFFFF
|
|
seq_base = ((r2 &&& 0xFF) ||| ((r3 <<< 8) &&& 0xFF00)) &&& 0xFFFF
|
|
|
|
bxor((raw_seq &&& 0xFFFF), seq_base) == enc_version
|
|
end
|
|
|
|
@doc """
|
|
Gets the current send IV (for handshake).
|
|
"""
|
|
@spec get_send_iv(t()) :: binary()
|
|
def get_send_iv(%__MODULE__{} = crypto), do: crypto.send_iv
|
|
|
|
@doc """
|
|
Gets the current recv IV (for handshake).
|
|
"""
|
|
@spec get_recv_iv(t()) :: binary()
|
|
def get_recv_iv(%__MODULE__{} = crypto), do: crypto.recv_iv
|
|
|
|
# Basic XOR cipher with 0x69 (fallback when custom_crypt is enabled)
|
|
@spec basic_cipher(binary()) :: binary()
|
|
defp basic_cipher(data) do
|
|
data
|
|
|> :binary.bin_to_list()
|
|
|> Enum.map(fn byte -> Bitwise.bxor(byte, 0x69) end)
|
|
|> :binary.list_to_bin()
|
|
end
|
|
end
|