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 <> = 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