Files
odinsea-elixir/lib/odinsea/net/cipher/login_crypto.ex

232 lines
6.6 KiB
Elixir

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