diff --git a/src/xenia/kernel/CMakeLists.txt b/src/xenia/kernel/CMakeLists.txt index 766ec05e1..fefff2234 100644 --- a/src/xenia/kernel/CMakeLists.txt +++ b/src/xenia/kernel/CMakeLists.txt @@ -21,3 +21,8 @@ target_link_libraries(xenia-kernel PUBLIC aes_128 fmt zlib-ng pugixml xenia-apu xenia-base xenia-cpu xenia-hid xenia-vfs ) xe_target_defaults(xenia-kernel) + +if(XENIA_BUILD_TESTS) + set(CMAKE_FOLDER "tests") + add_subdirectory(testing) +endif() diff --git a/src/xenia/kernel/testing/CMakeLists.txt b/src/xenia/kernel/testing/CMakeLists.txt new file mode 100644 index 000000000..9b95093c2 --- /dev/null +++ b/src/xenia/kernel/testing/CMakeLists.txt @@ -0,0 +1,3 @@ +xe_test_suite(xenia-kernel-tests ${CMAKE_CURRENT_SOURCE_DIR} + LINKS fmt xenia-base +) diff --git a/src/xenia/kernel/testing/bignum_rsa_test.cc b/src/xenia/kernel/testing/bignum_rsa_test.cc new file mode 100644 index 000000000..75e03e37b --- /dev/null +++ b/src/xenia/kernel/testing/bignum_rsa_test.cc @@ -0,0 +1,159 @@ +#include "third_party/catch/include/catch.hpp" +#include "xenia/kernel/xboxkrnl/xecrypt_rsa.h" + +namespace { + +// Helper to build an XECRYPT_RSA-compatible blob in memory. +// Layout: [4 bytes size BE][4 bytes exponent BE][8 bytes pad][modulus limbs...] +std::vector build_rsa_blob(uint32_t num_qwords, uint32_t exponent, + const uint8_t* modulus_limbs) { + std::vector blob(0x10 + num_qwords * 8); + // size (big-endian) + blob[0] = (num_qwords >> 24) & 0xFF; + blob[1] = (num_qwords >> 16) & 0xFF; + blob[2] = (num_qwords >> 8) & 0xFF; + blob[3] = num_qwords & 0xFF; + // exponent (big-endian) + blob[4] = (exponent >> 24) & 0xFF; + blob[5] = (exponent >> 16) & 0xFF; + blob[6] = (exponent >> 8) & 0xFF; + blob[7] = exponent & 0xFF; + // pad = 0 + // modulus data + std::memcpy(blob.data() + 0x10, modulus_limbs, num_qwords * 8); + return blob; +} + +// Pirs public key modulus (32 qwords = 2048 bits), exponent = 3 +static const uint8_t pirs_modulus[] = { + 0xE6, 0x3B, 0x32, 0xB2, 0x8D, 0x9E, 0x9E, 0xE7, 0x9D, 0xFC, 0x5C, 0x72, + 0x41, 0x94, 0x58, 0x47, 0xDE, 0x0D, 0x18, 0x40, 0x72, 0xD6, 0xE3, 0x46, + 0x8E, 0xBA, 0x8E, 0xBC, 0x1A, 0x90, 0xAC, 0x20, 0xBA, 0x03, 0x85, 0xB5, + 0x1A, 0x3E, 0x25, 0xF9, 0xA6, 0x58, 0xEB, 0xB6, 0xA3, 0xC4, 0xA3, 0xEE, + 0xB2, 0xB0, 0xAE, 0x97, 0x69, 0xEB, 0xFE, 0x71, 0xFC, 0x02, 0xAB, 0x77, + 0xBA, 0xC8, 0xE6, 0x74, 0xE6, 0x7C, 0x63, 0x0E, 0xAF, 0x4C, 0xF7, 0xE7, + 0x11, 0x4A, 0x80, 0x24, 0x72, 0x05, 0x7A, 0x63, 0xD0, 0xF8, 0x91, 0x02, + 0xA6, 0xE7, 0x7D, 0x77, 0xC5, 0xA7, 0x9B, 0x08, 0x11, 0x2E, 0xA0, 0x64, + 0x45, 0x60, 0x46, 0xBC, 0x36, 0xE1, 0x17, 0x71, 0xBE, 0x66, 0x49, 0x2F, + 0xAE, 0x20, 0xA4, 0x76, 0x9C, 0x27, 0x51, 0xCF, 0x4B, 0x34, 0x7A, 0x35, + 0xBC, 0xA4, 0xAA, 0x1C, 0x47, 0x4B, 0xF4, 0x97, 0x22, 0x4E, 0x13, 0x24, + 0xD3, 0xC1, 0x57, 0xDF, 0x4D, 0x84, 0xB9, 0x18, 0x97, 0x99, 0xAC, 0x00, + 0xB3, 0x3D, 0x03, 0x25, 0x60, 0xC8, 0x7A, 0x59, 0xFE, 0x48, 0xFF, 0x28, + 0x3D, 0x10, 0xBB, 0x9E, 0x09, 0x06, 0x2A, 0x61, 0x20, 0x2C, 0xF8, 0x72, + 0xEB, 0x87, 0xE6, 0xD1, 0xFB, 0xB3, 0x66, 0xFC, 0x4A, 0x02, 0xAE, 0xD4, + 0xD8, 0x37, 0xCF, 0xA6, 0x32, 0x25, 0x79, 0x36, 0x0E, 0xF4, 0xED, 0x19, + 0xA2, 0x10, 0x27, 0x96, 0x2F, 0x9F, 0xA9, 0x3D, 0xA4, 0x37, 0x30, 0x11, + 0x51, 0x83, 0xBD, 0xF7, 0xC7, 0xE5, 0xCE, 0xAA, 0xEC, 0xDE, 0x48, 0xA0, + 0x84, 0xF7, 0xB0, 0xF6, 0x4B, 0x8E, 0xF0, 0x89, 0xBD, 0x47, 0x7C, 0x90, + 0xDD, 0x88, 0x12, 0x17, 0x40, 0xD2, 0x4E, 0xA6, 0xC6, 0x11, 0x04, 0x1B, + 0x57, 0xA8, 0x68, 0xB4, 0x61, 0xF4, 0x1B, 0xC6, 0x8B, 0xE8, 0xD9, 0x20, + 0xF2, 0x05, 0xE0, 0x70, +}; + +} // namespace + +TEST_CASE("XeCryptBnQwNeRsaPubCrypt_rejects_small_keys", "[crypt]") { + // Keys below 512 bits (8 qwords) must be rejected + const uint8_t modulus[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + }; + const uint8_t input[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + }; + uint8_t output[8] = {}; + + REQUIRE(XeCryptBnQwNeRsaPubCrypt(input, output, modulus, 1, 3) == 0); + + const uint8_t modulus2[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, + }; + const uint8_t input2[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + uint8_t output2[16] = {}; + + REQUIRE(XeCryptBnQwNeRsaPubCrypt(input2, output2, modulus2, 2, 65537) == 0); +} + +TEST_CASE("XeCryptBnQwNeRsaPubCrypt_2048bit_identity", "[crypt]") { + // 1^3 mod modulus = 1 + uint8_t input[256] = {}; + input[7] = 0x01; + + uint8_t output[256] = {}; + uint32_t ret = XeCryptBnQwNeRsaPubCrypt(input, output, pirs_modulus, 0x20, 3); + + REQUIRE(ret == 1); + uint8_t expected[256] = {}; + expected[7] = 0x01; + REQUIRE(std::memcmp(output, expected, 256) == 0); +} + +TEST_CASE("XeCryptBnQwNeRsaPubCrypt_2048bit_small_base", "[crypt]") { + // 2^3 = 8 < modulus, so result is 8 + uint8_t input[256] = {}; + input[7] = 0x02; + + uint8_t output[256] = {}; + uint32_t ret = XeCryptBnQwNeRsaPubCrypt(input, output, pirs_modulus, 0x20, 3); + + REQUIRE(ret == 1); + uint8_t expected[256] = {}; + expected[7] = 0x08; + REQUIRE(std::memcmp(output, expected, 256) == 0); +} + +TEST_CASE("XeCryptBnQwNeRsaPubCrypt_2048bit_large_base", "[crypt]") { + // Large base that exercises modular reduction + uint8_t input[256]; + for (int i = 0; i < 256; i++) { + input[i] = static_cast(i + 1); + } + // Ensure top limb is smaller than modulus top limb (0xF2...) + input[248] = 0x01; + input[249] = 0x02; + input[250] = 0x03; + input[251] = 0x04; + input[252] = 0x05; + input[253] = 0x06; + input[254] = 0x07; + input[255] = 0x08; + + uint8_t output[256] = {}; + uint32_t ret = XeCryptBnQwNeRsaPubCrypt(input, output, pirs_modulus, 0x20, 3); + REQUIRE(ret == 1); + + // Deterministic: same input produces same output + uint8_t output2[256] = {}; + uint32_t ret2 = + XeCryptBnQwNeRsaPubCrypt(input, output2, pirs_modulus, 0x20, 3); + REQUIRE(ret2 == 1); + REQUIRE(std::memcmp(output, output2, 256) == 0); + + // Non-trivial result + bool all_zero = true; + for (int i = 0; i < 256; i++) { + if (output[i] != 0) { + all_zero = false; + break; + } + } + REQUIRE_FALSE(all_zero); +} + +TEST_CASE("XeCryptBnQwNeRsaPubCrypt_2048bit_zero_base", "[crypt]") { + // 0^e mod m = 0 + uint8_t input[256] = {}; + uint8_t output[256]; + std::memset(output, 0xFF, 256); + + uint32_t ret = + XeCryptBnQwNeRsaPubCrypt(input, output, pirs_modulus, 0x20, 65537); + + REQUIRE(ret == 1); + for (int i = 0; i < 256; i++) { + REQUIRE(output[i] == 0x00); + } +} diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_crypt.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_crypt.cc index 3c920dc70..1e11bd462 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_crypt.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_crypt.cc @@ -15,10 +15,6 @@ #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_private.h" -#ifdef XE_PLATFORM_WIN32 -#include "xenia/base/platform_win.h" // for bcrypt.h -#endif - #include "third_party/crypto/TinySHA1.hpp" #include "third_party/crypto/des/des.cpp" #include "third_party/crypto/des/des.h" @@ -26,6 +22,7 @@ #include "third_party/crypto/des/descbc.h" #include "third_party/crypto/sha256.cpp" #include "third_party/crypto/sha256.h" +#include "xenia/kernel/xboxkrnl/xecrypt_rsa.h" extern "C" { #include "third_party/FFmpeg/libavutil/md5.h" @@ -697,105 +694,17 @@ DECLARE_XBOXKRNL_EXPORT1(XeCryptBnQw_SwapDwQwLeBe, kNone, kImplemented); dword_result_t XeCryptBnQwNeRsaPubCrypt_entry(pointer_t qw_a, pointer_t qw_b, pointer_t rsa) { - // 0 indicates failure (but not a BOOL return value) -#ifndef XE_PLATFORM_WIN32 - XELOGE( - "XeCryptBnQwNeRsaPubCrypt called but no implementation available for " - "this platform!"); - assert_always(); - return 1; -#else - uint32_t modulus_size = rsa->size * 8; + uint32_t num_qwords = rsa->size; + uint32_t exponent = rsa->public_exponent; + const uint8_t* input_bytes = reinterpret_cast(&qw_a[0]); + uint8_t* output_bytes = reinterpret_cast(&qw_b[0]); + const uint8_t* mod_bytes = + reinterpret_cast(&rsa[1]); // modulus follows header - // Convert XECRYPT blob into BCrypt format - ULONG key_size = sizeof(BCRYPT_RSAKEY_BLOB) + sizeof(uint32_t) + modulus_size; - auto key_buf = std::make_unique(key_size); - auto* key_header = reinterpret_cast(key_buf.get()); - - key_header->Magic = BCRYPT_RSAPUBLIC_MAGIC; - key_header->BitLength = modulus_size * 8; - key_header->cbPublicExp = sizeof(uint32_t); - key_header->cbModulus = modulus_size; - key_header->cbPrime1 = key_header->cbPrime2 = 0; - - // Copy in exponent/modulus, luckily these are BE inside BCrypt blob - uint32_t* key_exponent = reinterpret_cast(&key_header[1]); - *key_exponent = rsa->public_exponent.value; - - // ...except modulus needs to be reversed in 64-bit chunks for BCrypt to make - // use of it properly for some reason - uint64_t* key_modulus = reinterpret_cast(&key_exponent[1]); - uint64_t* xecrypt_modulus = reinterpret_cast(&rsa[1]); - std::reverse_copy(xecrypt_modulus, xecrypt_modulus + rsa->size, key_modulus); - - BCRYPT_ALG_HANDLE hAlgorithm = NULL; - NTSTATUS status = BCryptOpenAlgorithmProvider( - &hAlgorithm, BCRYPT_RSA_ALGORITHM, MS_PRIMITIVE_PROVIDER, 0); - - if (!BCRYPT_SUCCESS(status)) { - XELOGE( - "XeCryptBnQwNeRsaPubCrypt: BCryptOpenAlgorithmProvider failed with " - "status {:#X}!", - status); - return 0; - } - - BCRYPT_KEY_HANDLE hKey = NULL; - status = BCryptImportKeyPair(hAlgorithm, NULL, BCRYPT_RSAPUBLIC_BLOB, &hKey, - key_buf.get(), key_size, 0); - - if (!BCRYPT_SUCCESS(status)) { - XELOGE( - "XeCryptBnQwNeRsaPubCrypt: BCryptImportKeyPair failed with status " - "{:#X}!", - status); - - if (hAlgorithm) { - BCryptCloseAlgorithmProvider(hAlgorithm, 0); - } - - return 0; - } - - // Byteswap & reverse the input into output, as BCrypt wants MSB first - uint64_t* output = qw_b; - uint8_t* output_bytes = reinterpret_cast(output); - xe::copy_and_swap(output, qw_a, rsa->size); - std::reverse(output_bytes, output_bytes + modulus_size); - - // BCryptDecrypt only works with private keys, fortunately BCryptEncrypt - // performs the right actions needed for us to decrypt the input - ULONG result_size = 0; - status = - BCryptEncrypt(hKey, output_bytes, modulus_size, nullptr, nullptr, 0, - output_bytes, modulus_size, &result_size, BCRYPT_PAD_NONE); - - assert(result_size == modulus_size); - - if (!BCRYPT_SUCCESS(status)) { - XELOGE("XeCryptBnQwNeRsaPubCrypt: BCryptEncrypt failed with status {:#X}!", - status); - } else { - // Reverse data & byteswap again so data is as game expects - std::reverse(output_bytes, output_bytes + modulus_size); - xe::copy_and_swap(output, output, rsa->size); - } - - if (hKey) { - BCryptDestroyKey(hKey); - } - if (hAlgorithm) { - BCryptCloseAlgorithmProvider(hAlgorithm, 0); - } - - return BCRYPT_SUCCESS(status) ? 1 : 0; -#endif + return XeCryptBnQwNeRsaPubCrypt(input_bytes, output_bytes, mod_bytes, + num_qwords, exponent); } -#ifdef XE_PLATFORM_WIN32 DECLARE_XBOXKRNL_EXPORT1(XeCryptBnQwNeRsaPubCrypt, kNone, kImplemented); -#else -DECLARE_XBOXKRNL_EXPORT1(XeCryptBnQwNeRsaPubCrypt, kNone, kStub); -#endif dword_result_t XeCryptBnQwBeSigVerify_entry(pointer_t sig, lpvoid_t hash, lpstring_t salt, diff --git a/src/xenia/kernel/xboxkrnl/xecrypt_rsa.h b/src/xenia/kernel/xboxkrnl/xecrypt_rsa.h new file mode 100644 index 000000000..6e3533be7 --- /dev/null +++ b/src/xenia/kernel/xboxkrnl/xecrypt_rsa.h @@ -0,0 +1,430 @@ +#ifndef XECRYPT_RSA_H_ +#define XECRYPT_RSA_H_ + +#include +#include +#include +#include + +namespace xecrypt { +namespace bignum { + +// Portable 128-bit unsigned helpers using only 64-bit arithmetic +struct u128 { + uint64_t lo; + uint64_t hi; +}; + +static inline u128 u128_from(uint64_t v) { return {v, 0}; } + +static inline u128 u128_add(u128 a, uint64_t b) { + u128 r; + r.lo = a.lo + b; + r.hi = a.hi + (r.lo < a.lo ? 1 : 0); + return r; +} + +static inline u128 u128_sub(u128 a, uint64_t b) { + u128 r; + r.hi = a.hi - (a.lo < b ? 1 : 0); + r.lo = a.lo - b; + return r; +} + +static inline u128 u128_shl(u128 v, int s) { + if (s == 0) return v; + if (s >= 64) return {0, v.lo << (s - 64)}; + return {v.lo << s, (v.hi << s) | (v.lo >> (64 - s))}; +} + +static inline u128 u128_shr(u128 v, int s) { + if (s == 0) return v; + if (s >= 64) return {v.hi >> (s - 64), 0}; + return {(v.lo >> s) | (v.hi << (64 - s)), v.hi >> s}; +} + +static inline u128 u128_or64(u128 a, uint64_t b) { return {a.lo | b, a.hi}; } + +// Portable 64x64 -> 128 multiply +static inline u128 u128_mul64(uint64_t a, uint64_t b) { + uint64_t a_lo = a & 0xFFFFFFFF; + uint64_t a_hi = a >> 32; + uint64_t b_lo = b & 0xFFFFFFFF; + uint64_t b_hi = b >> 32; + + uint64_t p0 = a_lo * b_lo; + uint64_t p1 = a_lo * b_hi; + uint64_t p2 = a_hi * b_lo; + uint64_t p3 = a_hi * b_hi; + + uint64_t mid = p1 + (p0 >> 32); + uint64_t mid_carry = 0; + uint64_t mid2 = mid + p2; + if (mid < p1) mid_carry++; + if (mid2 < mid) mid_carry++; + + u128 r; + r.lo = (mid2 << 32) | (p0 & 0xFFFFFFFF); + r.hi = p3 + (mid2 >> 32) + (mid_carry << 32); + return r; +} + +// 128 / 64 -> quotient and remainder +static inline uint64_t u128_div64(u128 num, uint64_t den, uint64_t* rem) { + if (num.hi == 0) { + *rem = num.lo % den; + return num.lo / den; + } + + uint64_t q = 0; + uint64_t r = 0; + for (int i = 127; i >= 0; i--) { + r = (r << 1); + if (i >= 64) { + r |= (num.hi >> (i - 64)) & 1; + } else { + r |= (num.lo >> i) & 1; + } + if (r >= den) { + r -= den; + if (i < 64) { + q |= (1ULL << i); + } + } + } + *rem = r; + return q; +} + +// Count leading zeros - portable +static inline int clz64(uint64_t v) { + if (v == 0) return 64; + int n = 0; + if ((v & 0xFFFFFFFF00000000ULL) == 0) { + n += 32; + v <<= 32; + } + if ((v & 0xFFFF000000000000ULL) == 0) { + n += 16; + v <<= 16; + } + if ((v & 0xFF00000000000000ULL) == 0) { + n += 8; + v <<= 8; + } + if ((v & 0xF000000000000000ULL) == 0) { + n += 4; + v <<= 4; + } + if ((v & 0xC000000000000000ULL) == 0) { + n += 2; + v <<= 2; + } + if ((v & 0x8000000000000000ULL) == 0) { + n += 1; + } + return n; +} + +static inline int clz32(uint32_t v) { + if (v == 0) return 32; + int n = 0; + if ((v & 0xFFFF0000U) == 0) { + n += 16; + v <<= 16; + } + if ((v & 0xFF000000U) == 0) { + n += 8; + v <<= 8; + } + if ((v & 0xF0000000U) == 0) { + n += 4; + v <<= 4; + } + if ((v & 0xC0000000U) == 0) { + n += 2; + v <<= 2; + } + if ((v & 0x80000000U) == 0) { + n += 1; + } + return n; +} + +class BigNum { + public: + std::vector limbs; + + BigNum() = default; + + void trim() { + while (limbs.size() > 1 && limbs.back() == 0) { + limbs.pop_back(); + } + } + + static BigNum from_bytes_be(const uint8_t* data, size_t len) { + BigNum r; + size_t n = (len + 7) / 8; + r.limbs.resize(n, 0); + for (size_t i = 0; i < len; i++) { + size_t byte_pos = len - 1 - i; + r.limbs[byte_pos / 8] |= static_cast(data[i]) + << (8 * (byte_pos % 8)); + } + r.trim(); + return r; + } + + void to_bytes_be(uint8_t* out, size_t len) const { + std::memset(out, 0, len); + for (size_t i = 0; i < len; i++) { + size_t byte_pos = len - 1 - i; + size_t li = byte_pos / 8; + if (li < limbs.size()) { + out[i] = static_cast(limbs[li] >> (8 * (byte_pos % 8))); + } + } + } + + static int compare(const BigNum& a, const BigNum& b) { + size_t an = a.limbs.size(), bn = b.limbs.size(); + size_t n = std::max(an, bn); + for (size_t i = n; i > 0; i--) { + uint64_t al = (i - 1 < an) ? a.limbs[i - 1] : 0; + uint64_t bl = (i - 1 < bn) ? b.limbs[i - 1] : 0; + if (al < bl) return -1; + if (al > bl) return 1; + } + return 0; + } + + static BigNum sub(const BigNum& a, const BigNum& b) { + BigNum r; + size_t n = a.limbs.size(); + r.limbs.resize(n, 0); + uint64_t borrow = 0; + for (size_t i = 0; i < n; i++) { + uint64_t bl = (i < b.limbs.size()) ? b.limbs[i] : 0; + u128 diff = u128_sub(u128_sub(u128_from(a.limbs[i]), bl), borrow); + r.limbs[i] = diff.lo; + borrow = (diff.hi >> 63) ? 1 : 0; + } + r.trim(); + return r; + } + + static BigNum mul(const BigNum& a, const BigNum& b) { + size_t an = a.limbs.size(), bn = b.limbs.size(); + BigNum r; + r.limbs.resize(an + bn, 0); + for (size_t i = 0; i < an; i++) { + uint64_t carry = 0; + for (size_t j = 0; j < bn; j++) { + u128 prod = u128_mul64(a.limbs[i], b.limbs[j]); + prod = u128_add(prod, r.limbs[i + j]); + prod = u128_add(prod, carry); + r.limbs[i + j] = prod.lo; + carry = prod.hi; + } + r.limbs[i + bn] += carry; + } + r.trim(); + return r; + } + + static BigNum mod(const BigNum& a, const BigNum& m) { + if (compare(a, m) < 0) return a; + + size_t n = m.limbs.size(); + size_t total = a.limbs.size(); + + if (n == 0 || (n == 1 && m.limbs[0] == 0)) { + return BigNum(); + } + + if (n == 1) { + uint64_t d = m.limbs[0]; + uint64_t rem = 0; + for (size_t i = total; i > 0; i--) { + u128 cur = u128_or64(u128_shl(u128_from(rem), 64), a.limbs[i - 1]); + u128_div64(cur, d, &rem); + } + BigNum r; + r.limbs = {rem}; + r.trim(); + return r; + } + + int shift = 0; + uint64_t top = m.limbs[n - 1]; + if (top != 0) { + shift = clz64(top); + } + + BigNum u, v; + u.limbs.resize(total + 1, 0); + if (shift > 0) { + uint64_t carry = 0; + for (size_t i = 0; i < total; i++) { + u128 val = u128_or64(u128_shl(u128_from(a.limbs[i]), shift), carry); + u.limbs[i] = val.lo; + carry = val.hi; + } + u.limbs[total] = carry; + } else { + for (size_t i = 0; i < total; i++) u.limbs[i] = a.limbs[i]; + u.limbs[total] = 0; + } + + v.limbs.resize(n, 0); + if (shift > 0) { + uint64_t carry = 0; + for (size_t i = 0; i < n; i++) { + u128 val = u128_or64(u128_shl(u128_from(m.limbs[i]), shift), carry); + v.limbs[i] = val.lo; + carry = val.hi; + } + } else { + v.limbs = m.limbs; + } + + uint64_t vn_1 = v.limbs[n - 1]; + uint64_t vn_2 = (n >= 2) ? v.limbs[n - 2] : 0; + + for (size_t j = total; j >= n; j--) { + u128 num_top = + u128_or64(u128_shl(u128_from(u.limbs[j]), 64), u.limbs[j - 1]); + uint64_t rhat_val; + uint64_t qhat_val = u128_div64(num_top, vn_1, &rhat_val); + + while (true) { + u128 qv2 = u128_mul64(qhat_val, vn_2); + u128 rhs = u128_or64(u128_shl(u128_from(rhat_val), 64), u.limbs[j - 2]); + bool gt = (qv2.hi > rhs.hi) || (qv2.hi == rhs.hi && qv2.lo > rhs.lo); + if (!gt) break; + qhat_val--; + uint64_t old_rhat = rhat_val; + rhat_val += vn_1; + if (rhat_val < old_rhat) break; + } + + uint64_t carry = 0; + for (size_t i = 0; i < n; i++) { + u128 prod = u128_mul64(qhat_val, v.limbs[i]); + prod = u128_add(prod, carry); + uint64_t prod_lo = prod.lo; + carry = prod.hi; + uint64_t u_val = u.limbs[j - n + i]; + u.limbs[j - n + i] = u_val - prod_lo; + if (u_val < prod_lo) carry++; + } + int64_t final_diff = + static_cast(u.limbs[j]) - static_cast(carry); + u.limbs[j] = static_cast(final_diff); + + if (final_diff < 0) { + uint64_t c = 0; + for (size_t i = 0; i < n; i++) { + u128 sum = u128_add(u128_from(u.limbs[j - n + i]), v.limbs[i]); + sum = u128_add(sum, c); + u.limbs[j - n + i] = sum.lo; + c = sum.hi; + } + u.limbs[j] += c; + } + } + + BigNum r; + r.limbs.resize(n, 0); + if (shift > 0) { + uint64_t carry = 0; + for (size_t i = n; i > 0; i--) { + u128 val = u128_or64(u128_shl(u128_from(carry), 64), u.limbs[i - 1]); + r.limbs[i - 1] = u128_shr(val, shift).lo; + carry = u.limbs[i - 1] & ((1ULL << shift) - 1); + } + } else { + for (size_t i = 0; i < n; i++) r.limbs[i] = u.limbs[i]; + } + + r.trim(); + return r; + } + + static BigNum modexp(const BigNum& base, uint32_t exp, + const BigNum& mod_val) { + BigNum result; + result.limbs = {1}; + + if (exp == 0) { + return mod(result, mod_val); + } + + int highest_bit = 31 - clz32(exp); + BigNum b = mod(base, mod_val); + + for (int i = highest_bit; i >= 0; i--) { + result = mod(mul(result, result), mod_val); + if ((exp >> i) & 1) { + result = mod(mul(result, b), mod_val); + } + } + + return result; + } +}; + +} // namespace bignum +} // namespace xecrypt + +// Performs RSA public-key encryption/decryption matching +// XeCryptBnQwNeRsaPubCrypt. All buffers use the Xbox 360 layout: big-endian +// uint64 limbs in little-endian limb order. +// +// qw_a: input data (num_qwords * 8 bytes) +// qw_b: output buffer (num_qwords * 8 bytes) +// modulus: RSA modulus following the XECRYPT_RSA header (num_qwords * 8 bytes) +// num_qwords: number of 64-bit limbs +// exponent: public exponent +// +// Returns 1 on success, 0 on failure. +inline uint32_t XeCryptBnQwNeRsaPubCrypt(const uint8_t* qw_a, uint8_t* qw_b, + const uint8_t* modulus, + uint32_t num_qwords, + uint32_t exponent) { + // Reject keys below 512 bits (8 qwords) to match original BCrypt behavior. + if (num_qwords < 8) { + return 0; + } + + uint32_t modulus_size = num_qwords * 8; + + auto input_be = std::vector(modulus_size); + auto mod_be = std::vector(modulus_size); + + // Reverse qword order to produce big-endian byte arrays + for (uint32_t i = 0; i < num_qwords; i++) { + std::memcpy(&input_be[i * 8], &qw_a[(num_qwords - 1 - i) * 8], 8); + std::memcpy(&mod_be[i * 8], &modulus[(num_qwords - 1 - i) * 8], 8); + } + + auto base = + xecrypt::bignum::BigNum::from_bytes_be(input_be.data(), modulus_size); + auto mod = + xecrypt::bignum::BigNum::from_bytes_be(mod_be.data(), modulus_size); + + auto result = xecrypt::bignum::BigNum::modexp(base, exponent, mod); + + auto result_be = std::vector(modulus_size); + result.to_bytes_be(result_be.data(), modulus_size); + + // Convert back to Xbox format: reverse qword order + for (uint32_t i = 0; i < num_qwords; i++) { + std::memcpy(&qw_b[i * 8], &result_be[(num_qwords - 1 - i) * 8], 8); + } + + return 1; +} + +#endif // XECRYPT_RSA_H_