From cc49624c7a0857815ab3742bbb58182a0e6359eb Mon Sep 17 00:00:00 2001 From: Brandon Lehmann Date: Tue, 24 Feb 2026 18:11:26 -0500 Subject: [PATCH] initial commit --- CMakeLists.txt | 213 +++++++++++++ LICENSE | 25 ++ bench/CMakeLists.txt | 20 ++ bench/bench_all.cpp | 85 ++++++ fuzz/CMakeLists.txt | 24 ++ fuzz/fuzz_cbc.cpp | 35 +++ fuzz/fuzz_ctr.cpp | 29 ++ fuzz/fuzz_ecb.cpp | 32 ++ fuzz/fuzz_gcm.cpp | 35 +++ include/tinyaes.h | 34 +++ include/tinyaes/cbc.h | 111 +++++++ include/tinyaes/common.h | 107 +++++++ include/tinyaes/ctr.h | 66 ++++ include/tinyaes/ecb.h | 75 +++++ include/tinyaes/gcm.h | 91 ++++++ include/tinyaes/version.h | 32 ++ src/backend/aes_aesni.cpp | 229 ++++++++++++++ src/backend/aes_arm_ce.cpp | 127 ++++++++ src/backend/aes_portable.cpp | 338 +++++++++++++++++++++ src/backend/aes_vaes.cpp | 116 +++++++ src/backend/ghash_arm_ce.cpp | 114 +++++++ src/backend/ghash_pclmulqdq.cpp | 128 ++++++++ src/backend/ghash_portable.cpp | 113 +++++++ src/backend/ghash_vpclmulqdq.cpp | 49 +++ src/cbc.cpp | 300 ++++++++++++++++++ src/cpuid.cpp | 124 ++++++++ src/cpuid.h | 52 ++++ src/ctr.cpp | 118 ++++++++ src/ecb.cpp | 143 +++++++++ src/gcm.cpp | 354 ++++++++++++++++++++++ src/internal/aes_impl.h | 94 ++++++ src/internal/endian.h | 79 +++++ src/internal/ghash.h | 57 ++++ src/iv_generate.cpp | 66 ++++ src/keyschedule.cpp | 193 ++++++++++++ src/secure_zero.cpp | 80 +++++ tests/CMakeLists.txt | 43 +++ tests/test_cbc.cpp | 81 +++++ tests/test_cpuid.cpp | 39 +++ tests/test_ctr.cpp | 77 +++++ tests/test_ecb.cpp | 93 ++++++ tests/test_gcm.cpp | 111 +++++++ tests/test_gcm_auth_failure.cpp | 97 ++++++ tests/test_harness.h | 125 ++++++++ tests/test_iv_generation.cpp | 50 +++ tests/test_keyschedule.cpp | 116 +++++++ tests/test_main.cpp | 32 ++ tests/test_padding.cpp | 90 ++++++ tests/vectors/aes_cbc_vectors.inl | 51 ++++ tests/vectors/aes_ctr_vectors.inl | 51 ++++ tests/vectors/aes_ecb_vectors.inl | 58 ++++ tests/vectors/aes_gcm_vectors.inl | 110 +++++++ tests/vectors/aes_keyschedule_vectors.inl | 41 +++ 53 files changed, 5153 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 bench/CMakeLists.txt create mode 100644 bench/bench_all.cpp create mode 100644 fuzz/CMakeLists.txt create mode 100644 fuzz/fuzz_cbc.cpp create mode 100644 fuzz/fuzz_ctr.cpp create mode 100644 fuzz/fuzz_ecb.cpp create mode 100644 fuzz/fuzz_gcm.cpp create mode 100644 include/tinyaes.h create mode 100644 include/tinyaes/cbc.h create mode 100644 include/tinyaes/common.h create mode 100644 include/tinyaes/ctr.h create mode 100644 include/tinyaes/ecb.h create mode 100644 include/tinyaes/gcm.h create mode 100644 include/tinyaes/version.h create mode 100644 src/backend/aes_aesni.cpp create mode 100644 src/backend/aes_arm_ce.cpp create mode 100644 src/backend/aes_portable.cpp create mode 100644 src/backend/aes_vaes.cpp create mode 100644 src/backend/ghash_arm_ce.cpp create mode 100644 src/backend/ghash_pclmulqdq.cpp create mode 100644 src/backend/ghash_portable.cpp create mode 100644 src/backend/ghash_vpclmulqdq.cpp create mode 100644 src/cbc.cpp create mode 100644 src/cpuid.cpp create mode 100644 src/cpuid.h create mode 100644 src/ctr.cpp create mode 100644 src/ecb.cpp create mode 100644 src/gcm.cpp create mode 100644 src/internal/aes_impl.h create mode 100644 src/internal/endian.h create mode 100644 src/internal/ghash.h create mode 100644 src/iv_generate.cpp create mode 100644 src/keyschedule.cpp create mode 100644 src/secure_zero.cpp create mode 100644 tests/CMakeLists.txt create mode 100644 tests/test_cbc.cpp create mode 100644 tests/test_cpuid.cpp create mode 100644 tests/test_ctr.cpp create mode 100644 tests/test_ecb.cpp create mode 100644 tests/test_gcm.cpp create mode 100644 tests/test_gcm_auth_failure.cpp create mode 100644 tests/test_harness.h create mode 100644 tests/test_iv_generation.cpp create mode 100644 tests/test_keyschedule.cpp create mode 100644 tests/test_main.cpp create mode 100644 tests/test_padding.cpp create mode 100644 tests/vectors/aes_cbc_vectors.inl create mode 100644 tests/vectors/aes_ctr_vectors.inl create mode 100644 tests/vectors/aes_ecb_vectors.inl create mode 100644 tests/vectors/aes_gcm_vectors.inl create mode 100644 tests/vectors/aes_keyschedule_vectors.inl diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c3837e3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,213 @@ +cmake_minimum_required(VERSION 3.10) +project(tinyaes VERSION 0.1.0 LANGUAGES CXX) + +include(CheckCXXCompilerFlag) + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) +endif() + +option(BUILD_SHARED_LIBS "Build shared library" OFF) +option(BUILD_TESTS "Build unit tests" OFF) +option(BUILD_BENCHMARKS "Build benchmarks" OFF) +option(FORCE_PORTABLE "Disable SIMD backends; use only portable code" OFF) + +# --- Library sources --- +set(TINYAES_SOURCES + src/secure_zero.cpp + src/cpuid.cpp + src/iv_generate.cpp + src/keyschedule.cpp + src/ecb.cpp + src/cbc.cpp + src/ctr.cpp + src/gcm.cpp + src/backend/aes_portable.cpp + src/backend/ghash_portable.cpp +) + +# Conditionally add SIMD backend sources on x86_64 +if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64" AND NOT FORCE_PORTABLE) + list(APPEND TINYAES_SOURCES + src/backend/aes_aesni.cpp + src/backend/aes_vaes.cpp + src/backend/ghash_pclmulqdq.cpp + src/backend/ghash_vpclmulqdq.cpp + ) +endif() + +# Conditionally add ARM64 crypto extension backend sources +if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64" AND NOT FORCE_PORTABLE) + list(APPEND TINYAES_SOURCES + src/backend/aes_arm_ce.cpp + src/backend/ghash_arm_ce.cpp + ) +endif() + +add_library(tinyaes ${TINYAES_SOURCES}) + +# --- Shared library symbol visibility --- +if(BUILD_SHARED_LIBS) + set_target_properties(tinyaes PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + ) + target_compile_definitions(tinyaes PUBLIC TINYAES_SHARED=1) + target_compile_definitions(tinyaes PRIVATE TINYAES_BUILDING=1) +endif() + +if(FORCE_PORTABLE) + target_compile_definitions(tinyaes PUBLIC TINYAES_FORCE_PORTABLE=1) +endif() + +target_include_directories(tinyaes PUBLIC + $ +) +# Internal headers (src/) for the library itself +target_include_directories(tinyaes PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +set_target_properties(tinyaes PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) + +# --- Link bcrypt on Windows for BCryptGenRandom --- +if(WIN32) + target_link_libraries(tinyaes PRIVATE bcrypt) +endif() + +# --- Detect MinGW environment --- +# Only use CMake's built-in MINGW (set for GCC with MinGW). Clang on Windows can +# target either MinGW or MSVC ABI, and the linker varies (GNU ld vs lld-link) — +# applying the wrong linker flags breaks the build. We let Clang on Windows rely +# on the OS defaults for DEP/ASLR (enabled by default on modern Windows) rather +# than guessing the linker style. + +# --- Hardening flags --- +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(tinyaes PRIVATE + -Wall -Wextra -Wpedantic -Werror + -Wconversion -Wsign-conversion -Wshadow + -Wcast-align -Wundef + -Wformat=2 -Wformat-security + -fno-strict-overflow + ) + # Stack protector: requires libssp or compiler-rt at link time. + include(CheckCXXSourceCompiles) + set(CMAKE_REQUIRED_FLAGS "-fstack-protector-strong") + check_cxx_source_compiles("int main() { char buf[64]; buf[0] = 0; return buf[0]; }" HAS_STACK_PROTECTOR_STRONG) + unset(CMAKE_REQUIRED_FLAGS) + if(HAS_STACK_PROTECTOR_STRONG) + target_compile_options(tinyaes PRIVATE -fstack-protector-strong) + endif() + # -fPIC is implied on Windows PE; only needed on ELF/Mach-O + if(NOT WIN32) + target_compile_options(tinyaes PRIVATE -fPIC) + # Stack clash protection (Linux only) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_compile_options(tinyaes PRIVATE -fstack-clash-protection) + endif() + endif() + # Intel CET control flow integrity (x86_64 only) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + check_cxx_compiler_flag("-fcf-protection=full" HAS_CF_PROTECTION) + if(HAS_CF_PROTECTION) + target_compile_options(tinyaes PRIVATE -fcf-protection=full) + endif() + endif() + # -Wstrict-aliasing=2 only for GCC + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options(tinyaes PRIVATE -Wstrict-aliasing=2) + target_compile_options(tinyaes PRIVATE + $<$:-D_GLIBCXX_ASSERTIONS> + ) + endif() + # _FORTIFY_SOURCE requires optimization; not reliably supported on MinGW + if(NOT WIN32) + target_compile_options(tinyaes PRIVATE + $<$>:-D_FORTIFY_SOURCE=2> + ) + endif() + # Linker hardening + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set_property(TARGET tinyaes APPEND_STRING PROPERTY LINK_FLAGS + " -Wl,-z,relro,-z,now -Wl,-z,noexecstack") + if(BUILD_SHARED_LIBS) + set_property(TARGET tinyaes APPEND_STRING PROPERTY LINK_FLAGS " -Wl,-z,defs") + endif() + endif() + # macOS: -bind_at_load is deprecated on modern macOS (eager binding is the default) + # MinGW linker hardening (DEP + ASLR) — only for GCC+MinGW (known GNU ld) + if(MINGW) + set_property(TARGET tinyaes APPEND_STRING PROPERTY LINK_FLAGS + " -Wl,--nxcompat -Wl,--dynamicbase -Wl,--high-entropy-va") + endif() +elseif(MSVC) + target_compile_options(tinyaes PRIVATE + /W4 /WX + /sdl + /GS + /guard:cf + ) + check_cxx_compiler_flag("/Qspectre" HAS_QSPECTRE) + if(HAS_QSPECTRE) + target_compile_options(tinyaes PRIVATE /Qspectre) + endif() + set_property(TARGET tinyaes APPEND_STRING PROPERTY LINK_FLAGS + " /DYNAMICBASE /NXCOMPAT /HIGHENTROPYVA") + check_cxx_compiler_flag("/guard:ehcont" HAS_GUARD_EHCONT) + if(HAS_GUARD_EHCONT) + target_compile_options(tinyaes PRIVATE /guard:ehcont) + set_property(TARGET tinyaes APPEND_STRING PROPERTY LINK_FLAGS " /guard:ehcont /CETCOMPAT") + endif() +endif() + +# --- Per-backend SIMD compile flags --- +if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64" AND NOT FORCE_PORTABLE) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + set_source_files_properties(src/backend/aes_aesni.cpp PROPERTIES + COMPILE_FLAGS "-maes -msse4.1") + set_source_files_properties(src/backend/aes_vaes.cpp PROPERTIES + COMPILE_FLAGS "-mavx512f -mvaes -maes -msse4.1") + set_source_files_properties(src/backend/ghash_pclmulqdq.cpp PROPERTIES + COMPILE_FLAGS "-mpclmul -msse4.1") + set_source_files_properties(src/backend/ghash_vpclmulqdq.cpp PROPERTIES + COMPILE_FLAGS "-mavx512f -mvpclmulqdq -mpclmul -msse4.1") + elseif(MSVC) + # MSVC: AES-NI and PCLMULQDQ available without extra flags on x64 + set_source_files_properties(src/backend/aes_vaes.cpp PROPERTIES + COMPILE_FLAGS "/arch:AVX512") + set_source_files_properties(src/backend/ghash_vpclmulqdq.cpp PROPERTIES + COMPILE_FLAGS "/arch:AVX512") + endif() +endif() + +# --- Per-backend ARM64 crypto extension compile flags --- +if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64" AND NOT FORCE_PORTABLE) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + set_source_files_properties(src/backend/aes_arm_ce.cpp PROPERTIES + COMPILE_FLAGS "-march=armv8-a+crypto") + set_source_files_properties(src/backend/ghash_arm_ce.cpp PROPERTIES + COMPILE_FLAGS "-march=armv8-a+crypto") + endif() + # MSVC on ARM64: crypto extensions are enabled by default +endif() + +# --- Tests --- +if(BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() + +# --- Benchmarks --- +if(BUILD_BENCHMARKS) + add_subdirectory(bench) +endif() + +# --- Fuzzing (Clang only, Linux only) --- +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_subdirectory(fuzz) +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3463bbe --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2025-2026, Brandon Lehmann + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be + used to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/bench/CMakeLists.txt b/bench/CMakeLists.txt new file mode 100644 index 0000000..f8d5325 --- /dev/null +++ b/bench/CMakeLists.txt @@ -0,0 +1,20 @@ +add_executable(tinyaes_benchmarks + bench_all.cpp +) + +target_link_libraries(tinyaes_benchmarks PRIVATE tinyaes) +target_include_directories(tinyaes_benchmarks PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src +) +set_target_properties(tinyaes_benchmarks PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} +) + +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(tinyaes_benchmarks PRIVATE -Wall -Wextra -Wpedantic) +elseif(MSVC) + target_compile_options(tinyaes_benchmarks PRIVATE /W4) +endif() diff --git a/bench/bench_all.cpp b/bench/bench_all.cpp new file mode 100644 index 0000000..1c3788c --- /dev/null +++ b/bench/bench_all.cpp @@ -0,0 +1,85 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "tinyaes.h" + +#include +#include +#include +#include + +static constexpr size_t BENCH_SIZE = 1024 * 1024; // 1 MiB +static constexpr int ITERATIONS = 100; + +template static double bench(const char *name, Fn &&fn) +{ + // Warmup + fn(); + + auto start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < ITERATIONS; ++i) + { + fn(); + } + auto end = std::chrono::high_resolution_clock::now(); + double ms = std::chrono::duration(end - start).count(); + double total_bytes = static_cast(BENCH_SIZE) * ITERATIONS; + double mib_per_sec = (total_bytes / (1024.0 * 1024.0)) / (ms / 1000.0); + std::printf("%-30s %8.2f MiB/s (%6.2f ms total)\n", name, mib_per_sec, ms); + return mib_per_sec; +} + +int main() +{ + std::vector key_128(16, 0x42); + std::vector key_256(32, 0x42); + std::vector iv_16(16, 0x01); + std::vector iv_12(12, 0x01); + std::vector plaintext(BENCH_SIZE, 0x55); + std::vector aad = {0xAA, 0xBB, 0xCC, 0xDD}; + + std::printf("TinyAES Benchmarks (%zu bytes x %d iterations)\n", BENCH_SIZE, ITERATIONS); + std::printf("================================================================\n"); + + // ECB + bench("ECB-128 encrypt", [&]() { + std::vector ct; + tinyaes::ecb_encrypt(key_128, plaintext, ct); + }); + bench("ECB-256 encrypt", [&]() { + std::vector ct; + tinyaes::ecb_encrypt(key_256, plaintext, ct); + }); + + // CBC + bench("CBC-128 encrypt", [&]() { + std::vector ct; + tinyaes::cbc_encrypt(key_128, iv_16, plaintext, ct); + }); + bench("CBC-256 encrypt", [&]() { + std::vector ct; + tinyaes::cbc_encrypt(key_256, iv_16, plaintext, ct); + }); + + // CTR + bench("CTR-128 encrypt", [&]() { + std::vector ct; + tinyaes::ctr_crypt(key_128, iv_16, plaintext, ct); + }); + bench("CTR-256 encrypt", [&]() { + std::vector ct; + tinyaes::ctr_crypt(key_256, iv_16, plaintext, ct); + }); + + // GCM + bench("GCM-128 encrypt", [&]() { + std::vector ct, tag; + tinyaes::gcm_encrypt(key_128, iv_12, aad, plaintext, ct, tag); + }); + bench("GCM-256 encrypt", [&]() { + std::vector ct, tag; + tinyaes::gcm_encrypt(key_256, iv_12, aad, plaintext, ct, tag); + }); + + return 0; +} diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt new file mode 100644 index 0000000..576f69a --- /dev/null +++ b/fuzz/CMakeLists.txt @@ -0,0 +1,24 @@ +if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") + return() +endif() +if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + return() +endif() + +set(FUZZ_TARGETS fuzz_ecb fuzz_cbc fuzz_ctr fuzz_gcm) + +foreach(target ${FUZZ_TARGETS}) + add_executable(${target} ${target}.cpp) + target_link_libraries(${target} PRIVATE tinyaes) + target_include_directories(${target} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src + ) + set_target_properties(${target} PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/fuzz + ) + target_compile_options(${target} PRIVATE -fsanitize=fuzzer,address) + set_property(TARGET ${target} APPEND_STRING PROPERTY LINK_FLAGS " -fsanitize=fuzzer,address") +endforeach() diff --git a/fuzz/fuzz_cbc.cpp b/fuzz/fuzz_cbc.cpp new file mode 100644 index 0000000..5f0bc87 --- /dev/null +++ b/fuzz/fuzz_cbc.cpp @@ -0,0 +1,35 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "tinyaes/cbc.h" +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + if (size < 32) + return 0; + + // First 16 bytes = key, next 16 bytes = IV, rest = plaintext + std::vector key(data, data + 16); + std::vector iv(data + 16, data + 32); + + size_t remaining = size - 32; + if (remaining == 0) + return 0; + + // Use PKCS#7 which accepts any length + std::vector plaintext(data + 32, data + size); + std::vector ct, pt; + + auto result = tinyaes::cbc_encrypt_pkcs7(key, iv, plaintext, ct); + if (result != tinyaes::Result::Success) + return 0; + + result = tinyaes::cbc_decrypt_pkcs7(key, iv, ct, pt); + assert(result == tinyaes::Result::Success); + assert(pt == plaintext); + + return 0; +} diff --git a/fuzz/fuzz_ctr.cpp b/fuzz/fuzz_ctr.cpp new file mode 100644 index 0000000..279555d --- /dev/null +++ b/fuzz/fuzz_ctr.cpp @@ -0,0 +1,29 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "tinyaes/ctr.h" +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + if (size < 33) + return 0; + + // First 16 bytes = key, next 16 bytes = IV, rest = plaintext + std::vector key(data, data + 16); + std::vector iv(data + 16, data + 32); + std::vector plaintext(data + 32, data + size); + std::vector ct, pt; + + auto result = tinyaes::ctr_crypt(key, iv, plaintext, ct); + if (result != tinyaes::Result::Success) + return 0; + + result = tinyaes::ctr_crypt(key, iv, ct, pt); + assert(result == tinyaes::Result::Success); + assert(pt == plaintext); + + return 0; +} diff --git a/fuzz/fuzz_ecb.cpp b/fuzz/fuzz_ecb.cpp new file mode 100644 index 0000000..7ea5213 --- /dev/null +++ b/fuzz/fuzz_ecb.cpp @@ -0,0 +1,32 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "tinyaes/ecb.h" +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + if (size < 16) + return 0; + + // Use first 16 bytes as key, rest as plaintext (block-aligned) + std::vector key(data, data + 16); + size_t pt_len = ((size - 16) / 16) * 16; + if (pt_len == 0) + return 0; + + std::vector plaintext(data + 16, data + 16 + pt_len); + std::vector ct, pt; + + auto result = tinyaes::ecb_encrypt(key, plaintext, ct); + if (result != tinyaes::Result::Success) + return 0; + + result = tinyaes::ecb_decrypt(key, ct, pt); + assert(result == tinyaes::Result::Success); + assert(pt == plaintext); + + return 0; +} diff --git a/fuzz/fuzz_gcm.cpp b/fuzz/fuzz_gcm.cpp new file mode 100644 index 0000000..ba48631 --- /dev/null +++ b/fuzz/fuzz_gcm.cpp @@ -0,0 +1,35 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "tinyaes/gcm.h" +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + if (size < 29) + return 0; + + // First 16 bytes = key, next 12 bytes = IV, 1 byte = AAD length, rest split + std::vector key(data, data + 16); + std::vector iv(data + 16, data + 28); + size_t aad_len = data[28] % (size - 29 + 1); + if (29 + aad_len > size) + aad_len = 0; + + std::vector aad(data + 29, data + 29 + aad_len); + std::vector plaintext(data + 29 + aad_len, data + size); + + std::vector ct, tag, pt; + + auto result = tinyaes::gcm_encrypt(key, iv, aad, plaintext, ct, tag); + if (result != tinyaes::Result::Success) + return 0; + + result = tinyaes::gcm_decrypt(key, iv, aad, ct, tag, pt); + assert(result == tinyaes::Result::Success); + assert(pt == plaintext); + + return 0; +} diff --git a/include/tinyaes.h b/include/tinyaes.h new file mode 100644 index 0000000..e4a16d8 --- /dev/null +++ b/include/tinyaes.h @@ -0,0 +1,34 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "tinyaes/version.h" +#include "tinyaes/common.h" +#include "tinyaes/ecb.h" +#include "tinyaes/cbc.h" +#include "tinyaes/ctr.h" +#include "tinyaes/gcm.h" diff --git a/include/tinyaes/cbc.h b/include/tinyaes/cbc.h new file mode 100644 index 0000000..439c18b --- /dev/null +++ b/include/tinyaes/cbc.h @@ -0,0 +1,111 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "tinyaes/common.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + // CBC without padding — input must be multiple of 16 bytes + TINYAES_EXPORT int tinyaes_cbc_encrypt( + const uint8_t *key, + size_t key_len, + const uint8_t iv[16], + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *ciphertext, + size_t ciphertext_len); + + TINYAES_EXPORT int tinyaes_cbc_decrypt( + const uint8_t *key, + size_t key_len, + const uint8_t iv[16], + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *plaintext, + size_t plaintext_len); + + // CBC with PKCS#7 padding + TINYAES_EXPORT int tinyaes_cbc_encrypt_pkcs7( + const uint8_t *key, + size_t key_len, + const uint8_t iv[16], + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *ciphertext, + size_t *ciphertext_len); + + TINYAES_EXPORT int tinyaes_cbc_decrypt_pkcs7( + const uint8_t *key, + size_t key_len, + const uint8_t iv[16], + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *plaintext, + size_t *plaintext_len); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus + +#include + +namespace tinyaes +{ + + Result cbc_encrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &plaintext, + std::vector &ciphertext); + + Result cbc_decrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &ciphertext, + std::vector &plaintext); + + Result cbc_encrypt_pkcs7( + const std::vector &key, + const std::vector &iv, + const std::vector &plaintext, + std::vector &ciphertext); + + Result cbc_decrypt_pkcs7( + const std::vector &key, + const std::vector &iv, + const std::vector &ciphertext, + std::vector &plaintext); + +} // namespace tinyaes + +#endif diff --git a/include/tinyaes/common.h b/include/tinyaes/common.h new file mode 100644 index 0000000..ba0749c --- /dev/null +++ b/include/tinyaes/common.h @@ -0,0 +1,107 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include + +// Symbol visibility for shared library builds +#if defined(TINYAES_SHARED) +#if defined(_WIN32) || defined(__CYGWIN__) +#if defined(TINYAES_BUILDING) +#define TINYAES_EXPORT __declspec(dllexport) +#else +#define TINYAES_EXPORT __declspec(dllimport) +#endif +#elif defined(__GNUC__) || defined(__clang__) +#define TINYAES_EXPORT __attribute__((visibility("default"))) +#else +#define TINYAES_EXPORT +#endif +#else +#define TINYAES_EXPORT +#endif + +// AES key sizes +#define TINYAES_KEY_128 16 +#define TINYAES_KEY_192 24 +#define TINYAES_KEY_256 32 +#define TINYAES_BLOCK_SIZE 16 +#define TINYAES_GCM_TAG_SIZE 16 +#define TINYAES_GCM_IV_SIZE 12 + +// C error codes +#define TINYAES_SUCCESS 0 +#define TINYAES_ERROR_INVALID_KEY_SIZE (-1) +#define TINYAES_ERROR_INVALID_INPUT (-2) +#define TINYAES_ERROR_INVALID_PADDING (-3) +#define TINYAES_ERROR_AUTH_FAILED (-4) +#define TINYAES_ERROR_BUFFER_TOO_SMALL (-5) + +#ifdef __cplusplus +extern "C" +{ +#endif + + TINYAES_EXPORT int tinyaes_constant_time_equal(const uint8_t *a, const uint8_t *b, size_t len); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus + +namespace tinyaes +{ + + enum class Result + { + Success = 0, + InvalidKeySize = -1, + InvalidInput = -2, + InvalidPadding = -3, + AuthFailed = -4, + BufferTooSmall = -5 + }; + + void secure_zero(void *ptr, size_t len); + + bool constant_time_equal(const uint8_t *a, const uint8_t *b, size_t len); + + inline bool constant_time_equal(const std::vector &a, const std::vector &b) + { + if (a.size() != b.size()) + return false; + return constant_time_equal(a.data(), b.data(), a.size()); + } + + int generate_iv(uint8_t *out, size_t len); + +} // namespace tinyaes + +#endif diff --git a/include/tinyaes/ctr.h b/include/tinyaes/ctr.h new file mode 100644 index 0000000..c263c95 --- /dev/null +++ b/include/tinyaes/ctr.h @@ -0,0 +1,66 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "tinyaes/common.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + // CTR encrypt/decrypt (symmetric operation) + TINYAES_EXPORT int tinyaes_ctr_crypt( + const uint8_t *key, + size_t key_len, + const uint8_t iv[16], + const uint8_t *input, + size_t input_len, + uint8_t *output, + size_t output_len); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus + +#include + +namespace tinyaes +{ + + // CTR mode: encrypt and decrypt are the same operation + Result ctr_crypt( + const std::vector &key, + const std::vector &iv, + const std::vector &input, + std::vector &output); + +} // namespace tinyaes + +#endif diff --git a/include/tinyaes/ecb.h b/include/tinyaes/ecb.h new file mode 100644 index 0000000..dc04648 --- /dev/null +++ b/include/tinyaes/ecb.h @@ -0,0 +1,75 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "tinyaes/common.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + TINYAES_EXPORT int tinyaes_ecb_encrypt( + const uint8_t *key, + size_t key_len, + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *ciphertext, + size_t ciphertext_len); + + TINYAES_EXPORT int tinyaes_ecb_decrypt( + const uint8_t *key, + size_t key_len, + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *plaintext, + size_t plaintext_len); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus + +#include + +namespace tinyaes +{ + + Result ecb_encrypt( + const std::vector &key, + const std::vector &plaintext, + std::vector &ciphertext); + + Result ecb_decrypt( + const std::vector &key, + const std::vector &ciphertext, + std::vector &plaintext); + +} // namespace tinyaes + +#endif diff --git a/include/tinyaes/gcm.h b/include/tinyaes/gcm.h new file mode 100644 index 0000000..39db48f --- /dev/null +++ b/include/tinyaes/gcm.h @@ -0,0 +1,91 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "tinyaes/common.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + TINYAES_EXPORT int tinyaes_gcm_encrypt( + const uint8_t *key, + size_t key_len, + const uint8_t *iv, + size_t iv_len, + const uint8_t *aad, + size_t aad_len, + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t tag[16]); + + TINYAES_EXPORT int tinyaes_gcm_decrypt( + const uint8_t *key, + size_t key_len, + const uint8_t *iv, + size_t iv_len, + const uint8_t *aad, + size_t aad_len, + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *plaintext, + size_t plaintext_len, + const uint8_t tag[16]); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus + +#include + +namespace tinyaes +{ + + Result gcm_encrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &aad, + const std::vector &plaintext, + std::vector &ciphertext, + std::vector &tag); + + Result gcm_decrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &aad, + const std::vector &ciphertext, + const std::vector &tag, + std::vector &plaintext); + +} // namespace tinyaes + +#endif diff --git a/include/tinyaes/version.h b/include/tinyaes/version.h new file mode 100644 index 0000000..d67482c --- /dev/null +++ b/include/tinyaes/version.h @@ -0,0 +1,32 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#define TINYAES_VERSION_MAJOR 0 +#define TINYAES_VERSION_MINOR 1 +#define TINYAES_VERSION_PATCH 0 +#define TINYAES_VERSION_STRING "0.1.0" diff --git a/src/backend/aes_aesni.cpp b/src/backend/aes_aesni.cpp new file mode 100644 index 0000000..2f1576f --- /dev/null +++ b/src/backend/aes_aesni.cpp @@ -0,0 +1,229 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// AES-NI (x86_64) backend: single-block encrypt/decrypt + pipelined CTR + +#if defined(__x86_64__) || defined(_M_X64) + +#include "internal/aes_impl.h" +#include "internal/endian.h" + +#include +#include // AES-NI intrinsics +#include // SSE4.1 + +namespace tinyaes +{ + namespace internal + { + + // Helper: AES-128 key expansion assist + static inline __m128i aes_128_key_assist(__m128i key, __m128i gen) + { + gen = _mm_shuffle_epi32(gen, 0xFF); + key = _mm_xor_si128(key, _mm_slli_si128(key, 4)); + key = _mm_xor_si128(key, _mm_slli_si128(key, 4)); + key = _mm_xor_si128(key, _mm_slli_si128(key, 4)); + return _mm_xor_si128(key, gen); + } + + // AES-NI key expansion + void aes_key_expand_aesni(const uint8_t *key, size_t key_len, uint32_t *rk) + { + __m128i *rk128 = reinterpret_cast<__m128i *>(rk); + + if (key_len == 16) + { + // AES-128: 11 round keys + __m128i k = _mm_loadu_si128(reinterpret_cast(key)); + rk128[0] = k; + k = aes_128_key_assist(k, _mm_aeskeygenassist_si128(k, 0x01)); + rk128[1] = k; + k = aes_128_key_assist(k, _mm_aeskeygenassist_si128(k, 0x02)); + rk128[2] = k; + k = aes_128_key_assist(k, _mm_aeskeygenassist_si128(k, 0x04)); + rk128[3] = k; + k = aes_128_key_assist(k, _mm_aeskeygenassist_si128(k, 0x08)); + rk128[4] = k; + k = aes_128_key_assist(k, _mm_aeskeygenassist_si128(k, 0x10)); + rk128[5] = k; + k = aes_128_key_assist(k, _mm_aeskeygenassist_si128(k, 0x20)); + rk128[6] = k; + k = aes_128_key_assist(k, _mm_aeskeygenassist_si128(k, 0x40)); + rk128[7] = k; + k = aes_128_key_assist(k, _mm_aeskeygenassist_si128(k, 0x80)); + rk128[8] = k; + k = aes_128_key_assist(k, _mm_aeskeygenassist_si128(k, 0x1B)); + rk128[9] = k; + k = aes_128_key_assist(k, _mm_aeskeygenassist_si128(k, 0x36)); + rk128[10] = k; + } + else + { + // For AES-192 and AES-256, use portable key expansion then convert + // from big-endian uint32_t words to native byte order so that + // _mm_loadu_si128 loads the correct key bytes for AES-NI. + aes_key_expand_portable(key, key_len, rk); + int nr = static_cast(key_len / 4) + 6; + int total_words = 4 * (nr + 1); + for (int i = 0; i < total_words; ++i) + { + uint32_t w = rk[i]; + rk[i] = ((w >> 24) & 0xFF) | ((w >> 8) & 0xFF00) | ((w << 8) & 0xFF0000) + | ((w << 24) & 0xFF000000u); + } + } + } + + void aes_encrypt_block_aesni(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]) + { + const __m128i *rk128 = reinterpret_cast(rk); + __m128i block = _mm_loadu_si128(reinterpret_cast(in)); + + block = _mm_xor_si128(block, rk128[0]); + for (int i = 1; i < rounds; ++i) + { + block = _mm_aesenc_si128(block, rk128[i]); + } + block = _mm_aesenclast_si128(block, rk128[rounds]); + + _mm_storeu_si128(reinterpret_cast<__m128i *>(out), block); + } + + void aes_decrypt_block_aesni(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]) + { + const __m128i *rk128 = reinterpret_cast(rk); + + // Need inverse round keys for AES-NI decrypt + // InvMixColumns on round keys 1..rounds-1 + __m128i inv_rk[15]; + inv_rk[0] = rk128[rounds]; + for (int i = 1; i < rounds; ++i) + { + inv_rk[i] = _mm_aesimc_si128(rk128[rounds - i]); + } + inv_rk[rounds] = rk128[0]; + + __m128i block = _mm_loadu_si128(reinterpret_cast(in)); + block = _mm_xor_si128(block, inv_rk[0]); + for (int i = 1; i < rounds; ++i) + { + block = _mm_aesdec_si128(block, inv_rk[i]); + } + block = _mm_aesdeclast_si128(block, inv_rk[rounds]); + + _mm_storeu_si128(reinterpret_cast<__m128i *>(out), block); + } + + // Pipelined CTR: process 4 blocks at a time + void aes_ctr_pipeline_aesni(const uint32_t *rk, int rounds, const uint8_t *in, uint8_t *out, size_t blocks, + uint8_t ctr[16]) + { + const __m128i *rk128 = reinterpret_cast(rk); + const __m128i one = _mm_set_epi32(0, 0, 0, 1); + // AES counter is big-endian in last 4 bytes, but _mm_loadu loads LE + // We need byte-swap for the counter increment + const __m128i bswap_mask = + _mm_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); + + __m128i ctr_block = _mm_loadu_si128(reinterpret_cast(ctr)); + + // Process 4 blocks at a time + size_t i = 0; + for (; i + 4 <= blocks; i += 4) + { + __m128i c0 = ctr_block; + __m128i c0_le = _mm_shuffle_epi8(c0, bswap_mask); + __m128i c1_le = _mm_add_epi64(c0_le, one); + __m128i c2_le = _mm_add_epi64(c1_le, one); + __m128i c3_le = _mm_add_epi64(c2_le, one); + __m128i c1 = _mm_shuffle_epi8(c1_le, bswap_mask); + __m128i c2 = _mm_shuffle_epi8(c2_le, bswap_mask); + __m128i c3 = _mm_shuffle_epi8(c3_le, bswap_mask); + + // Initial round key XOR + __m128i b0 = _mm_xor_si128(c0, rk128[0]); + __m128i b1 = _mm_xor_si128(c1, rk128[0]); + __m128i b2 = _mm_xor_si128(c2, rk128[0]); + __m128i b3 = _mm_xor_si128(c3, rk128[0]); + + // Middle rounds + for (int r = 1; r < rounds; ++r) + { + b0 = _mm_aesenc_si128(b0, rk128[r]); + b1 = _mm_aesenc_si128(b1, rk128[r]); + b2 = _mm_aesenc_si128(b2, rk128[r]); + b3 = _mm_aesenc_si128(b3, rk128[r]); + } + + // Final round + b0 = _mm_aesenclast_si128(b0, rk128[rounds]); + b1 = _mm_aesenclast_si128(b1, rk128[rounds]); + b2 = _mm_aesenclast_si128(b2, rk128[rounds]); + b3 = _mm_aesenclast_si128(b3, rk128[rounds]); + + // XOR with plaintext + __m128i p0 = _mm_loadu_si128(reinterpret_cast(in + i * 16)); + __m128i p1 = _mm_loadu_si128(reinterpret_cast(in + i * 16 + 16)); + __m128i p2 = _mm_loadu_si128(reinterpret_cast(in + i * 16 + 32)); + __m128i p3 = _mm_loadu_si128(reinterpret_cast(in + i * 16 + 48)); + + _mm_storeu_si128(reinterpret_cast<__m128i *>(out + i * 16), _mm_xor_si128(b0, p0)); + _mm_storeu_si128(reinterpret_cast<__m128i *>(out + i * 16 + 16), _mm_xor_si128(b1, p1)); + _mm_storeu_si128(reinterpret_cast<__m128i *>(out + i * 16 + 32), _mm_xor_si128(b2, p2)); + _mm_storeu_si128(reinterpret_cast<__m128i *>(out + i * 16 + 48), _mm_xor_si128(b3, p3)); + + // Advance counter by 4 + __m128i next_le = _mm_add_epi64(c3_le, one); + ctr_block = _mm_shuffle_epi8(next_le, bswap_mask); + } + + // Process remaining blocks one at a time + for (; i < blocks; ++i) + { + __m128i b = _mm_xor_si128(ctr_block, rk128[0]); + for (int r = 1; r < rounds; ++r) + { + b = _mm_aesenc_si128(b, rk128[r]); + } + b = _mm_aesenclast_si128(b, rk128[rounds]); + + __m128i p = _mm_loadu_si128(reinterpret_cast(in + i * 16)); + _mm_storeu_si128(reinterpret_cast<__m128i *>(out + i * 16), _mm_xor_si128(b, p)); + + __m128i ctr_le = _mm_shuffle_epi8(ctr_block, bswap_mask); + ctr_le = _mm_add_epi64(ctr_le, one); + ctr_block = _mm_shuffle_epi8(ctr_le, bswap_mask); + } + + // Store updated counter + _mm_storeu_si128(reinterpret_cast<__m128i *>(ctr), ctr_block); + } + + } // namespace internal +} // namespace tinyaes + +#endif // x86_64 diff --git a/src/backend/aes_arm_ce.cpp b/src/backend/aes_arm_ce.cpp new file mode 100644 index 0000000..f00ac45 --- /dev/null +++ b/src/backend/aes_arm_ce.cpp @@ -0,0 +1,127 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// ARM Crypto Extensions AES backend + +#if defined(__aarch64__) || defined(_M_ARM64) + +#include "internal/aes_impl.h" +#include "internal/endian.h" + +#include + +#if defined(_MSC_VER) +#include +#else +#include +#endif + +namespace tinyaes +{ + namespace internal + { + + // ARM AES: vaeseq + vaesmcq for encryption, vaesdq + vaesimcq for decryption + // Note: ARM AES instructions combine SubBytes+ShiftRows (vaese) and MixColumns (vaesmc) + // The key XOR is done separately (veorq). + + void aes_encrypt_block_arm_ce(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]) + { + const uint8_t *rk8 = reinterpret_cast(rk); + uint8x16_t block = vld1q_u8(in); + + // ARM vaese does: AddRoundKey ^ SubBytes ^ ShiftRows + // So we need: vaese(block, key[i]) then vaesmcq for MixColumns + for (int i = 0; i < rounds - 1; ++i) + { + uint8x16_t key = vld1q_u8(rk8 + i * 16); + block = vaeseq_u8(block, key); + block = vaesmcq_u8(block); + } + // Last round: no MixColumns + uint8x16_t key_last = vld1q_u8(rk8 + (rounds - 1) * 16); + block = vaeseq_u8(block, key_last); + // Final AddRoundKey + uint8x16_t key_final = vld1q_u8(rk8 + rounds * 16); + block = veorq_u8(block, key_final); + + vst1q_u8(out, block); + } + + void aes_decrypt_block_arm_ce(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]) + { + const uint8_t *rk8 = reinterpret_cast(rk); + uint8x16_t block = vld1q_u8(in); + + // Decryption uses inverse round keys in reverse order + for (int i = rounds; i > 1; --i) + { + uint8x16_t key = vld1q_u8(rk8 + i * 16); + block = vaesdq_u8(block, key); + block = vaesimcq_u8(block); + } + // Last round: no InvMixColumns + uint8x16_t key_1 = vld1q_u8(rk8 + 16); + block = vaesdq_u8(block, key_1); + // Final AddRoundKey + uint8x16_t key_0 = vld1q_u8(rk8); + block = veorq_u8(block, key_0); + + vst1q_u8(out, block); + } + + void aes_ctr_pipeline_arm_ce(const uint32_t *rk, int rounds, const uint8_t *in, uint8_t *out, size_t blocks, + uint8_t ctr[16]) + { + const uint8_t *rk8 = reinterpret_cast(rk); + + for (size_t i = 0; i < blocks; ++i) + { + uint8x16_t block = vld1q_u8(ctr); + + for (int r = 0; r < rounds - 1; ++r) + { + uint8x16_t key = vld1q_u8(rk8 + r * 16); + block = vaeseq_u8(block, key); + block = vaesmcq_u8(block); + } + uint8x16_t key_last = vld1q_u8(rk8 + (rounds - 1) * 16); + block = vaeseq_u8(block, key_last); + uint8x16_t key_final = vld1q_u8(rk8 + rounds * 16); + block = veorq_u8(block, key_final); + + // XOR with plaintext + uint8x16_t pt = vld1q_u8(in + i * 16); + vst1q_u8(out + i * 16, veorq_u8(block, pt)); + + increment_be32(ctr); + } + } + + } // namespace internal +} // namespace tinyaes + +#endif // aarch64 diff --git a/src/backend/aes_portable.cpp b/src/backend/aes_portable.cpp new file mode 100644 index 0000000..15351eb --- /dev/null +++ b/src/backend/aes_portable.cpp @@ -0,0 +1,338 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Portable AES implementation using T-tables (FIPS 197). +// NOTE: T-table implementations are susceptible to cache-timing side channels. +// For production use on x86, the AES-NI backend is preferred. + +#include "internal/aes_impl.h" +#include "internal/endian.h" + +#include + +namespace tinyaes +{ + namespace internal + { + + // AES S-box (SubBytes) + // clang-format off + static const uint8_t sbox[256] = { + 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, + 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, + 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, + 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, + 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, + 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, + 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, + 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, + 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, + 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, + 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, + 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, + 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, + 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, + 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, + 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16 + }; + + // Inverse S-box (InvSubBytes) + static const uint8_t inv_sbox[256] = { + 0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb, + 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb, + 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e, + 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25, + 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92, + 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84, + 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06, + 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b, + 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73, + 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e, + 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b, + 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4, + 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f, + 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef, + 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61, + 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d + }; + // clang-format on + + // GF(2^8) multiply by 2 + static inline uint8_t xtime(uint8_t x) + { + return static_cast((x << 1) ^ (((x >> 7) & 1) * 0x1b)); + } + + // GF(2^8) multiply + static inline uint8_t gmul(uint8_t a, uint8_t b) + { + uint8_t p = 0; + for (int i = 0; i < 8; ++i) + { + if (b & 1) + p ^= a; + uint8_t hi = a & 0x80; + a = static_cast(a << 1); + if (hi) + a ^= 0x1b; + b >>= 1; + } + return p; + } + + // Encryption T-tables (Te0..Te3) + // Te0[x] = S[x].[02, 01, 01, 03] (column of MixColumns * SubBytes) + static uint32_t Te0[256], Te1[256], Te2[256], Te3[256]; + static uint32_t Td0[256], Td1[256], Td2[256], Td3[256]; + static bool tables_initialized = false; + + static void init_tables() + { + if (tables_initialized) + return; + + for (int i = 0; i < 256; ++i) + { + uint8_t s = sbox[i]; + uint8_t s2 = xtime(s); + uint8_t s3 = static_cast(s2 ^ s); + + // Te0[i] = [s2, s, s, s3] as big-endian 32-bit word + Te0[i] = (static_cast(s2) << 24) | (static_cast(s) << 16) + | (static_cast(s) << 8) | static_cast(s3); + Te1[i] = (Te0[i] >> 8) | (Te0[i] << 24); + Te2[i] = (Te0[i] >> 16) | (Te0[i] << 16); + Te3[i] = (Te0[i] >> 24) | (Te0[i] << 8); + + // Decryption tables + uint8_t si = inv_sbox[i]; + uint8_t si9 = gmul(si, 0x09); + uint8_t sib = gmul(si, 0x0b); + uint8_t sid = gmul(si, 0x0d); + uint8_t sie = gmul(si, 0x0e); + + Td0[i] = (static_cast(sie) << 24) | (static_cast(si9) << 16) + | (static_cast(sid) << 8) | static_cast(sib); + Td1[i] = (Td0[i] >> 8) | (Td0[i] << 24); + Td2[i] = (Td0[i] >> 16) | (Td0[i] << 16); + Td3[i] = (Td0[i] >> 24) | (Td0[i] << 8); + } + + tables_initialized = true; + } + + // Round constants + static const uint8_t rcon[11] = {0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36}; + + // Key expansion (FIPS 197 Section 5.2) + void aes_key_expand_portable(const uint8_t *key, size_t key_len, uint32_t *rk) + { + init_tables(); + + int nk = static_cast(key_len / 4); // 4, 6, or 8 + int nr = nk + 6; // 10, 12, or 14 + int total = 4 * (nr + 1); // total words + + // Copy key into first Nk words + for (int i = 0; i < nk; ++i) + { + rk[i] = load_be32(key + 4 * i); + } + + for (int i = nk; i < total; ++i) + { + uint32_t temp = rk[i - 1]; + if (i % nk == 0) + { + // RotWord + SubWord + Rcon + temp = (static_cast(sbox[(temp >> 16) & 0xff]) << 24) + | (static_cast(sbox[(temp >> 8) & 0xff]) << 16) + | (static_cast(sbox[temp & 0xff]) << 8) + | (static_cast(sbox[(temp >> 24) & 0xff])); + temp ^= static_cast(rcon[i / nk]) << 24; + } + else if (nk > 6 && (i % nk == 4)) + { + // SubWord only for AES-256 + temp = (static_cast(sbox[(temp >> 24) & 0xff]) << 24) + | (static_cast(sbox[(temp >> 16) & 0xff]) << 16) + | (static_cast(sbox[(temp >> 8) & 0xff]) << 8) + | (static_cast(sbox[temp & 0xff])); + } + rk[i] = rk[i - nk] ^ temp; + } + } + + void aes_encrypt_block_portable(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]) + { + init_tables(); + + uint32_t s0 = load_be32(in) ^ rk[0]; + uint32_t s1 = load_be32(in + 4) ^ rk[1]; + uint32_t s2 = load_be32(in + 8) ^ rk[2]; + uint32_t s3 = load_be32(in + 12) ^ rk[3]; + + uint32_t t0 = s0, t1 = s1, t2 = s2, t3 = s3; + int r = 1; + for (; r < rounds; ++r) + { + t0 = Te0[(s0 >> 24) & 0xff] ^ Te1[(s1 >> 16) & 0xff] ^ Te2[(s2 >> 8) & 0xff] ^ Te3[s3 & 0xff] + ^ rk[4 * r]; + t1 = Te0[(s1 >> 24) & 0xff] ^ Te1[(s2 >> 16) & 0xff] ^ Te2[(s3 >> 8) & 0xff] ^ Te3[s0 & 0xff] + ^ rk[4 * r + 1]; + t2 = Te0[(s2 >> 24) & 0xff] ^ Te1[(s3 >> 16) & 0xff] ^ Te2[(s0 >> 8) & 0xff] ^ Te3[s1 & 0xff] + ^ rk[4 * r + 2]; + t3 = Te0[(s3 >> 24) & 0xff] ^ Te1[(s0 >> 16) & 0xff] ^ Te2[(s1 >> 8) & 0xff] ^ Te3[s2 & 0xff] + ^ rk[4 * r + 3]; + s0 = t0; + s1 = t1; + s2 = t2; + s3 = t3; + } + + // Final round (no MixColumns) + s0 = (static_cast(sbox[(t0 >> 24) & 0xff]) << 24) + | (static_cast(sbox[(t1 >> 16) & 0xff]) << 16) + | (static_cast(sbox[(t2 >> 8) & 0xff]) << 8) + | (static_cast(sbox[t3 & 0xff])); + s0 ^= rk[4 * rounds]; + + s1 = (static_cast(sbox[(t1 >> 24) & 0xff]) << 24) + | (static_cast(sbox[(t2 >> 16) & 0xff]) << 16) + | (static_cast(sbox[(t3 >> 8) & 0xff]) << 8) + | (static_cast(sbox[t0 & 0xff])); + s1 ^= rk[4 * rounds + 1]; + + s2 = (static_cast(sbox[(t2 >> 24) & 0xff]) << 24) + | (static_cast(sbox[(t3 >> 16) & 0xff]) << 16) + | (static_cast(sbox[(t0 >> 8) & 0xff]) << 8) + | (static_cast(sbox[t1 & 0xff])); + s2 ^= rk[4 * rounds + 2]; + + s3 = (static_cast(sbox[(t3 >> 24) & 0xff]) << 24) + | (static_cast(sbox[(t0 >> 16) & 0xff]) << 16) + | (static_cast(sbox[(t1 >> 8) & 0xff]) << 8) + | (static_cast(sbox[t2 & 0xff])); + s3 ^= rk[4 * rounds + 3]; + + store_be32(out, s0); + store_be32(out + 4, s1); + store_be32(out + 8, s2); + store_be32(out + 12, s3); + } + + // Apply InvMixColumns to a single round-key word using Td tables + sbox. + // Td0[sbox[b]] = InvMixColumns column for byte b (since inv_sbox[sbox[b]] = b). + static inline uint32_t inv_mix_column(uint32_t w) + { + return Td0[sbox[(w >> 24) & 0xff]] ^ Td1[sbox[(w >> 16) & 0xff]] ^ Td2[sbox[(w >> 8) & 0xff]] + ^ Td3[sbox[w & 0xff]]; + } + + void aes_decrypt_block_portable(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]) + { + init_tables(); + + uint32_t s0 = load_be32(in) ^ rk[4 * rounds]; + uint32_t s1 = load_be32(in + 4) ^ rk[4 * rounds + 1]; + uint32_t s2 = load_be32(in + 8) ^ rk[4 * rounds + 2]; + uint32_t s3 = load_be32(in + 12) ^ rk[4 * rounds + 3]; + + uint32_t t0 = s0, t1 = s1, t2 = s2, t3 = s3; + int r = rounds - 1; + for (; r > 0; --r) + { + // Equivalent Inverse Cipher (FIPS 197 §5.3.5): + // Td tables combine InvSubBytes + InvMixColumns, so the round keys + // for middle rounds must be preprocessed with InvMixColumns. + uint32_t dk0 = inv_mix_column(rk[4 * r]); + uint32_t dk1 = inv_mix_column(rk[4 * r + 1]); + uint32_t dk2 = inv_mix_column(rk[4 * r + 2]); + uint32_t dk3 = inv_mix_column(rk[4 * r + 3]); + + t0 = Td0[(s0 >> 24) & 0xff] ^ Td1[(s3 >> 16) & 0xff] ^ Td2[(s2 >> 8) & 0xff] ^ Td3[s1 & 0xff] + ^ dk0; + t1 = Td0[(s1 >> 24) & 0xff] ^ Td1[(s0 >> 16) & 0xff] ^ Td2[(s3 >> 8) & 0xff] ^ Td3[s2 & 0xff] + ^ dk1; + t2 = Td0[(s2 >> 24) & 0xff] ^ Td1[(s1 >> 16) & 0xff] ^ Td2[(s0 >> 8) & 0xff] ^ Td3[s3 & 0xff] + ^ dk2; + t3 = Td0[(s3 >> 24) & 0xff] ^ Td1[(s2 >> 16) & 0xff] ^ Td2[(s1 >> 8) & 0xff] ^ Td3[s0 & 0xff] + ^ dk3; + s0 = t0; + s1 = t1; + s2 = t2; + s3 = t3; + } + + // Final round (no InvMixColumns) + s0 = (static_cast(inv_sbox[(t0 >> 24) & 0xff]) << 24) + | (static_cast(inv_sbox[(t3 >> 16) & 0xff]) << 16) + | (static_cast(inv_sbox[(t2 >> 8) & 0xff]) << 8) + | (static_cast(inv_sbox[t1 & 0xff])); + s0 ^= rk[0]; + + s1 = (static_cast(inv_sbox[(t1 >> 24) & 0xff]) << 24) + | (static_cast(inv_sbox[(t0 >> 16) & 0xff]) << 16) + | (static_cast(inv_sbox[(t3 >> 8) & 0xff]) << 8) + | (static_cast(inv_sbox[t2 & 0xff])); + s1 ^= rk[1]; + + s2 = (static_cast(inv_sbox[(t2 >> 24) & 0xff]) << 24) + | (static_cast(inv_sbox[(t1 >> 16) & 0xff]) << 16) + | (static_cast(inv_sbox[(t0 >> 8) & 0xff]) << 8) + | (static_cast(inv_sbox[t3 & 0xff])); + s2 ^= rk[2]; + + s3 = (static_cast(inv_sbox[(t3 >> 24) & 0xff]) << 24) + | (static_cast(inv_sbox[(t2 >> 16) & 0xff]) << 16) + | (static_cast(inv_sbox[(t1 >> 8) & 0xff]) << 8) + | (static_cast(inv_sbox[t0 & 0xff])); + s3 ^= rk[3]; + + store_be32(out, s0); + store_be32(out + 4, s1); + store_be32(out + 8, s2); + store_be32(out + 12, s3); + } + + // Portable CTR pipeline: encrypt blocks sequentially + void aes_ctr_pipeline_portable(const uint32_t *rk, int rounds, const uint8_t *in, uint8_t *out, size_t blocks, + uint8_t ctr[16]) + { + uint8_t keystream[16]; + for (size_t i = 0; i < blocks; ++i) + { + aes_encrypt_block_portable(rk, rounds, ctr, keystream); + for (size_t j = 0; j < 16; ++j) + { + out[i * 16 + j] = in[i * 16 + j] ^ keystream[j]; + } + increment_be32(ctr); + } + } + + } // namespace internal +} // namespace tinyaes diff --git a/src/backend/aes_vaes.cpp b/src/backend/aes_vaes.cpp new file mode 100644 index 0000000..d53d191 --- /dev/null +++ b/src/backend/aes_vaes.cpp @@ -0,0 +1,116 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// VAES (AVX-512) backend: 4-block-wide CTR pipeline using 512-bit operations + +#if defined(__x86_64__) || defined(_M_X64) + +#include "internal/aes_impl.h" +#include "internal/endian.h" + +#include +#include + +namespace tinyaes +{ + namespace internal + { + + void aes_ctr_pipeline_vaes(const uint32_t *rk, int rounds, const uint8_t *in, uint8_t *out, size_t blocks, + uint8_t ctr[16]) + { + const __m128i *rk128 = reinterpret_cast(rk); + const __m128i bswap_mask = + _mm_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); + + __m128i ctr_block = _mm_loadu_si128(reinterpret_cast(ctr)); + + // Process 4 blocks at a time using ZMM registers + size_t i = 0; + for (; i + 4 <= blocks; i += 4) + { + // Generate 4 counter values + __m128i ctr_le = _mm_shuffle_epi8(ctr_block, bswap_mask); + const __m128i one = _mm_set_epi32(0, 0, 0, 1); + __m128i c0 = ctr_block; + __m128i c1_le = _mm_add_epi64(ctr_le, one); + __m128i c2_le = _mm_add_epi64(c1_le, one); + __m128i c3_le = _mm_add_epi64(c2_le, one); + __m128i c1 = _mm_shuffle_epi8(c1_le, bswap_mask); + __m128i c2 = _mm_shuffle_epi8(c2_le, bswap_mask); + __m128i c3 = _mm_shuffle_epi8(c3_le, bswap_mask); + + // Pack into ZMM + __m512i ctrs = _mm512_inserti32x4( + _mm512_inserti32x4( + _mm512_inserti32x4(_mm512_castsi128_si512(c0), c1, 1), + c2, 2), + c3, 3); + + // Broadcast round key and XOR + __m512i state = _mm512_xor_si512(ctrs, _mm512_broadcast_i32x4(rk128[0])); + + // Middle rounds + for (int r = 1; r < rounds; ++r) + { + state = _mm512_aesenc_epi128(state, _mm512_broadcast_i32x4(rk128[r])); + } + state = _mm512_aesenclast_epi128(state, _mm512_broadcast_i32x4(rk128[rounds])); + + // XOR with plaintext + __m512i pt = _mm512_loadu_si512(in + i * 16); + _mm512_storeu_si512(out + i * 16, _mm512_xor_si512(state, pt)); + + // Advance counter by 4 + __m128i next_le = _mm_add_epi64(c3_le, one); + ctr_block = _mm_shuffle_epi8(next_le, bswap_mask); + } + + // Remaining blocks: fall back to single-block AES-NI + for (; i < blocks; ++i) + { + __m128i b = _mm_xor_si128(ctr_block, rk128[0]); + for (int r = 1; r < rounds; ++r) + { + b = _mm_aesenc_si128(b, rk128[r]); + } + b = _mm_aesenclast_si128(b, rk128[rounds]); + + __m128i p = _mm_loadu_si128(reinterpret_cast(in + i * 16)); + _mm_storeu_si128(reinterpret_cast<__m128i *>(out + i * 16), _mm_xor_si128(b, p)); + + __m128i ctr_le = _mm_shuffle_epi8(ctr_block, bswap_mask); + ctr_le = _mm_add_epi64(ctr_le, _mm_set_epi32(0, 0, 0, 1)); + ctr_block = _mm_shuffle_epi8(ctr_le, bswap_mask); + } + + _mm_storeu_si128(reinterpret_cast<__m128i *>(ctr), ctr_block); + } + + } // namespace internal +} // namespace tinyaes + +#endif // x86_64 diff --git a/src/backend/ghash_arm_ce.cpp b/src/backend/ghash_arm_ce.cpp new file mode 100644 index 0000000..3d483ad --- /dev/null +++ b/src/backend/ghash_arm_ce.cpp @@ -0,0 +1,114 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// ARM PMULL GHASH implementation (vmull_p64) + +#if defined(__aarch64__) || defined(_M_ARM64) + +#include "internal/ghash.h" + +#include + +#if defined(_MSC_VER) +#include +#else +#include +#endif + +namespace tinyaes +{ + namespace internal + { + + // GF(2^128) multiplication using PMULL (carry-less multiply) + static inline uint8x16_t gf128_mul_pmull(uint8x16_t a, uint8x16_t b) + { + // Reflect byte order for GCM convention + poly64_t a_lo = vgetq_lane_p64(vreinterpretq_p64_u8(a), 0); + poly64_t a_hi = vgetq_lane_p64(vreinterpretq_p64_u8(a), 1); + poly64_t b_lo = vgetq_lane_p64(vreinterpretq_p64_u8(b), 0); + poly64_t b_hi = vgetq_lane_p64(vreinterpretq_p64_u8(b), 1); + + // Karatsuba multiplication + poly128_t lo = vmull_p64(a_lo, b_lo); + poly128_t hi = vmull_p64(a_hi, b_hi); + poly128_t mid0 = vmull_p64(a_lo, b_hi); + poly128_t mid1 = vmull_p64(a_hi, b_lo); + + uint8x16_t lo_v = vreinterpretq_u8_p128(lo); + uint8x16_t hi_v = vreinterpretq_u8_p128(hi); + uint8x16_t mid = veorq_u8(vreinterpretq_u8_p128(mid0), vreinterpretq_u8_p128(mid1)); + + // Combine: shift mid and XOR into lo/hi + uint8x16_t mid_lo = vextq_u8(vdupq_n_u8(0), mid, 8); + uint8x16_t mid_hi = vextq_u8(mid, vdupq_n_u8(0), 8); + lo_v = veorq_u8(lo_v, mid_lo); + hi_v = veorq_u8(hi_v, mid_hi); + + // Reduction modulo x^128 + x^7 + x^2 + x + 1 + // Using the reflected reduction polynomial 0xC2...01 + poly64_t r = (poly64_t)0xC200000000000000ULL; + poly128_t t1 = vmull_p64(vgetq_lane_p64(vreinterpretq_p64_u8(hi_v), 0), r); + uint8x16_t f = veorq_u8(lo_v, vreinterpretq_u8_p128(t1)); + uint8x16_t f_swap = vextq_u8(f, f, 8); + poly128_t t2 = vmull_p64(vgetq_lane_p64(vreinterpretq_p64_u8(f_swap), 0), r); + + return veorq_u8(veorq_u8(f_swap, vreinterpretq_u8_p128(t2)), hi_v); + } + + void ghash_arm_ce(const uint8_t H[16], const uint8_t *data, size_t data_len, uint8_t Y[16]) + { + // GCM uses big-endian bit reflection; ARM PMULL operates on reflected bits + // The byte reversal handles the endianness conversion + uint8x16_t rev_mask = {15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; + uint8x16_t h = vqtbl1q_u8(vld1q_u8(H), rev_mask); + uint8x16_t y = vqtbl1q_u8(vld1q_u8(Y), rev_mask); + + size_t full_blocks = data_len / 16; + for (size_t i = 0; i < full_blocks; ++i) + { + uint8x16_t d = vqtbl1q_u8(vld1q_u8(data + i * 16), rev_mask); + y = veorq_u8(y, d); + y = gf128_mul_pmull(y, h); + } + + size_t remainder = data_len % 16; + if (remainder > 0) + { + uint8_t padded[16] = {0}; + std::memcpy(padded, data + full_blocks * 16, remainder); + uint8x16_t d = vqtbl1q_u8(vld1q_u8(padded), rev_mask); + y = veorq_u8(y, d); + y = gf128_mul_pmull(y, h); + } + + vst1q_u8(Y, vqtbl1q_u8(y, rev_mask)); + } + + } // namespace internal +} // namespace tinyaes + +#endif // aarch64 diff --git a/src/backend/ghash_pclmulqdq.cpp b/src/backend/ghash_pclmulqdq.cpp new file mode 100644 index 0000000..3f97562 --- /dev/null +++ b/src/backend/ghash_pclmulqdq.cpp @@ -0,0 +1,128 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// PCLMULQDQ GHASH implementation for x86_64 + +#if defined(__x86_64__) || defined(_M_X64) + +#include "internal/ghash.h" + +#include +#include +#include +#include + +namespace tinyaes +{ + namespace internal + { + + // GF(2^128) multiplication using PCLMULQDQ with Karatsuba method + // and two-phase reduction (Intel CLMUL whitepaper "Algorithm 5"). + // GCM polynomial: x^128 + x^7 + x^2 + x + 1 + // Reflected: x^128 + x^127 + x^126 + x^121 + 1 + // Low 64 bits of reflected poly (excl. x^128 and x^0): 0xC200000000000000 + static inline void gf128_mul_pclmul(const __m128i a, const __m128i b, __m128i &result) + { + // Karatsuba carry-less multiplication → 256-bit product [hi:lo] + __m128i lo = _mm_clmulepi64_si128(a, b, 0x00); + __m128i hi = _mm_clmulepi64_si128(a, b, 0x11); + __m128i mid0 = _mm_clmulepi64_si128(a, b, 0x01); + __m128i mid1 = _mm_clmulepi64_si128(a, b, 0x10); + __m128i mid = _mm_xor_si128(mid0, mid1); + + hi = _mm_xor_si128(hi, _mm_srli_si128(mid, 8)); + lo = _mm_xor_si128(lo, _mm_slli_si128(mid, 8)); + + // Two-phase reduction. The constant 0xC200000000000000 must be in + // the LOW 64 bits of the __m128i so that selector 0x00 picks it up. + const __m128i poly = + _mm_set_epi64x(0, static_cast(0xC200000000000000ULL)); + + // Phase 1: multiply lo's low 64 bits by the polynomial constant + __m128i t1 = _mm_clmulepi64_si128(lo, poly, 0x00); + // Swap lo's 64-bit halves (handles the implicit x^64 term) and XOR + lo = _mm_xor_si128(_mm_shuffle_epi32(lo, 0x4E), t1); + + // Phase 2: repeat on the intermediate result + __m128i t2 = _mm_clmulepi64_si128(lo, poly, 0x00); + lo = _mm_xor_si128(_mm_shuffle_epi32(lo, 0x4E), t2); + + // Combine with the high half + result = _mm_xor_si128(lo, hi); + } + + void ghash_pclmulqdq(const uint8_t H[16], const uint8_t *data, size_t data_len, uint8_t Y[16]) + { + const __m128i bswap_mask = + _mm_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); + + __m128i h = _mm_shuffle_epi8(_mm_loadu_si128(reinterpret_cast(H)), bswap_mask); + + // Pre-shift H left by 1 bit in the reflected domain to compensate for + // the x factor introduced by CLMUL's polynomial multiplication convention. + // This is the standard GHASH-CLMUL preprocessing step. + __m128i h_carry = _mm_srli_epi64(h, 63); // bit 63 of each half + __m128i h_carry_low = _mm_slli_si128(h_carry, 8); // carry from low to high + __m128i h_overflow = _mm_srli_si128(h_carry, 8); // overflow bit (was bit 127) + h = _mm_or_si128(_mm_slli_epi64(h, 1), h_carry_low); + // If bit 127 was set, reduce: x^128 mod p_reflected = x^127+x^126+x^121+1 + const __m128i reduce_const = + _mm_set_epi64x(static_cast(0xC200000000000000ULL), 1); + // Broadcast overflow to both 64-bit halves for a full 128-bit mask + __m128i reduce_mask = _mm_shuffle_epi32( + _mm_sub_epi64(_mm_setzero_si128(), h_overflow), 0x44); // [lo, lo] + h = _mm_xor_si128(h, _mm_and_si128(reduce_const, reduce_mask)); + + __m128i y = _mm_shuffle_epi8(_mm_loadu_si128(reinterpret_cast(Y)), bswap_mask); + + size_t full_blocks = data_len / 16; + for (size_t i = 0; i < full_blocks; ++i) + { + __m128i d = _mm_shuffle_epi8( + _mm_loadu_si128(reinterpret_cast(data + i * 16)), bswap_mask); + y = _mm_xor_si128(y, d); + gf128_mul_pclmul(y, h, y); + } + + // Handle partial block + size_t remainder = data_len % 16; + if (remainder > 0) + { + uint8_t padded[16] = {0}; + std::memcpy(padded, data + full_blocks * 16, remainder); + __m128i d = _mm_shuffle_epi8(_mm_loadu_si128(reinterpret_cast(padded)), bswap_mask); + y = _mm_xor_si128(y, d); + gf128_mul_pclmul(y, h, y); + } + + _mm_storeu_si128(reinterpret_cast<__m128i *>(Y), _mm_shuffle_epi8(y, bswap_mask)); + } + + } // namespace internal +} // namespace tinyaes + +#endif // x86_64 diff --git a/src/backend/ghash_portable.cpp b/src/backend/ghash_portable.cpp new file mode 100644 index 0000000..187211d --- /dev/null +++ b/src/backend/ghash_portable.cpp @@ -0,0 +1,113 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Portable constant-time GHASH implementation in GF(2^128). +// MSB-first convention, reduction polynomial x^128 + x^7 + x^2 + x + 1 (0xE1). + +#include "internal/ghash.h" +#include "internal/endian.h" + +namespace tinyaes +{ + namespace internal + { + + // GF(2^128) multiplication: Z = X * Y in GF(2^128) + // Uses bit-by-bit algorithm (constant-time: no data-dependent branches). + static void gf128_mul(const uint8_t X[16], const uint8_t Y[16], uint8_t Z[16]) + { + uint64_t Xh = load_be64(X); + uint64_t Xl = load_be64(X + 8); + uint64_t Zh = 0, Zl = 0; + uint64_t Vh = load_be64(Y); + uint64_t Vl = load_be64(Y + 8); + + for (int i = 0; i < 128; ++i) + { + // If bit i of X is set, XOR V into Z + // Constant-time: compute mask from bit value + uint64_t xi; + if (i < 64) + xi = (Xh >> (63 - i)) & 1; + else + xi = (Xl >> (127 - i)) & 1; + + uint64_t mask = static_cast(0) - xi; // 0 or 0xFFFFFFFFFFFFFFFF + Zh ^= (Vh & mask); + Zl ^= (Vl & mask); + + // V = V >> 1 in GF(2^128) with reduction + uint64_t carry = Vl & 1; + Vl = (Vl >> 1) | (Vh << 63); + Vh >>= 1; + // If carry, XOR with R = 0xE1 << 56 (0xE100000000000000) + uint64_t reduce_mask = static_cast(0) - carry; + Vh ^= (UINT64_C(0xE100000000000000) & reduce_mask); + } + + store_be64(Z, Zh); + store_be64(Z + 8, Zl); + } + + void ghash_portable(const uint8_t H[16], const uint8_t *data, size_t data_len, uint8_t Y[16]) + { + // Process complete 16-byte blocks + size_t full_blocks = data_len / 16; + for (size_t i = 0; i < full_blocks; ++i) + { + // Y = (Y XOR data_block) * H + for (int j = 0; j < 16; ++j) + { + Y[j] ^= data[i * 16 + static_cast(j)]; + } + uint8_t tmp[16]; + gf128_mul(Y, H, tmp); + for (int j = 0; j < 16; ++j) + { + Y[j] = tmp[j]; + } + } + + // Handle final partial block (zero-padded) + size_t remainder = data_len % 16; + if (remainder > 0) + { + for (size_t j = 0; j < remainder; ++j) + { + Y[j] ^= data[full_blocks * 16 + j]; + } + // Remaining bytes stay as-is (zero-padded XOR is identity) + uint8_t tmp[16]; + gf128_mul(Y, H, tmp); + for (int j = 0; j < 16; ++j) + { + Y[j] = tmp[j]; + } + } + } + + } // namespace internal +} // namespace tinyaes diff --git a/src/backend/ghash_vpclmulqdq.cpp b/src/backend/ghash_vpclmulqdq.cpp new file mode 100644 index 0000000..1545d9d --- /dev/null +++ b/src/backend/ghash_vpclmulqdq.cpp @@ -0,0 +1,49 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// VPCLMULQDQ (AVX-512) GHASH — delegates to PCLMULQDQ for now. +// A full AVX-512 implementation would process 4 blocks in parallel using +// powers of H (H^4, H^3, H^2, H) and aggregate, but correctness-first. + +#if defined(__x86_64__) || defined(_M_X64) + +#include "internal/ghash.h" + +namespace tinyaes +{ + namespace internal + { + + void ghash_vpclmulqdq(const uint8_t H[16], const uint8_t *data, size_t data_len, uint8_t Y[16]) + { + // Delegate to PCLMULQDQ implementation for correctness + ghash_pclmulqdq(H, data, data_len, Y); + } + + } // namespace internal +} // namespace tinyaes + +#endif // x86_64 diff --git a/src/cbc.cpp b/src/cbc.cpp new file mode 100644 index 0000000..3f5f70a --- /dev/null +++ b/src/cbc.cpp @@ -0,0 +1,300 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "tinyaes/cbc.h" +#include "internal/aes_impl.h" + +#include + +namespace tinyaes +{ + + Result cbc_encrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &plaintext, + std::vector &ciphertext) + { + int rounds = internal::aes_rounds(key.size()); + if (rounds == 0) + return Result::InvalidKeySize; + if (iv.size() != 16) + return Result::InvalidInput; + if (plaintext.empty() || (plaintext.size() % 16) != 0) + return Result::InvalidInput; + + uint32_t rk[internal::AES_MAX_RK_WORDS]; + auto key_expand = internal::get_key_expand(); + key_expand(key.data(), key.size(), rk); + + auto encrypt_block = internal::get_encrypt_block(); + ciphertext.resize(plaintext.size()); + + uint8_t block[16]; + std::memcpy(block, iv.data(), 16); + + for (size_t i = 0; i < plaintext.size(); i += 16) + { + // XOR plaintext block with previous ciphertext (or IV) + for (int j = 0; j < 16; ++j) + { + block[j] ^= plaintext[i + static_cast(j)]; + } + encrypt_block(rk, rounds, block, ciphertext.data() + i); + std::memcpy(block, ciphertext.data() + i, 16); + } + + secure_zero(rk, sizeof(rk)); + secure_zero(block, sizeof(block)); + return Result::Success; + } + + Result cbc_decrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &ciphertext, + std::vector &plaintext) + { + int rounds = internal::aes_rounds(key.size()); + if (rounds == 0) + return Result::InvalidKeySize; + if (iv.size() != 16) + return Result::InvalidInput; + if (ciphertext.empty() || (ciphertext.size() % 16) != 0) + return Result::InvalidInput; + + uint32_t rk[internal::AES_MAX_RK_WORDS]; + auto key_expand = internal::get_key_expand(); + key_expand(key.data(), key.size(), rk); + + auto decrypt_block = internal::get_decrypt_block(); + plaintext.resize(ciphertext.size()); + + const uint8_t *prev = iv.data(); + + for (size_t i = 0; i < ciphertext.size(); i += 16) + { + decrypt_block(rk, rounds, ciphertext.data() + i, plaintext.data() + i); + // XOR with previous ciphertext block (or IV) + for (int j = 0; j < 16; ++j) + { + plaintext[i + static_cast(j)] ^= prev[j]; + } + prev = ciphertext.data() + i; + } + + secure_zero(rk, sizeof(rk)); + return Result::Success; + } + + // PKCS#7 padding: append N bytes of value N where N = 16 - (len % 16) + // If input is already block-aligned, a full padding block is added. + Result cbc_encrypt_pkcs7( + const std::vector &key, + const std::vector &iv, + const std::vector &plaintext, + std::vector &ciphertext) + { + size_t pad_len = 16 - (plaintext.size() % 16); + std::vector padded(plaintext.size() + pad_len); + std::memcpy(padded.data(), plaintext.data(), plaintext.size()); + std::memset(padded.data() + plaintext.size(), static_cast(pad_len), pad_len); + + auto result = cbc_encrypt(key, iv, padded, ciphertext); + secure_zero(padded.data(), padded.size()); + return result; + } + + // Constant-time PKCS#7 unpadding + Result cbc_decrypt_pkcs7( + const std::vector &key, + const std::vector &iv, + const std::vector &ciphertext, + std::vector &plaintext) + { + std::vector decrypted; + auto result = cbc_decrypt(key, iv, ciphertext, decrypted); + if (result != Result::Success) + return result; + + if (decrypted.empty()) + return Result::InvalidPadding; + + // Constant-time PKCS#7 validation: scan entire last block + uint8_t pad_val = decrypted.back(); + if (pad_val == 0 || pad_val > 16) + return Result::InvalidPadding; + + // Verify all padding bytes match (constant-time) + volatile uint8_t bad = 0; + size_t start = decrypted.size() - pad_val; + for (size_t i = start; i < decrypted.size(); ++i) + { + bad |= static_cast(decrypted[i] ^ pad_val); + } + + if (bad != 0) + { + secure_zero(decrypted.data(), decrypted.size()); + return Result::InvalidPadding; + } + + plaintext.assign(decrypted.begin(), decrypted.begin() + static_cast(start)); + secure_zero(decrypted.data(), decrypted.size()); + return Result::Success; + } + +} // namespace tinyaes + +// C API implementations +extern "C" int tinyaes_cbc_encrypt( + const uint8_t *key, + size_t key_len, + const uint8_t iv[16], + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *ciphertext, + size_t ciphertext_len) +{ + if (!key || !iv || !plaintext || !ciphertext) + return TINYAES_ERROR_INVALID_INPUT; + if (ciphertext_len < plaintext_len) + return TINYAES_ERROR_BUFFER_TOO_SMALL; + + std::vector k(key, key + key_len); + std::vector v(iv, iv + 16); + std::vector pt(plaintext, plaintext + plaintext_len); + std::vector ct; + + auto result = tinyaes::cbc_encrypt(k, v, pt, ct); + tinyaes::secure_zero(k.data(), k.size()); + + if (result != tinyaes::Result::Success) + return static_cast(result); + + std::memcpy(ciphertext, ct.data(), ct.size()); + return TINYAES_SUCCESS; +} + +extern "C" int tinyaes_cbc_decrypt( + const uint8_t *key, + size_t key_len, + const uint8_t iv[16], + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *plaintext, + size_t plaintext_len) +{ + if (!key || !iv || !ciphertext || !plaintext) + return TINYAES_ERROR_INVALID_INPUT; + if (plaintext_len < ciphertext_len) + return TINYAES_ERROR_BUFFER_TOO_SMALL; + + std::vector k(key, key + key_len); + std::vector v(iv, iv + 16); + std::vector ct(ciphertext, ciphertext + ciphertext_len); + std::vector pt; + + auto result = tinyaes::cbc_decrypt(k, v, ct, pt); + tinyaes::secure_zero(k.data(), k.size()); + + if (result != tinyaes::Result::Success) + return static_cast(result); + + std::memcpy(plaintext, pt.data(), pt.size()); + return TINYAES_SUCCESS; +} + +extern "C" int tinyaes_cbc_encrypt_pkcs7( + const uint8_t *key, + size_t key_len, + const uint8_t iv[16], + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *ciphertext, + size_t *ciphertext_len) +{ + if (!key || !iv || !plaintext || !ciphertext || !ciphertext_len) + return TINYAES_ERROR_INVALID_INPUT; + + size_t padded_len = plaintext_len + (16 - (plaintext_len % 16)); + if (*ciphertext_len < padded_len) + { + *ciphertext_len = padded_len; + return TINYAES_ERROR_BUFFER_TOO_SMALL; + } + + std::vector k(key, key + key_len); + std::vector v(iv, iv + 16); + std::vector pt(plaintext, plaintext + plaintext_len); + std::vector ct; + + auto result = tinyaes::cbc_encrypt_pkcs7(k, v, pt, ct); + tinyaes::secure_zero(k.data(), k.size()); + + if (result != tinyaes::Result::Success) + return static_cast(result); + + std::memcpy(ciphertext, ct.data(), ct.size()); + *ciphertext_len = ct.size(); + return TINYAES_SUCCESS; +} + +extern "C" int tinyaes_cbc_decrypt_pkcs7( + const uint8_t *key, + size_t key_len, + const uint8_t iv[16], + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *plaintext, + size_t *plaintext_len) +{ + if (!key || !iv || !ciphertext || !plaintext || !plaintext_len) + return TINYAES_ERROR_INVALID_INPUT; + + std::vector k(key, key + key_len); + std::vector v(iv, iv + 16); + std::vector ct(ciphertext, ciphertext + ciphertext_len); + std::vector pt; + + auto result = tinyaes::cbc_decrypt_pkcs7(k, v, ct, pt); + tinyaes::secure_zero(k.data(), k.size()); + + if (result != tinyaes::Result::Success) + return static_cast(result); + + if (*plaintext_len < pt.size()) + { + *plaintext_len = pt.size(); + tinyaes::secure_zero(pt.data(), pt.size()); + return TINYAES_ERROR_BUFFER_TOO_SMALL; + } + + std::memcpy(plaintext, pt.data(), pt.size()); + *plaintext_len = pt.size(); + tinyaes::secure_zero(pt.data(), pt.size()); + return TINYAES_SUCCESS; +} diff --git a/src/cpuid.cpp b/src/cpuid.cpp new file mode 100644 index 0000000..03a80d8 --- /dev/null +++ b/src/cpuid.cpp @@ -0,0 +1,124 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cpuid.h" + +#if defined(__x86_64__) || defined(_M_X64) +#if defined(_MSC_VER) && !defined(__clang__) +#include +#endif +#endif + +#if defined(__aarch64__) && defined(__linux__) +#include +#include +#endif + +namespace tinyaes +{ + namespace internal + { + +#if defined(__x86_64__) || defined(_M_X64) + + static void cpuid(uint32_t leaf, uint32_t subleaf, uint32_t &eax, uint32_t &ebx, uint32_t &ecx, uint32_t &edx) + { +#if defined(_MSC_VER) && !defined(__clang__) + int regs[4]; + __cpuidex(regs, static_cast(leaf), static_cast(subleaf)); + eax = static_cast(regs[0]); + ebx = static_cast(regs[1]); + ecx = static_cast(regs[2]); + edx = static_cast(regs[3]); +#else + __asm__ __volatile__("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(leaf), "c"(subleaf)); +#endif + } + + CpuFeatures detect_cpu_features() + { + CpuFeatures f; + uint32_t eax, ebx, ecx, edx; + + // Check max leaf + cpuid(0, 0, eax, ebx, ecx, edx); + uint32_t max_leaf = eax; + + // Leaf 1: AES-NI and PCLMULQDQ + if (max_leaf >= 1) + { + cpuid(1, 0, eax, ebx, ecx, edx); + f.aesni = (ecx & (1u << 25)) != 0; + f.pclmulqdq = (ecx & (1u << 1)) != 0; + } + + // Leaf 7, subleaf 0: AVX-512F, VAES, VPCLMULQDQ + if (max_leaf >= 7) + { + cpuid(7, 0, eax, ebx, ecx, edx); + f.avx512f = (ebx & (1u << 16)) != 0; + f.vaes = (ecx & (1u << 9)) != 0; + f.vpclmulqdq = (ecx & (1u << 10)) != 0; + } + + return f; + } + +#elif defined(__aarch64__) || defined(_M_ARM64) + + CpuFeatures detect_cpu_features() + { + CpuFeatures f; + +#if defined(__APPLE__) + // Apple Silicon (M1+) always has AES and PMULL extensions + f.arm_aes = true; + f.arm_pmull = true; +#elif defined(__linux__) + unsigned long hwcap = getauxval(AT_HWCAP); + f.arm_aes = (hwcap & HWCAP_AES) != 0; + f.arm_pmull = (hwcap & HWCAP_PMULL) != 0; +#elif defined(_M_ARM64) + // Windows on ARM64: AES CE is baseline + f.arm_aes = true; + f.arm_pmull = true; +#endif + + return f; + } + +#else + + // Non-x86, non-ARM64: no SIMD features + CpuFeatures detect_cpu_features() + { + return CpuFeatures {}; + } + +#endif + + } // namespace internal +} // namespace tinyaes diff --git a/src/cpuid.h b/src/cpuid.h new file mode 100644 index 0000000..3976551 --- /dev/null +++ b/src/cpuid.h @@ -0,0 +1,52 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include + +namespace tinyaes +{ + namespace internal + { + + struct CpuFeatures + { + // x86_64 + bool aesni = false; + bool pclmulqdq = false; + bool avx512f = false; + bool vaes = false; + bool vpclmulqdq = false; + // aarch64 + bool arm_aes = false; + bool arm_pmull = false; + }; + + CpuFeatures detect_cpu_features(); + + } // namespace internal +} // namespace tinyaes diff --git a/src/ctr.cpp b/src/ctr.cpp new file mode 100644 index 0000000..4dd251e --- /dev/null +++ b/src/ctr.cpp @@ -0,0 +1,118 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "tinyaes/ctr.h" +#include "internal/aes_impl.h" +#include "internal/endian.h" + +#include + +namespace tinyaes +{ + + Result ctr_crypt( + const std::vector &key, + const std::vector &iv, + const std::vector &input, + std::vector &output) + { + int rounds = internal::aes_rounds(key.size()); + if (rounds == 0) + return Result::InvalidKeySize; + if (iv.size() != 16) + return Result::InvalidInput; + if (input.empty()) + return Result::InvalidInput; + + uint32_t rk[internal::AES_MAX_RK_WORDS]; + auto key_expand = internal::get_key_expand(); + key_expand(key.data(), key.size(), rk); + + output.resize(input.size()); + + uint8_t ctr[16]; + std::memcpy(ctr, iv.data(), 16); + + // Process full blocks via pipeline + size_t full_blocks = input.size() / 16; + size_t remainder = input.size() % 16; + + if (full_blocks > 0) + { + auto ctr_pipeline = internal::get_ctr_pipeline(); + ctr_pipeline(rk, rounds, input.data(), output.data(), full_blocks, ctr); + } + + // Handle final partial block + if (remainder > 0) + { + uint8_t keystream[16]; + auto encrypt_block = internal::get_encrypt_block(); + encrypt_block(rk, rounds, ctr, keystream); + + size_t offset = full_blocks * 16; + for (size_t i = 0; i < remainder; ++i) + { + output[offset + i] = input[offset + i] ^ keystream[i]; + } + secure_zero(keystream, sizeof(keystream)); + } + + secure_zero(rk, sizeof(rk)); + secure_zero(ctr, sizeof(ctr)); + return Result::Success; + } + +} // namespace tinyaes + +extern "C" int tinyaes_ctr_crypt( + const uint8_t *key, + size_t key_len, + const uint8_t iv[16], + const uint8_t *input, + size_t input_len, + uint8_t *output, + size_t output_len) +{ + if (!key || !iv || !input || !output) + return TINYAES_ERROR_INVALID_INPUT; + if (output_len < input_len) + return TINYAES_ERROR_BUFFER_TOO_SMALL; + + std::vector k(key, key + key_len); + std::vector v(iv, iv + 16); + std::vector in(input, input + input_len); + std::vector out; + + auto result = tinyaes::ctr_crypt(k, v, in, out); + tinyaes::secure_zero(k.data(), k.size()); + + if (result != tinyaes::Result::Success) + return static_cast(result); + + std::memcpy(output, out.data(), out.size()); + return TINYAES_SUCCESS; +} diff --git a/src/ecb.cpp b/src/ecb.cpp new file mode 100644 index 0000000..d11f4b6 --- /dev/null +++ b/src/ecb.cpp @@ -0,0 +1,143 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "tinyaes/ecb.h" +#include "internal/aes_impl.h" + +#include + +namespace tinyaes +{ + + Result ecb_encrypt( + const std::vector &key, + const std::vector &plaintext, + std::vector &ciphertext) + { + int rounds = internal::aes_rounds(key.size()); + if (rounds == 0) + return Result::InvalidKeySize; + if (plaintext.empty() || (plaintext.size() % 16) != 0) + return Result::InvalidInput; + + uint32_t rk[internal::AES_MAX_RK_WORDS]; + auto key_expand = internal::get_key_expand(); + key_expand(key.data(), key.size(), rk); + + auto encrypt_block = internal::get_encrypt_block(); + ciphertext.resize(plaintext.size()); + + for (size_t i = 0; i < plaintext.size(); i += 16) + { + encrypt_block(rk, rounds, plaintext.data() + i, ciphertext.data() + i); + } + + secure_zero(rk, sizeof(rk)); + return Result::Success; + } + + Result ecb_decrypt( + const std::vector &key, + const std::vector &ciphertext, + std::vector &plaintext) + { + int rounds = internal::aes_rounds(key.size()); + if (rounds == 0) + return Result::InvalidKeySize; + if (ciphertext.empty() || (ciphertext.size() % 16) != 0) + return Result::InvalidInput; + + uint32_t rk[internal::AES_MAX_RK_WORDS]; + auto key_expand = internal::get_key_expand(); + key_expand(key.data(), key.size(), rk); + + auto decrypt_block = internal::get_decrypt_block(); + plaintext.resize(ciphertext.size()); + + for (size_t i = 0; i < ciphertext.size(); i += 16) + { + decrypt_block(rk, rounds, ciphertext.data() + i, plaintext.data() + i); + } + + secure_zero(rk, sizeof(rk)); + return Result::Success; + } + +} // namespace tinyaes + +extern "C" int tinyaes_ecb_encrypt( + const uint8_t *key, + size_t key_len, + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *ciphertext, + size_t ciphertext_len) +{ + if (!key || !plaintext || !ciphertext) + return TINYAES_ERROR_INVALID_INPUT; + if (ciphertext_len < plaintext_len) + return TINYAES_ERROR_BUFFER_TOO_SMALL; + + std::vector k(key, key + key_len); + std::vector pt(plaintext, plaintext + plaintext_len); + std::vector ct; + + auto result = tinyaes::ecb_encrypt(k, pt, ct); + tinyaes::secure_zero(k.data(), k.size()); + + if (result != tinyaes::Result::Success) + return static_cast(result); + + std::memcpy(ciphertext, ct.data(), ct.size()); + return TINYAES_SUCCESS; +} + +extern "C" int tinyaes_ecb_decrypt( + const uint8_t *key, + size_t key_len, + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *plaintext, + size_t plaintext_len) +{ + if (!key || !ciphertext || !plaintext) + return TINYAES_ERROR_INVALID_INPUT; + if (plaintext_len < ciphertext_len) + return TINYAES_ERROR_BUFFER_TOO_SMALL; + + std::vector k(key, key + key_len); + std::vector ct(ciphertext, ciphertext + ciphertext_len); + std::vector pt; + + auto result = tinyaes::ecb_decrypt(k, ct, pt); + tinyaes::secure_zero(k.data(), k.size()); + + if (result != tinyaes::Result::Success) + return static_cast(result); + + std::memcpy(plaintext, pt.data(), pt.size()); + return TINYAES_SUCCESS; +} diff --git a/src/gcm.cpp b/src/gcm.cpp new file mode 100644 index 0000000..e341c28 --- /dev/null +++ b/src/gcm.cpp @@ -0,0 +1,354 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "tinyaes/gcm.h" +#include "internal/aes_impl.h" +#include "internal/endian.h" +#include "internal/ghash.h" + +#include + +namespace tinyaes +{ + + // Compute J0 from IV per NIST SP 800-38D Section 7.1 + static void compute_j0( + const uint8_t *iv, + size_t iv_len, + const uint8_t H[16], + internal::ghash_fn ghash, + uint8_t J0[16]) + { + if (iv_len == 12) + { + // If len(IV) = 96 bits: J0 = IV || 0^31 || 1 + std::memcpy(J0, iv, 12); + J0[12] = 0; + J0[13] = 0; + J0[14] = 0; + J0[15] = 1; + } + else + { + // J0 = GHASH_H(IV || 0^s || len(IV)_64) + // where s = 128*ceil(len(IV)/128) - len(IV) + 64 + std::memset(J0, 0, 16); + + // GHASH the IV data (handles padding internally) + ghash(H, iv, iv_len, J0); + + // Pad to 16-byte boundary, then add 8 zero bytes + 8-byte big-endian bit length + uint8_t len_block[16] = {0}; + uint64_t iv_bits = static_cast(iv_len) * 8; + internal::store_be64(len_block + 8, iv_bits); + ghash(H, len_block, 16, J0); + } + } + + // Compute GCM tag per NIST SP 800-38D + static void compute_tag( + const uint32_t *rk, + int rounds, + const uint8_t H[16], + const uint8_t J0[16], + const uint8_t *aad, + size_t aad_len, + const uint8_t *ciphertext, + size_t ciphertext_len, + internal::ghash_fn ghash, + internal::encrypt_block_fn encrypt_block, + uint8_t tag[16]) + { + uint8_t S[16] = {0}; + + // GHASH AAD + if (aad_len > 0) + { + ghash(H, aad, aad_len, S); + } + + // GHASH ciphertext + if (ciphertext_len > 0) + { + ghash(H, ciphertext, ciphertext_len, S); + } + + // Append length block: [len(A)_64 || len(C)_64] in bits + uint8_t len_block[16]; + internal::store_be64(len_block, static_cast(aad_len) * 8); + internal::store_be64(len_block + 8, static_cast(ciphertext_len) * 8); + ghash(H, len_block, 16, S); + + // T = S XOR E_K(J0) + uint8_t E_J0[16]; + encrypt_block(rk, rounds, J0, E_J0); + for (int i = 0; i < 16; ++i) + { + tag[i] = S[i] ^ E_J0[i]; + } + } + + Result gcm_encrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &aad, + const std::vector &plaintext, + std::vector &ciphertext, + std::vector &tag) + { + int rounds = internal::aes_rounds(key.size()); + if (rounds == 0) + return Result::InvalidKeySize; + if (iv.empty()) + return Result::InvalidInput; + + uint32_t rk[internal::AES_MAX_RK_WORDS]; + auto key_expand = internal::get_key_expand(); + key_expand(key.data(), key.size(), rk); + + auto encrypt_block = internal::get_encrypt_block(); + auto ghash = internal::get_ghash(); + + // Compute H = E_K(0^128) + uint8_t H[16] = {0}; + uint8_t zero_block[16] = {0}; + encrypt_block(rk, rounds, zero_block, H); + + // Compute J0 + uint8_t J0[16]; + compute_j0(iv.data(), iv.size(), H, ghash, J0); + + // Encrypt plaintext using CTR mode starting from inc32(J0) + ciphertext.resize(plaintext.size()); + + if (!plaintext.empty()) + { + uint8_t ctr[16]; + std::memcpy(ctr, J0, 16); + internal::increment_be32(ctr); + + size_t full_blocks = plaintext.size() / 16; + size_t remainder = plaintext.size() % 16; + + if (full_blocks > 0) + { + auto ctr_pipeline = internal::get_ctr_pipeline(); + ctr_pipeline(rk, rounds, plaintext.data(), ciphertext.data(), full_blocks, ctr); + } + + if (remainder > 0) + { + uint8_t keystream[16]; + encrypt_block(rk, rounds, ctr, keystream); + size_t offset = full_blocks * 16; + for (size_t i = 0; i < remainder; ++i) + { + ciphertext[offset + i] = plaintext[offset + i] ^ keystream[i]; + } + secure_zero(keystream, sizeof(keystream)); + } + } + + // Compute authentication tag + tag.resize(16); + compute_tag(rk, rounds, H, J0, aad.data(), aad.size(), ciphertext.data(), ciphertext.size(), ghash, + encrypt_block, tag.data()); + + secure_zero(rk, sizeof(rk)); + secure_zero(H, sizeof(H)); + secure_zero(J0, sizeof(J0)); + return Result::Success; + } + + Result gcm_decrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &aad, + const std::vector &ciphertext, + const std::vector &tag, + std::vector &plaintext) + { + int rounds = internal::aes_rounds(key.size()); + if (rounds == 0) + return Result::InvalidKeySize; + if (iv.empty()) + return Result::InvalidInput; + if (tag.size() != 16) + return Result::InvalidInput; + + uint32_t rk[internal::AES_MAX_RK_WORDS]; + auto key_expand = internal::get_key_expand(); + key_expand(key.data(), key.size(), rk); + + auto encrypt_block = internal::get_encrypt_block(); + auto ghash = internal::get_ghash(); + + // Compute H = E_K(0^128) + uint8_t H[16] = {0}; + uint8_t zero_block[16] = {0}; + encrypt_block(rk, rounds, zero_block, H); + + // Compute J0 + uint8_t J0[16]; + compute_j0(iv.data(), iv.size(), H, ghash, J0); + + // Verify tag BEFORE decrypting (decrypt into temp buffer) + uint8_t computed_tag[16]; + compute_tag(rk, rounds, H, J0, aad.data(), aad.size(), ciphertext.data(), ciphertext.size(), ghash, + encrypt_block, computed_tag); + + if (!constant_time_equal(computed_tag, tag.data(), 16)) + { + secure_zero(rk, sizeof(rk)); + secure_zero(H, sizeof(H)); + secure_zero(J0, sizeof(J0)); + secure_zero(computed_tag, sizeof(computed_tag)); + return Result::AuthFailed; + } + + // Tag verified — decrypt ciphertext using CTR mode + plaintext.resize(ciphertext.size()); + + if (!ciphertext.empty()) + { + uint8_t ctr[16]; + std::memcpy(ctr, J0, 16); + internal::increment_be32(ctr); + + size_t full_blocks = ciphertext.size() / 16; + size_t remainder = ciphertext.size() % 16; + + if (full_blocks > 0) + { + auto ctr_pipeline = internal::get_ctr_pipeline(); + ctr_pipeline(rk, rounds, ciphertext.data(), plaintext.data(), full_blocks, ctr); + } + + if (remainder > 0) + { + uint8_t keystream[16]; + encrypt_block(rk, rounds, ctr, keystream); + size_t offset = full_blocks * 16; + for (size_t i = 0; i < remainder; ++i) + { + plaintext[offset + i] = ciphertext[offset + i] ^ keystream[i]; + } + secure_zero(keystream, sizeof(keystream)); + } + } + + secure_zero(rk, sizeof(rk)); + secure_zero(H, sizeof(H)); + secure_zero(J0, sizeof(J0)); + secure_zero(computed_tag, sizeof(computed_tag)); + return Result::Success; + } + +} // namespace tinyaes + +extern "C" int tinyaes_gcm_encrypt( + const uint8_t *key, + size_t key_len, + const uint8_t *iv, + size_t iv_len, + const uint8_t *aad, + size_t aad_len, + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t tag[16]) +{ + if (!key || !iv || !tag) + return TINYAES_ERROR_INVALID_INPUT; + if (plaintext_len > 0 && (!plaintext || !ciphertext)) + return TINYAES_ERROR_INVALID_INPUT; + if (ciphertext_len < plaintext_len) + return TINYAES_ERROR_BUFFER_TOO_SMALL; + + std::vector k(key, key + key_len); + std::vector v(iv, iv + iv_len); + std::vector a(aad ? aad : key, aad ? aad + aad_len : key); + if (!aad) + a.clear(); + std::vector pt(plaintext ? plaintext : key, plaintext ? plaintext + plaintext_len : key); + if (!plaintext) + pt.clear(); + std::vector ct, t; + + auto result = tinyaes::gcm_encrypt(k, v, a, pt, ct, t); + tinyaes::secure_zero(k.data(), k.size()); + + if (result != tinyaes::Result::Success) + return static_cast(result); + + if (!ct.empty()) + std::memcpy(ciphertext, ct.data(), ct.size()); + std::memcpy(tag, t.data(), 16); + return TINYAES_SUCCESS; +} + +extern "C" int tinyaes_gcm_decrypt( + const uint8_t *key, + size_t key_len, + const uint8_t *iv, + size_t iv_len, + const uint8_t *aad, + size_t aad_len, + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *plaintext, + size_t plaintext_len, + const uint8_t tag[16]) +{ + if (!key || !iv || !tag) + return TINYAES_ERROR_INVALID_INPUT; + if (ciphertext_len > 0 && (!ciphertext || !plaintext)) + return TINYAES_ERROR_INVALID_INPUT; + if (plaintext_len < ciphertext_len) + return TINYAES_ERROR_BUFFER_TOO_SMALL; + + std::vector k(key, key + key_len); + std::vector v(iv, iv + iv_len); + std::vector a(aad ? aad : key, aad ? aad + aad_len : key); + if (!aad) + a.clear(); + std::vector ct(ciphertext ? ciphertext : key, ciphertext ? ciphertext + ciphertext_len : key); + if (!ciphertext) + ct.clear(); + std::vector t(tag, tag + 16); + std::vector pt; + + auto result = tinyaes::gcm_decrypt(k, v, a, ct, t, pt); + tinyaes::secure_zero(k.data(), k.size()); + + if (result != tinyaes::Result::Success) + return static_cast(result); + + if (!pt.empty()) + std::memcpy(plaintext, pt.data(), pt.size()); + return TINYAES_SUCCESS; +} diff --git a/src/internal/aes_impl.h b/src/internal/aes_impl.h new file mode 100644 index 0000000..1adae6d --- /dev/null +++ b/src/internal/aes_impl.h @@ -0,0 +1,94 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +namespace tinyaes +{ + namespace internal + { + + // Maximum round keys: AES-256 has 15 round keys * 4 words = 60 words + static constexpr size_t AES_MAX_RK_WORDS = 60; + + // Backend function signatures + using encrypt_block_fn = void (*)(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]); + using decrypt_block_fn = void (*)(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]); + using key_expand_fn = void (*)(const uint8_t *key, size_t key_len, uint32_t *rk); + using ctr_pipeline_fn = void (*)(const uint32_t *rk, int rounds, const uint8_t *in, uint8_t *out, size_t blocks, + uint8_t ctr[16]); + + // Portable backend declarations + void aes_encrypt_block_portable(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]); + void aes_decrypt_block_portable(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]); + void aes_key_expand_portable(const uint8_t *key, size_t key_len, uint32_t *rk); + void aes_ctr_pipeline_portable(const uint32_t *rk, int rounds, const uint8_t *in, uint8_t *out, size_t blocks, + uint8_t ctr[16]); + + // x86 AES-NI backend declarations + void aes_encrypt_block_aesni(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]); + void aes_decrypt_block_aesni(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]); + void aes_key_expand_aesni(const uint8_t *key, size_t key_len, uint32_t *rk); + void aes_ctr_pipeline_aesni(const uint32_t *rk, int rounds, const uint8_t *in, uint8_t *out, size_t blocks, + uint8_t ctr[16]); + + // x86 VAES (AVX-512) backend declarations + void aes_ctr_pipeline_vaes(const uint32_t *rk, int rounds, const uint8_t *in, uint8_t *out, size_t blocks, + uint8_t ctr[16]); + + // ARM CE backend declarations + void aes_encrypt_block_arm_ce(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]); + void aes_decrypt_block_arm_ce(const uint32_t *rk, int rounds, const uint8_t in[16], uint8_t out[16]); + void aes_ctr_pipeline_arm_ce(const uint32_t *rk, int rounds, const uint8_t *in, uint8_t *out, size_t blocks, + uint8_t ctr[16]); + + // Dispatch getters (lazy-resolved via CPUID) + encrypt_block_fn get_encrypt_block(); + decrypt_block_fn get_decrypt_block(); + key_expand_fn get_key_expand(); + ctr_pipeline_fn get_ctr_pipeline(); + + // Number of rounds for a given key size + inline int aes_rounds(size_t key_len) + { + switch (key_len) + { + case 16: + return 10; + case 24: + return 12; + case 32: + return 14; + default: + return 0; + } + } + + } // namespace internal +} // namespace tinyaes diff --git a/src/internal/endian.h b/src/internal/endian.h new file mode 100644 index 0000000..6862921 --- /dev/null +++ b/src/internal/endian.h @@ -0,0 +1,79 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include + +namespace tinyaes +{ + namespace internal + { + + inline uint32_t load_be32(const uint8_t *p) + { + return (static_cast(p[0]) << 24) | (static_cast(p[1]) << 16) + | (static_cast(p[2]) << 8) | (static_cast(p[3])); + } + + inline uint64_t load_be64(const uint8_t *p) + { + return (static_cast(p[0]) << 56) | (static_cast(p[1]) << 48) + | (static_cast(p[2]) << 40) | (static_cast(p[3]) << 32) + | (static_cast(p[4]) << 24) | (static_cast(p[5]) << 16) + | (static_cast(p[6]) << 8) | (static_cast(p[7])); + } + + inline void store_be32(uint8_t *p, uint32_t v) + { + p[0] = static_cast(v >> 24); + p[1] = static_cast(v >> 16); + p[2] = static_cast(v >> 8); + p[3] = static_cast(v); + } + + inline void store_be64(uint8_t *p, uint64_t v) + { + p[0] = static_cast(v >> 56); + p[1] = static_cast(v >> 48); + p[2] = static_cast(v >> 40); + p[3] = static_cast(v >> 32); + p[4] = static_cast(v >> 24); + p[5] = static_cast(v >> 16); + p[6] = static_cast(v >> 8); + p[7] = static_cast(v); + } + + // Increment big-endian 32-bit counter in the last 4 bytes of a 16-byte block + inline void increment_be32(uint8_t block[16]) + { + uint32_t ctr = load_be32(block + 12); + ++ctr; + store_be32(block + 12, ctr); + } + + } // namespace internal +} // namespace tinyaes diff --git a/src/internal/ghash.h b/src/internal/ghash.h new file mode 100644 index 0000000..08cac97 --- /dev/null +++ b/src/internal/ghash.h @@ -0,0 +1,57 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +namespace tinyaes +{ + namespace internal + { + + // GHASH function signature: processes AAD and ciphertext, produces 16-byte tag component + // H is the hash key (AES_K(0^128)), Y is the running accumulator + using ghash_fn = void (*)(const uint8_t H[16], const uint8_t *data, size_t data_len, uint8_t Y[16]); + + // Portable GHASH + void ghash_portable(const uint8_t H[16], const uint8_t *data, size_t data_len, uint8_t Y[16]); + + // x86 PCLMULQDQ GHASH + void ghash_pclmulqdq(const uint8_t H[16], const uint8_t *data, size_t data_len, uint8_t Y[16]); + + // x86 VPCLMULQDQ GHASH (AVX-512) + void ghash_vpclmulqdq(const uint8_t H[16], const uint8_t *data, size_t data_len, uint8_t Y[16]); + + // ARM PMULL GHASH + void ghash_arm_ce(const uint8_t H[16], const uint8_t *data, size_t data_len, uint8_t Y[16]); + + // Dispatch getter + ghash_fn get_ghash(); + + } // namespace internal +} // namespace tinyaes diff --git a/src/iv_generate.cpp b/src/iv_generate.cpp new file mode 100644 index 0000000..bf86644 --- /dev/null +++ b/src/iv_generate.cpp @@ -0,0 +1,66 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "tinyaes/common.h" + +#if defined(_WIN32) +#include +#include +#elif defined(__linux__) +#include +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) +#include +#endif + +namespace tinyaes +{ + + int generate_iv(uint8_t *out, size_t len) + { + if (!out || len == 0) + return -1; + +#if defined(_WIN32) + NTSTATUS status = BCryptGenRandom(nullptr, out, static_cast(len), BCRYPT_USE_SYSTEM_PREFERRED_RNG); + return BCRYPT_SUCCESS(status) ? 0 : -1; +#elif defined(__linux__) + ssize_t ret = getrandom(out, len, 0); + return (ret == static_cast(len)) ? 0 : -1; +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) + arc4random_buf(out, len); + return 0; +#else + // Fallback: read from /dev/urandom + FILE *f = fopen("/dev/urandom", "rb"); + if (!f) + return -1; + size_t n = fread(out, 1, len, f); + fclose(f); + return (n == len) ? 0 : -1; +#endif + } + +} // namespace tinyaes diff --git a/src/keyschedule.cpp b/src/keyschedule.cpp new file mode 100644 index 0000000..f204b66 --- /dev/null +++ b/src/keyschedule.cpp @@ -0,0 +1,193 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "internal/aes_impl.h" +#include "internal/ghash.h" +#include "cpuid.h" + +#include + +namespace tinyaes +{ + namespace internal + { + + // --- Encrypt block dispatch --- + static encrypt_block_fn resolve_encrypt_block(); + static std::atomic encrypt_block_impl {nullptr}; + + encrypt_block_fn get_encrypt_block() + { + auto fn = encrypt_block_impl.load(std::memory_order_acquire); + if (fn) + return fn; + fn = resolve_encrypt_block(); + encrypt_block_impl.store(fn, std::memory_order_release); + return fn; + } + + static encrypt_block_fn resolve_encrypt_block() + { +#if (defined(__x86_64__) || defined(_M_X64)) && !defined(TINYAES_FORCE_PORTABLE) + auto features = detect_cpu_features(); + if (features.aesni) + return aes_encrypt_block_aesni; + return aes_encrypt_block_portable; +#elif (defined(__aarch64__) || defined(_M_ARM64)) && !defined(TINYAES_FORCE_PORTABLE) + auto features = detect_cpu_features(); + if (features.arm_aes) + return aes_encrypt_block_arm_ce; + return aes_encrypt_block_portable; +#else + return aes_encrypt_block_portable; +#endif + } + + // --- Decrypt block dispatch --- + static decrypt_block_fn resolve_decrypt_block(); + static std::atomic decrypt_block_impl {nullptr}; + + decrypt_block_fn get_decrypt_block() + { + auto fn = decrypt_block_impl.load(std::memory_order_acquire); + if (fn) + return fn; + fn = resolve_decrypt_block(); + decrypt_block_impl.store(fn, std::memory_order_release); + return fn; + } + + static decrypt_block_fn resolve_decrypt_block() + { +#if (defined(__x86_64__) || defined(_M_X64)) && !defined(TINYAES_FORCE_PORTABLE) + auto features = detect_cpu_features(); + if (features.aesni) + return aes_decrypt_block_aesni; + return aes_decrypt_block_portable; +#elif (defined(__aarch64__) || defined(_M_ARM64)) && !defined(TINYAES_FORCE_PORTABLE) + auto features = detect_cpu_features(); + if (features.arm_aes) + return aes_decrypt_block_arm_ce; + return aes_decrypt_block_portable; +#else + return aes_decrypt_block_portable; +#endif + } + + // --- Key expand dispatch --- + static key_expand_fn resolve_key_expand(); + static std::atomic key_expand_impl {nullptr}; + + key_expand_fn get_key_expand() + { + auto fn = key_expand_impl.load(std::memory_order_acquire); + if (fn) + return fn; + fn = resolve_key_expand(); + key_expand_impl.store(fn, std::memory_order_release); + return fn; + } + + static key_expand_fn resolve_key_expand() + { +#if (defined(__x86_64__) || defined(_M_X64)) && !defined(TINYAES_FORCE_PORTABLE) + auto features = detect_cpu_features(); + if (features.aesni) + return aes_key_expand_aesni; + return aes_key_expand_portable; +#else + return aes_key_expand_portable; +#endif + } + + // --- CTR pipeline dispatch --- + static ctr_pipeline_fn resolve_ctr_pipeline(); + static std::atomic ctr_pipeline_impl {nullptr}; + + ctr_pipeline_fn get_ctr_pipeline() + { + auto fn = ctr_pipeline_impl.load(std::memory_order_acquire); + if (fn) + return fn; + fn = resolve_ctr_pipeline(); + ctr_pipeline_impl.store(fn, std::memory_order_release); + return fn; + } + + static ctr_pipeline_fn resolve_ctr_pipeline() + { +#if (defined(__x86_64__) || defined(_M_X64)) && !defined(TINYAES_FORCE_PORTABLE) + auto features = detect_cpu_features(); + if (features.vaes && features.avx512f) + return aes_ctr_pipeline_vaes; + if (features.aesni) + return aes_ctr_pipeline_aesni; + return aes_ctr_pipeline_portable; +#elif (defined(__aarch64__) || defined(_M_ARM64)) && !defined(TINYAES_FORCE_PORTABLE) + auto features = detect_cpu_features(); + if (features.arm_aes) + return aes_ctr_pipeline_arm_ce; + return aes_ctr_pipeline_portable; +#else + return aes_ctr_pipeline_portable; +#endif + } + + // --- GHASH dispatch --- + static ghash_fn resolve_ghash(); + static std::atomic ghash_impl {nullptr}; + + ghash_fn get_ghash() + { + auto fn = ghash_impl.load(std::memory_order_acquire); + if (fn) + return fn; + fn = resolve_ghash(); + ghash_impl.store(fn, std::memory_order_release); + return fn; + } + + static ghash_fn resolve_ghash() + { +#if (defined(__x86_64__) || defined(_M_X64)) && !defined(TINYAES_FORCE_PORTABLE) + auto features = detect_cpu_features(); + if (features.vpclmulqdq && features.avx512f) + return ghash_vpclmulqdq; + if (features.pclmulqdq) + return ghash_pclmulqdq; + return ghash_portable; +#elif (defined(__aarch64__) || defined(_M_ARM64)) && !defined(TINYAES_FORCE_PORTABLE) + auto features = detect_cpu_features(); + if (features.arm_pmull) + return ghash_arm_ce; + return ghash_portable; +#else + return ghash_portable; +#endif + } + + } // namespace internal +} // namespace tinyaes diff --git a/src/secure_zero.cpp b/src/secure_zero.cpp new file mode 100644 index 0000000..2762f64 --- /dev/null +++ b/src/secure_zero.cpp @@ -0,0 +1,80 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "tinyaes/common.h" + +#include + +#if defined(_WIN32) +#include +#endif + +namespace tinyaes +{ + +#if defined(_WIN32) + + void secure_zero(void *ptr, size_t len) + { + SecureZeroMemory(ptr, len); + } + +#elif defined(__STDC_LIB_EXT1__) + + void secure_zero(void *ptr, size_t len) + { + memset_s(ptr, len, 0, len); + } + +#else + + // Use a volatile function pointer to prevent dead-store elimination + static void *(*const volatile memset_ptr)(void *, int, size_t) = std::memset; + + void secure_zero(void *ptr, size_t len) + { + memset_ptr(ptr, 0, len); + } + +#endif + + bool constant_time_equal(const uint8_t *a, const uint8_t *b, size_t len) + { + volatile uint8_t diff = 0; + for (size_t i = 0; i < len; ++i) + { + diff |= static_cast(a[i] ^ b[i]); + } + volatile uint8_t result = static_cast(diff == 0); + return result != 0; + } + +} // namespace tinyaes + +extern "C" int tinyaes_constant_time_equal(const uint8_t *a, const uint8_t *b, size_t len) +{ + return tinyaes::constant_time_equal(a, b, len) ? 1 : 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..7283bd3 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,43 @@ +add_executable(tinyaes_tests + test_main.cpp + test_keyschedule.cpp + test_ecb.cpp + test_cbc.cpp + test_padding.cpp + test_ctr.cpp + test_gcm.cpp + test_gcm_auth_failure.cpp + test_iv_generation.cpp + test_cpuid.cpp +) + +target_link_libraries(tinyaes_tests PRIVATE tinyaes) +target_include_directories(tinyaes_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../src +) +set_target_properties(tinyaes_tests PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} +) + +# Warning flags for tests +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(tinyaes_tests PRIVATE -Wall -Wextra -Wpedantic -Werror) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set_property(TARGET tinyaes_tests APPEND_STRING PROPERTY LINK_FLAGS + " -Wl,-z,relro,-z,now -Wl,-z,noexecstack") + endif() + if(MINGW) + set_property(TARGET tinyaes_tests APPEND_STRING PROPERTY LINK_FLAGS + " -Wl,--nxcompat -Wl,--dynamicbase -Wl,--high-entropy-va") + endif() +elseif(MSVC) + target_compile_options(tinyaes_tests PRIVATE /W4 /WX) + set_property(TARGET tinyaes_tests APPEND_STRING PROPERTY LINK_FLAGS + " /DYNAMICBASE /NXCOMPAT /HIGHENTROPYVA") +endif() + +add_test(NAME tinyaes_tests COMMAND tinyaes_tests) diff --git a/tests/test_cbc.cpp b/tests/test_cbc.cpp new file mode 100644 index 0000000..8bf81bd --- /dev/null +++ b/tests/test_cbc.cpp @@ -0,0 +1,81 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "test_harness.h" +#include "tinyaes/cbc.h" + +#include "vectors/aes_cbc_vectors.inl" + +#define VEC(arr) std::vector(arr, arr + sizeof(arr)) + +TEST(cbc_aes128_encrypt) +{ + std::vector ct; + auto result = tinyaes::cbc_encrypt(VEC(cbc_128_key), VEC(cbc_128_iv), VEC(cbc_128_plain), ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(cbc_128_cipher)); +} + +TEST(cbc_aes128_decrypt) +{ + std::vector pt; + auto result = tinyaes::cbc_decrypt(VEC(cbc_128_key), VEC(cbc_128_iv), VEC(cbc_128_cipher), pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, VEC(cbc_128_plain)); +} + +TEST(cbc_aes192_encrypt) +{ + std::vector ct; + auto result = tinyaes::cbc_encrypt(VEC(cbc_192_key), VEC(cbc_192_iv), VEC(cbc_192_plain), ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(cbc_192_cipher)); +} + +TEST(cbc_aes192_decrypt) +{ + std::vector pt; + auto result = tinyaes::cbc_decrypt(VEC(cbc_192_key), VEC(cbc_192_iv), VEC(cbc_192_cipher), pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, VEC(cbc_192_plain)); +} + +TEST(cbc_aes256_encrypt) +{ + std::vector ct; + auto result = tinyaes::cbc_encrypt(VEC(cbc_256_key), VEC(cbc_256_iv), VEC(cbc_256_plain), ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(cbc_256_cipher)); +} + +TEST(cbc_aes256_decrypt) +{ + std::vector pt; + auto result = tinyaes::cbc_decrypt(VEC(cbc_256_key), VEC(cbc_256_iv), VEC(cbc_256_cipher), pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, VEC(cbc_256_plain)); +} + +TEST(cbc_roundtrip_pkcs7) +{ + std::vector key(16, 0x42); + std::vector iv(16, 0x00); + std::vector plaintext = {0x48, 0x65, 0x6c, 0x6c, 0x6f}; // "Hello" + std::vector ct, pt; + + auto result = tinyaes::cbc_encrypt_pkcs7(key, iv, plaintext, ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_TRUE(ct.size() == 16); // 5 bytes + 11 padding = 16 + + result = tinyaes::cbc_decrypt_pkcs7(key, iv, ct, pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, plaintext); +} + +TEST(cbc_invalid_iv_size) +{ + std::vector key(16, 0), iv(15, 0), pt(16, 0), ct; + ASSERT_TRUE(tinyaes::cbc_encrypt(key, iv, pt, ct) == tinyaes::Result::InvalidInput); +} + +#undef VEC diff --git a/tests/test_cpuid.cpp b/tests/test_cpuid.cpp new file mode 100644 index 0000000..d4053a7 --- /dev/null +++ b/tests/test_cpuid.cpp @@ -0,0 +1,39 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "test_harness.h" +#include "cpuid.h" +#include "internal/aes_impl.h" + +TEST(cpuid_detect_no_crash) +{ + // Just verify detection doesn't crash + auto features = tinyaes::internal::detect_cpu_features(); + (void)features; + ASSERT_TRUE(true); +} + +TEST(cpuid_dispatch_encrypt_block) +{ + // Verify dispatch resolves to a non-null function pointer + auto fn = tinyaes::internal::get_encrypt_block(); + ASSERT_TRUE(fn != nullptr); +} + +TEST(cpuid_dispatch_decrypt_block) +{ + auto fn = tinyaes::internal::get_decrypt_block(); + ASSERT_TRUE(fn != nullptr); +} + +TEST(cpuid_dispatch_key_expand) +{ + auto fn = tinyaes::internal::get_key_expand(); + ASSERT_TRUE(fn != nullptr); +} + +TEST(cpuid_dispatch_ctr_pipeline) +{ + auto fn = tinyaes::internal::get_ctr_pipeline(); + ASSERT_TRUE(fn != nullptr); +} diff --git a/tests/test_ctr.cpp b/tests/test_ctr.cpp new file mode 100644 index 0000000..4d7f0c6 --- /dev/null +++ b/tests/test_ctr.cpp @@ -0,0 +1,77 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "test_harness.h" +#include "tinyaes/ctr.h" + +#include "vectors/aes_ctr_vectors.inl" + +#define VEC(arr) std::vector(arr, arr + sizeof(arr)) + +TEST(ctr_aes128_encrypt) +{ + std::vector ct; + auto result = tinyaes::ctr_crypt(VEC(ctr_128_key), VEC(ctr_128_iv), VEC(ctr_128_plain), ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(ctr_128_cipher)); +} + +TEST(ctr_aes128_decrypt) +{ + std::vector pt; + auto result = tinyaes::ctr_crypt(VEC(ctr_128_key), VEC(ctr_128_iv), VEC(ctr_128_cipher), pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, VEC(ctr_128_plain)); +} + +TEST(ctr_aes192_encrypt) +{ + std::vector ct; + auto result = tinyaes::ctr_crypt(VEC(ctr_192_key), VEC(ctr_192_iv), VEC(ctr_192_plain), ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(ctr_192_cipher)); +} + +TEST(ctr_aes256_encrypt) +{ + std::vector ct; + auto result = tinyaes::ctr_crypt(VEC(ctr_256_key), VEC(ctr_256_iv), VEC(ctr_256_plain), ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(ctr_256_cipher)); +} + +TEST(ctr_partial_block) +{ + // Encrypt 7 bytes with CTR — should handle partial block + std::vector key(16, 0x42); + std::vector iv(16, 0x00); + std::vector plaintext = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; + std::vector ct, pt; + + auto result = tinyaes::ctr_crypt(key, iv, plaintext, ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_TRUE(ct.size() == 7); + + // Decrypt should recover original + result = tinyaes::ctr_crypt(key, iv, ct, pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, plaintext); +} + +TEST(ctr_roundtrip_multi_block) +{ + std::vector key(32, 0xAB); + std::vector iv(16, 0x01); + std::vector plaintext(100, 0x55); // 6 full blocks + 4 remainder + std::vector ct, pt; + + auto result = tinyaes::ctr_crypt(key, iv, plaintext, ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_TRUE(ct.size() == 100); + + result = tinyaes::ctr_crypt(key, iv, ct, pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, plaintext); +} + +#undef VEC diff --git a/tests/test_ecb.cpp b/tests/test_ecb.cpp new file mode 100644 index 0000000..6c6e163 --- /dev/null +++ b/tests/test_ecb.cpp @@ -0,0 +1,93 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "test_harness.h" +#include "tinyaes/ecb.h" + +#include "vectors/aes_ecb_vectors.inl" + +#define VEC(arr) std::vector(arr, arr + sizeof(arr)) + +TEST(ecb_aes128_encrypt) +{ + std::vector ct; + auto result = tinyaes::ecb_encrypt(VEC(ecb_128_key), VEC(ecb_128_plain), ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(ecb_128_cipher)); +} + +TEST(ecb_aes128_decrypt) +{ + std::vector pt; + auto result = tinyaes::ecb_decrypt(VEC(ecb_128_key), VEC(ecb_128_cipher), pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, VEC(ecb_128_plain)); +} + +TEST(ecb_aes192_encrypt) +{ + std::vector ct; + auto result = tinyaes::ecb_encrypt(VEC(ecb_192_key), VEC(ecb_192_plain), ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(ecb_192_cipher)); +} + +TEST(ecb_aes192_decrypt) +{ + std::vector pt; + auto result = tinyaes::ecb_decrypt(VEC(ecb_192_key), VEC(ecb_192_cipher), pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, VEC(ecb_192_plain)); +} + +TEST(ecb_aes256_encrypt) +{ + std::vector ct; + auto result = tinyaes::ecb_encrypt(VEC(ecb_256_key), VEC(ecb_256_plain), ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(ecb_256_cipher)); +} + +TEST(ecb_aes256_decrypt) +{ + std::vector pt; + auto result = tinyaes::ecb_decrypt(VEC(ecb_256_key), VEC(ecb_256_cipher), pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, VEC(ecb_256_plain)); +} + +TEST(ecb_aes128_multi_block) +{ + std::vector ct; + auto result = tinyaes::ecb_encrypt(VEC(ecb_128_multi_key), VEC(ecb_128_multi_plain), ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(ecb_128_multi_cipher)); +} + +TEST(ecb_aes128_multi_block_decrypt) +{ + std::vector pt; + auto result = tinyaes::ecb_decrypt(VEC(ecb_128_multi_key), VEC(ecb_128_multi_cipher), pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, VEC(ecb_128_multi_plain)); +} + +TEST(ecb_invalid_key_size) +{ + std::vector key(15, 0), pt(16, 0), ct; + ASSERT_TRUE(tinyaes::ecb_encrypt(key, pt, ct) == tinyaes::Result::InvalidKeySize); +} + +TEST(ecb_non_block_aligned) +{ + std::vector key(16, 0), pt(17, 0), ct; + ASSERT_TRUE(tinyaes::ecb_encrypt(key, pt, ct) == tinyaes::Result::InvalidInput); +} + +TEST(ecb_empty_input) +{ + std::vector key(16, 0), pt, ct; + ASSERT_TRUE(tinyaes::ecb_encrypt(key, pt, ct) == tinyaes::Result::InvalidInput); +} + +#undef VEC diff --git a/tests/test_gcm.cpp b/tests/test_gcm.cpp new file mode 100644 index 0000000..45b6a35 --- /dev/null +++ b/tests/test_gcm.cpp @@ -0,0 +1,111 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "test_harness.h" +#include "tinyaes/gcm.h" + +#include "vectors/aes_gcm_vectors.inl" + +#define VEC(arr) std::vector(arr, arr + sizeof(arr)) +#define EMPTY_VEC std::vector() + +TEST(gcm_tc1_aes128_no_plaintext_no_aad) +{ + std::vector ct, tag; + auto result = tinyaes::gcm_encrypt(VEC(gcm_tc1_key), VEC(gcm_tc1_iv), EMPTY_VEC, EMPTY_VEC, ct, tag); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_TRUE(ct.empty()); + ASSERT_EQ(tag, VEC(gcm_tc1_tag)); +} + +TEST(gcm_tc2_aes128_16byte_plaintext) +{ + std::vector ct, tag; + auto result = tinyaes::gcm_encrypt(VEC(gcm_tc2_key), VEC(gcm_tc2_iv), EMPTY_VEC, VEC(gcm_tc2_plain), ct, tag); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(gcm_tc2_cipher)); + ASSERT_EQ(tag, VEC(gcm_tc2_tag)); +} + +TEST(gcm_tc3_aes128_64byte_plaintext) +{ + std::vector ct, tag; + auto result = tinyaes::gcm_encrypt(VEC(gcm_tc3_key), VEC(gcm_tc3_iv), EMPTY_VEC, VEC(gcm_tc3_plain), ct, tag); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(gcm_tc3_cipher)); + ASSERT_EQ(tag, VEC(gcm_tc3_tag)); +} + +TEST(gcm_tc4_aes128_with_aad) +{ + std::vector ct, tag; + auto result = + tinyaes::gcm_encrypt(VEC(gcm_tc4_key), VEC(gcm_tc4_iv), VEC(gcm_tc4_aad), VEC(gcm_tc4_plain), ct, tag); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(gcm_tc4_cipher)); + ASSERT_EQ(tag, VEC(gcm_tc4_tag)); +} + +TEST(gcm_tc13_aes256_no_plaintext_no_aad) +{ + std::vector ct, tag; + auto result = tinyaes::gcm_encrypt(VEC(gcm_tc13_key), VEC(gcm_tc13_iv), EMPTY_VEC, EMPTY_VEC, ct, tag); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_TRUE(ct.empty()); + ASSERT_EQ(tag, VEC(gcm_tc13_tag)); +} + +TEST(gcm_tc14_aes256_16byte_plaintext) +{ + std::vector ct, tag; + auto result = tinyaes::gcm_encrypt(VEC(gcm_tc14_key), VEC(gcm_tc14_iv), EMPTY_VEC, VEC(gcm_tc14_plain), ct, tag); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(ct, VEC(gcm_tc14_cipher)); + ASSERT_EQ(tag, VEC(gcm_tc14_tag)); +} + +TEST(gcm_tc2_decrypt_verify) +{ + std::vector pt; + auto result = + tinyaes::gcm_decrypt(VEC(gcm_tc2_key), VEC(gcm_tc2_iv), EMPTY_VEC, VEC(gcm_tc2_cipher), VEC(gcm_tc2_tag), pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, VEC(gcm_tc2_plain)); +} + +TEST(gcm_tc3_decrypt_verify) +{ + std::vector pt; + auto result = + tinyaes::gcm_decrypt(VEC(gcm_tc3_key), VEC(gcm_tc3_iv), EMPTY_VEC, VEC(gcm_tc3_cipher), VEC(gcm_tc3_tag), pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, VEC(gcm_tc3_plain)); +} + +TEST(gcm_tc4_decrypt_verify) +{ + std::vector pt; + auto result = tinyaes::gcm_decrypt(VEC(gcm_tc4_key), VEC(gcm_tc4_iv), VEC(gcm_tc4_aad), VEC(gcm_tc4_cipher), + VEC(gcm_tc4_tag), pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, VEC(gcm_tc4_plain)); +} + +TEST(gcm_roundtrip) +{ + std::vector key(16, 0x42); + std::vector iv(12, 0x01); + std::vector aad = {0xAA, 0xBB, 0xCC}; + std::vector plaintext = {0x48, 0x65, 0x6c, 0x6c, 0x6f}; // "Hello" + std::vector ct, tag, pt; + + auto result = tinyaes::gcm_encrypt(key, iv, aad, plaintext, ct, tag); + ASSERT_TRUE(result == tinyaes::Result::Success); + + result = tinyaes::gcm_decrypt(key, iv, aad, ct, tag, pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, plaintext); +} + +#undef VEC +#undef EMPTY_VEC diff --git a/tests/test_gcm_auth_failure.cpp b/tests/test_gcm_auth_failure.cpp new file mode 100644 index 0000000..ced866e --- /dev/null +++ b/tests/test_gcm_auth_failure.cpp @@ -0,0 +1,97 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "test_harness.h" +#include "tinyaes/gcm.h" + +static std::vector make_key() +{ + return std::vector(16, 0x42); +} +static std::vector make_iv() +{ + return std::vector(12, 0x01); +} + +// Helper: encrypt a known message +static void encrypt_helper( + std::vector &ct, + std::vector &tag, + const std::vector &aad = {}, + const std::vector &pt = {0x48, 0x65, 0x6c, 0x6c, 0x6f}) +{ + auto key = make_key(); + auto iv = make_iv(); + tinyaes::gcm_encrypt(key, iv, aad, pt, ct, tag); +} + +TEST(gcm_auth_fail_tampered_ciphertext) +{ + std::vector ct, tag; + encrypt_helper(ct, tag); + + // Tamper with ciphertext + ct[0] ^= 0x01; + + std::vector pt; + auto result = tinyaes::gcm_decrypt(make_key(), make_iv(), {}, ct, tag, pt); + ASSERT_TRUE(result == tinyaes::Result::AuthFailed); + ASSERT_TRUE(pt.empty()); +} + +TEST(gcm_auth_fail_tampered_tag) +{ + std::vector ct, tag; + encrypt_helper(ct, tag); + + // Tamper with tag + tag[0] ^= 0x01; + + std::vector pt; + auto result = tinyaes::gcm_decrypt(make_key(), make_iv(), {}, ct, tag, pt); + ASSERT_TRUE(result == tinyaes::Result::AuthFailed); + ASSERT_TRUE(pt.empty()); +} + +TEST(gcm_auth_fail_tampered_aad) +{ + std::vector aad = {0xAA, 0xBB, 0xCC}; + std::vector ct, tag; + encrypt_helper(ct, tag, aad); + + // Tamper with AAD + std::vector bad_aad = {0xAA, 0xBB, 0xCD}; + + std::vector pt; + auto result = tinyaes::gcm_decrypt(make_key(), make_iv(), bad_aad, ct, tag, pt); + ASSERT_TRUE(result == tinyaes::Result::AuthFailed); + ASSERT_TRUE(pt.empty()); +} + +TEST(gcm_auth_fail_wrong_key) +{ + std::vector ct, tag; + encrypt_helper(ct, tag); + + // Use wrong key + std::vector wrong_key(16, 0x43); + + std::vector pt; + auto result = tinyaes::gcm_decrypt(wrong_key, make_iv(), {}, ct, tag, pt); + ASSERT_TRUE(result == tinyaes::Result::AuthFailed); + ASSERT_TRUE(pt.empty()); +} + +TEST(gcm_auth_fail_wrong_iv) +{ + std::vector ct, tag; + encrypt_helper(ct, tag); + + // Use wrong IV + std::vector wrong_iv(12, 0x02); + + std::vector pt; + auto result = tinyaes::gcm_decrypt(make_key(), wrong_iv, {}, ct, tag, pt); + ASSERT_TRUE(result == tinyaes::Result::AuthFailed); + ASSERT_TRUE(pt.empty()); +} diff --git a/tests/test_harness.h b/tests/test_harness.h new file mode 100644 index 0000000..4c3efa1 --- /dev/null +++ b/tests/test_harness.h @@ -0,0 +1,125 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace test +{ + + struct TestCase + { + std::string name; + std::function fn; + }; + + inline int &fail_count() + { + static int n = 0; + return n; + } + inline int &pass_count() + { + static int n = 0; + return n; + } + + inline std::vector &test_registry() + { + static std::vector cases; + return cases; + } + + struct TestRegistrar + { + TestRegistrar(const char *name, std::function fn) + { + test_registry().push_back({name, std::move(fn)}); + } + }; + + inline void + assert_eq_bytes(const std::vector &a, const std::vector &b, const char *file, int line) + { + if (a.size() != b.size() || std::memcmp(a.data(), b.data(), a.size()) != 0) + { + std::fprintf(stderr, "%s:%d: ASSERT_EQ failed\n got: ", file, line); + for (auto x : a) + std::fprintf(stderr, "%02x", x); + std::fprintf(stderr, "\n expected: "); + for (auto x : b) + std::fprintf(stderr, "%02x", x); + std::fprintf(stderr, "\n"); + fail_count()++; + } + else + { + pass_count()++; + } + } + + inline void assert_true(bool cond, const char *expr, const char *file, int line) + { + if (!cond) + { + std::fprintf(stderr, "%s:%d: ASSERT_TRUE(%s) failed\n", file, line, expr); + fail_count()++; + } + else + { + pass_count()++; + } + } + + inline int run_all() + { + for (auto &tc : test_registry()) + { + std::printf(" RUN %s\n", tc.name.c_str()); + tc.fn(); + } + std::printf("\n%d passed, %d failed\n", pass_count(), fail_count()); + return fail_count() > 0 ? 1 : 0; + } + +} // namespace test + +#define TEST(name) \ + static void test_##name(); \ + static test::TestRegistrar reg_##name(#name, test_##name); \ + static void test_##name() + +#define ASSERT_EQ(a, b) test::assert_eq_bytes((a), (b), __FILE__, __LINE__) +#define ASSERT_TRUE(cond) test::assert_true((cond), #cond, __FILE__, __LINE__) + +#define RUN_ALL_TESTS() test::run_all() diff --git a/tests/test_iv_generation.cpp b/tests/test_iv_generation.cpp new file mode 100644 index 0000000..9144d0f --- /dev/null +++ b/tests/test_iv_generation.cpp @@ -0,0 +1,50 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "test_harness.h" +#include "tinyaes/common.h" + +#include + +TEST(iv_generate_12_bytes) +{ + uint8_t iv[12] = {0}; + int result = tinyaes::generate_iv(iv, 12); + ASSERT_TRUE(result == 0); + + // Check that it's not all zeros (extremely unlikely for CSPRNG) + uint8_t zeros[12] = {0}; + ASSERT_TRUE(std::memcmp(iv, zeros, 12) != 0); +} + +TEST(iv_generate_16_bytes) +{ + uint8_t iv[16] = {0}; + int result = tinyaes::generate_iv(iv, 16); + ASSERT_TRUE(result == 0); + + uint8_t zeros[16] = {0}; + ASSERT_TRUE(std::memcmp(iv, zeros, 16) != 0); +} + +TEST(iv_generate_uniqueness) +{ + // Two consecutive IV generations should produce different values + uint8_t iv1[16], iv2[16]; + tinyaes::generate_iv(iv1, 16); + tinyaes::generate_iv(iv2, 16); + ASSERT_TRUE(std::memcmp(iv1, iv2, 16) != 0); +} + +TEST(iv_generate_null_rejected) +{ + int result = tinyaes::generate_iv(nullptr, 16); + ASSERT_TRUE(result == -1); +} + +TEST(iv_generate_zero_len_rejected) +{ + uint8_t iv[1]; + int result = tinyaes::generate_iv(iv, 0); + ASSERT_TRUE(result == -1); +} diff --git a/tests/test_keyschedule.cpp b/tests/test_keyschedule.cpp new file mode 100644 index 0000000..a26ed05 --- /dev/null +++ b/tests/test_keyschedule.cpp @@ -0,0 +1,116 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "test_harness.h" +#include "internal/aes_impl.h" + +#include "vectors/aes_keyschedule_vectors.inl" + +// Test portable key expansion directly (always big-endian uint32_t format) +TEST(keyschedule_portable_aes128) +{ + uint32_t rk[tinyaes::internal::AES_MAX_RK_WORDS] = {0}; + tinyaes::internal::aes_key_expand_portable(ks_128_key, 16, rk); + + // Verify all 44 words in big-endian format + std::vector got, expected; + for (int i = 0; i < 44; ++i) + { + uint8_t buf[4]; + buf[0] = static_cast(rk[i] >> 24); + buf[1] = static_cast(rk[i] >> 16); + buf[2] = static_cast(rk[i] >> 8); + buf[3] = static_cast(rk[i]); + got.insert(got.end(), buf, buf + 4); + + buf[0] = static_cast(ks_128_expected[i] >> 24); + buf[1] = static_cast(ks_128_expected[i] >> 16); + buf[2] = static_cast(ks_128_expected[i] >> 8); + buf[3] = static_cast(ks_128_expected[i]); + expected.insert(expected.end(), buf, buf + 4); + } + ASSERT_EQ(got, expected); +} + +TEST(keyschedule_portable_aes192_first_words) +{ + uint32_t rk[tinyaes::internal::AES_MAX_RK_WORDS] = {0}; + tinyaes::internal::aes_key_expand_portable(ks_192_key, 24, rk); + + for (int i = 0; i < 4; ++i) + { + ASSERT_TRUE(rk[i] == ks_192_expected_first[i]); + } +} + +TEST(keyschedule_portable_aes256_first_words) +{ + uint32_t rk[tinyaes::internal::AES_MAX_RK_WORDS] = {0}; + tinyaes::internal::aes_key_expand_portable(ks_256_key, 32, rk); + + for (int i = 0; i < 8; ++i) + { + ASSERT_TRUE(rk[i] == ks_256_expected_first[i]); + } +} + +// Functional test: dispatched key expand + encrypt + decrypt roundtrip +TEST(keyschedule_dispatch_roundtrip_128) +{ + uint32_t rk[tinyaes::internal::AES_MAX_RK_WORDS] = {0}; + auto key_expand = tinyaes::internal::get_key_expand(); + auto encrypt_block = tinyaes::internal::get_encrypt_block(); + auto decrypt_block = tinyaes::internal::get_decrypt_block(); + + key_expand(ks_128_key, 16, rk); + + uint8_t plain[16] = {0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, + 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, 0x34}; + uint8_t cipher[16], recovered[16]; + encrypt_block(rk, 10, plain, cipher); + decrypt_block(rk, 10, cipher, recovered); + + std::vector p(plain, plain + 16); + std::vector r(recovered, recovered + 16); + ASSERT_EQ(p, r); +} + +TEST(keyschedule_dispatch_roundtrip_192) +{ + uint32_t rk[tinyaes::internal::AES_MAX_RK_WORDS] = {0}; + auto key_expand = tinyaes::internal::get_key_expand(); + auto encrypt_block = tinyaes::internal::get_encrypt_block(); + auto decrypt_block = tinyaes::internal::get_decrypt_block(); + + key_expand(ks_192_key, 24, rk); + + uint8_t plain[16] = {0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, + 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a}; + uint8_t cipher[16], recovered[16]; + encrypt_block(rk, 12, plain, cipher); + decrypt_block(rk, 12, cipher, recovered); + + std::vector p(plain, plain + 16); + std::vector r(recovered, recovered + 16); + ASSERT_EQ(p, r); +} + +TEST(keyschedule_dispatch_roundtrip_256) +{ + uint32_t rk[tinyaes::internal::AES_MAX_RK_WORDS] = {0}; + auto key_expand = tinyaes::internal::get_key_expand(); + auto encrypt_block = tinyaes::internal::get_encrypt_block(); + auto decrypt_block = tinyaes::internal::get_decrypt_block(); + + key_expand(ks_256_key, 32, rk); + + uint8_t plain[16] = {0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, + 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a}; + uint8_t cipher[16], recovered[16]; + encrypt_block(rk, 14, plain, cipher); + decrypt_block(rk, 14, cipher, recovered); + + std::vector p(plain, plain + 16); + std::vector r(recovered, recovered + 16); + ASSERT_EQ(p, r); +} diff --git a/tests/test_main.cpp b/tests/test_main.cpp new file mode 100644 index 0000000..88350f1 --- /dev/null +++ b/tests/test_main.cpp @@ -0,0 +1,32 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "test_harness.h" + +int main() +{ + return RUN_ALL_TESTS(); +} diff --git a/tests/test_padding.cpp b/tests/test_padding.cpp new file mode 100644 index 0000000..a7655d4 --- /dev/null +++ b/tests/test_padding.cpp @@ -0,0 +1,90 @@ +// Copyright (c) 2025-2026, Brandon Lehmann +// BSD 3-Clause License (see LICENSE) + +#include "test_harness.h" +#include "tinyaes/cbc.h" + +TEST(pkcs7_full_block_padding) +{ + // When input is exactly block-aligned, a full padding block (16 bytes of 0x10) is added + std::vector key(16, 0xAA); + std::vector iv(16, 0x00); + std::vector plaintext(16, 0x42); // Exactly one block + std::vector ct, pt; + + auto result = tinyaes::cbc_encrypt_pkcs7(key, iv, plaintext, ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_TRUE(ct.size() == 32); // 16 data + 16 padding + + result = tinyaes::cbc_decrypt_pkcs7(key, iv, ct, pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, plaintext); +} + +TEST(pkcs7_single_byte) +{ + std::vector key(16, 0xBB); + std::vector iv(16, 0x00); + std::vector plaintext = {0xFF}; + std::vector ct, pt; + + auto result = tinyaes::cbc_encrypt_pkcs7(key, iv, plaintext, ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_TRUE(ct.size() == 16); // 1 + 15 padding + + result = tinyaes::cbc_decrypt_pkcs7(key, iv, ct, pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, plaintext); +} + +TEST(pkcs7_empty_plaintext) +{ + std::vector key(16, 0xCC); + std::vector iv(16, 0x00); + std::vector plaintext; // Empty + std::vector ct, pt; + + auto result = tinyaes::cbc_encrypt_pkcs7(key, iv, plaintext, ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_TRUE(ct.size() == 16); // Full block of padding (0x10) + + result = tinyaes::cbc_decrypt_pkcs7(key, iv, ct, pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, plaintext); +} + +TEST(pkcs7_15_byte_plaintext) +{ + std::vector key(16, 0xDD); + std::vector iv(16, 0x00); + std::vector plaintext(15, 0x42); + std::vector ct, pt; + + auto result = tinyaes::cbc_encrypt_pkcs7(key, iv, plaintext, ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_TRUE(ct.size() == 16); // 15 + 1 padding byte + + result = tinyaes::cbc_decrypt_pkcs7(key, iv, ct, pt); + ASSERT_TRUE(result == tinyaes::Result::Success); + ASSERT_EQ(pt, plaintext); +} + +TEST(pkcs7_invalid_padding_rejected) +{ + // Construct ciphertext that decrypts to invalid padding + std::vector key(16, 0xEE); + std::vector iv(16, 0x00); + + // Encrypt known plaintext, then corrupt last byte of ciphertext + std::vector plaintext(16, 0x42); + std::vector ct, pt; + + auto result = tinyaes::cbc_encrypt_pkcs7(key, iv, plaintext, ct); + ASSERT_TRUE(result == tinyaes::Result::Success); + + // Flip a bit in the last block (padding block) + ct.back() ^= 0x01; + + result = tinyaes::cbc_decrypt_pkcs7(key, iv, ct, pt); + ASSERT_TRUE(result == tinyaes::Result::InvalidPadding); +} diff --git a/tests/vectors/aes_cbc_vectors.inl b/tests/vectors/aes_cbc_vectors.inl new file mode 100644 index 0000000..51d6d9b --- /dev/null +++ b/tests/vectors/aes_cbc_vectors.inl @@ -0,0 +1,51 @@ +// NIST SP 800-38A CBC test vectors + +// F.2.1: AES-128 CBC Encrypt +static const uint8_t cbc_128_key[] = { + 0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6,0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c +}; +static const uint8_t cbc_128_iv[] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f +}; +static const uint8_t cbc_128_plain[] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a, + 0xae,0x2d,0x8a,0x57,0x1e,0x03,0xac,0x9c,0x9e,0xb7,0x6f,0xac,0x45,0xaf,0x8e,0x51, + 0x30,0xc8,0x1c,0x46,0xa3,0x5c,0xe4,0x11,0xe5,0xfb,0xc1,0x19,0x1a,0x0a,0x52,0xef, + 0xf6,0x9f,0x24,0x45,0xdf,0x4f,0x9b,0x17,0xad,0x2b,0x41,0x7b,0xe6,0x6c,0x37,0x10 +}; +static const uint8_t cbc_128_cipher[] = { + 0x76,0x49,0xab,0xac,0x81,0x19,0xb2,0x46,0xce,0xe9,0x8e,0x9b,0x12,0xe9,0x19,0x7d, + 0x50,0x86,0xcb,0x9b,0x50,0x72,0x19,0xee,0x95,0xdb,0x11,0x3a,0x91,0x76,0x78,0xb2, + 0x73,0xbe,0xd6,0xb8,0xe3,0xc1,0x74,0x3b,0x71,0x16,0xe6,0x9e,0x22,0x22,0x95,0x16, + 0x3f,0xf1,0xca,0xa1,0x68,0x1f,0xac,0x09,0x12,0x0e,0xca,0x30,0x75,0x86,0xe1,0xa7 +}; + +// F.2.3: AES-192 CBC Encrypt +static const uint8_t cbc_192_key[] = { + 0x8e,0x73,0xb0,0xf7,0xda,0x0e,0x64,0x52,0xc8,0x10,0xf3,0x2b,0x80,0x90,0x79,0xe5, + 0x62,0xf8,0xea,0xd2,0x52,0x2c,0x6b,0x7b +}; +static const uint8_t cbc_192_iv[] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f +}; +static const uint8_t cbc_192_plain[] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a +}; +static const uint8_t cbc_192_cipher[] = { + 0x4f,0x02,0x1d,0xb2,0x43,0xbc,0x63,0x3d,0x71,0x78,0x18,0x3a,0x9f,0xa0,0x71,0xe8 +}; + +// F.2.5: AES-256 CBC Encrypt +static const uint8_t cbc_256_key[] = { + 0x60,0x3d,0xeb,0x10,0x15,0xca,0x71,0xbe,0x2b,0x73,0xae,0xf0,0x85,0x7d,0x77,0x81, + 0x1f,0x35,0x2c,0x07,0x3b,0x61,0x08,0xd7,0x2d,0x98,0x10,0xa3,0x09,0x14,0xdf,0xf4 +}; +static const uint8_t cbc_256_iv[] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f +}; +static const uint8_t cbc_256_plain[] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a +}; +static const uint8_t cbc_256_cipher[] = { + 0xf5,0x8c,0x4c,0x04,0xd6,0xe5,0xf1,0xba,0x77,0x9e,0xab,0xfb,0x5f,0x7b,0xfb,0xd6 +}; diff --git a/tests/vectors/aes_ctr_vectors.inl b/tests/vectors/aes_ctr_vectors.inl new file mode 100644 index 0000000..56ea58d --- /dev/null +++ b/tests/vectors/aes_ctr_vectors.inl @@ -0,0 +1,51 @@ +// NIST SP 800-38A CTR test vectors + +// F.5.1: AES-128 CTR Encrypt +static const uint8_t ctr_128_key[] = { + 0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6,0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c +}; +static const uint8_t ctr_128_iv[] = { + 0xf0,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa,0xfb,0xfc,0xfd,0xfe,0xff +}; +static const uint8_t ctr_128_plain[] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a, + 0xae,0x2d,0x8a,0x57,0x1e,0x03,0xac,0x9c,0x9e,0xb7,0x6f,0xac,0x45,0xaf,0x8e,0x51, + 0x30,0xc8,0x1c,0x46,0xa3,0x5c,0xe4,0x11,0xe5,0xfb,0xc1,0x19,0x1a,0x0a,0x52,0xef, + 0xf6,0x9f,0x24,0x45,0xdf,0x4f,0x9b,0x17,0xad,0x2b,0x41,0x7b,0xe6,0x6c,0x37,0x10 +}; +static const uint8_t ctr_128_cipher[] = { + 0x87,0x4d,0x61,0x91,0xb6,0x20,0xe3,0x26,0x1b,0xef,0x68,0x64,0x99,0x0d,0xb6,0xce, + 0x98,0x06,0xf6,0x6b,0x79,0x70,0xfd,0xff,0x86,0x17,0x18,0x7b,0xb9,0xff,0xfd,0xff, + 0x5a,0xe4,0xdf,0x3e,0xdb,0xd5,0xd3,0x5e,0x5b,0x4f,0x09,0x02,0x0d,0xb0,0x3e,0xab, + 0x1e,0x03,0x1d,0xda,0x2f,0xbe,0x03,0xd1,0x79,0x21,0x70,0xa0,0xf3,0x00,0x9c,0xee +}; + +// F.5.3: AES-192 CTR Encrypt +static const uint8_t ctr_192_key[] = { + 0x8e,0x73,0xb0,0xf7,0xda,0x0e,0x64,0x52,0xc8,0x10,0xf3,0x2b,0x80,0x90,0x79,0xe5, + 0x62,0xf8,0xea,0xd2,0x52,0x2c,0x6b,0x7b +}; +static const uint8_t ctr_192_iv[] = { + 0xf0,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa,0xfb,0xfc,0xfd,0xfe,0xff +}; +static const uint8_t ctr_192_plain[] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a +}; +static const uint8_t ctr_192_cipher[] = { + 0x1a,0xbc,0x93,0x24,0x17,0x52,0x1c,0xa2,0x4f,0x2b,0x04,0x59,0xfe,0x7e,0x6e,0x0b +}; + +// F.5.5: AES-256 CTR Encrypt +static const uint8_t ctr_256_key[] = { + 0x60,0x3d,0xeb,0x10,0x15,0xca,0x71,0xbe,0x2b,0x73,0xae,0xf0,0x85,0x7d,0x77,0x81, + 0x1f,0x35,0x2c,0x07,0x3b,0x61,0x08,0xd7,0x2d,0x98,0x10,0xa3,0x09,0x14,0xdf,0xf4 +}; +static const uint8_t ctr_256_iv[] = { + 0xf0,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa,0xfb,0xfc,0xfd,0xfe,0xff +}; +static const uint8_t ctr_256_plain[] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a +}; +static const uint8_t ctr_256_cipher[] = { + 0x60,0x1e,0xc3,0x13,0x77,0x57,0x89,0xa5,0xb7,0xa7,0xf5,0x04,0xbb,0xf3,0xd2,0x28 +}; diff --git a/tests/vectors/aes_ecb_vectors.inl b/tests/vectors/aes_ecb_vectors.inl new file mode 100644 index 0000000..f9eeebe --- /dev/null +++ b/tests/vectors/aes_ecb_vectors.inl @@ -0,0 +1,58 @@ +// NIST AESAVS / FIPS 197 ECB test vectors + +// FIPS 197 Appendix B: AES-128 ECB +// Key: 2b7e151628aed2a6abf7158809cf4f3c +// Plain: 3243f6a8885a308d313198a2e0370734 +// Cipher: 3925841d02dc09fbdc118597196a0b32 +static const uint8_t ecb_128_key[] = { + 0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6,0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c +}; +static const uint8_t ecb_128_plain[] = { + 0x32,0x43,0xf6,0xa8,0x88,0x5a,0x30,0x8d,0x31,0x31,0x98,0xa2,0xe0,0x37,0x07,0x34 +}; +static const uint8_t ecb_128_cipher[] = { + 0x39,0x25,0x84,0x1d,0x02,0xdc,0x09,0xfb,0xdc,0x11,0x85,0x97,0x19,0x6a,0x0b,0x32 +}; + +// NIST SP 800-38A F.1.3: AES-192 ECB Encrypt +// Key: 8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b +static const uint8_t ecb_192_key[] = { + 0x8e,0x73,0xb0,0xf7,0xda,0x0e,0x64,0x52,0xc8,0x10,0xf3,0x2b,0x80,0x90,0x79,0xe5, + 0x62,0xf8,0xea,0xd2,0x52,0x2c,0x6b,0x7b +}; +static const uint8_t ecb_192_plain[] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a +}; +static const uint8_t ecb_192_cipher[] = { + 0xbd,0x33,0x4f,0x1d,0x6e,0x45,0xf2,0x5f,0xf7,0x12,0xa2,0x14,0x57,0x1f,0xa5,0xcc +}; + +// NIST SP 800-38A F.1.5: AES-256 ECB Encrypt +// Key: 603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4 +static const uint8_t ecb_256_key[] = { + 0x60,0x3d,0xeb,0x10,0x15,0xca,0x71,0xbe,0x2b,0x73,0xae,0xf0,0x85,0x7d,0x77,0x81, + 0x1f,0x35,0x2c,0x07,0x3b,0x61,0x08,0xd7,0x2d,0x98,0x10,0xa3,0x09,0x14,0xdf,0xf4 +}; +static const uint8_t ecb_256_plain[] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a +}; +static const uint8_t ecb_256_cipher[] = { + 0xf3,0xee,0xd1,0xbd,0xb5,0xd2,0xa0,0x3c,0x06,0x4b,0x5a,0x7e,0x3d,0xb1,0x81,0xf8 +}; + +// NIST SP 800-38A F.1.1: AES-128 ECB Multi-block +static const uint8_t ecb_128_multi_plain[] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a, + 0xae,0x2d,0x8a,0x57,0x1e,0x03,0xac,0x9c,0x9e,0xb7,0x6f,0xac,0x45,0xaf,0x8e,0x51, + 0x30,0xc8,0x1c,0x46,0xa3,0x5c,0xe4,0x11,0xe5,0xfb,0xc1,0x19,0x1a,0x0a,0x52,0xef, + 0xf6,0x9f,0x24,0x45,0xdf,0x4f,0x9b,0x17,0xad,0x2b,0x41,0x7b,0xe6,0x6c,0x37,0x10 +}; +static const uint8_t ecb_128_multi_key[] = { + 0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6,0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c +}; +static const uint8_t ecb_128_multi_cipher[] = { + 0x3a,0xd7,0x7b,0xb4,0x0d,0x7a,0x36,0x60,0xa8,0x9e,0xca,0xf3,0x24,0x66,0xef,0x97, + 0xf5,0xd3,0xd5,0x85,0x03,0xb9,0x69,0x9d,0xe7,0x85,0x89,0x5a,0x96,0xfd,0xba,0xaf, + 0x43,0xb1,0xcd,0x7f,0x59,0x8e,0xce,0x23,0x88,0x1b,0x00,0xe3,0xed,0x03,0x06,0x88, + 0x7b,0x0c,0x78,0x5e,0x27,0xe8,0xad,0x3f,0x82,0x23,0x20,0x71,0x04,0x72,0x5d,0xd4 +}; diff --git a/tests/vectors/aes_gcm_vectors.inl b/tests/vectors/aes_gcm_vectors.inl new file mode 100644 index 0000000..001b83e --- /dev/null +++ b/tests/vectors/aes_gcm_vectors.inl @@ -0,0 +1,110 @@ +// NIST SP 800-38D GCM test vectors + +// Test Case 1: AES-128, no plaintext, no AAD +static const uint8_t gcm_tc1_key[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; +static const uint8_t gcm_tc1_iv[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; +// No plaintext, no AAD, no ciphertext +static const uint8_t gcm_tc1_tag[] = { + 0x58,0xe2,0xfc,0xce,0xfa,0x7e,0x30,0x61,0x36,0x7f,0x1d,0x57,0xa4,0xe7,0x45,0x5a +}; + +// Test Case 2: AES-128, 16 bytes plaintext, no AAD +static const uint8_t gcm_tc2_key[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; +static const uint8_t gcm_tc2_iv[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; +static const uint8_t gcm_tc2_plain[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; +static const uint8_t gcm_tc2_cipher[] = { + 0x03,0x88,0xda,0xce,0x60,0xb6,0xa3,0x92,0xf3,0x28,0xc2,0xb9,0x71,0xb2,0xfe,0x78 +}; +static const uint8_t gcm_tc2_tag[] = { + 0xab,0x6e,0x47,0xd4,0x2c,0xec,0x13,0xbd,0xf5,0x3a,0x67,0xb2,0x12,0x57,0xbd,0xdf +}; + +// Test Case 3: AES-128, 64 bytes plaintext, no AAD +static const uint8_t gcm_tc3_key[] = { + 0xfe,0xff,0xe9,0x92,0x86,0x65,0x73,0x1c,0x6d,0x6a,0x8f,0x94,0x67,0x30,0x83,0x08 +}; +static const uint8_t gcm_tc3_iv[] = { + 0xca,0xfe,0xba,0xbe,0xfa,0xce,0xdb,0xad,0xde,0xca,0xf8,0x88 +}; +static const uint8_t gcm_tc3_plain[] = { + 0xd9,0x31,0x32,0x25,0xf8,0x84,0x06,0xe5,0xa5,0x59,0x09,0xc5,0xaf,0xf5,0x26,0x9a, + 0x86,0xa7,0xa9,0x53,0x15,0x34,0xf7,0xda,0x2e,0x4c,0x30,0x3d,0x8a,0x31,0x8a,0x72, + 0x1c,0x3c,0x0c,0x95,0x95,0x68,0x09,0x53,0x2f,0xcf,0x0e,0x24,0x49,0xa6,0xb5,0x25, + 0xb1,0x6a,0xed,0xf5,0xaa,0x0d,0xe6,0x57,0xba,0x63,0x7b,0x39,0x1a,0xaf,0xd2,0x55 +}; +static const uint8_t gcm_tc3_cipher[] = { + 0x42,0x83,0x1e,0xc2,0x21,0x77,0x74,0x24,0x4b,0x72,0x21,0xb7,0x84,0xd0,0xd4,0x9c, + 0xe3,0xaa,0x21,0x2f,0x2c,0x02,0xa4,0xe0,0x35,0xc1,0x7e,0x23,0x29,0xac,0xa1,0x2e, + 0x21,0xd5,0x14,0xb2,0x54,0x66,0x93,0x1c,0x7d,0x8f,0x6a,0x5a,0xac,0x84,0xaa,0x05, + 0x1b,0xa3,0x0b,0x39,0x6a,0x0a,0xac,0x97,0x3d,0x58,0xe0,0x91,0x47,0x3f,0x59,0x85 +}; +static const uint8_t gcm_tc3_tag[] = { + 0x4d,0x5c,0x2a,0xf3,0x27,0xcd,0x64,0xa6,0x2c,0xf3,0x5a,0xbd,0x2b,0xa6,0xfa,0xb4 +}; + +// Test Case 4: AES-128 with AAD (60 bytes plaintext, 20 bytes AAD) +static const uint8_t gcm_tc4_key[] = { + 0xfe,0xff,0xe9,0x92,0x86,0x65,0x73,0x1c,0x6d,0x6a,0x8f,0x94,0x67,0x30,0x83,0x08 +}; +static const uint8_t gcm_tc4_iv[] = { + 0xca,0xfe,0xba,0xbe,0xfa,0xce,0xdb,0xad,0xde,0xca,0xf8,0x88 +}; +static const uint8_t gcm_tc4_aad[] = { + 0xfe,0xed,0xfa,0xce,0xde,0xad,0xbe,0xef,0xfe,0xed,0xfa,0xce,0xde,0xad,0xbe,0xef, + 0xab,0xad,0xda,0xd2 +}; +static const uint8_t gcm_tc4_plain[] = { + 0xd9,0x31,0x32,0x25,0xf8,0x84,0x06,0xe5,0xa5,0x59,0x09,0xc5,0xaf,0xf5,0x26,0x9a, + 0x86,0xa7,0xa9,0x53,0x15,0x34,0xf7,0xda,0x2e,0x4c,0x30,0x3d,0x8a,0x31,0x8a,0x72, + 0x1c,0x3c,0x0c,0x95,0x95,0x68,0x09,0x53,0x2f,0xcf,0x0e,0x24,0x49,0xa6,0xb5,0x25, + 0xb1,0x6a,0xed,0xf5,0xaa,0x0d,0xe6,0x57,0xba,0x63,0x7b,0x39 +}; +static const uint8_t gcm_tc4_cipher[] = { + 0x42,0x83,0x1e,0xc2,0x21,0x77,0x74,0x24,0x4b,0x72,0x21,0xb7,0x84,0xd0,0xd4,0x9c, + 0xe3,0xaa,0x21,0x2f,0x2c,0x02,0xa4,0xe0,0x35,0xc1,0x7e,0x23,0x29,0xac,0xa1,0x2e, + 0x21,0xd5,0x14,0xb2,0x54,0x66,0x93,0x1c,0x7d,0x8f,0x6a,0x5a,0xac,0x84,0xaa,0x05, + 0x1b,0xa3,0x0b,0x39,0x6a,0x0a,0xac,0x97,0x3d,0x58,0xe0,0x91 +}; +static const uint8_t gcm_tc4_tag[] = { + 0x5b,0xc9,0x4f,0xbc,0x32,0x21,0xa5,0xdb,0x94,0xfa,0xe9,0x5a,0xe7,0x12,0x1a,0x47 +}; + +// Test Case 13: AES-256, no plaintext, no AAD +static const uint8_t gcm_tc13_key[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; +static const uint8_t gcm_tc13_iv[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; +static const uint8_t gcm_tc13_tag[] = { + 0x53,0x0f,0x8a,0xfb,0xc7,0x45,0x36,0xb9,0xa9,0x63,0xb4,0xf1,0xc4,0xcb,0x73,0x8b +}; + +// Test Case 14: AES-256, 16 bytes plaintext, no AAD +static const uint8_t gcm_tc14_key[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; +static const uint8_t gcm_tc14_iv[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; +static const uint8_t gcm_tc14_plain[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; +static const uint8_t gcm_tc14_cipher[] = { + 0xce,0xa7,0x40,0x3d,0x4d,0x60,0x6b,0x6e,0x07,0x4e,0xc5,0xd3,0xba,0xf3,0x9d,0x18 +}; +static const uint8_t gcm_tc14_tag[] = { + 0xd0,0xd1,0xc8,0xa7,0x99,0x99,0x6b,0xf0,0x26,0x5b,0x98,0xb5,0xd4,0x8a,0xb9,0x19 +}; diff --git a/tests/vectors/aes_keyschedule_vectors.inl b/tests/vectors/aes_keyschedule_vectors.inl new file mode 100644 index 0000000..4c84348 --- /dev/null +++ b/tests/vectors/aes_keyschedule_vectors.inl @@ -0,0 +1,41 @@ +// FIPS 197 Appendix A: Key Expansion test vectors + +// A.1: AES-128 Key Expansion +static const uint8_t ks_128_key[] = { + 0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6,0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c +}; +// Expected round keys (44 words = 176 bytes) +static const uint32_t ks_128_expected[] = { + 0x2b7e1516, 0x28aed2a6, 0xabf71588, 0x09cf4f3c, + 0xa0fafe17, 0x88542cb1, 0x23a33939, 0x2a6c7605, + 0xf2c295f2, 0x7a96b943, 0x5935807a, 0x7359f67f, + 0x3d80477d, 0x4716fe3e, 0x1e237e44, 0x6d7a883b, + 0xef44a541, 0xa8525b7f, 0xb671253b, 0xdb0bad00, + 0xd4d1c6f8, 0x7c839d87, 0xcaf2b8bc, 0x11f915bc, + 0x6d88a37a, 0x110b3efd, 0xdbf98641, 0xca0093fd, + 0x4e54f70e, 0x5f5fc9f3, 0x84a64fb2, 0x4ea6dc4f, + 0xead27321, 0xb58dbad2, 0x312bf560, 0x7f8d292f, + 0xac7766f3, 0x19fadc21, 0x28d12941, 0x575c006e, + 0xd014f9a8, 0xc9ee2589, 0xe13f0cc8, 0xb6630ca6 +}; + +// A.2: AES-192 Key Expansion +static const uint8_t ks_192_key[] = { + 0x8e,0x73,0xb0,0xf7,0xda,0x0e,0x64,0x52,0xc8,0x10,0xf3,0x2b,0x80,0x90,0x79,0xe5, + 0x62,0xf8,0xea,0xd2,0x52,0x2c,0x6b,0x7b +}; +// First 4 round key words +static const uint32_t ks_192_expected_first[] = { + 0x8e73b0f7, 0xda0e6452, 0xc810f32b, 0x809079e5 +}; + +// A.3: AES-256 Key Expansion +static const uint8_t ks_256_key[] = { + 0x60,0x3d,0xeb,0x10,0x15,0xca,0x71,0xbe,0x2b,0x73,0xae,0xf0,0x85,0x7d,0x77,0x81, + 0x1f,0x35,0x2c,0x07,0x3b,0x61,0x08,0xd7,0x2d,0x98,0x10,0xa3,0x09,0x14,0xdf,0xf4 +}; +// First 8 round key words +static const uint32_t ks_256_expected_first[] = { + 0x603deb10, 0x15ca71be, 0x2b73aef0, 0x857d7781, + 0x1f352c07, 0x3b6108d7, 0x2d9810a3, 0x0914dff4 +};