232 lines
6.6 KiB
Elixir
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
|