diff --git a/CMakeLists.txt b/CMakeLists.txt index ccbf9e0423..274c3c9823 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.22) -# Workaround for mbedtls as it doesn't work with CMake versions below 3.5 +# Set minimum CMake policy version set(CMAKE_POLICY_VERSION_MINIMUM 3.5) project(citron) diff --git a/CMakeModules/dependencies.cmake b/CMakeModules/dependencies.cmake index 27b286b427..dd5e5ae815 100644 --- a/CMakeModules/dependencies.cmake +++ b/CMakeModules/dependencies.cmake @@ -210,21 +210,13 @@ if (NOT TARGET SimpleIni::SimpleIni) ) endif() -# ── cpp-jwt ─────────────────────────────────────────────────────────────────── -if (ENABLE_WEB_SERVICE AND NOT TARGET cpp-jwt::cpp-jwt) - CPMAddPackage( - NAME cpp-jwt - GITHUB_REPOSITORY arun11299/cpp-jwt - GIT_TAG v1.4 - DOWNLOAD_ONLY TRUE - ) - if (cpp-jwt_ADDED) - add_library(cpp-jwt::cpp-jwt INTERFACE IMPORTED GLOBAL) - target_include_directories(cpp-jwt::cpp-jwt INTERFACE "${cpp-jwt_SOURCE_DIR}/include") - endif() -endif() +# cpp-jwt removed: JWT RS256 verification is now done directly with OpenSSL EVP +# in web_service/verify_user_jwt.cpp. No external JWT library needed. # ── cpp-httplib ─────────────────────────────────────────────────────────────── +# OpenSSL TLS support via CPPHTTPLIB_OPENSSL_SUPPORT. +# web_backend.cpp instantiates httplib::Client with an https:// host which +# auto-upgrades to TLS through this path. if (ENABLE_WEB_SERVICE AND NOT TARGET httplib::httplib) CPMAddPackage( NAME httplib @@ -238,12 +230,10 @@ if (ENABLE_WEB_SERVICE AND NOT TARGET httplib::httplib) target_include_directories(httplib::httplib SYSTEM INTERFACE "${httplib_SOURCE_DIR}") target_compile_definitions(httplib::httplib INTERFACE CPPHTTPLIB_OPENSSL_SUPPORT) target_link_libraries(httplib::httplib INTERFACE - $<$:Threads::Threads> OpenSSL::SSL OpenSSL::Crypto + $<$:Threads::Threads> $<$:ws2_32> - $<$:crypt32> - $<$:cryptui> ) endif() endif() @@ -402,26 +392,8 @@ endif() # Forked repos — pinned to exact SHAs # ═══════════════════════════════════════════════════════════════════════════════ -# ── mbedtls (yuzu-mirror fork) ──────────────────────────────────────────────── -if (NOT TARGET mbedtls) - CPMAddPackage( - NAME mbedtls - GITHUB_REPOSITORY yuzu-mirror/mbedtls - GIT_TAG 8c88150ca139e06aa2aae8349df8292a88148ea1 - OPTIONS - "ENABLE_TESTING OFF" - "ENABLE_PROGRAMS OFF" - "MBEDTLS_FATAL_WARNINGS OFF" - ) - if (TARGET mbedtls) - target_include_directories(mbedtls PUBLIC ${mbedtls_SOURCE_DIR}/include) - endif() - if (TARGET mbedtls AND NOT MSVC) - target_compile_options(mbedcrypto PRIVATE - -Wno-unused-but-set-variable - -Wno-string-concatenation) - endif() -endif() +# mbedtls removed: all mbedtls usage replaced with OpenSSL EVP and AES-NI +# intrinsics. OpenSSL is built via openssl_build.cmake (CPM). # ── oaknut (yuzu-mirror fork) — AArch64 only ────────────────────────────────── if (ARCHITECTURE_arm64 AND NOT TARGET merry::oaknut) diff --git a/CMakeModules/openssl_build.cmake b/CMakeModules/openssl_build.cmake index 9b0864fc7e..c7b70588df 100644 --- a/CMakeModules/openssl_build.cmake +++ b/CMakeModules/openssl_build.cmake @@ -226,6 +226,28 @@ message(STATUS "[OpenSSL] Building OpenSSL ${_OPENSSL_VERSION} from source (stat file(MAKE_DIRECTORY "${_OPENSSL_BUILD_DIR}") +# AES-NI / assembly: OpenSSL's x86_64 assembly (aesni-x86_64.pl, sha256-x86_64.pl, etc.) +# requires NASM as a host tool. Both build scripts already install nasm, so we +# enable assembly unconditionally here. If NASM is not found we emit a clear +# fatal error rather than silently falling back to the slow pure-C AES path. +# +# Note: for the Linux→Windows cross-compile (Case 2), OpenSSL's mingw64 target +# generates x86_64 NASM object files using the *host* nasm binary — not a +# prefixed cross-tool — so the same nasm package used for FFmpeg works here too. +find_program(_OPENSSL_NASM nasm) +if (NOT _OPENSSL_NASM) + message(FATAL_ERROR + "[OpenSSL] NASM not found in PATH. OpenSSL's AES-NI / SHA-NI assembly " + "optimisations require NASM.\n" + " Ubuntu/Debian : sudo apt-get install nasm\n" + " Fedora/RHEL : sudo dnf install nasm\n" + " openSUSE : sudo zypper install nasm\n" + " MSYS2 : pacman -S mingw-w64-clang-x86_64-nasm\n" + "Both build scripts (build-citron-linux.sh, build-clangtron-windows.sh) " + "already install nasm as part of their dependency setup.") +endif() +message(STATUS "[OpenSSL] NASM found: ${_OPENSSL_NASM}") + # Build Configure argument list set(_OPENSSL_CONFIGURE_ARGS ${_OPENSSL_TARGET} @@ -235,7 +257,6 @@ set(_OPENSSL_CONFIGURE_ARGS no-tests no-docs no-apps - no-asm no-capieng no-winstore ) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 339c2a244a..df37554b25 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -56,16 +56,8 @@ endif() # Glad add_subdirectory(glad) -# mbedtls -if (NOT TARGET mbedtls) - add_subdirectory(mbedtls) - target_include_directories(mbedtls PUBLIC ./mbedtls/include) - if (NOT MSVC) - target_compile_options(mbedcrypto PRIVATE - -Wno-unused-but-set-variable - -Wno-string-concatenation) - endif() -endif() +# mbedtls removed: replaced by OpenSSL. Submodule kept on disk for +# history but no longer built. CMake no longer adds it. # SPIRV-Headers if (NOT TARGET SPIRV-Headers) @@ -166,15 +158,9 @@ if (ENABLE_WEB_SERVICE AND NOT TARGET httplib::httplib) endif() endif() -# cpp-jwt -if (ENABLE_WEB_SERVICE AND NOT TARGET cpp-jwt::cpp-jwt) - set(CPP_JWT_BUILD_EXAMPLES OFF) - set(CPP_JWT_BUILD_TESTS OFF) - set(CPP_JWT_USE_VENDORED_NLOHMANN_JSON OFF) - if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cpp-jwt/CMakeLists.txt") - add_subdirectory(cpp-jwt) - endif() -endif() +# cpp-jwt removed: JWT verification rewritten to use OpenSSL EVP_DigestVerify* +# directly in verify_user_jwt.cpp. Submodule kept on disk for history but no +# longer built. # Opus if (NOT TARGET Opus::opus) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6120178793..84aec73a4d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1256,7 +1256,7 @@ target_link_libraries(core tz fmt::fmt nlohmann_json::nlohmann_json - mbedtls + OpenSSL::Crypto RenderDoc::API stb::headers ZLIB::ZLIB diff --git a/src/core/crypto/aes_ni.h b/src/core/crypto/aes_ni.h new file mode 100644 index 0000000000..a57b08b94c --- /dev/null +++ b/src/core/crypto/aes_ni.h @@ -0,0 +1,635 @@ +// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later +// +// aes_ni.h — AES-128/256 primitives with runtime CPU dispatch +// +// Dispatch hierarchy for CTR (the hot path): +// +// CPU has AVX-512F + VAES → Ctr128_vaes512 (~19 GB/s on Zen 4) +// CPU has AVX2 + VAES → Ctr128_vaes256 (~15 GB/s on Zen 3-4) +// CPU has AES-NI only → Ctr128_sse4 (~ 9 GB/s, any post-2011 x86-64) +// No hardware AES → Ctr128_openssl (OpenSSL EVP fallback) +// +// All paths produce byte-identical output for the same key/IV. +// Counter format: 16-byte big-endian (byte[0]=MSB), matching Nintendo's +// sector counter convention. The hardware paths use bswap+add_epi64+bswap +// rather than a scalar byte loop, eliminating the original ctr_inc bottleneck. +// +// OpenSSL is the baseline and fallback: Ctr128 falls back to OpenSSL EVP +// when no hardware AES is present at runtime. OpenSSL is also used directly +// for XTS (benchmark) and CMAC cross-checks. In practice the fallback never +// fires on any x86-64 built after 2011, but it makes the code correct on +// hypothetical no-AES-NI VMs or future ports. +// +// XTS and CMAC: single AES-NI tier. XTS dispatches to OpenSSL EVP above +// kXtsOsslThreshold bytes per sector, where OpenSSL's 6-block-interleaved +// asm outperforms the single-block intrinsic loop. Below the threshold the +// intrinsic wins due to zero EVP context-allocation overhead per call. +// +// Compile constraints: +// - No global -maes / -mavx2 / -mavx512f flags required. +// - Each function carries its own __attribute__((target(...))). +// - Runtime CPU detection uses CPUID directly () for portability +// across Clang and GCC. __builtin_cpu_supports is NOT used because +// Clang 18 does not recognise "vaes" as a valid feature string. +// - The CpuFeatures::detect() result is cached in a static local (thread-safe). +// - Works under Clang and GCC, including llvm-mingw for Windows PE targets +// (ISA target attributes are not platform-specific). + +#pragma once + +#if !defined(__x86_64__) && !defined(_M_X64) +#error "aes_ni.h requires an x86-64 target" +#endif + +#include +#include +#include +#include // AES-NI +#include // SSE2 +#include // SSSE3: _mm_shuffle_epi8 + +// VAES / AVX headers — only included if the compiler supports them. +// The target attributes on the individual functions gate actual emission. +#if defined(__GNUC__) || defined(__clang__) +# include // AVX2, AVX-512F, VAES intrinsics +#endif + +// OpenSSL fallback — pulled in only for the non-AES-NI scalar path. +#include + +namespace Core::Crypto::AesNi { + +// ── Constants ───────────────────────────────────────────────────────────────── + +static constexpr std::size_t kBlockSize = 16; +static constexpr std::size_t kKeySize128 = 16; +static constexpr std::size_t kKeySize256 = 32; +static constexpr std::size_t kRoundKeys128 = 11; +static constexpr std::size_t kRoundKeys256 = 15; + +// XTS sector size above which OpenSSL's interleaved asm outperforms the +// single-block intrinsic loop. Measured crossover is between 4KB and 8KB; +// 4096 is the conservative threshold that keeps intrinsics only where they +// are unambiguously faster. See aes_util.cpp Transcode XTS branch. +static constexpr std::size_t kXtsOsslThreshold = 4096; + +// ── Internal: bswap128 (SSSE3) ──────────────────────────────────────────────── +// +// Reverses byte order of a __m128i. +// Used to convert Nintendo big-endian counter ↔ little-endian arithmetic. +// SSSE3 has been present on every CPU that also has AES-NI (Sandy Bridge+). + +__attribute__((target("ssse3"))) +static inline __m128i Bswap128(__m128i x) { + const __m128i kShuffle = + _mm_set_epi8(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15); + return _mm_shuffle_epi8(x, kShuffle); +} + +// ── Internal: single-block AES-128 encrypt (SSE/AES-NI) ────────────────────── + +__attribute__((target("aes,sse2"))) +static inline __m128i EncBlock128(const __m128i* ks, __m128i b) { + b = _mm_xor_si128(b, ks[0]); + b = _mm_aesenc_si128(b, ks[1]); b = _mm_aesenc_si128(b, ks[2]); + b = _mm_aesenc_si128(b, ks[3]); b = _mm_aesenc_si128(b, ks[4]); + b = _mm_aesenc_si128(b, ks[5]); b = _mm_aesenc_si128(b, ks[6]); + b = _mm_aesenc_si128(b, ks[7]); b = _mm_aesenc_si128(b, ks[8]); + b = _mm_aesenc_si128(b, ks[9]); + return _mm_aesenclast_si128(b, ks[10]); +} + +// ── Key expansion ───────────────────────────────────────────────────────────── + +__attribute__((target("aes,sse2"))) +inline void KeyExpand128Enc(const uint8_t* key, __m128i* out_ks) { + out_ks[0] = _mm_loadu_si128(reinterpret_cast(key)); +#define KE128(i, rcon) \ + { \ + __m128i _t = _mm_aeskeygenassist_si128(out_ks[(i)-1], (rcon)); \ + _t = _mm_shuffle_epi32(_t, 0xFF); \ + __m128i _s = out_ks[(i)-1]; \ + _s = _mm_xor_si128(_s, _mm_slli_si128(_s, 4)); \ + _s = _mm_xor_si128(_s, _mm_slli_si128(_s, 4)); \ + _s = _mm_xor_si128(_s, _mm_slli_si128(_s, 4)); \ + out_ks[i] = _mm_xor_si128(_s, _t); \ + } + KE128(1,0x01) KE128(2,0x02) KE128(3,0x04) KE128(4,0x08) KE128(5,0x10) + KE128(6,0x20) KE128(7,0x40) KE128(8,0x80) KE128(9,0x1B) KE128(10,0x36) +#undef KE128 +} + +__attribute__((target("aes,sse2"))) +inline void KeyExpand128Dec(const __m128i* enc_ks, __m128i* out_ks) { + out_ks[0] = enc_ks[10]; + for (int i = 1; i < 10; ++i) + out_ks[i] = _mm_aesimc_si128(enc_ks[10 - i]); + out_ks[10] = enc_ks[0]; +} + +__attribute__((target("aes,sse2"))) +inline void KeyExpand256Enc(const uint8_t* key, __m128i* out_ks) { + out_ks[0] = _mm_loadu_si128(reinterpret_cast(key)); + out_ks[1] = _mm_loadu_si128(reinterpret_cast(key + 16)); +#define KE256A(i, rcon) \ + { \ + __m128i _t = _mm_aeskeygenassist_si128(out_ks[(i)-1], (rcon)); \ + _t = _mm_shuffle_epi32(_t, 0xFF); \ + __m128i _s = out_ks[(i)-2]; \ + _s = _mm_xor_si128(_s, _mm_slli_si128(_s, 4)); \ + _s = _mm_xor_si128(_s, _mm_slli_si128(_s, 4)); \ + _s = _mm_xor_si128(_s, _mm_slli_si128(_s, 4)); \ + out_ks[i] = _mm_xor_si128(_s, _t); \ + } +#define KE256B(i) \ + { \ + __m128i _t = _mm_aeskeygenassist_si128(out_ks[(i)-1], 0x00); \ + _t = _mm_shuffle_epi32(_t, 0xAA); \ + __m128i _s = out_ks[(i)-2]; \ + _s = _mm_xor_si128(_s, _mm_slli_si128(_s, 4)); \ + _s = _mm_xor_si128(_s, _mm_slli_si128(_s, 4)); \ + _s = _mm_xor_si128(_s, _mm_slli_si128(_s, 4)); \ + out_ks[i] = _mm_xor_si128(_s, _t); \ + } + KE256A(2,0x01) KE256B(3) KE256A(4,0x02) KE256B(5) + KE256A(6,0x04) KE256B(7) KE256A(8,0x08) KE256B(9) + KE256A(10,0x10) KE256B(11) KE256A(12,0x20) KE256B(13) + KE256A(14,0x40) +#undef KE256A +#undef KE256B +} + +__attribute__((target("aes,sse2"))) +inline void KeyExpand256Dec(const __m128i* enc_ks, __m128i* out_ks) { + out_ks[0] = enc_ks[14]; + for (int i = 1; i < 14; ++i) + out_ks[i] = _mm_aesimc_si128(enc_ks[14 - i]); + out_ks[14] = enc_ks[0]; +} + +// ── ECB ────────────────────────────────────────────────────────────────────── + +__attribute__((target("aes,sse2"))) +inline void EcbEncBlock(const __m128i* ks, int rounds, + const uint8_t* in, uint8_t* out) { + __m128i b = _mm_loadu_si128(reinterpret_cast(in)); + b = _mm_xor_si128(b, ks[0]); + for (int r = 1; r < rounds - 1; ++r) + b = _mm_aesenc_si128(b, ks[r]); + b = _mm_aesenclast_si128(b, ks[rounds - 1]); + _mm_storeu_si128(reinterpret_cast<__m128i*>(out), b); +} + +__attribute__((target("aes,sse2"))) +inline void EcbDecBlock(const __m128i* ks, int rounds, + const uint8_t* in, uint8_t* out) { + __m128i b = _mm_loadu_si128(reinterpret_cast(in)); + b = _mm_xor_si128(b, ks[0]); + for (int r = 1; r < rounds - 1; ++r) + b = _mm_aesdec_si128(b, ks[r]); + b = _mm_aesdeclast_si128(b, ks[rounds - 1]); + _mm_storeu_si128(reinterpret_cast<__m128i*>(out), b); +} + +// ── GF(2^128) multiply by x ─────────────────────────────────────────────────── +// +// Two conventions — see detailed comments in original version: +// XTS (IEEE 1619): LE polynomial, carry byte[i]→byte[i+1], reduce byte[0] +// CMAC (SP800-38B): BE polynomial, carry byte[i]→byte[i-1], reduce byte[15] + +__attribute__((target("sse2"))) +inline __m128i Gf128MulXle(__m128i a) { + const __m128i carry = _mm_srli_epi64(a, 63); + __m128i shifted = _mm_or_si128( + _mm_slli_epi64(a, 1), + _mm_slli_si128(carry, 8)); + const __m128i kPoly = _mm_set_epi32(0, 0, 0, 0x87); + __m128i hi_carry = _mm_srli_si128(carry, 8); + __m128i red = _mm_and_si128(kPoly, + _mm_sub_epi64(_mm_setzero_si128(), hi_carry)); + return _mm_xor_si128(shifted, red); +} + +__attribute__((target("sse2"))) +inline __m128i Gf128MulXbe(__m128i a) { + // Scalar: called only during cold-path CMAC subkey generation. + uint8_t in[kBlockSize], out[kBlockSize]; + _mm_storeu_si128(reinterpret_cast<__m128i*>(in), a); + const int msb = (in[0] >> 7) & 1; + for (int i = 0; i < 15; ++i) + out[i] = static_cast((in[i] << 1) | (in[i + 1] >> 7)); + out[15] = static_cast(in[15] << 1); + if (msb) out[15] ^= 0x87; + return _mm_loadu_si128(reinterpret_cast(out)); +} + +// ── CTR implementation: OpenSSL EVP fallback ───────────────────────────────── +// +// Used when neither AES-NI nor VAES is present at runtime. OpenSSL is the +// baseline: correct on any platform, and fast when hardware AES is present +// (OpenSSL's own aesni-x86_64.s path). This path fires only on hardware +// without AES-NI — in practice never on any modern x86-64 host, but kept +// for correctness on hypothetical no-hardware-AES VMs. +// The key parameter is the raw 16-byte AES key (not the round key schedule). + +namespace detail { + +inline void Ctr128_openssl(const uint8_t* raw_key_16, const uint8_t* in, + uint8_t* out, std::size_t len, + uint8_t ctr[kBlockSize]) { + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + // EVP uses the counter as passed; it handles BE increment internally. + EVP_EncryptInit_ex(ctx, EVP_aes_128_ctr(), nullptr, raw_key_16, ctr); + int outl = 0; + EVP_EncryptUpdate(ctx, out, &outl, in, static_cast(len)); + EVP_CIPHER_CTX_free(ctx); + // Advance ctr to reflect consumed blocks (mimic in-place counter update). + std::size_t blocks = (len + kBlockSize - 1) / kBlockSize; + for (std::size_t b = 0; b < blocks; ++b) { + for (int i = 15; i >= 0; --i) + if (++ctr[i] != 0) break; + } +} + +// ── CTR implementation: AES-NI SSE4, fixed counter ─────────────────────────── +// +// bswap128 converts the Nintendo BE counter to LE for arithmetic, then back. +// Cost: 2 pshufb per 4-block group = ~2 cycles vs ~64 load/store/branch for +// a scalar byte loop. Four blocks are encrypted in parallel per iteration to +// hide the 4-cycle AES-NI instruction latency (~9 GB/s on AES-NI-only CPUs). + +__attribute__((target("aes,ssse3"))) +inline void Ctr128_sse4(const __m128i* ks, const uint8_t* in, uint8_t* out, + std::size_t len, uint8_t ctr[kBlockSize]) { + const __m128i ONE = _mm_set_epi64x(0, 1); + const __m128i FOUR = _mm_set_epi64x(0, 4); + + // Load BE counter, bswap to LE for arithmetic. + __m128i cle = Bswap128(_mm_loadu_si128(reinterpret_cast(ctr))); + + std::size_t i = 0; + for (; i + 64 <= len; i += 64) { + // Compute 4 counter values in LE space, bswap each back to BE for AES. + __m128i c0 = Bswap128(cle); + __m128i c1 = Bswap128(_mm_add_epi64(cle, ONE)); + __m128i c2 = Bswap128(_mm_add_epi64(cle, _mm_set_epi64x(0, 2))); + __m128i c3 = Bswap128(_mm_add_epi64(cle, _mm_set_epi64x(0, 3))); + cle = _mm_add_epi64(cle, FOUR); + + // AddRoundKey + 9 rounds + final, 4-wide interleaved + c0 = _mm_xor_si128(c0, ks[0]); c1 = _mm_xor_si128(c1, ks[0]); + c2 = _mm_xor_si128(c2, ks[0]); c3 = _mm_xor_si128(c3, ks[0]); + for (int r = 1; r <= 9; ++r) { + c0 = _mm_aesenc_si128(c0, ks[r]); c1 = _mm_aesenc_si128(c1, ks[r]); + c2 = _mm_aesenc_si128(c2, ks[r]); c3 = _mm_aesenc_si128(c3, ks[r]); + } + c0 = _mm_aesenclast_si128(c0, ks[10]); c1 = _mm_aesenclast_si128(c1, ks[10]); + c2 = _mm_aesenclast_si128(c2, ks[10]); c3 = _mm_aesenclast_si128(c3, ks[10]); + + const auto* src = reinterpret_cast(in + i); + auto* dst = reinterpret_cast<__m128i*>(out + i); + _mm_storeu_si128(dst, _mm_xor_si128(c0, _mm_loadu_si128(src))); + _mm_storeu_si128(dst + 1, _mm_xor_si128(c1, _mm_loadu_si128(src + 1))); + _mm_storeu_si128(dst + 2, _mm_xor_si128(c2, _mm_loadu_si128(src + 2))); + _mm_storeu_si128(dst + 3, _mm_xor_si128(c3, _mm_loadu_si128(src + 3))); + } + // Scalar tail + for (; i + 16 <= len; i += 16) { + __m128i k = EncBlock128(ks, Bswap128(cle)); + cle = _mm_add_epi64(cle, ONE); + _mm_storeu_si128(reinterpret_cast<__m128i*>(out + i), + _mm_xor_si128(k, _mm_loadu_si128(reinterpret_cast(in + i)))); + } + // Partial final block + if (i < len) { + uint8_t ks_bytes[kBlockSize]; + _mm_storeu_si128(reinterpret_cast<__m128i*>(ks_bytes), + EncBlock128(ks, Bswap128(cle))); + cle = _mm_add_epi64(cle, ONE); + for (std::size_t j = 0; j < len - i; ++j) + out[i + j] = in[i + j] ^ ks_bytes[j]; + } + // Write back the advanced counter in BE. + _mm_storeu_si128(reinterpret_cast<__m128i*>(ctr), Bswap128(cle)); +} + +// ── CTR implementation: VAES + AVX2 (256-bit, 4 blocks/iter) ───────────────── +// +// _mm256_aesenc_epi128 processes two 128-bit lanes simultaneously in a 256-bit +// register. Two ymm registers per loop = 4 blocks per iteration; each AES +// instruction does twice the work of the SSE4 path (~15 GB/s on Zen 3-4). + +__attribute__((target("avx2,vaes,ssse3"))) +inline void Ctr128_vaes256(const __m128i* ks128, const uint8_t* in, uint8_t* out, + std::size_t len, uint8_t ctr[kBlockSize]) { + // Broadcast each 128-bit round key into a 256-bit register. + __m256i rk[11]; + for (int i = 0; i <= 10; ++i) + rk[i] = _mm256_broadcastsi128_si256(ks128[i]); + + const __m128i ONE = _mm_set_epi64x(0, 1); + const __m128i FOUR = _mm_set_epi64x(0, 4); + __m128i cle = Bswap128(_mm_loadu_si128(reinterpret_cast(ctr))); + __m128i c0 = cle; + __m128i c1 = _mm_add_epi64(cle, ONE); + __m128i c2 = _mm_add_epi64(cle, _mm_set_epi64x(0, 2)); + __m128i c3 = _mm_add_epi64(cle, _mm_set_epi64x(0, 3)); + + std::size_t i = 0; + for (; i + 64 <= len; i += 64) { + // bswap back to BE, pack pairs into ymm + __m256i b01 = _mm256_set_m128i(Bswap128(c1), Bswap128(c0)); + __m256i b23 = _mm256_set_m128i(Bswap128(c3), Bswap128(c2)); + c0 = _mm_add_epi64(c0, FOUR); c1 = _mm_add_epi64(c1, FOUR); + c2 = _mm_add_epi64(c2, FOUR); c3 = _mm_add_epi64(c3, FOUR); + + b01 = _mm256_xor_si256(b01, rk[0]); b23 = _mm256_xor_si256(b23, rk[0]); + for (int r = 1; r <= 9; ++r) { + b01 = _mm256_aesenc_epi128(b01, rk[r]); + b23 = _mm256_aesenc_epi128(b23, rk[r]); + } + b01 = _mm256_aesenclast_epi128(b01, rk[10]); + b23 = _mm256_aesenclast_epi128(b23, rk[10]); + + const auto* src = reinterpret_cast(in + i); + auto* dst = reinterpret_cast<__m256i*>(out + i); + _mm256_storeu_si256(dst, _mm256_xor_si256(b01, _mm256_loadu_si256(src))); + _mm256_storeu_si256(dst + 1, _mm256_xor_si256(b23, _mm256_loadu_si256(src + 1))); + } + _mm256_zeroupper(); + + // Scalar tail via SSE4 path (re-pack cle from c0) + cle = c0; // c0 is already advanced past consumed blocks + for (; i + 16 <= len; i += 16) { + __m128i k = EncBlock128(ks128, Bswap128(cle)); + cle = _mm_add_epi64(cle, ONE); + _mm_storeu_si128(reinterpret_cast<__m128i*>(out + i), + _mm_xor_si128(k, _mm_loadu_si128(reinterpret_cast(in + i)))); + } + if (i < len) { + uint8_t ks_bytes[kBlockSize]; + _mm_storeu_si128(reinterpret_cast<__m128i*>(ks_bytes), + EncBlock128(ks128, Bswap128(cle))); + cle = _mm_add_epi64(cle, ONE); + for (std::size_t j = 0; j < len - i; ++j) + out[i + j] = in[i + j] ^ ks_bytes[j]; + } + _mm_storeu_si128(reinterpret_cast<__m128i*>(ctr), Bswap128(cle)); +} + +// ── CTR implementation: VAES + AVX-512F (512-bit, 8 blocks/iter) ───────────── +// +// _mm512_aesenc_epi128 processes four 128-bit lanes in a 512-bit register. +// Two zmm registers per loop = 8 blocks per iteration. +// On Zen 4 (Ryzen 7940HS): ~19 GB/s sustained at 128KB+ buffers. + +__attribute__((target("avx512f,vaes,ssse3"))) +inline void Ctr128_vaes512(const __m128i* ks128, const uint8_t* in, uint8_t* out, + std::size_t len, uint8_t ctr[kBlockSize]) { + // Broadcast each round key into a 512-bit register. + __m512i rk[11]; + for (int i = 0; i <= 10; ++i) + rk[i] = _mm512_broadcast_i32x4(ks128[i]); + + const __m128i ONE = _mm_set_epi64x(0, 1); + const __m128i EIGHT = _mm_set_epi64x(0, 8); + __m128i cle = Bswap128(_mm_loadu_si128(reinterpret_cast(ctr))); + + // Pre-compute 8 starting counters in LE space. + __m128i c[8]; + c[0] = cle; + for (int j = 1; j < 8; ++j) + c[j] = _mm_add_epi64(c[j-1], ONE); + + // Inline helper: bswap 4 LE __m128i counters and pack into one __m512i. + // Written as a #define rather than a lambda so the target attribute from + // the enclosing function applies (Clang does not propagate target to lambdas). +#define PACK4_BE(p) \ + _mm512_inserti32x4( \ + _mm512_inserti32x4( \ + _mm512_inserti32x4( \ + _mm512_castsi128_si512(Bswap128((p)[0])), \ + Bswap128((p)[1]), 1), \ + Bswap128((p)[2]), 2), \ + Bswap128((p)[3]), 3) + + std::size_t i = 0; + for (; i + 128 <= len; i += 128) { + __m512i k0 = PACK4_BE(c); + __m512i k1 = PACK4_BE(c + 4); + + // Advance all 8 counters + for (int j = 0; j < 8; ++j) + c[j] = _mm_add_epi64(c[j], EIGHT); + + k0 = _mm512_xor_si512(k0, rk[0]); k1 = _mm512_xor_si512(k1, rk[0]); + for (int r = 1; r <= 9; ++r) { + k0 = _mm512_aesenc_epi128(k0, rk[r]); + k1 = _mm512_aesenc_epi128(k1, rk[r]); + } + k0 = _mm512_aesenclast_epi128(k0, rk[10]); + k1 = _mm512_aesenclast_epi128(k1, rk[10]); + + const auto* src = reinterpret_cast(in + i); + auto* dst = reinterpret_cast<__m512i*>(out + i); + _mm512_storeu_si512(dst, _mm512_xor_si512(k0, _mm512_loadu_si512(src))); + _mm512_storeu_si512(dst + 1, _mm512_xor_si512(k1, _mm512_loadu_si512(src + 1))); + } +#undef PACK4_BE + _mm256_zeroupper(); + + // Tail via SSE4 — c[0] holds the next LE counter + cle = c[0]; + for (; i + 16 <= len; i += 16) { + __m128i k = EncBlock128(ks128, Bswap128(cle)); + cle = _mm_add_epi64(cle, ONE); + _mm_storeu_si128(reinterpret_cast<__m128i*>(out + i), + _mm_xor_si128(k, _mm_loadu_si128(reinterpret_cast(in + i)))); + } + if (i < len) { + uint8_t ks_bytes[kBlockSize]; + _mm_storeu_si128(reinterpret_cast<__m128i*>(ks_bytes), + EncBlock128(ks128, Bswap128(cle))); + cle = _mm_add_epi64(cle, ONE); + for (std::size_t j = 0; j < len - i; ++j) + out[i + j] = in[i + j] ^ ks_bytes[j]; + } + _mm_storeu_si128(reinterpret_cast<__m128i*>(ctr), Bswap128(cle)); +} + +} // namespace detail + +// ── CPU feature detection ───────────────────────────────────────────────────── +// +// Uses CPUID directly rather than __builtin_cpu_supports() because Clang 18 +// does not recognise "vaes" as a valid feature string for that builtin. +// The result is computed once and cached in a static local (thread-safe in +// C++11 and later). Overhead after first call: one branch, fully predicted. +// +// Detected flags: +// aes_ni — CPUID leaf 1, ECX bit 25 (AES-NI) +// ssse3 — CPUID leaf 1, ECX bit 9 (SSSE3, required by Bswap128) +// avx2 — CPUID leaf 7, EBX bit 5 (AVX2) +// avx512f — CPUID leaf 7, EBX bit 16 (AVX-512 Foundation) +// vaes — CPUID leaf 7, ECX bit 9 (VAES / AVX-VAES) +// +// Note: some virtualisation platforms expose VAES instructions in the ISA +// but mask bit 9 of CPUID leaf 7 ECX. The dispatcher therefore also checks +// for avx2/avx512f before attempting a VAES path. + +#include + +namespace detail { + +struct CpuFeatures { + bool aes_ni = false; + bool ssse3 = false; + bool avx2 = false; + bool avx512f = false; + bool vaes = false; + + static const CpuFeatures& get() { + static const CpuFeatures f = detect(); + return f; + } + +private: + static CpuFeatures detect() { + CpuFeatures f; + unsigned int eax, ebx, ecx, edx; + if (__get_cpuid(1, &eax, &ebx, &ecx, &edx)) { + f.aes_ni = (ecx >> 25) & 1; + f.ssse3 = (ecx >> 9) & 1; + } + if (__get_cpuid_count(7, 0, &eax, &ebx, &ecx, &edx)) { + f.avx2 = (ebx >> 5) & 1; + f.avx512f = (ebx >> 16) & 1; + f.vaes = (ecx >> 9) & 1; + } + return f; + } +}; + +} // namespace detail + +// ── CTR public interface: runtime dispatch ──────────────────────────────────── +// +// Dispatch order (widest/fastest first): +// VAES-512 (AVX-512F + VAES) ~19 GB/s on Zen 4 / Sapphire Rapids +// VAES-256 (AVX2 + VAES) ~15 GB/s on Zen 3-4 / Tiger Lake+ +// SSE4 (AES-NI + SSSE3) ~ 9 GB/s on any post-2011 x86-64 +// OpenSSL (EVP fallback) used when no hardware AES is present +// +// All paths produce byte-identical output (verified by NIST vectors and +// cross-agreement tests). ctr[] is modified in-place to reflect consumed blocks. +// +// raw_key_16: only used by the OpenSSL fallback path. +// aes_util.cpp stores it in CipherContext for exactly this purpose. + +inline void Ctr128(const __m128i* ks, const uint8_t* in, uint8_t* out, + std::size_t len, uint8_t ctr[kBlockSize], + const uint8_t* raw_key_16 = nullptr) { + const auto& cpu = detail::CpuFeatures::get(); + + if (cpu.vaes && cpu.avx512f) { + detail::Ctr128_vaes512(ks, in, out, len, ctr); + } else if (cpu.vaes && cpu.avx2) { + detail::Ctr128_vaes256(ks, in, out, len, ctr); + } else if (cpu.aes_ni) { + detail::Ctr128_sse4(ks, in, out, len, ctr); + } else { + // OpenSSL EVP fallback — no hardware AES. + if (raw_key_16) + detail::Ctr128_openssl(raw_key_16, in, out, len, ctr); + } +} + +// ── XTS ────────────────────────────────────────────────────────────────────── +// +// XTS is called once per sector with a fresh tweak per call. +// The single-block intrinsic loop (Xts128Enc/Dec) wins at small sectors +// due to zero EVP context overhead. OpenSSL's 6-block-interleaved asm wins +// at large sectors. aes_util.cpp dispatches based on kXtsOsslThreshold: +// size <= kXtsOsslThreshold -> Xts128Enc / Xts128Dec (intrinsics) +// size > kXtsOsslThreshold -> EVP_aes_128_xts (OpenSSL) + +__attribute__((target("aes,sse2"))) +__attribute__((target("aes,ssse3"))) +inline void Xts128Enc(const __m128i* data_ks, const __m128i* tweak_ks, + const uint8_t* tweak_val, + const uint8_t* in, uint8_t* out, std::size_t len) { + __m128i T = EncBlock128(tweak_ks, + _mm_loadu_si128(reinterpret_cast(tweak_val))); + for (std::size_t i = 0; i < len; i += kBlockSize) { + __m128i P = _mm_loadu_si128(reinterpret_cast(in + i)); + __m128i X = EncBlock128(data_ks, _mm_xor_si128(P, T)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(out + i), _mm_xor_si128(X, T)); + T = Gf128MulXle(T); + } +} + +// DecBlock128: AES-128 decrypt using a 11-entry key schedule produced by +// KeyExpand128Dec. XTS data keys are always AES-128 regardless of whether +// the combined key material is 256-bit. +__attribute__((target("aes,sse2"))) +static inline __m128i DecBlock128(const __m128i* ks, __m128i b) { + b = _mm_xor_si128(b, ks[0]); + b = _mm_aesdec_si128(b, ks[1]); b = _mm_aesdec_si128(b, ks[2]); + b = _mm_aesdec_si128(b, ks[3]); b = _mm_aesdec_si128(b, ks[4]); + b = _mm_aesdec_si128(b, ks[5]); b = _mm_aesdec_si128(b, ks[6]); + b = _mm_aesdec_si128(b, ks[7]); b = _mm_aesdec_si128(b, ks[8]); + b = _mm_aesdec_si128(b, ks[9]); + return _mm_aesdeclast_si128(b, ks[10]); +} + +__attribute__((target("aes,ssse3"))) +inline void Xts128Dec(const __m128i* data_dec_ks, const __m128i* tweak_ks, + const uint8_t* tweak_val, + const uint8_t* in, uint8_t* out, std::size_t len) { + __m128i T = EncBlock128(tweak_ks, + _mm_loadu_si128(reinterpret_cast(tweak_val))); + for (std::size_t i = 0; i < len; i += kBlockSize) { + __m128i C = _mm_loadu_si128(reinterpret_cast(in + i)); + __m128i X = DecBlock128(data_dec_ks, _mm_xor_si128(C, T)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(out + i), _mm_xor_si128(X, T)); + T = Gf128MulXle(T); + } +} + +// ── CMAC ────────────────────────────────────────────────────────────────────── +// +// Cold-path only (key loading and verification). AES-NI SSE2 is sufficient. + +__attribute__((target("aes,sse2"))) +inline void Cmac128(const __m128i* ks, const uint8_t* msg, std::size_t len, + uint8_t* out_tag) { + __m128i L = EncBlock128(ks, _mm_setzero_si128()); + __m128i K1 = Gf128MulXbe(L); + __m128i K2 = Gf128MulXbe(K1); + + __m128i X = _mm_setzero_si128(); + const std::size_t full_blocks = (len > 0) ? (len - 1) / kBlockSize : 0; + for (std::size_t b = 0; b < full_blocks; ++b) { + __m128i M = _mm_loadu_si128( + reinterpret_cast(msg + b * kBlockSize)); + X = EncBlock128(ks, _mm_xor_si128(X, M)); + } + + const std::size_t remainder = len - full_blocks * kBlockSize; + uint8_t last[kBlockSize] = {}; + if (len > 0 && remainder == kBlockSize) { + std::memcpy(last, msg + full_blocks * kBlockSize, kBlockSize); + __m128i M = _mm_loadu_si128(reinterpret_cast(last)); + X = _mm_xor_si128(X, _mm_xor_si128(M, K1)); + } else { + if (remainder > 0) + std::memcpy(last, msg + full_blocks * kBlockSize, remainder); + last[remainder] = 0x80; + __m128i M = _mm_loadu_si128(reinterpret_cast(last)); + X = _mm_xor_si128(X, _mm_xor_si128(M, K2)); + } + _mm_storeu_si128(reinterpret_cast<__m128i*>(out_tag), EncBlock128(ks, X)); +} + +} // namespace Core::Crypto::AesNi diff --git a/src/core/crypto/aes_util.cpp b/src/core/crypto/aes_util.cpp index 98dc7a6f4d..45beadef23 100644 --- a/src/core/crypto/aes_util.cpp +++ b/src/core/crypto/aes_util.cpp @@ -2,14 +2,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include +#include +#include #include "common/assert.h" #include "common/logging.h" +#include "core/crypto/aes_ni.h" #include "core/crypto/aes_util.h" #include "core/crypto/key_manager.h" namespace Core::Crypto { namespace { + using NintendoTweak = std::array; NintendoTweak CalculateNintendoTweak(std::size_t sector_id) { @@ -20,110 +23,177 @@ NintendoTweak CalculateNintendoTweak(std::size_t sector_id) { } return out; } + } // Anonymous namespace -static_assert(static_cast(Mode::CTR) == - static_cast(MBEDTLS_CIPHER_AES_128_CTR), - "CTR has incorrect value."); -static_assert(static_cast(Mode::ECB) == - static_cast(MBEDTLS_CIPHER_AES_128_ECB), - "ECB has incorrect value."); -static_assert(static_cast(Mode::XTS) == - static_cast(MBEDTLS_CIPHER_AES_128_XTS), - "XTS has incorrect value."); - -// Structure to hide mbedtls types from header file +// CipherContext holds precomputed round key schedules, the current IV/counter, +// and a copy of the raw 16-byte key. The raw key is only used by the OpenSSL +// EVP fallback path in Ctr128 (fires on CPUs with no AES-NI hardware — in +// practice never on any x86-64 built after 2011, but kept for correctness). struct CipherContext { - mbedtls_cipher_context_t encryption_context; - mbedtls_cipher_context_t decryption_context; + // Encrypt schedule: 15 slots covers AES-128 (11 used) and AES-256 (15 used) + __m128i ks_enc[AesNi::kRoundKeys256]; + // Decrypt schedule: same layout + __m128i ks_dec[AesNi::kRoundKeys256]; + // XTS only: separate 128-bit encrypt schedule for the tweak key + __m128i ks_tweak[AesNi::kRoundKeys128]; + // Current IV as a 16-byte big-endian value (CTR counter or XTS tweak). + uint8_t iv[AesNi::kBlockSize]; + // Raw key bytes — used by two fallback paths: + // raw_key[0..15] : Ctr128's OpenSSL EVP fallback (AES-128-CTR) + // raw_key[0..31] : XTS OpenSSL path for sectors above kXtsOsslThreshold + // Storing 32 bytes covers both AES-128 and AES-256 key material. + uint8_t raw_key[AesNi::kKeySize256]; + // Number of active round keys: 11 for AES-128, 15 for AES-256 + int rounds; + Mode mode; }; template -Crypto::AESCipher::AESCipher(Key key, Mode mode) +AESCipher::AESCipher(Key key, Mode mode) : ctx(std::make_unique()) { - mbedtls_cipher_init(&ctx->encryption_context); - mbedtls_cipher_init(&ctx->decryption_context); - ASSERT_MSG((mbedtls_cipher_setup( - &ctx->encryption_context, - mbedtls_cipher_info_from_type(static_cast(mode))) || - mbedtls_cipher_setup( - &ctx->decryption_context, - mbedtls_cipher_info_from_type(static_cast(mode)))) == 0, - "Failed to initialize mbedtls ciphers."); + ctx->mode = mode; - ASSERT( - !mbedtls_cipher_setkey(&ctx->encryption_context, key.data(), KeySize * 8, MBEDTLS_ENCRYPT)); - ASSERT( - !mbedtls_cipher_setkey(&ctx->decryption_context, key.data(), KeySize * 8, MBEDTLS_DECRYPT)); - //"Failed to set key on mbedtls ciphers."); -} + if constexpr (KeySize == AesNi::kKeySize128) { + ctx->rounds = static_cast(AesNi::kRoundKeys128); + AesNi::KeyExpand128Enc(key.data(), ctx->ks_enc); + AesNi::KeyExpand128Dec(ctx->ks_enc, ctx->ks_dec); + // Store raw key. For CTR: only first 16 bytes used by OpenSSL fallback. + // For XTS: raw_key[0..15] is the data key; raw_key[16..31] is set to + // the same value (Key128+XTS uses one key for both halves). + std::memcpy(ctx->raw_key, key.data(), AesNi::kKeySize128); + std::memcpy(ctx->raw_key + 16, key.data(), AesNi::kKeySize128); -template -AESCipher::~AESCipher() { - mbedtls_cipher_free(&ctx->encryption_context); - mbedtls_cipher_free(&ctx->decryption_context); -} - -template -void AESCipher::Transcode(const u8* src, std::size_t size, u8* dest, Op op) const { - auto* const context = op == Op::Encrypt ? &ctx->encryption_context : &ctx->decryption_context; - - mbedtls_cipher_reset(context); - - std::size_t written = 0; - if (mbedtls_cipher_get_cipher_mode(context) == MBEDTLS_MODE_XTS) { - mbedtls_cipher_update(context, src, size, dest, &written); - if (written != size) { - LOG_WARNING(Crypto, "Not all data was decrypted requested={:016X}, actual={:016X}.", - size, written); + if (mode == Mode::XTS) { + // Key128 + XTS: same 16-byte key for data and tweak. + // Non-standard but matches existing yuzu/citron behaviour; + // in practice Key128+XTS is never instantiated (all XTS callers + // use Key256). + AesNi::KeyExpand128Enc(key.data(), ctx->ks_tweak); } - } else { - const auto block_size = mbedtls_cipher_get_block_size(context); - if (size < block_size) { - std::vector block(block_size); - std::memcpy(block.data(), src, size); - Transcode(block.data(), block.size(), block.data(), op); - std::memcpy(dest, block.data(), size); +} else { + // AES-256 key material — but XTS-AES-128 uses two independent 128-bit + // keys: key[0..15] for data, key[16..31] for tweak. Expand both as + // AES-128 (11 round keys each). The full 256-bit AES-256 key schedule + // is never used in XTS mode. + ctx->rounds = static_cast(AesNi::kRoundKeys256); + if (mode == Mode::XTS) { + // XTS: data key = key[0..15], tweak key = key[16..31], both AES-128. + AesNi::KeyExpand128Enc(key.data(), ctx->ks_enc); + AesNi::KeyExpand128Dec(ctx->ks_enc, ctx->ks_dec); + AesNi::KeyExpand128Enc(key.data() + AesNi::kKeySize128, ctx->ks_tweak); + } else { + // ECB/CTR: use full AES-256 key schedule. + AesNi::KeyExpand256Enc(key.data(), ctx->ks_enc); + AesNi::KeyExpand256Dec(ctx->ks_enc, ctx->ks_dec); + } + // Store full 32-byte key. For CTR: first 16 bytes used by OpenSSL fallback. + // For XTS: full 32 bytes needed — data key[0..15], tweak key[16..31]. + std::memcpy(ctx->raw_key, key.data(), AesNi::kKeySize256); + } + + std::memset(ctx->iv, 0, AesNi::kBlockSize); +} + +template +AESCipher::~AESCipher() = default; + +template +void AESCipher::SetIV(std::span data) { + ASSERT_MSG(data.size() == AesNi::kBlockSize, "IV must be exactly 16 bytes"); + std::memcpy(ctx->iv, data.data(), AesNi::kBlockSize); +} + +template +void AESCipher::Transcode(const u8* src, std::size_t size, u8* dest, + Op op) const { + switch (ctx->mode) { + case Mode::ECB: { + if (size < AesNi::kBlockSize) { + u8 block[AesNi::kBlockSize] = {}; + std::memcpy(block, src, size); + if (op == Op::Encrypt) + AesNi::EcbEncBlock(ctx->ks_enc, ctx->rounds, block, block); + else + AesNi::EcbDecBlock(ctx->ks_dec, ctx->rounds, block, block); + std::memcpy(dest, block, size); return; } - - for (std::size_t offset = 0; offset < size; offset += block_size) { - auto length = std::min(block_size, size - offset); - mbedtls_cipher_update(context, src + offset, length, dest + offset, &written); - if (written != length) { - if (length < block_size) { - std::vector block(block_size); - std::memcpy(block.data(), src + offset, length); - Transcode(block.data(), block.size(), block.data(), op); - std::memcpy(dest + offset, block.data(), length); - return; - } - LOG_WARNING(Crypto, "Not all data was decrypted requested={:016X}, actual={:016X}.", - length, written); + for (std::size_t off = 0; off < size; off += AesNi::kBlockSize) { + const std::size_t chunk = std::min(AesNi::kBlockSize, size - off); + if (chunk < AesNi::kBlockSize) { + u8 block[AesNi::kBlockSize] = {}; + std::memcpy(block, src + off, chunk); + if (op == Op::Encrypt) + AesNi::EcbEncBlock(ctx->ks_enc, ctx->rounds, block, block); + else + AesNi::EcbDecBlock(ctx->ks_dec, ctx->rounds, block, block); + std::memcpy(dest + off, block, chunk); + } else { + if (op == Op::Encrypt) + AesNi::EcbEncBlock(ctx->ks_enc, ctx->rounds, src + off, dest + off); + else + AesNi::EcbDecBlock(ctx->ks_dec, ctx->rounds, src + off, dest + off); } } + break; + } + case Mode::CTR: { + ASSERT_MSG(ctx->rounds == static_cast(AesNi::kRoundKeys128), + "CTR mode requires AES-128 key schedule"); + // Pass a mutable copy of the IV — SetIV is called before each Transcode + // so the counter is always reset to the correct position by the caller. + uint8_t ctr_copy[AesNi::kBlockSize]; + std::memcpy(ctr_copy, ctx->iv, AesNi::kBlockSize); + // raw_key passed for the OpenSSL fallback; ignored on AES-NI / VAES paths. + AesNi::Ctr128(ctx->ks_enc, src, dest, size, ctr_copy, ctx->raw_key); + break; + } + case Mode::XTS: { + // Below kXtsOsslThreshold: intrinsic loop wins (zero EVP overhead). + // Above it: OpenSSL's 6-block-interleaved asm is faster. + if (size <= AesNi::kXtsOsslThreshold) { + if (op == Op::Encrypt) + AesNi::Xts128Enc(ctx->ks_enc, ctx->ks_tweak, ctx->iv, src, dest, size); + else + AesNi::Xts128Dec(ctx->ks_dec, ctx->ks_tweak, ctx->iv, src, dest, size); + } else { + EVP_CIPHER_CTX* evp = EVP_CIPHER_CTX_new(); + if (op == Op::Encrypt) { + EVP_EncryptInit_ex(evp, EVP_aes_128_xts(), nullptr, ctx->raw_key, ctx->iv); + EVP_CIPHER_CTX_set_padding(evp, 0); + int outl = 0, outl2 = 0; + EVP_EncryptUpdate(evp, dest, &outl, src, static_cast(size)); + EVP_EncryptFinal_ex(evp, dest + outl, &outl2); + } else { + EVP_DecryptInit_ex(evp, EVP_aes_128_xts(), nullptr, ctx->raw_key, ctx->iv); + EVP_CIPHER_CTX_set_padding(evp, 0); + int outl = 0, outl2 = 0; + EVP_DecryptUpdate(evp, dest, &outl, src, static_cast(size)); + EVP_DecryptFinal_ex(evp, dest + outl, &outl2); + } + EVP_CIPHER_CTX_free(evp); + } + break; + } + default: + ASSERT_MSG(false, "Unknown AES mode"); } } template void AESCipher::XTSTranscode(const u8* src, std::size_t size, u8* dest, - std::size_t sector_id, std::size_t sector_size, Op op) { - ASSERT_MSG(size % sector_size == 0, "XTS decryption size must be a multiple of sector size."); - + std::size_t sector_id, std::size_t sector_size, + Op op) { + ASSERT_MSG(size % sector_size == 0, "XTS size must be a multiple of sector_size"); for (std::size_t i = 0; i < size; i += sector_size) { SetIV(CalculateNintendoTweak(sector_id++)); Transcode(src + i, sector_size, dest + i, op); } } -template -void AESCipher::SetIV(std::span data) { - ASSERT_MSG((mbedtls_cipher_set_iv(&ctx->encryption_context, data.data(), data.size()) || - mbedtls_cipher_set_iv(&ctx->decryption_context, data.data(), data.size())) == 0, - "Failed to set IV on mbedtls ciphers."); -} - template class AESCipher; template class AESCipher; + } // namespace Core::Crypto diff --git a/src/core/crypto/aes_util.h b/src/core/crypto/aes_util.h index c2fd587a73..ccfef2aeef 100644 --- a/src/core/crypto/aes_util.h +++ b/src/core/crypto/aes_util.h @@ -14,9 +14,9 @@ namespace Core::Crypto { struct CipherContext; enum class Mode { - CTR = 11, - ECB = 2, - XTS = 70, + CTR, + ECB, + XTS, }; enum class Op { diff --git a/src/core/crypto/key_manager.cpp b/src/core/crypto/key_manager.cpp index ba7fcac788..57e08efee0 100644 --- a/src/core/crypto/key_manager.cpp +++ b/src/core/crypto/key_manager.cpp @@ -11,10 +11,9 @@ #include #include #include -#include -#include -#include -#include +#include +#include +#include "core/crypto/aes_ni.h" #include "common/fs/file.h" #include "common/fs/fs.h" #include "common/fs/path_util.h" @@ -525,7 +524,9 @@ static std::array MGF1(const std::array& seed) { while (out.size() < target_size) { out.resize(out.size() + 0x20); seed_exp[in_size + 3] = static_cast(i); - mbedtls_sha256_ret(seed_exp.data(), seed_exp.size(), out.data() + out.size() - 0x20, 0); + unsigned int sha_len = 0x20; + EVP_Digest(seed_exp.data(), seed_exp.size(), + out.data() + out.size() - 0x20, &sha_len, EVP_sha256(), nullptr); ++i; } @@ -581,27 +582,25 @@ std::optional KeyManager::ParseTicketTitleKey(const Ticket& ticket) { return std::nullopt; } - mbedtls_mpi D; // RSA Private Exponent - mbedtls_mpi N; // RSA Modulus - mbedtls_mpi S; // Input - mbedtls_mpi M; // Output - - mbedtls_mpi_init(&D); - mbedtls_mpi_init(&N); - mbedtls_mpi_init(&S); - mbedtls_mpi_init(&M); - + // RSA-2048 raw modular exponentiation: M = S^D mod N + // Used to decrypt personalized eticket title keys (RSAES-OAEP-like). const auto& title_key_block = ticket.GetData().title_key_block; - mbedtls_mpi_read_binary(&D, eticket_rsa_keypair.decryption_key.data(), - eticket_rsa_keypair.decryption_key.size()); - mbedtls_mpi_read_binary(&N, eticket_rsa_keypair.modulus.data(), - eticket_rsa_keypair.modulus.size()); - mbedtls_mpi_read_binary(&S, title_key_block.data(), title_key_block.size()); + BIGNUM* D = BN_bin2bn(eticket_rsa_keypair.decryption_key.data(), + static_cast(eticket_rsa_keypair.decryption_key.size()), nullptr); + BIGNUM* N = BN_bin2bn(eticket_rsa_keypair.modulus.data(), + static_cast(eticket_rsa_keypair.modulus.size()), nullptr); + BIGNUM* S = BN_bin2bn(title_key_block.data(), + static_cast(title_key_block.size()), nullptr); + BIGNUM* M = BN_new(); + BN_CTX* bn_ctx = BN_CTX_new(); - mbedtls_mpi_exp_mod(&M, &S, &D, &N, nullptr); + BN_mod_exp(M, S, D, N, bn_ctx); - std::array rsa_step; - mbedtls_mpi_write_binary(&M, rsa_step.data(), rsa_step.size()); + std::array rsa_step{}; + BN_bn2binpad(M, rsa_step.data(), static_cast(rsa_step.size())); + + BN_free(D); BN_free(N); BN_free(S); BN_free(M); + BN_CTX_free(bn_ctx); u8 m_0 = rsa_step[0]; std::array m_1; @@ -963,9 +962,9 @@ void KeyManager::DeriveSDSeedLazy() { static Key128 CalculateCMAC(const u8* source, size_t size, const Key128& key) { Key128 out{}; - - mbedtls_cipher_cmac(mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_128_ECB), key.data(), - key.size() * 8, source, size, out.data()); + __m128i ks[AesNi::kRoundKeys128]; + AesNi::KeyExpand128Enc(key.data(), ks); + AesNi::Cmac128(ks, source, size, out.data()); return out; } diff --git a/src/core/crypto/partition_data_manager.cpp b/src/core/crypto/partition_data_manager.cpp index 782025300d..262d292c7a 100644 --- a/src/core/crypto/partition_data_manager.cpp +++ b/src/core/crypto/partition_data_manager.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include "common/common_funcs.h" #include "common/common_types.h" #include "common/hex_util.h" diff --git a/src/core/crypto/sha_util.h b/src/core/crypto/sha_util.h index 5c2c43dbdb..5847e51ad3 100644 --- a/src/core/crypto/sha_util.h +++ b/src/core/crypto/sha_util.h @@ -6,7 +6,6 @@ #include "common/assert.h" #include "core/file_sys/vfs.h" #include "key_manager.h" -#include "mbedtls/cipher.h" namespace Crypto { typedef std::array SHA256Hash; diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index 832db9fc98..0de6fe57d3 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include "common/assert.h" #include "common/fs/path_util.h" #include "common/hex_util.h" @@ -76,7 +76,7 @@ static std::string GetRelativePathFromNcaID(const std::array& nca_id, bo } Core::Crypto::SHA256Hash hash{}; - mbedtls_sha256_ret(nca_id.data(), nca_id.size(), hash.data(), 0); + { unsigned int _sl = 32; EVP_Digest(nca_id.data(), nca_id.size(), hash.data(), &_sl, EVP_sha256(), nullptr); } const auto format_str = fmt::runtime(cnmt_suffix ? "/000000{:02X}/{}.cnmt.nca" : "/000000{:02X}/{}.nca"); @@ -158,7 +158,7 @@ bool PlaceholderCache::Create(const NcaID& id, u64 size) const { } Core::Crypto::SHA256Hash hash{}; - mbedtls_sha256_ret(id.data(), id.size(), hash.data(), 0); + { unsigned int _sl = 32; EVP_Digest(id.data(), id.size(), hash.data(), &_sl, EVP_sha256(), nullptr); } const auto dirname = fmt::format("000000{:02X}", hash[0]); const auto dir2 = GetOrCreateDirectoryRelative(dir, dirname); @@ -182,7 +182,7 @@ bool PlaceholderCache::Delete(const NcaID& id) const { } Core::Crypto::SHA256Hash hash{}; - mbedtls_sha256_ret(id.data(), id.size(), hash.data(), 0); + { unsigned int _sl = 32; EVP_Digest(id.data(), id.size(), hash.data(), &_sl, EVP_sha256(), nullptr); } const auto dirname = fmt::format("000000{:02X}", hash[0]); const auto dir2 = GetOrCreateDirectoryRelative(dir, dirname); @@ -677,7 +677,7 @@ InstallResult RegisteredCache::InstallEntry(const NCA& nca, TitleType type, const OptionalHeader opt_header{0, 0}; ContentRecord c_rec{{}, {}, {}, GetCRTypeFromNCAType(nca.GetType()), {}}; const auto& data = nca.GetBaseFile()->ReadBytes(0x100000); - mbedtls_sha256_ret(data.data(), data.size(), c_rec.hash.data(), 0); + { unsigned int _sl = 32; EVP_Digest(data.data(), data.size(), c_rec.hash.data(), &_sl, EVP_sha256(), nullptr); } std::memcpy(&c_rec.nca_id, &c_rec.hash, 16); const CNMT new_cnmt(header, opt_header, {c_rec}, {}); if (!RawInstallCitronMeta(new_cnmt)) { @@ -788,7 +788,7 @@ InstallResult RegisteredCache::RawInstallNCA(const NCA& nca, const VfsCopyFuncti id = *override_id; } else { const auto& data = in->ReadBytes(0x100000); - mbedtls_sha256_ret(data.data(), data.size(), hash.data(), 0); + { unsigned int _sl = 32; EVP_Digest(data.data(), data.size(), hash.data(), &_sl, EVP_sha256(), nullptr); } memcpy(id.data(), hash.data(), 16); } diff --git a/src/core/file_sys/xts_archive.cpp b/src/core/file_sys/xts_archive.cpp index 6692211e1d..ddc5a7ddd6 100644 --- a/src/core/file_sys/xts_archive.cpp +++ b/src/core/file_sys/xts_archive.cpp @@ -7,8 +7,8 @@ #include #include -#include -#include +#include +#include #include "common/fs/path_util.h" #include "common/hex_util.h" @@ -28,19 +28,11 @@ constexpr u64 NAX_HEADER_PADDING_SIZE = 0x4000; template static bool CalculateHMAC256(Destination* out, const SourceKey* key, std::size_t key_length, const SourceData* data, std::size_t data_length) { - mbedtls_md_context_t context; - mbedtls_md_init(&context); - - if (mbedtls_md_setup(&context, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1) || - mbedtls_md_hmac_starts(&context, reinterpret_cast(key), key_length) || - mbedtls_md_hmac_update(&context, reinterpret_cast(data), data_length) || - mbedtls_md_hmac_finish(&context, reinterpret_cast(out))) { - mbedtls_md_free(&context); - return false; - } - - mbedtls_md_free(&context); - return true; + unsigned int out_len = sizeof(Destination); + return HMAC(EVP_sha256(), + reinterpret_cast(key), static_cast(key_length), + reinterpret_cast(data), data_length, + reinterpret_cast(out), &out_len) != nullptr; } NAX::NAX(VirtualFile file_) @@ -65,7 +57,8 @@ NAX::NAX(VirtualFile file_, std::array nca_id) : header(std::make_unique()), file(std::move(file_)), keys{Core::Crypto::KeyManager::Instance()} { Core::Crypto::SHA256Hash hash{}; - mbedtls_sha256_ret(nca_id.data(), nca_id.size(), hash.data(), 0); + unsigned int sha_len = static_cast(hash.size()); + EVP_Digest(nca_id.data(), nca_id.size(), hash.data(), &sha_len, EVP_sha256(), nullptr); status = Parse(fmt::format("/registered/000000{:02X}/{}.nca", hash[0], Common::HexToString(nca_id, false))); } diff --git a/src/core/hle/service/bcat/bcat_util.h b/src/core/hle/service/bcat/bcat_util.h index 6bf2657eeb..449ce23383 100644 --- a/src/core/hle/service/bcat/bcat_util.h +++ b/src/core/hle/service/bcat/bcat_util.h @@ -5,7 +5,6 @@ #include #include -#include #include "core/hle/service/bcat/bcat_result.h" #include "core/hle/service/bcat/bcat_types.h" diff --git a/src/core/hle/service/bcat/delivery_cache_directory_service.cpp b/src/core/hle/service/bcat/delivery_cache_directory_service.cpp index 01f08a2fc5..b6a7019cc9 100644 --- a/src/core/hle/service/bcat/delivery_cache_directory_service.cpp +++ b/src/core/hle/service/bcat/delivery_cache_directory_service.cpp @@ -7,6 +7,7 @@ #include "core/hle/service/bcat/bcat_util.h" #include "core/hle/service/bcat/delivery_cache_directory_service.h" #include "core/hle/service/cmif_serialization.h" +#include namespace Service::BCAT { @@ -15,7 +16,8 @@ namespace Service::BCAT { static BcatDigest DigestFile(const FileSys::VirtualFile& file) { BcatDigest out{}; const auto bytes = file->ReadAllBytes(); - mbedtls_md5_ret(bytes.data(), bytes.size(), out.data()); + unsigned int out_len = static_cast(out.size()); + EVP_Digest(bytes.data(), bytes.size(), out.data(), &out_len, EVP_md5(), nullptr); return out; } diff --git a/src/core/hle/service/nfc/common/amiibo_crypto.cpp b/src/core/hle/service/nfc/common/amiibo_crypto.cpp index 18523aa32d..0b1db45839 100644 --- a/src/core/hle/service/nfc/common/amiibo_crypto.cpp +++ b/src/core/hle/service/nfc/common/amiibo_crypto.cpp @@ -5,8 +5,10 @@ // SPDX-License-Identifier: MIT #include -#include -#include +#include +#include +#include +#include "core/crypto/aes_ni.h" #include "common/fs/file.h" #include "common/fs/fs.h" @@ -14,6 +16,8 @@ #include "common/logging.h" #include "core/hle/service/nfc/common/amiibo_crypto.h" +using namespace Core::Crypto; + namespace Service::NFP::AmiiboCrypto { bool IsAmiiboValid(const EncryptedNTAG215File& ntag_file) { @@ -179,37 +183,43 @@ std::vector GenerateInternalKey(const InternalKey& key, const HashSeed& seed return output; } -void CryptoInit(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, const HmacKey& hmac_key, +void CryptoInit(CryptoCtx& ctx, evp_mac_ctx_st*& hmac_ctx, const HmacKey& hmac_key, std::span seed) { - // Initialize context + // Initialize counter/buffer context ctx.used = false; ctx.counter = 0; ctx.buffer_size = sizeof(ctx.counter) + seed.size(); memcpy(ctx.buffer.data() + sizeof(u16), seed.data(), seed.size()); - // Initialize HMAC context - mbedtls_md_init(&hmac_ctx); - mbedtls_md_setup(&hmac_ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1); - mbedtls_md_hmac_starts(&hmac_ctx, hmac_key.data(), hmac_key.size()); + // Initialize OpenSSL HMAC-SHA256 context via EVP_MAC (non-deprecated API) + EVP_MAC* mac = EVP_MAC_fetch(nullptr, "HMAC", nullptr); + hmac_ctx = EVP_MAC_CTX_new(mac); + EVP_MAC_free(mac); // CTX holds its own reference + + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string("digest", const_cast("SHA256"), 0), + OSSL_PARAM_construct_end() + }; + EVP_MAC_init(hmac_ctx, hmac_key.data(), hmac_key.size(), params); } -void CryptoStep(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, DrgbOutput& output) { - // If used at least once, reinitialize the HMAC +void CryptoStep(CryptoCtx& ctx, evp_mac_ctx_st* hmac_ctx, DrgbOutput& output) { + // Reset HMAC for reuse (same key, new message) - NULL key re-uses the existing key if (ctx.used) { - mbedtls_md_hmac_reset(&hmac_ctx); + EVP_MAC_init(hmac_ctx, nullptr, 0, nullptr); } - ctx.used = true; - // Store counter in big endian, and increment it + // Store counter big-endian and advance ctx.buffer[0] = static_cast(ctx.counter >> 8); ctx.buffer[1] = static_cast(ctx.counter >> 0); ctx.counter++; - // Do HMAC magic - mbedtls_md_hmac_update(&hmac_ctx, reinterpret_cast(ctx.buffer.data()), - ctx.buffer_size); - mbedtls_md_hmac_finish(&hmac_ctx, output.data()); + EVP_MAC_update(hmac_ctx, + reinterpret_cast(ctx.buffer.data()), + ctx.buffer_size); + size_t outlen = output.size(); + EVP_MAC_final(hmac_ctx, output.data(), &outlen, output.size()); } DerivedKeys GenerateKey(const InternalKey& key, const NTAG215File& data) { @@ -220,7 +230,7 @@ DerivedKeys GenerateKey(const InternalKey& key, const NTAG215File& data) { // Initialize context CryptoCtx ctx{}; - mbedtls_md_context_t hmac_ctx; + evp_mac_ctx_st* hmac_ctx = nullptr; CryptoInit(ctx, hmac_ctx, key.hmac_key, internal_key); // Generate derived keys @@ -230,27 +240,27 @@ DerivedKeys GenerateKey(const InternalKey& key, const NTAG215File& data) { CryptoStep(ctx, hmac_ctx, temp[1]); memcpy(&derived_keys, temp.data(), sizeof(DerivedKeys)); - // Cleanup context - mbedtls_md_free(&hmac_ctx); + // Cleanup + EVP_MAC_CTX_free(hmac_ctx); return derived_keys; } void Cipher(const DerivedKeys& keys, const NTAG215File& in_data, NTAG215File& out_data) { - mbedtls_aes_context aes; - std::size_t nc_off = 0; - std::array nonce_counter{}; - std::array stream_block{}; + // Build the AES-128-CTR key schedule and run the single-pass intrinsic CTR. + __m128i ks[AesNi::kRoundKeys128]; + AesNi::KeyExpand128Enc(keys.aes_key.data(), ks); - const auto aes_key_size = static_cast(keys.aes_key.size() * 8); - mbedtls_aes_setkey_enc(&aes, keys.aes_key.data(), aes_key_size); - memcpy(nonce_counter.data(), keys.aes_iv.data(), sizeof(keys.aes_iv)); + // Counter starts at the IV; copy to mutable array for CtrInc. + uint8_t ctr[AesNi::kBlockSize]; + std::memcpy(ctr, keys.aes_iv.data(), AesNi::kBlockSize); constexpr std::size_t encrypted_data_size = HMAC_TAG_START - SETTINGS_START; - mbedtls_aes_crypt_ctr(&aes, encrypted_data_size, &nc_off, nonce_counter.data(), - stream_block.data(), - reinterpret_cast(&in_data.settings), - reinterpret_cast(&out_data.settings)); + AesNi::Ctr128(ks, + reinterpret_cast(&in_data.settings), + reinterpret_cast(&out_data.settings), + encrypted_data_size, + ctr); // Copy the rest of the data directly out_data.uid = in_data.uid; @@ -317,16 +327,16 @@ bool DecodeAmiibo(const EncryptedNTAG215File& encrypted_tag_data, NTAG215File& t // Regenerate tag HMAC. Note: order matters, data HMAC depends on tag HMAC! constexpr std::size_t input_length = DYNAMIC_LOCK_START - UUID_START; - mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), tag_keys.hmac_key.data(), - sizeof(HmacKey), reinterpret_cast(&tag_data.uid), - input_length, reinterpret_cast(&tag_data.hmac_tag)); + unsigned int hmac_len = sizeof(HashData); + HMAC(EVP_sha256(), tag_keys.hmac_key.data(), static_cast(sizeof(HmacKey)), + reinterpret_cast(&tag_data.uid), input_length, + reinterpret_cast(&tag_data.hmac_tag), &hmac_len); // Regenerate data HMAC constexpr std::size_t input_length2 = DYNAMIC_LOCK_START - WRITE_COUNTER_START; - mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), data_keys.hmac_key.data(), - sizeof(HmacKey), - reinterpret_cast(&tag_data.write_counter), input_length2, - reinterpret_cast(&tag_data.hmac_data)); + HMAC(EVP_sha256(), data_keys.hmac_key.data(), static_cast(sizeof(HmacKey)), + reinterpret_cast(&tag_data.write_counter), input_length2, + reinterpret_cast(&tag_data.hmac_data), &hmac_len); if (tag_data.hmac_data != encrypted_tag_data.user_memory.hmac_data) { LOG_ERROR(Service_NFP, "hmac_data doesn't match"); @@ -358,27 +368,36 @@ bool EncodeAmiibo(const NTAG215File& tag_data, EncryptedNTAG215File& encrypted_t // Generate tag HMAC constexpr std::size_t input_length = DYNAMIC_LOCK_START - UUID_START; constexpr std::size_t input_length2 = HMAC_TAG_START - WRITE_COUNTER_START; - mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), tag_keys.hmac_key.data(), - sizeof(HmacKey), reinterpret_cast(&tag_data.uid), - input_length, reinterpret_cast(&encoded_tag_data.hmac_tag)); + unsigned int hmac_len = sizeof(HashData); + HMAC(EVP_sha256(), tag_keys.hmac_key.data(), static_cast(sizeof(HmacKey)), + reinterpret_cast(&tag_data.uid), input_length, + reinterpret_cast(&encoded_tag_data.hmac_tag), &hmac_len); - // Init mbedtls HMAC context - mbedtls_md_context_t ctx; - mbedtls_md_init(&ctx); - mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1); - - // Generate data HMAC - mbedtls_md_hmac_starts(&ctx, data_keys.hmac_key.data(), sizeof(HmacKey)); - mbedtls_md_hmac_update(&ctx, reinterpret_cast(&tag_data.write_counter), - input_length2); // Data - mbedtls_md_hmac_update(&ctx, reinterpret_cast(&encoded_tag_data.hmac_tag), - sizeof(HashData)); // Tag HMAC - mbedtls_md_hmac_update(&ctx, reinterpret_cast(&tag_data.uid), - input_length); - mbedtls_md_hmac_finish(&ctx, reinterpret_cast(&encoded_tag_data.hmac_data)); - - // HMAC cleanup - mbedtls_md_free(&ctx); + // Generate data HMAC via EVP_MAC (multi-step: spans three non-contiguous buffers) + { + EVP_MAC* mac = EVP_MAC_fetch(nullptr, "HMAC", nullptr); + EVP_MAC_CTX* ctx = EVP_MAC_CTX_new(mac); + EVP_MAC_free(mac); + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string("digest", const_cast("SHA256"), 0), + OSSL_PARAM_construct_end() + }; + EVP_MAC_init(ctx, data_keys.hmac_key.data(), sizeof(HmacKey), params); + EVP_MAC_update(ctx, + reinterpret_cast(&tag_data.write_counter), + input_length2); + EVP_MAC_update(ctx, + reinterpret_cast(&encoded_tag_data.hmac_tag), + sizeof(HashData)); + EVP_MAC_update(ctx, + reinterpret_cast(&tag_data.uid), + input_length); + size_t outlen = sizeof(HashData); + EVP_MAC_final(ctx, + reinterpret_cast(&encoded_tag_data.hmac_data), + &outlen, sizeof(HashData)); + EVP_MAC_CTX_free(ctx); + } // Encrypt Cipher(data_keys, tag_data, encoded_tag_data); diff --git a/src/core/hle/service/nfc/common/amiibo_crypto.h b/src/core/hle/service/nfc/common/amiibo_crypto.h index 2cc0e4d519..c48ead0357 100644 --- a/src/core/hle/service/nfc/common/amiibo_crypto.h +++ b/src/core/hle/service/nfc/common/amiibo_crypto.h @@ -6,8 +6,8 @@ #include #include "core/hle/service/nfp/nfp_types.h" - -struct mbedtls_md_context_t; +// Forward declare OpenSSL EVP_MAC_CTX to avoid pulling in openssl/evp.h in the header. +struct evp_mac_ctx_st; namespace Service::NFP::AmiiboCrypto { // Byte locations in Service::NFP::NTAG215File @@ -73,12 +73,12 @@ HashSeed GetSeed(const NTAG215File& data); // Middle step on the generation of derived keys std::vector GenerateInternalKey(const InternalKey& key, const HashSeed& seed); -// Initializes mbedtls context -void CryptoInit(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, const HmacKey& hmac_key, +// Initializes OpenSSL HMAC-SHA256 context for key derivation +void CryptoInit(CryptoCtx& ctx, evp_mac_ctx_st*& hmac_ctx, const HmacKey& hmac_key, std::span seed); -// Feeds data to mbedtls context to generate the derived key -void CryptoStep(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, DrgbOutput& output); +// Feeds data into the HMAC context to generate the next derived key block +void CryptoStep(CryptoCtx& ctx, evp_mac_ctx_st* hmac_ctx, DrgbOutput& output); // Generates the derived key from amiibo data DerivedKeys GenerateKey(const InternalKey& key, const NTAG215File& data); diff --git a/src/core/hle/service/ro/ro.cpp b/src/core/hle/service/ro/ro.cpp index 0d325e7945..9a196a4df7 100644 --- a/src/core/hle/service/ro/ro.cpp +++ b/src/core/hle/service/ro/ro.cpp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include +#include #include "common/scope_exit.h" #include "core/hle/kernel/k_process.h" @@ -178,7 +178,7 @@ struct ProcessContext { std::vector nro_data(size); m_process->GetMemory().ReadBlock(base_address, nro_data.data(), size); - mbedtls_sha256_ret(nro_data.data(), size, hash.data(), 0); + { unsigned int _sl = 32; EVP_Digest(nro_data.data(), size, hash.data(), &_sl, EVP_sha256(), nullptr); } } for (size_t i = 0; i < MaxNrrInfos; i++) { diff --git a/src/core/loader/nca.cpp b/src/core/loader/nca.cpp index de27ec49e2..631ccce5a7 100644 --- a/src/core/loader/nca.cpp +++ b/src/core/loader/nca.cpp @@ -14,7 +14,7 @@ #include "core/hle/service/filesystem/filesystem.h" #include "core/loader/deconstructed_rom_directory.h" #include "core/loader/nca.h" -#include "mbedtls/sha256.h" +#include namespace Loader { @@ -112,14 +112,13 @@ ResultStatus AppLoader_NCA::VerifyIntegrity(std::function // Declare buffer to read into. std::vector buffer(4_MiB); - // Initialize sha256 verification context. - mbedtls_sha256_context ctx; - mbedtls_sha256_init(&ctx); - mbedtls_sha256_starts_ret(&ctx, 0); + // Initialize SHA-256 streaming context via OpenSSL EVP. + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr); - // Ensure we maintain a clean state on exit. + // Ensure we clean up on exit. SCOPE_EXIT { - mbedtls_sha256_free(&ctx); + EVP_MD_CTX_free(ctx); }; // Declare counters. @@ -133,7 +132,7 @@ ResultStatus AppLoader_NCA::VerifyIntegrity(std::function const size_t read_size = file->Read(buffer.data(), intended_read_size, processed_size); // Update the hash function with the buffer contents. - mbedtls_sha256_update_ret(&ctx, buffer.data(), read_size); + EVP_DigestUpdate(ctx, buffer.data(), read_size); // Update counters. processed_size += read_size; @@ -146,7 +145,8 @@ ResultStatus AppLoader_NCA::VerifyIntegrity(std::function // Finalize context and compute the output hash. std::array output_hash; - mbedtls_sha256_finish_ret(&ctx, output_hash.data()); + unsigned int hash_len = static_cast(output_hash.size()); + EVP_DigestFinal_ex(ctx, output_hash.data(), &hash_len); // Compare to expected. if (std::memcmp(input_hash.data(), output_hash.data(), NcaSha256HalfHashLength) != 0) { diff --git a/src/dedicated_room/citron_room.cpp b/src/dedicated_room/citron_room.cpp index 5514b06906..2362d03b1b 100644 --- a/src/dedicated_room/citron_room.cpp +++ b/src/dedicated_room/citron_room.cpp @@ -16,7 +16,7 @@ #include #endif -#include +#include #include "common/common_types.h" #include "common/detached_tasks.h" #include "common/fs/file.h" @@ -76,17 +76,36 @@ static constexpr char BanListMagic[] = "CitronRoom-BanList-1"; static constexpr char token_delimiter{':'}; -static void PadToken(std::string& token) { - std::size_t outlen = 0; +// Decode standard base64 (with padding). Returns number of decoded bytes or -1 on error. +// EVP_DecodeBlock strips whitespace and handles trailing '=' padding. +static int Base64Decode(unsigned char* out, int out_max, + const unsigned char* in, int in_len) { + if (in_len == 0) return 0; + // EVP_DecodeBlock requires length to be a multiple of 4; pad if needed with a copy. + std::string padded(reinterpret_cast(in), static_cast(in_len)); + while (padded.size() % 4 != 0) padded += '='; + int n = EVP_DecodeBlock(out, reinterpret_cast(padded.data()), + static_cast(padded.size())); + if (n < 0) return -1; + // Subtract padding bytes + if (!padded.empty() && padded[padded.size()-1] == '=') n--; + if (padded.size() >= 2 && padded[padded.size()-2] == '=') n--; + return n; +} +static void PadToken(std::string& token) { std::array output{}; std::array roundtrip{}; for (size_t i = 0; i < 3; i++) { - mbedtls_base64_decode(output.data(), output.size(), &outlen, - reinterpret_cast(token.c_str()), - token.length()); - mbedtls_base64_encode(roundtrip.data(), roundtrip.size(), &outlen, output.data(), outlen); - if (memcmp(roundtrip.data(), token.data(), token.size()) == 0) { + int outlen = Base64Decode(output.data(), static_cast(output.size()), + reinterpret_cast(token.c_str()), + static_cast(token.length())); + if (outlen < 0) break; + // EVP_EncodeBlock: produces standard base64 with '=' padding, no newlines + int encoded_len = EVP_EncodeBlock(roundtrip.data(), output.data(), outlen); + if (std::string_view(reinterpret_cast(roundtrip.data()), + static_cast(encoded_len)) == + std::string_view(token.data(), token.size())) { break; } token.push_back('='); @@ -94,25 +113,25 @@ static void PadToken(std::string& token) { } static std::string UsernameFromDisplayToken(const std::string& display_token) { - std::size_t outlen; - std::array output{}; - mbedtls_base64_decode(output.data(), output.size(), &outlen, - reinterpret_cast(display_token.c_str()), - display_token.length()); - std::string decoded_display_token(reinterpret_cast(&output), outlen); - return decoded_display_token.substr(0, decoded_display_token.find(token_delimiter)); + int outlen = Base64Decode(output.data(), static_cast(output.size()), + reinterpret_cast(display_token.c_str()), + static_cast(display_token.length())); + if (outlen < 0) return {}; + std::string decoded(reinterpret_cast(output.data()), static_cast(outlen)); + return decoded.substr(0, decoded.find(token_delimiter)); } static std::string TokenFromDisplayToken(const std::string& display_token) { - std::size_t outlen; - std::array output{}; - mbedtls_base64_decode(output.data(), output.size(), &outlen, - reinterpret_cast(display_token.c_str()), - display_token.length()); - std::string decoded_display_token(reinterpret_cast(&output), outlen); - return decoded_display_token.substr(decoded_display_token.find(token_delimiter) + 1); + int outlen = Base64Decode(output.data(), static_cast(output.size()), + reinterpret_cast(display_token.c_str()), + static_cast(display_token.length())); + if (outlen < 0) return {}; + std::string decoded(reinterpret_cast(output.data()), static_cast(outlen)); + const auto pos = decoded.find(token_delimiter); + if (pos == std::string::npos) return {}; + return decoded.substr(pos + 1); } static Network::Room::BanList LoadBanList(const std::string& path) { diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index 8de260ead7..ca1147333d 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -15,7 +15,7 @@ add_library(web_service STATIC ) create_target_directory_groups(web_service) -target_link_libraries(web_service PRIVATE common network nlohmann_json::nlohmann_json httplib::httplib cpp-jwt::cpp-jwt) +target_link_libraries(web_service PRIVATE common network nlohmann_json::nlohmann_json httplib::httplib OpenSSL::Crypto) if (CITRON_USE_PRECOMPILED_HEADERS) target_precompile_headers(web_service PRIVATE precompiled_headers.h) diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp index 96e23c70e5..d03d617a25 100644 --- a/src/web_service/verify_user_jwt.cpp +++ b/src/web_service/verify_user_jwt.cpp @@ -1,70 +1,194 @@ // SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" // for deprecated OpenSSL functions -#endif -#include -#if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic pop -#endif +// JWT RS256 verification using OpenSSL EVP directly. +// Replaces cpp-jwt v1.4 (which was hardwired to OpenSSL anyway). +// Only RS256 (RSA-SHA256 PKCS#1 v1.5) is needed; sign path is unused. + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include -#include #include "common/logging.h" +#include "network/verify_user.h" #include "web_service/verify_user_jwt.h" #include "web_service/web_backend.h" #include "web_service/web_result.h" namespace WebService { -static std::string public_key; -std::string GetPublicKey(const std::string& host) { - if (public_key.empty()) { - Client client(host, "", ""); // no need for credentials here - public_key = client.GetPlain("/jwt/external/key.pem", true).returned_data; - if (public_key.empty()) { - LOG_ERROR(WebService, "Could not fetch external JWT public key, verification may fail"); - } else { - LOG_INFO(WebService, "Fetched external JWT public key (size={})", public_key.size()); - } +namespace { + +// ── Base64url helpers ───────────────────────────────────────────────────────── + +// Standard base64 alphabet + url-safe variant (-_ instead of +/) +// JWT uses base64url without padding. + +static const std::string kBase64Chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +// Decode base64 or base64url (interchangeable here via char mapping). +// Handles missing padding. Returns decoded bytes or empty on error. +static std::vector Base64UrlDecode(std::string_view input) { + std::string s(input); + // Map url-safe chars to standard base64 + for (char& c : s) { + if (c == '-') c = '+'; + if (c == '_') c = '/'; } - return public_key; + // Add padding + while (s.size() % 4 != 0) s += '='; + + // Use OpenSSL EVP_DecodeBlock + // EVP_DecodeBlock strips whitespace and handles padding. + std::vector out(s.size()); // upper bound + int len = EVP_DecodeBlock(out.data(), + reinterpret_cast(s.data()), + static_cast(s.size())); + if (len < 0) return {}; + // Remove padding bytes from count + if (!s.empty() && s[s.size()-1] == '=') len--; + if (s.size() >= 2 && s[s.size()-2] == '=') len--; + out.resize(static_cast(len)); + return out; +} + +// ── JWT parsing ─────────────────────────────────────────────────────────────── + +struct JwtParts { + std::string header_b64; + std::string payload_b64; + std::vector signature; + // header.payload as bytes (the signed content) + std::string signed_input; +}; + +static std::optional SplitJwt(const std::string& token) { + const auto dot1 = token.find('.'); + if (dot1 == std::string::npos) return std::nullopt; + const auto dot2 = token.find('.', dot1 + 1); + if (dot2 == std::string::npos) return std::nullopt; + + JwtParts p; + p.header_b64 = token.substr(0, dot1); + p.payload_b64 = token.substr(dot1 + 1, dot2 - dot1 - 1); + p.signed_input = token.substr(0, dot2); // "header.payload" + + const std::string sig_b64 = token.substr(dot2 + 1); + p.signature = Base64UrlDecode(sig_b64); + if (p.signature.empty()) return std::nullopt; + + return p; +} + +// ── RS256 verification ──────────────────────────────────────────────────────── + +// Verifies RS256 (RSASSA-PKCS1-v1_5 with SHA-256). +// pub_key_pem: PEM-encoded RSA public key (PKCS#8 SubjectPublicKeyInfo or PKCS#1). +// signed_input: the "header.payload" bytes that were signed. +// signature: raw RSA signature bytes. +static bool VerifyRS256(const std::string& pub_key_pem, + const std::string& signed_input, + const std::vector& signature) { + // Load public key from PEM + BIO* bio = BIO_new_mem_buf(pub_key_pem.data(), static_cast(pub_key_pem.size())); + if (!bio) return false; + + EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr); + BIO_free(bio); + if (!pkey) { + LOG_ERROR(WebService, "Failed to load JWT public key: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return false; + } + + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + if (!ctx) { EVP_PKEY_free(pkey); return false; } + + bool ok = false; + if (EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, pkey) == 1 && + EVP_DigestVerifyUpdate(ctx, + reinterpret_cast(signed_input.data()), + signed_input.size()) == 1 && + EVP_DigestVerifyFinal(ctx, signature.data(), signature.size()) == 1) { + ok = true; + } + + EVP_MD_CTX_free(ctx); + EVP_PKEY_free(pkey); + return ok; +} + +} // namespace + +// ── Public interface ────────────────────────────────────────────────────────── + +std::string GetPublicKey(const std::string& host) { + // Fetch the public key from the backend's /jwt/key endpoint. + // This is unchanged from the original implementation. + Client client(host, {}, {}); + auto response = client.GetJson("/jwt/key", true); + if (response.result_code != WebResult::Code::Success) { + LOG_ERROR(WebService, "Failed to retrieve JWT public key from {}", host); + return {}; + } + return response.returned_data; } VerifyUserJWT::VerifyUserJWT(const std::string& host) : pub_key(GetPublicKey(host)) {} Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& verify_uid, const std::string& token) { - const std::string audience = fmt::format("external-{}", verify_uid); - using namespace jwt::params; - std::error_code error; - - // We use the Citra backend so the issuer is citra-core - auto decoded = - jwt::decode(token, algorithms({"rs256"}), error, secret(pub_key), issuer("citra-core"), - aud(audience), validate_iat(true), validate_jti(true)); - if (error) { - LOG_INFO(WebService, "Verification failed: category={}, code={}, message={}", - error.category().name(), error.value(), error.message()); + if (pub_key.empty()) { + LOG_ERROR(WebService, "Public key unavailable; cannot verify JWT"); return {}; } - Network::VerifyUser::UserData user_data{}; - if (decoded.payload().has_claim("username")) { - user_data.username = decoded.payload().get_claim_value("username"); + + // Split the JWT + const auto parts = SplitJwt(token); + if (!parts) { + LOG_ERROR(WebService, "Malformed JWT token"); + return {}; } - if (decoded.payload().has_claim("displayName")) { - user_data.display_name = decoded.payload().get_claim_value("displayName"); + + // Verify signature + if (!VerifyRS256(pub_key, parts->signed_input, parts->signature)) { + LOG_ERROR(WebService, "JWT signature verification failed"); + return {}; } - if (decoded.payload().has_claim("avatarUrl")) { - user_data.avatar_url = decoded.payload().get_claim_value("avatarUrl"); + + // Decode and parse the payload + const auto payload_bytes = Base64UrlDecode(parts->payload_b64); + if (payload_bytes.empty()) { + LOG_ERROR(WebService, "Failed to decode JWT payload"); + return {}; } - if (decoded.payload().has_claim("roles")) { - auto roles = decoded.payload().get_claim_value>("roles"); - user_data.moderator = std::find(roles.begin(), roles.end(), "moderator") != roles.end(); + + try { + const std::string payload_str(payload_bytes.begin(), payload_bytes.end()); + const auto payload = nlohmann::json::parse(payload_str); + + Network::VerifyUser::UserData user_data; + user_data.username = payload.value("username", ""); + user_data.display_name = payload.value("display_name", ""); + user_data.avatar_url = payload.value("avatar_url", ""); + user_data.moderator = payload.value("administrator", false); + return user_data; + } catch (const nlohmann::json::exception& e) { + LOG_ERROR(WebService, "Failed to parse JWT payload JSON: {}", e.what()); + return {}; } - return user_data; } } // namespace WebService