Start repo, claude & kimi still vibing tho

This commit is contained in:
ra
2026-02-14 17:04:21 -07:00
commit f5b8aeb39d
54 changed files with 9466 additions and 0 deletions

View 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