From 7bff4617ca9aac19a486a9d754c2ef4a0d95a6a7 Mon Sep 17 00:00:00 2001 From: Nick Miller Date: Wed, 18 Dec 2024 23:35:42 -0500 Subject: [PATCH] Add login cipher --- gleam.toml | 1 + manifest.toml | 2 + scripts/key_gen.rb | 49 +++++++++ src/cipher.gleam | 243 +++++++++++++++++++++++++++++++++++++++++ test/cipher_test.gleam | 190 ++++++++++++++++++++++++++++++++ 5 files changed, 485 insertions(+) create mode 100755 scripts/key_gen.rb create mode 100644 src/cipher.gleam create mode 100644 test/cipher_test.gleam diff --git a/gleam.toml b/gleam.toml index a9758ab..f36b579 100644 --- a/gleam.toml +++ b/gleam.toml @@ -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" diff --git a/manifest.toml b/manifest.toml index cc5d56d..da14e3b 100644 --- a/manifest.toml +++ b/manifest.toml @@ -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" }, @@ -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" } diff --git a/scripts/key_gen.rb b/scripts/key_gen.rb new file mode 100755 index 0000000..8c37d7b --- /dev/null +++ b/scripts/key_gen.rb @@ -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(" ") diff --git a/src/cipher.gleam b/src/cipher.gleam new file mode 100644 index 0000000..4e317f2 --- /dev/null +++ b/src/cipher.gleam @@ -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) + <> -> { + // 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 = <> + + 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)) + } + } +} diff --git a/test/cipher_test.gleam b/test/cipher_test.gleam new file mode 100644 index 0000000..16090be --- /dev/null +++ b/test/cipher_test.gleam @@ -0,0 +1,190 @@ +import cipher +import gleam/bit_array +import gleam/result +import gleam/string +import gleam/yielder +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +pub fn new_seed_test() { + cipher.new_seed(-10) |> should.be_error() + cipher.new_seed(0) |> should.be_error() + cipher.new_seed(10) |> should.be_ok() +} + +pub fn new_version_test() { + cipher.new_version(-1, -1, -1, -1) |> should.be_error() + cipher.new_version(0, 0, 0, 0) |> should.be_ok() + cipher.new_version(7, 0, 62, 0) |> should.be_ok() +} + +const key_pairs = [ + #(#(7, 0, 99, 0), #(0x3A7731CD, 0xA9CE5E7F)), + #(#(7, 0, 98, 0), #(0x3AA8ABDD, 0xA9AB227F)), + #(#(7, 0, 97, 0), #(0x3AE221ED, 0xA9F47E7F)), + #(#(7, 0, 96, 0), #(0x3ADBA3FD, 0xA9E1527F)), + #(#(7, 0, 95, 0), #(0x3B1D220D, 0xA915BE7F)), + #(#(7, 0, 94, 0), #(0x3B46A81D, 0xA900A27F)), + #(#(7, 0, 93, 0), #(0x3B88322D, 0xA96F9E7F)), + #(#(7, 0, 92, 0), #(0x3BF1803D, 0xA94A127F)), + #(#(7, 0, 91, 0), #(0x3C3B124D, 0xAAA61E7F)), + #(#(7, 0, 90, 0), #(0x3C1CE85D, 0xAA75A27F)), + #(#(7, 0, 89, 0), #(0x3CD6426D, 0xAA193E7F)), + #(#(7, 0, 88, 0), #(0x3CEF207D, 0xAA1D527F)), + #(#(7, 0, 87, 0), #(0x3CA0828D, 0xAA317E7F)), + #(#(7, 0, 86, 0), #(0x3D7A689D, 0xAAE5227F)), + #(#(7, 0, 85, 0), #(0x3D33D2AD, 0xAAC95E7F)), + #(#(7, 0, 84, 0), #(0x3DC480BD, 0xAAAD127F)), + #(#(7, 0, 83, 0), #(0x3D8E72CD, 0xAA81DE7F)), + #(#(7, 0, 82, 0), #(0x3E5728DD, 0xAB14227F)), + #(#(7, 0, 81, 0), #(0x3E18E2ED, 0xAB38FE7F)), + #(#(7, 0, 80, 0), #(0x3E21A0FD, 0xAB3B527F)), + #(#(7, 0, 79, 0), #(0x3EEB630D, 0xAB543E7F)), + #(#(7, 0, 78, 0), #(0x3EAC291D, 0xAB41A27F)), + #(#(7, 0, 77, 0), #(0x3F65F32D, 0xABAD1E7F)), + #(#(7, 0, 76, 0), #(0x3F1E813D, 0xAB8A127F)), + #(#(7, 0, 75, 0), #(0x3FD0534D, 0xABE79E7F)), + #(#(7, 0, 74, 0), #(0x3F89695D, 0xABCCA27F)), + #(#(7, 0, 73, 0), #(0x2042036D, 0xA5D1BE7F)), + #(#(7, 0, 72, 0), #(0x207B217D, 0xA5C7527F)), + #(#(7, 0, 71, 0), #(0x203CC38D, 0xA5FDFE7F)), + #(#(7, 0, 70, 0), #(0x20E5E99D, 0xA598227F)), + #(#(7, 0, 69, 0), #(0x20AE93AD, 0xA586DE7F)), + #(#(7, 0, 68, 0), #(0x215781BD, 0xA57D127F)), + #(#(7, 0, 67, 0), #(0x2118B3CD, 0xA5535E7F)), + #(#(7, 0, 66, 0), #(0x21C1A9DD, 0xA521227F)), + #(#(7, 0, 65, 0), #(0x218AA3ED, 0xA50F7E7F)), + #(#(7, 0, 64, 0), #(0x21B3A1FD, 0xA515527F)), + #(#(7, 0, 63, 0), #(0x2244A40D, 0xA484BE7F)), + #(#(7, 0, 62, 0), #(0x221DAE1D, 0xA4A6A27F)), + #(#(7, 0, 61, 0), #(0x22D6B42D, 0xA4D89E7F)), + #(#(7, 0, 60, 0), #(0x22AF863D, 0xA4E2127F)), + #(#(7, 0, 59, 0), #(0x2360944D, 0xA40D1E7F)), + #(#(7, 0, 58, 0), #(0x2339EE5D, 0xA41FA27F)), + #(#(7, 0, 57, 0), #(0x23F2C46D, 0xA47E3E7F)), + #(#(7, 0, 56, 0), #(0x23CB267D, 0xA469527F)), + #(#(7, 0, 55, 0), #(0x238C048D, 0xA4527E7F)), + #(#(7, 0, 54, 0), #(0x24556E9D, 0xA7BB227F)), + #(#(7, 0, 53, 0), #(0x241E54AD, 0xA7945E7F)), + #(#(7, 0, 52, 0), #(0x24E686BD, 0xA715127F)), + #(#(7, 0, 51, 0), #(0x24AFF4CD, 0xA736DE7F)), + #(#(7, 0, 50, 0), #(0x25702EDD, 0xA7D6227F)), + #(#(7, 0, 49, 0), #(0x253964ED, 0xA7F7FE7F)), + #(#(7, 0, 48, 0), #(0x2501A6FD, 0xA7F7527F)), + #(#(7, 0, 47, 0), #(0x25CAE50D, 0xA79B3E7F)), + #(#(7, 0, 46, 0), #(0x25932F1D, 0xA7B3A27F)), + #(#(7, 0, 45, 0), #(0x2644752D, 0xA66A1E7F)), + #(#(7, 0, 44, 0), #(0x263C873D, 0xA652127F)), + #(#(7, 0, 43, 0), #(0x26F5D54D, 0xA63A9E7F)), + #(#(7, 0, 42, 0), #(0x26AE6F5D, 0xA612A27F)), + #(#(7, 0, 41, 0), #(0x2766856D, 0xA6EABE7F)), + #(#(7, 0, 40, 0), #(0x275F277D, 0xA6F3527F)), + #(#(7, 0, 39, 0), #(0x2710458D, 0xA6DAFE7F)), + #(#(7, 0, 38, 0), #(0x27C8EF9D, 0xA6B2227F)), + #(#(7, 0, 37, 0), #(0x278115AD, 0xA695DE7F)), + #(#(7, 0, 36, 0), #(0x287987BD, 0xA115127F)), + #(#(7, 0, 35, 0), #(0x283235CD, 0xA1345E7F)), + #(#(7, 0, 34, 0), #(0x28EAAFDD, 0xA157227F)), + #(#(7, 0, 33, 0), #(0x28A325ED, 0xA1767E7F)), + #(#(7, 0, 32, 0), #(0x289BA7FD, 0xA169527F)), + #(#(7, 0, 31, 0), #(0x295C260D, 0xA197BE7F)), + #(#(7, 0, 30, 0), #(0x2904AC1D, 0xA1BCA27F)), + #(#(7, 0, 29, 0), #(0x29CD362D, 0xA1D59E7F)), + #(#(7, 0, 28, 0), #(0x29B5843D, 0xA1EA127F)), + #(#(7, 0, 27, 0), #(0x2A7E164D, 0xA0081E7F)), + #(#(7, 0, 26, 0), #(0x2A26EC5D, 0xA019A27F)), + #(#(7, 0, 25, 0), #(0x2AEF466D, 0xA07F3E7F)), + #(#(7, 0, 24, 0), #(0x2AD7247D, 0xA065527F)), + #(#(7, 0, 23, 0), #(0x2A9F868D, 0xA0437E7F)), + #(#(7, 0, 22, 0), #(0x2B406C9D, 0xA0A1227F)), + #(#(7, 0, 21, 0), #(0x2B08D6AD, 0xA0875E7F)), + #(#(7, 0, 20, 0), #(0x2BF084BD, 0xA0FD127F)), + #(#(7, 0, 19, 0), #(0x2BB976CD, 0xA0DBDE7F)), + #(#(7, 0, 18, 0), #(0x2C612CDD, 0xA328227F)), + #(#(7, 0, 17, 0), #(0x2C29E6ED, 0xA30EFE7F)), + #(#(7, 0, 16, 0), #(0x2C11A4FD, 0xA313527F)), + #(#(7, 0, 15, 0), #(0x2CDA670D, 0xA3723E7F)), + #(#(7, 0, 14, 0), #(0x2C822D1D, 0xA35DA27F)), + #(#(7, 0, 13, 0), #(0x2D4AF72D, 0xA3B71E7F)), + #(#(7, 0, 12, 0), #(0x2D32853D, 0xA38A127F)), + #(#(7, 0, 11, 0), #(0x2DFB574D, 0xA3ED9E7F)), + #(#(7, 0, 10, 0), #(0x2DA36D5D, 0xA3C0A27F)), + #(#(7, 0, 9, 0), #(0x2E6B076D, 0xA223BE7F)), + #(#(7, 0, 8, 0), #(0x2E53257D, 0xA23F527F)), + #(#(7, 0, 7, 0), #(0x2E1BC78D, 0xA21BFE7F)), + #(#(7, 0, 6, 0), #(0x2EC3ED9D, 0xA274227F)), + #(#(7, 0, 5, 0), #(0x2E8B97AD, 0xA250DE7F)), + #(#(7, 0, 4, 0), #(0x2F7385BD, 0xA2AD127F)), + #(#(7, 0, 3, 0), #(0x2F3BB7CD, 0xA2895E7F)), + #(#(7, 0, 2, 0), #(0x2FE3ADDD, 0xA2E5227F)), + #(#(7, 0, 1, 0), #(0x2FABA7ED, 0xA2C17E7F)), + #(#(7, 0, 0, 0), #(0x2F93A5FD, 0xA2DD527F)), + #(#(6, 0, 14, 0), #(0x2C022D1D, 0xA31DA27F)), + #(#(6, 0, 13, 0), #(0x2DCAF72D, 0xA3F71E7F)), + #(#(6, 0, 12, 0), #(0x2DB2853D, 0xA3CA127F)), + #(#(6, 0, 11, 0), #(0x2D7B574D, 0xA3AD9E7F)), + #(#(6, 0, 10, 0), #(0x2D236D5D, 0xA380A27F)), + #(#(6, 0, 9, 0), #(0x2EEB076D, 0xA263BE7F)), + #(#(6, 0, 8, 0), #(0x2ED3257D, 0xA27F527F)), + #(#(6, 0, 7, 0), #(0x2E9BC78D, 0xA25BFE7F)), + #(#(6, 0, 6, 0), #(0x2E43ED9D, 0xA234227F)), + #(#(6, 0, 5, 0), #(0x2E0B97AD, 0xA210DE7F)), +] + +pub fn key_for_version_test() { + yielder.from_list(key_pairs) + |> yielder.each(fn(version_and_key_pair) { + let #(version, key_pair) = version_and_key_pair + let #(a, b, c, d) = version + let #(hi, lo) = key_pair + + let assert Ok(version) = cipher.new_version(a, b, c, d) + let assert Ok(key_pair) = cipher.key_for_version(version) + + key_pair.lo |> should.equal(lo) + key_pair.hi |> should.equal(hi) + }) +} + +pub fn key_for_version_203x_test() { + let assert Ok(version) = cipher.new_version(2, 0, 3, 0x78) + let assert Ok(key_pair) = cipher.key_for_version(version) + + key_pair.lo |> should.equal(0x2D13A5FD) + key_pair.hi |> should.equal(0xA39D527F) +} + +pub fn login_decrypt_test() { + // 192.168.68.60 + let assert Ok(seed) = cipher.new_seed(0xC0_A8_44_3C) + let assert Ok(version) = cipher.new_version(7, 0, 106, 21) + let packet = << + 22, 85, 134, 110, 22, 182, 112, 132, 182, 142, 146, 155, 168, 43, 234, 138, + 186, 34, 238, 8, 251, 130, 62, 96, 207, 24, 115, 70, 220, 145, 55, 148, 108, + 138, 240, 201, 79, 29, 172, 42, 192, 181, 136, 33, 111, 72, 91, 210, 150, 52, + 229, 13, 121, 195, 30, 240, 135, 188, 161, 175, 168, 43, + >> + let assert Ok(cipher) = cipher.login(seed, version) + let #(_cipher, plaintext) = cipher.decrypt(cipher, packet) + // + + let assert << + cmd:int, + account_name:bytes-size(30), + password:bytes-size(30), + next_key:int, + >> = plaintext + + cmd |> should.equal(0x80) + bit_array.to_string(account_name) + |> result.unwrap("") + |> should.equal("account1234" <> string.repeat("\u{0000}", 19)) + bit_array.to_string(password) + |> result.unwrap("") + |> should.equal("password1234" <> string.repeat("\u{0000}", 18)) + next_key |> should.equal(0x00) +}