Skip to content

Commit

Permalink
Add login cipher
Browse files Browse the repository at this point in the history
  • Loading branch information
jadefish committed Dec 19, 2024
1 parent 9257425 commit 7bff461
Show file tree
Hide file tree
Showing 5 changed files with 485 additions and 0 deletions.
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ gleam_stdlib = ">= 0.46.0 and < 2.0.0"
gleam_otp = ">= 0.16.0 and < 1.0.0"
gleam_erlang = ">= 0.31.0 and < 1.0.0"
glisten = ">= 7.0.0 and < 8.0.0"
gleam_yielder = ">= 1.1.0 and < 2.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
2 changes: 2 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ packages = [
{ name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" },
{ name = "gleam_otp", version = "0.16.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "FA0EB761339749B4E82D63016C6A18C4E6662DA05BAB6F1346F9AF2E679E301A" },
{ name = "gleam_stdlib", version = "0.47.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "3B22D46743C46498C8355365243327AC731ECD3959216344FA9CF9AD348620AC" },
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
{ name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
{ name = "glisten", version = "7.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "028C0882EAC7ABEDEFBE92CE4D1FEDADE95FA81B1B1AB099C4F91C133BEF2C42" },
{ name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" },
Expand All @@ -15,5 +16,6 @@ packages = [
gleam_erlang = { version = ">= 0.31.0 and < 1.0.0" }
gleam_otp = { version = ">= 0.16.0 and < 1.0.0" }
gleam_stdlib = { version = ">= 0.46.0 and < 2.0.0" }
gleam_yielder = { version = ">= 1.1.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
glisten = { version = ">= 7.0.0 and < 8.0.0" }
49 changes: 49 additions & 0 deletions scripts/key_gen.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env ruby

version = ARGV[0]

if version.to_s.strip.empty?
warn "usage: key_gen version-string"
exit(1)
end

parts = version.split(".", 4).map(&:to_i)
version_int = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]

warn "#{version.inspect} -> #{sprintf("0x%X", version_int)} (#{version_int})"

# https://github.com/ClassicUO/ClassicUO/blob/3ad74a6/src/ClassicUO.Client/Network/Encryption/Encryption.cs#L67-L98
=begin
int a = ((int)version >> 24) & 0xFF;
int b = ((int)version >> 16) & 0xFF;
int c = ((int)version >> 8) & 0xFF;
int temp = ((((a << 9) | b) << 10) | c) ^ ((c * c) << 5);
var key2 = (uint)((temp << 4) ^ (b * b) ^ (b * 0x0B000000) ^ (c * 0x380000) ^ 0x2C13A5FD);
temp = (((((a << 9) | c) << 10) | b) * 8) ^ (c * c * 0x0c00);
var key3 = (uint)(temp ^ (b * b) ^ (b * 0x6800000) ^ (c * 0x1c0000) ^ 0x0A31D527F);
var key1 = key2 - 1;
=end

a = (version_int >> 24) & 0xFF
b = (version_int >> 16) & 0xFF
c = (version_int >> 8) & 0xFF
# the fourth version component is not used.

temp = ((((a << 9) | b) << 10) | c) ^ ((c * c) << 5)
puts temp

key2 = ((temp << 4) ^ (b * b) ^ (b * 0x0B000000) ^ (c * 0x380000) ^ 0x2C13A5FD) & 0xFFFFFFFF
puts key2

temp = (((((a << 9) | c) << 10) | b) * 8) ^ (c * c * 0x0c00)
puts temp

key3 = (temp ^ (b * b) ^ (b * 0x6800000) ^ (c * 0x1c0000) ^ 0x0A31D527F) & 0xFFFFFFFF
puts key3

key1 = (key2 - 1) & 0xFFFFFFFF
puts key1

puts [key1, key2, key3].map { |n| sprintf("0x%X", n) }.join(" ")
243 changes: 243 additions & 0 deletions src/cipher.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import gleam/bool
import gleam/erlang
import gleam/int
import gleam/io
import gleam/list
import gleam/result
import gleam/yielder

const lo_mask1 = 0x0000_1357

const lo_mask2 = 0xffff_aaaa

const lo_mask3 = 0x0000_ffff

const hi_mask1 = 0x4321_0000

const hi_mask2 = 0xabcd_ffff

const hi_mask3 = 0xffff_0000

// SERENITY NOW!
const bnot = int.bitwise_not

const bor = int.bitwise_or

const band = int.bitwise_and

const bxor = int.bitwise_exclusive_or

const bsl = int.bitwise_shift_left

const bsr = int.bitwise_shift_right

pub type Error {
InvalidSeed
UnsupportedVersion
}

pub opaque type Seed {
Seed(value: Int)
}

pub fn new_seed(value: Int) -> Result(Seed, Error) {
use <- bool.guard(when: value <= 0, return: Error(InvalidSeed))

Ok(Seed(value))
}

pub fn seed_to_string(seed: Seed) -> String {
int.to_base16(seed.value)
}

pub opaque type Version {
Version(major: Int, minor: Int, patch: Int, revision: Int)
}

pub fn new_version(
major: Int,
minor: Int,
patch: Int,
revision: Int,
) -> Result(Version, Error) {
use <- bool.guard(major < 0, return: Error(UnsupportedVersion))
use <- bool.guard(minor < 0, return: Error(UnsupportedVersion))
use <- bool.guard(patch < 0, return: Error(UnsupportedVersion))
use <- bool.guard(revision < 0, return: Error(UnsupportedVersion))

Ok(Version(major, minor, patch, revision))
}

pub fn version_to_string(version: Version) -> String {
int.to_string(version.major)
<> "."
<> int.to_string(version.minor)
<> "."
<> int.to_string(version.patch)
<> "."
<> int.to_string(version.revision)
}

pub opaque type Cipher {
NilCipher
LoginCipher(seed: Seed, mask: KeyPair, key: KeyPair)
GameCipher(seed: Seed, mask: KeyPair, key: KeyPair)
}

pub fn login(seed: Seed, version: Version) -> Result(Cipher, Error) {
io.println(
"cipher.login("
<> erlang.format(seed)
<> ", "
<> erlang.format(version)
<> ")",
)
use key <- result.map(key_for_version(version))
io.println("cipher.login: key = " <> erlang.format(key))
let value = seed.value

// ((^seed ^ lo_mask1) << 16) | ((seed ^ lo_mask2) & lo_mask3)
let mask_lo =
bor(
bnot(value) |> bxor(lo_mask1) |> bsl(16),
value |> bxor(lo_mask2) |> band(lo_mask3),
)
|> band(0xFFFF_FFFF)

// ((seed ^ hi_mask1) >> 16) | ((^seed ^ hi_mask2) & hi_mask3)
let mask_hi =
bor(
bsr(bxor(value, hi_mask1), 16),
band(bxor(bnot(value), hi_mask2), hi_mask3),
)
|> band(0xFFFF_FFFF)

let mask = KeyPair(mask_lo, mask_hi)
io.println("cipher.login: mask = " <> erlang.format(mask))
LoginCipher(seed, mask, key)
}

pub fn game(_seed: Seed, _version: Version) -> Result(Cipher, Error) {
todo
}

pub fn nil() -> Cipher {
NilCipher
}

pub fn encrypt(cipher: Cipher, plain data: BitArray) -> #(Cipher, BitArray) {
case cipher {
NilCipher -> #(cipher, data)
// The Login cipher doesn't support encrypting data.
LoginCipher(_, _, _) -> #(cipher, data)
GameCipher(_, _, _) -> todo
}
}

/// Decrypt data using the provided cipher.
///
/// Decryption operations utilize a rolling cipher on both ends, so a new Cipher
/// is returned along with the decrypted data. The old Cipher will no longer be
/// capable of decrypting data, so it should be discarded.
pub fn decrypt(cipher: Cipher, data: BitArray) -> #(Cipher, BitArray) {
case cipher {
NilCipher -> #(cipher, data)
LoginCipher(seed, mask, key) -> {
let #(data, mask2, key2) = login_decrypt_loop(mask, key, data, <<>>)
#(LoginCipher(seed, mask2, key2), data)
}
GameCipher(_seed, _mask, _key) -> todo
}
}

fn login_decrypt_loop(
mask mask: KeyPair,
key key: KeyPair,
cipher cipher_bits: BitArray,
plain plain_bits: BitArray,
) -> #(BitArray, KeyPair, KeyPair) {
case cipher_bits {
<<>> -> #(plain_bits, mask, key)
<<byte:int, remaining:bytes>> -> {
// dst[i] = src[i] ^ byte(cs.maskLo)
let mask_byte = band(mask.lo, 0xFF)
let plain_byte = byte |> bxor(mask_byte)

// cs.maskLo = ((maskLo >> 1) | (maskHi << 31)) ^ cs.keyLo
let new_mask_lo =
bor(mask.lo |> bsr(1), mask.hi |> bsl(31))
|> bxor(key.lo)
|> band(0xFFFF_FFFF)

// maskHi = ((maskHi >> 1) | (maskLo << 31)) ^ cs.keyHi
let mask_hi =
bor(mask.hi |> bsr(1), mask.lo |> bsl(31))
|> bxor(key.hi)
|> band(0xFFFF_FFFF)

// cs.maskHi = ((maskHi >> 1) | (maskLo << 31)) ^ cs.keyHi
let new_mask_hi =
bor(mask_hi |> bsr(1), mask.lo |> bsl(31))
|> bxor(key.hi)
|> band(0xFFFF_FFFF)

let mask = KeyPair(new_mask_lo, new_mask_hi)
let derp = <<plain_bits:bits, plain_byte>>

login_decrypt_loop(mask, key, remaining, derp)
}
_ -> todo
}
}

pub type KeyPair {
KeyPair(lo: Int, hi: Int)
}

// from https://github.com/ClassicUO/ClassicUO/blob/3ad74a6/src/ClassicUO.Client/Network/Encryption/Encryption.cs#L67-L98
fn compute_key(a: Int, b: Int, c: Int) -> #(Int, Int, Int) {
// truncate ints to 32-bit:
let a = band(a, 0xFFFF_FFFF)
let b = band(b, 0xFFFF_FFFF)
let c = band(c, 0xFFFF_FFFF)

// int temp = ((((a << 9) | b) << 10) | c) ^ ((c * c) << 5)
let temp = a |> bsl(9) |> bor(b) |> bsl(10) |> bor(c) |> bxor(c * c |> bsl(5))

// var key2 = (uint)((temp << 4) ^ (b * b) ^ (b * 0x0B000000) ^ (c * 0x380000) ^ 0x2C13A5FD)
let key2 =
bsl(temp, 4)
|> bxor(b * b)
|> bxor(b * 0x0B00_0000)
|> bxor(c * 0x0038_0000)
|> bxor(0x2C13_A5FD)

// temp = (((((a << 9) | c) << 10) | b) * 8) ^ (c * c * 0x0c00)
let temp =
{ { bsl(a, 9) |> bor(c) |> bsl(10) |> bor(b) } * 8 }
|> bxor(c * c * 0x0000_0c00)

// var key3 = temp ^ (b * b) ^ (b * 0x6800000) ^ (c * 0x1c0000) ^ 0x0A31D527F
let key3 =
temp
|> bxor(b * b)
|> bxor(b * 0x0680_0000)
|> bxor(c * 0x001c_0000)
|> bxor(0xA31D_527F)

// var key1 = key2 - 1
let key1 = key2 - 1 |> band(0xFFFF_FFFF)

#(key1, key2, key3)
}

pub fn key_for_version(version: Version) -> Result(KeyPair, Error) {
case version {
// 2.0.3.x is a special case.
Version(2, 0, 3, 0x78) -> Ok(KeyPair(0x2D13A5FD, 0xA39D527F))
Version(major, minor, patch, _) -> {
let #(_, hi, lo) = compute_key(major, minor, patch)
Ok(KeyPair(lo, hi))
}
}
}
Loading

0 comments on commit 7bff461

Please sign in to comment.