Start repo, claude & kimi still vibing tho
This commit is contained in:
203
lib/odinsea/net/cipher/client_crypto.ex
Normal file
203
lib/odinsea/net/cipher/client_crypto.ex
Normal file
@@ -0,0 +1,203 @@
|
||||
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
|
||||
Reference in New Issue
Block a user