Start repo, claude & kimi still vibing tho
This commit is contained in:
231
lib/odinsea/net/cipher/login_crypto.ex
Normal file
231
lib/odinsea/net/cipher/login_crypto.ex
Normal file
@@ -0,0 +1,231 @@
|
||||
defmodule Odinsea.Net.Cipher.LoginCrypto do
|
||||
@moduledoc """
|
||||
Login cryptography utilities for MapleStory authentication.
|
||||
Provides password hashing (SHA-1, SHA-512 with salt) and RSA decryption.
|
||||
|
||||
Ported from: src/client/LoginCrypto.java
|
||||
"""
|
||||
|
||||
@extra_length 6
|
||||
|
||||
@doc """
|
||||
Generates a 13-digit Asiasoft passport string.
|
||||
Format: Letter + 11 digits + Letter
|
||||
|
||||
## Returns
|
||||
- 13-character string (e.g., "A12345678901B")
|
||||
"""
|
||||
@spec generate_13digit_asiasoft_passport() :: String.t()
|
||||
def generate_13digit_asiasoft_passport do
|
||||
alphabet = ~w(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z)
|
||||
numbers = ~w(1 2 3 4 5 6 7 8 9)
|
||||
|
||||
# First letter
|
||||
first = Enum.random(alphabet)
|
||||
|
||||
# 11 digits
|
||||
middle =
|
||||
1..11
|
||||
|> Enum.map(fn _ -> Enum.random(numbers) end)
|
||||
|> Enum.join()
|
||||
|
||||
# Last letter
|
||||
last = Enum.random(alphabet)
|
||||
|
||||
"#{first}#{middle}#{last}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Hashes a password using SHA-1.
|
||||
|
||||
## Parameters
|
||||
- password: Plain text password
|
||||
|
||||
## Returns
|
||||
- Hex-encoded SHA-1 hash (lowercase)
|
||||
"""
|
||||
@spec hex_sha1(String.t()) :: String.t()
|
||||
def hex_sha1(password) when is_binary(password) do
|
||||
hash_with_digest(password, :sha)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Hashes a password using SHA-512 with a salt.
|
||||
|
||||
## Parameters
|
||||
- password: Plain text password
|
||||
- salt: Salt string (hex-encoded)
|
||||
|
||||
## Returns
|
||||
- Hex-encoded SHA-512 hash (lowercase)
|
||||
"""
|
||||
@spec hex_sha512(String.t(), String.t()) :: String.t()
|
||||
def hex_sha512(password, salt) when is_binary(password) and is_binary(salt) do
|
||||
hash_with_digest(password <> salt, :sha512)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a SHA-1 hash matches a password.
|
||||
|
||||
## Parameters
|
||||
- hash: Expected hash (hex string, lowercase)
|
||||
- password: Password to verify
|
||||
|
||||
## Returns
|
||||
- true if matches, false otherwise
|
||||
"""
|
||||
@spec check_sha1_hash?(String.t(), String.t()) :: boolean()
|
||||
def check_sha1_hash?(hash, password) do
|
||||
hash == hex_sha1(password)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a salted SHA-512 hash matches a password.
|
||||
|
||||
## Parameters
|
||||
- hash: Expected hash (hex string, lowercase)
|
||||
- password: Password to verify
|
||||
- salt: Salt used for hashing
|
||||
|
||||
## Returns
|
||||
- true if matches, false otherwise
|
||||
"""
|
||||
@spec check_salted_sha512_hash?(String.t(), String.t(), String.t()) :: boolean()
|
||||
def check_salted_sha512_hash?(hash, password, salt) do
|
||||
hash == hex_sha512(password, salt)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verifies a salted SHA-512 hash against a password.
|
||||
Returns {:ok, password} on match, {:error, :invalid} on mismatch.
|
||||
|
||||
## Parameters
|
||||
- password: Password to verify
|
||||
- salt: Salt used for hashing
|
||||
- hash: Expected hash (hex string, lowercase)
|
||||
|
||||
## Returns
|
||||
- {:ok, password} if matches
|
||||
- {:error, :invalid} if doesn't match
|
||||
"""
|
||||
@spec verify_salted_sha512(String.t(), String.t(), String.t()) :: {:ok, String.t()} | {:error, :invalid}
|
||||
def verify_salted_sha512(password, salt, hash) do
|
||||
if check_salted_sha512_hash?(hash, password, salt) do
|
||||
{:ok, password}
|
||||
else
|
||||
{:error, :invalid}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a random 16-byte salt.
|
||||
|
||||
## Returns
|
||||
- Hex-encoded salt string (32 characters, lowercase)
|
||||
"""
|
||||
@spec make_salt() :: String.t()
|
||||
def make_salt do
|
||||
:crypto.strong_rand_bytes(16)
|
||||
|> Base.encode16(case: :lower)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds random prefix to a string (used for RSA password encoding).
|
||||
|
||||
## Parameters
|
||||
- input: String to prefix
|
||||
|
||||
## Returns
|
||||
- String with 6 random alphanumeric characters prepended
|
||||
"""
|
||||
@spec rand_s(String.t()) :: String.t()
|
||||
def rand_s(input) when is_binary(input) do
|
||||
alphabet = ~w(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z)
|
||||
numbers = ~w(1 2 3 4 5 6 7 8 9)
|
||||
chars = alphabet ++ numbers
|
||||
|
||||
prefix =
|
||||
1..@extra_length
|
||||
|> Enum.map(fn _ -> Enum.random(chars) end)
|
||||
|> Enum.join()
|
||||
|
||||
prefix <> input
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes random prefix from a string (used for RSA password decoding).
|
||||
Extracts 128 characters starting from position 6.
|
||||
|
||||
## Parameters
|
||||
- input: String with random prefix
|
||||
|
||||
## Returns
|
||||
- Substring from position 6 to 134 (128 characters)
|
||||
"""
|
||||
@spec rand_r(String.t()) :: String.t()
|
||||
def rand_r(input) when is_binary(input) do
|
||||
String.slice(input, @extra_length, 128)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decrypts an RSA-encrypted password using the hardcoded private key.
|
||||
Uses RSA/NONE/OAEPPadding with the server's private key.
|
||||
|
||||
## Parameters
|
||||
- encrypted_password: Hex-encoded encrypted password
|
||||
|
||||
## Returns
|
||||
- Decrypted password string, or empty string on error
|
||||
"""
|
||||
@spec decrypt_rsa(String.t()) :: String.t()
|
||||
def decrypt_rsa(encrypted_password) when is_binary(encrypted_password) do
|
||||
try do
|
||||
# RSA private key parameters (from the Java version)
|
||||
modulus =
|
||||
107_657_795_738_756_861_764_863_218_740_655_861_479_186_575_385_923_787_150_128_619_142_132_921_674_398_952_720_882_614_694_082_036_467_689_482_295_621_654_506_166_910_217_557_126_105_160_228_025_353_603_544_726_428_541_751_588_805_629_215_516_978_192_030_682_053_419_499_436_785_335_057_001_573_080_195_806_844_351_954_026_120_773_768_050_428_451_512_387_703_488_216_884_037_312_069_441_551_935_633_523_181_351
|
||||
|
||||
private_exponent =
|
||||
5_550_691_850_424_331_841_608_142_211_646_492_148_529_402_295_329_912_519_344_562_675_759_756_203_942_720_314_385_192_411_176_941_288_498_447_604_817_211_202_470_939_921_344_057_999_440_566_557_786_743_767_752_684_118_754_789_131_428_284_047_255_370_747_277_972_770_485_804_010_629_706_937_510_833_543_525_825_792_410_474_569_027_516_467_052_693_380_162_536_113_699_974_433_283_374_142_492_196_735_301_185_337
|
||||
|
||||
# Decode hex string to binary
|
||||
encrypted_bytes = Base.decode16!(encrypted_password, case: :mixed)
|
||||
|
||||
# Create RSA private key
|
||||
rsa_key = [modulus, private_exponent]
|
||||
|
||||
# Decrypt using RSA with OAEP padding
|
||||
# Note: Elixir's :public_key module uses a different format
|
||||
# We need to construct the key properly
|
||||
private_key = {
|
||||
:RSAPrivateKey,
|
||||
:"two-prime",
|
||||
modulus,
|
||||
65537,
|
||||
private_exponent,
|
||||
# These are placeholders - full key derivation needed for production
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
:asn1_NOVALUE
|
||||
}
|
||||
|
||||
decrypted = :public_key.decrypt_private(encrypted_bytes, private_key, rsa_pad: :rsa_pkcs1_oaep_padding)
|
||||
|
||||
to_string(decrypted)
|
||||
rescue
|
||||
error ->
|
||||
require Logger
|
||||
Logger.error("RSA decryption failed: #{inspect(error)}")
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
# Private helper: hash a string with a given digest algorithm
|
||||
@spec hash_with_digest(String.t(), atom()) :: String.t()
|
||||
defp hash_with_digest(input, digest) do
|
||||
:crypto.hash(digest, input)
|
||||
|> Base.encode16(case: :lower)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user