diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..366bd8a --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,81 @@ +name: Android + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + BUILD_TYPE: Release + +jobs: + build: + runs-on: macos-latest + timeout-minutes: 30 + strategy: + matrix: + abi: [x86_64, x86, arm64-v8a, armeabi-v7a] + + steps: + - uses: actions/checkout@v3 + with: + submodules: true + + - name: Build ${{matrix.abi}} + run: | + sh build-android.sh ${{matrix.abi}} -DBUILD_TESTSUITE=OFF -DCMAKE_INSTALL_PREFIX:PATH="$HOME/android/${{matrix.abi}}" + cmake --install build-${{matrix.abi}} + - name: Archive artifacts + uses: actions/upload-artifact@v3 + with: + name: android + path: ~/android/ + + test: + timeout-minutes: 30 + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + abi: [x86_64] + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v3 + with: + submodules: true + + - name: AVD cache + uses: actions/cache@v3 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: hostos-${{matrix.os}}-api-21-abi-${{matrix.abi}} + - name: Create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + emulator-options: -no-window -gpu swiftshader_indirect -no-audio -no-boot-anim -camera-back none + api-level: 21 + arch: ${{matrix.abi}} + ndk: 25.2.9519653 + force-avd-creation: false + disable-animations: false + script: echo "Generated AVD snapshot for caching." + - name: Run tests + uses: reactivecircus/android-emulator-runner@v2 + with: + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -no-audio -no-boot-anim -camera-back none + api-level: 21 + arch: ${{matrix.abi}} + ndk: 25.2.9519653 + force-avd-creation: false + disable-animations: true + script: sh build-android.sh ${{matrix.abi}} -DCMAKE_INSTALL_PREFIX:PATH="$HOME/android/${{matrix.abi}}" + - name: Archive artifacts + uses: actions/upload-artifact@v3 + with: + name: android-testsuite-${{matrix.os}}-${{matrix.abi}} + path: build-${{matrix.abi}}/testsuite/log.txt diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml new file mode 100644 index 0000000..184f5c0 --- /dev/null +++ b/.github/workflows/cmake.yml @@ -0,0 +1,31 @@ +name: CMake + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + BUILD_TYPE: Release + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v3 + with: + submodules: true + + - name: Configure CMake + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + + - name: Build + run: cmake --build build --config ${{env.BUILD_TYPE}} + + - name: Test + run: ctest --test-dir build -C ${{env.BUILD_TYPE}} -V diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11f6d31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/out/ +/.vs/ +/.vscode/ +/build/ +/build-*/ +/Testing/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3a9a4aa --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lighter"] + path = lighter + url = git@github.com:maroontress/lighter.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8aeea7a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.24) + +project("mimicssl-md5" VERSION 1.0.0) + +add_compile_options("$<$:/utf-8>") + +add_subdirectory(libmimicssl-md5) +add_subdirectory(mimicssl-md5-cli) + +option(BUILD_TESTSUITE "Build testsuite" ON) +if (${BUILD_TESTSUITE}) + include(CTest) + add_subdirectory(testsuite) +endif() diff --git a/COPYRIGHT.md b/COPYRIGHT.md new file mode 100644 index 0000000..1800b89 --- /dev/null +++ b/COPYRIGHT.md @@ -0,0 +1,23 @@ +Copyright (c) 2023 Maroontress Fast Software. All rights reserved. + +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. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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/README.md b/README.md new file mode 100644 index 0000000..e89d27f --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# MimicSSL-MD5 + +This is an [MD5][wikipedia::md5] implementation in C23, and its API is +compatible with [OpenSSL 1.1][openssl::md5_init]. See the +[RFC 1321][ietf::rfc1321] for the MD5 specifications. + +Note that the current implementation works only on little-endian platforms. + +## Example + +An example usage would be as follows: + +```c +⋮ +MD5_CTX ctx; +uint8_t md[16]; +char buffer[1024]; + +MD5_Init(&ctx); +for (;;) { + size_t size = fread(buffer, 1, sizeof(buffer), file); + if (size == 0) { + break; + } + MD5_Update(&ctx, buffer, size); +} +MD5_Final(md, &ctx); +⋮ +``` + +## Build + +This repository uses [lighter][maroontress::lighter] for testing as a sub-module +of Git. Therefore, clone it as follows: + +```plaintext +git clone --recursive URL +``` + +Then build the library as follows: + +```textplain +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release +ctest --test-dir build -C Release +``` + +## Build for Android + +Set environment variables `ANDROID_HOME` and `ANDROID_NDK` appropriately. For +example: + +```sh +export ANDROID_HOME=/usr/local/lib/android/sdk +export ANDROID_NDK=$ANDROID_HOME/ndk/25.2.9519653 +``` + +Note that the value of `ANDROID_HOME` will vary depending on your environment, +but a typical configuration would be as follows: + +- Windows: `C:\\Users\\USERNAME\\AppData\\Local\\Android\\sdk` +- Linux: `/home/USERNAME/Android/Sdk` +- macOS: `/Users/USERNAME/Library/Android/sdk` + +Then build as follows: + +``` +sh build-android.sh ABI -DBUILD_TESTSUITE=OFF +``` + +`ABI` should be replaced by `arm64-v8a`, `armeabi-v7a`, `x86`, or `x86_64`. + +[wikipedia::md5]: https://en.wikipedia.org/wiki/MD5 +[ietf::rfc1321]: https://www.ietf.org/rfc/rfc1321.txt +[openssl::md5_init]: https://www.openssl.org/docs/man1.1.1/man3/MD5_Init.html +[maroontress::lighter]: https://github.com/maroontress/lighter diff --git a/build-android.sh b/build-android.sh new file mode 100644 index 0000000..19ad673 --- /dev/null +++ b/build-android.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +if [ "$#" = 0 ] ; then + echo usage: $0 ABI [CMAKE_OPTIONS...] + exit 1 +fi + +# Environment variables: +# ANDROID_NDK (e.g., "$HOME/Library/Android/sdk/ndk/25.2.9519653") +# ANDROID_HOME (e.g., "$HOME/Library/Android/sdk") + +# ABI: +# "x86" +# "x86_64" +# "arm64-v8a" +# "armeabi-v7a" + +# Options: +# -DCMAKE_INSTALL_PREFIX:PATH="$HOME/android/$ABI" +# -DBUILD_TESTSUITE=OFF + +ABI=$1 +if [ "$ABI" = "armeabi-v7a" ] ; then + EXTRA_ARGS='-DCMAKE_ANDROID_ARM_NEON=ON' +fi +shift +rm -rf "build-$ABI" || exit 1 +cmake -S . -B "build-$ABI" -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_SYSTEM_NAME=Android \ + -DCMAKE_CROSSCOMPILING_EMULATOR="$PWD/emulator-android.sh" \ + -DCMAKE_SYSTEM_VERSION="21" \ + -DCMAKE_ANDROID_ARCH_ABI="$ABI" \ + -DCMAKE_ANDROID_NDK="$ANDROID_NDK" \ + $EXTRA_ARGS "$@" || exit 1 +cmake --build "build-$ABI" --config Release -v || exit 1 +if test -f "build-$ABI/testsuite/testsuite" ; then + sh emulator-android.sh --adb-push "build-$ABI/testsuite" + ctest --test-dir "build-$ABI" -C Release -V || exit 1 + sh emulator-android.sh --adb-pull "build-$ABI/testsuite" +fi diff --git a/emulator-android.sh b/emulator-android.sh new file mode 100755 index 0000000..dc63d74 --- /dev/null +++ b/emulator-android.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +# Usage: +# emulator-android.sh --adb-push DIR +# emulator-android.sh --adb-pull DIR +# emulator-android.sh TESTCASE [ARGS...] + +ADB="$ANDROID_HOME/platform-tools/adb" +REMOTE_DIR="/data/local/tmp" +LOG="log.txt" + +setupLog() { + echo "---" >> $LOG + echo args: "$@" >> $LOG + echo pwd: $PWD >> $LOG + echo log: >> $LOG +} + +if [ "$1" = "--adb-push" ] ; then + cd "$2" + setupLog "$@" + echo "--adb-push: do nothing" >> $LOG + exit +fi +if [ "$1" = "--adb-pull" ] ; then + cd "$2" + setupLog "$@" + echo "--adb-pull: do nothing" >> $LOG + exit +fi + +setupLog "$@" +name="$1" +if [ "${name##*/}" != "testsuite" ] ; then + echo unexpected testcase >> $LOG + exit 1 +fi +shift +if [ "$1" = "--gtest_list_tests" ] ; then + echo devices: >> $LOG + $ADB devices >> $LOG + $ADB push testsuite $REMOTE_DIR/testsuite >> $LOG + $ADB shell chmod +x $REMOTE_DIR/testsuite >> $LOG +fi + +n="'" +args=$n$1$n +shift +for i in "$@" ; do + args="$args $n$i$n" +done +echo $ADB shell "cd $REMOTE_DIR && ./testsuite $args" >> $LOG +$ADB shell "cd $REMOTE_DIR && ./testsuite $args" diff --git a/libmimicssl-md5/CMakeLists.txt b/libmimicssl-md5/CMakeLists.txt new file mode 100644 index 0000000..a6ff9aa --- /dev/null +++ b/libmimicssl-md5/CMakeLists.txt @@ -0,0 +1,40 @@ +set(CMAKE_C_STANDARD 23) + +if("${CMAKE_C_COMPILER_ID}" STREQUAL "Clang" + OR "${CMAKE_C_COMPILER_ID}" STREQUAL "AppleClang" + OR "${CMAKE_C_COMPILER_ID}" STREQUAL "GNU") + add_compile_options(-Wall -Wextra -Wpedantic) + set(CMAKE_C_FLAGS_DEBUG "-O0 -g") + set(CMAKE_C_FLAGS_RELEASE "-O3") +elseif("${CMAKE_C_COMPILER_ID}" STREQUAL "MSVC") + add_compile_options(/W4 /WX) +endif() + +include(GenerateExportHeader) + +add_library(mimicssl-md5 STATIC) +generate_export_header(mimicssl-md5 + BASE_NAME MD5 + EXPORT_FILE_NAME ${PROJECT_BINARY_DIR}/md5_export.h) +target_sources(mimicssl-md5 PRIVATE + src/md5.c) +target_include_directories(mimicssl-md5 PUBLIC + include + ${PROJECT_BINARY_DIR}) + +add_library(mimicssl-md5-shared SHARED) +target_sources(mimicssl-md5-shared PRIVATE + src/md5.c) +target_include_directories(mimicssl-md5-shared PUBLIC + include + ${PROJECT_BINARY_DIR}) +set_target_properties(mimicssl-md5-shared + PROPERTIES OUTPUT_NAME mimicssl-md5) + +include(GNUInstallDirs) +install(TARGETS mimicssl-md5 DESTINATION lib) +install(TARGETS mimicssl-md5-shared DESTINATION lib) +install(FILES + include/md5.h + ${PROJECT_BINARY_DIR}/md5_export.h + DESTINATION include/mimicssl) diff --git a/libmimicssl-md5/include/md5.h b/libmimicssl-md5/include/md5.h new file mode 100644 index 0000000..315ca21 --- /dev/null +++ b/libmimicssl-md5/include/md5.h @@ -0,0 +1,37 @@ +#ifndef md5_H +#define md5_H + +#include +#include + +#include "md5_export.h" + +#define MD5_LONG uint32_t +#define MD5_CBLOCK 64 +#define MD5_LBLOCK (MD5_CBLOCK / 4) +#define MD5_DIGEST_LENGTH 16 + +struct MD5_Hash { + uint32_t data[4]; +}; + +typedef struct { + struct MD5_Hash hash; + uint8_t input[MD5_CBLOCK]; + uint64_t size; + uint8_t unused[4]; +} MD5_CTX; + +#if defined(__cplusplus) +extern "C" { +#endif + +int MD5_EXPORT MD5_Init(MD5_CTX *c); +int MD5_EXPORT MD5_Update(MD5_CTX *c, const void *data, size_t len); +int MD5_EXPORT MD5_Final(unsigned char *md, MD5_CTX *c); + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/libmimicssl-md5/src/libext1.h b/libmimicssl-md5/src/libext1.h new file mode 100644 index 0000000..ea27758 --- /dev/null +++ b/libmimicssl-md5/src/libext1.h @@ -0,0 +1,20 @@ +#ifndef libext1_H +#define libext1_H + +#include + +#if defined(__STDC_LIB_EXT1__) || defined(_WIN32) +#include +static void * +MEMCPY(void *x, const void *y, size_t z) +{ + if (memcpy_s(x, z, y, z) != 0) { + abort(); + } + return x; +} +#else +#define MEMCPY(x, y, z) memcpy(x, y, z) +#endif + +#endif diff --git a/libmimicssl-md5/src/md5.c b/libmimicssl-md5/src/md5.c new file mode 100644 index 0000000..cadd871 --- /dev/null +++ b/libmimicssl-md5/src/md5.c @@ -0,0 +1,234 @@ +#if defined(__STDC_LIB_EXT1__) && (__STDC_LIB_EXT1__ >= 201112L) +#define __STDC_WANT_LIB_EXT1__ 1 +#endif + +#include +#include + +#include "md5.h" + +#include "libext1.h" + +struct Unit { + uint32_t data[16]; +}; + +struct X { + uint32_t k; + uint32_t r; +}; + +static const struct MD5_Hash ABCD = { + .data = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476}}; + +static const uint32_t K_ROUND_1[] = { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821}; + +static const struct X X_ROUND_2[] = { + {0xf61e2562, 1}, {0xc040b340, 6}, {0x265e5a51, 11}, {0xe9b6c7aa, 0}, + {0xd62f105d, 5}, {0x2441453, 10}, {0xd8a1e681, 15}, {0xe7d3fbc8, 4}, + {0x21e1cde6, 9}, {0xc33707d6, 14}, {0xf4d50d87, 3}, {0x455a14ed, 8}, + {0xa9e3e905, 13}, {0xfcefa3f8, 2}, {0x676f02d9, 7}, {0x8d2a4c8a, 12}}; +static const struct X X_ROUND_3[] = { + {0xfffa3942, 5}, {0x8771f681, 8}, {0x6d9d6122, 11}, {0xfde5380c, 14}, + {0xa4beea44, 1}, {0x4bdecfa9, 4}, {0xf6bb4b60, 7}, {0xbebfbc70, 10}, + {0x289b7ec6, 13}, {0xeaa127fa, 0}, {0xd4ef3085, 3}, {0x4881d05, 6}, + {0xd9d4d039, 9}, {0xe6db99e5, 12}, {0x1fa27cf8, 15}, {0xc4ac5665, 2}}; +static const struct X X_ROUND_4[] = { + {0xf4292244, 0}, {0x432aff97, 7}, {0xab9423a7, 14}, {0xfc93a039, 5}, + {0x655b59c3, 12}, {0x8f0ccc92, 3}, {0xffeff47d, 10}, {0x85845dd1, 1}, + {0x6fa87e4f, 8}, {0xfe2ce6e0, 15}, {0xa3014314, 6}, {0x4e0811a1, 13}, + {0xf7537e82, 4}, {0xbd3af235, 11}, {0x2ad7d2bb, 2}, {0xeb86d391, 9}}; + +static const uint32_t S_ROUND_1[] = {7, 12, 17, 22}; +static const uint32_t S_ROUND_2[] = {5, 9, 14, 20}; +static const uint32_t S_ROUND_3[] = {4, 11, 16, 23}; +static const uint32_t S_ROUND_4[] = {6, 10, 15, 21}; + +static uint32_t f(uint32_t x, uint32_t y, uint32_t z) +{ + return ((x & y) | (~x & z)); +} + +static uint32_t g(uint32_t x, uint32_t y, uint32_t z) +{ + return ((x & z) | (y & ~z)); +} + +static uint32_t h(uint32_t x, uint32_t y, uint32_t z) +{ + return (x ^ y ^ z); +} + +static uint32_t i(uint32_t x, uint32_t y, uint32_t z) +{ + return (y ^ (x | ~z)); +} + +static uint32_t rotate(uint32_t x, uint32_t s) +{ + return (x << s) | (x >> (32 - s)); +} + +static void roundUnit(struct MD5_Hash *hash, const struct Unit *unit) +{ + const uint32_t *const m = unit->data; + uint32_t *const hashData = hash->data; + uint32_t a0 = hashData[0]; + uint32_t b0 = hashData[1]; + uint32_t c0 = hashData[2]; + uint32_t d0 = hashData[3]; + + do { + const uint32_t *const s = S_ROUND_1; + const uint32_t *k = K_ROUND_1; + const uint32_t *z = m; + for (uint32_t j = 0; j < 4; ++j) { + a0 = rotate(a0 + f(b0, c0, d0) + z[0] + k[0], s[0]) + b0; + d0 = rotate(d0 + f(a0, b0, c0) + z[1] + k[1], s[1]) + a0; + c0 = rotate(c0 + f(d0, a0, b0) + z[2] + k[2], s[2]) + d0; + b0 = rotate(b0 + f(c0, d0, a0) + z[3] + k[3], s[3]) + c0; + z += 4; + k += 4; + } + } while (0); + do { + const uint32_t *const s = S_ROUND_2; + const struct X *x = X_ROUND_2; + for (uint32_t j = 0; j < 4; ++j) { + a0 = rotate(a0 + g(b0, c0, d0) + m[x[0].r] + x[0].k, s[0]) + b0; + d0 = rotate(d0 + g(a0, b0, c0) + m[x[1].r] + x[1].k, s[1]) + a0; + c0 = rotate(c0 + g(d0, a0, b0) + m[x[2].r] + x[2].k, s[2]) + d0; + b0 = rotate(b0 + g(c0, d0, a0) + m[x[3].r] + x[3].k, s[3]) + c0; + x += 4; + } + } while (0); + do { + const uint32_t *const s = S_ROUND_3; + const struct X *x = X_ROUND_3; + for (uint32_t j = 0; j < 4; ++j) { + a0 = rotate(a0 + h(b0, c0, d0) + m[x[0].r] + x[0].k, s[0]) + b0; + d0 = rotate(d0 + h(a0, b0, c0) + m[x[1].r] + x[1].k, s[1]) + a0; + c0 = rotate(c0 + h(d0, a0, b0) + m[x[2].r] + x[2].k, s[2]) + d0; + b0 = rotate(b0 + h(c0, d0, a0) + m[x[3].r] + x[3].k, s[3]) + c0; + x += 4; + } + } while (0); + do { + const uint32_t *const s = S_ROUND_4; + const struct X *x = X_ROUND_4; + for (uint32_t j = 0; j < 4; ++j) { + a0 = rotate(a0 + i(b0, c0, d0) + m[x[0].r] + x[0].k, s[0]) + b0; + d0 = rotate(d0 + i(a0, b0, c0) + m[x[1].r] + x[1].k, s[1]) + a0; + c0 = rotate(c0 + i(d0, a0, b0) + m[x[2].r] + x[2].k, s[2]) + d0; + b0 = rotate(b0 + i(c0, d0, a0) + m[x[3].r] + x[3].k, s[3]) + c0; + x += 4; + } + } while (0); + hashData[0] += a0; + hashData[1] += b0; + hashData[2] += c0; + hashData[3] += d0; +} + +static uint32_t toLE32(const uint8_t *m) +{ + return (uint32_t)m[0] | (uint32_t)m[1] << 8 | (uint32_t)m[2] << 16 + | (uint32_t)m[3] << 24; +} + +int MD5_Init(MD5_CTX *c) +{ + c->size = 0; + c->hash = ABCD; + return 1; +} + +int MD5_Update(MD5_CTX *c, const void *data, size_t len) +{ + struct Unit unit; + uint32_t *unitData = unit.data; + const uint8_t *source = data; + + size_t remaining = c->size % 64; + if (remaining > 0) { + size_t size = 64 - remaining; + if (len < size) { + MEMCPY(c->input + remaining, data, len); + c->size += len; + return 0; + } + MEMCPY(c->input + remaining, data, size); + const uint8_t *m = c->input; + for (uint32_t j = 0; j < 16; ++j) { + unitData[j] = toLE32(m); + m += 4; + } + roundUnit(&c->hash, &unit); + c->size += size; + len -= size; + source += size; + } + + while (len >= 64) { + for (uint32_t j = 0; j < 16; ++j) { + unitData[j] = toLE32(source); + source += 4; + } + roundUnit(&c->hash, &unit); + c->size += 64; + len -= 64; + } + if (len > 0) { + MEMCPY(c->input, source, len); + c->size += len; + } + return 1; +} + +int MD5_Final(unsigned char *md, MD5_CTX *c) +{ + uint64_t finalSize = c->size; + do { + uint8_t padding[64]; + uint32_t remainder = finalSize % 64; + uint32_t length = (remainder < 56) + ? 56 - remainder + : (56 + 64) - remainder; + padding[0] = 0x80; + for (uint32_t k = 1; k < length; ++k) { + padding[k] = 0; + } + MD5_Update(c, padding, length); + } while (0); + do { + uint32_t lowerSize = (uint32_t)(finalSize << 3); + uint32_t upperSize = (uint32_t)(finalSize >> 29); + const uint32_t sizeArray[] = {lowerSize, upperSize}; + uint8_t sizeData[8]; + uint8_t *d = sizeData; + for (uint32_t k = 0; k < 2; ++k) { + uint32_t size = sizeArray[k]; + d[0] = (uint8_t)size; + d[1] = (uint8_t)(size >> 8); + d[2] = (uint8_t)(size >> 16); + d[3] = (uint8_t)(size >> 24); + d += 4; + } + MD5_Update(c, sizeData, sizeof(sizeData)); + } while (0); + + const uint32_t *hashData = c->hash.data; + for (uint32_t k = 0; k < 4; ++k) { + uint32_t h = hashData[k]; + md[0] = (uint8_t)h; + md[1] = (uint8_t)(h >> 8); + md[2] = (uint8_t)(h >> 16); + md[3] = (uint8_t)(h >> 24); + md += 4; + } + return 1; +} diff --git a/lighter b/lighter new file mode 160000 index 0000000..f20f196 --- /dev/null +++ b/lighter @@ -0,0 +1 @@ +Subproject commit f20f19649dfdadadcb004b91dcc073efece13d0d diff --git a/mimicssl-md5-cli/CMakeLists.txt b/mimicssl-md5-cli/CMakeLists.txt new file mode 100644 index 0000000..d052314 --- /dev/null +++ b/mimicssl-md5-cli/CMakeLists.txt @@ -0,0 +1,7 @@ +set(CMAKE_C_STANDARD 23) + +add_executable(mimicssl-md5-cli main.c) + +target_include_directories(mimicssl-md5-cli PRIVATE mimicssl-md5) + +target_link_libraries(mimicssl-md5-cli mimicssl-md5) diff --git a/mimicssl-md5-cli/main.c b/mimicssl-md5-cli/main.c new file mode 100644 index 0000000..a4c7be9 --- /dev/null +++ b/mimicssl-md5-cli/main.c @@ -0,0 +1,44 @@ +#include +#include +#include + +#include "md5.h" + +static void +printHash(const uint8_t *p) +{ + for (unsigned int i = 0; i < 16; ++i) { + printf("%02x", p[i]); + } + printf("\n"); +} + +int +main(int ac, char **av) +{ + if (ac < 2) { + fprintf(stderr, "usage: %s FILE\n", av[0]); + exit(1); + } + FILE *file = fopen(av[1], "rb"); + if (file == NULL) { + fprintf(stderr, "file not found\n"); + exit(1); + } + MD5_CTX ctx; + uint8_t md[16]; + char buffer[1024]; + + MD5_Init(&ctx); + for (;;) { + size_t size = fread(buffer, 1, sizeof(buffer), file); + if (size == 0) { + break; + } + MD5_Update(&ctx, buffer, size); + } + MD5_Final(md, &ctx); + + printHash(md); + return 0; +} diff --git a/testsuite/CMakeLists.txt b/testsuite/CMakeLists.txt new file mode 100644 index 0000000..671f53a --- /dev/null +++ b/testsuite/CMakeLists.txt @@ -0,0 +1,14 @@ +set(CMAKE_CXX_STANDARD 23) + +enable_testing() + +add_executable(testsuite + main.cxx expect.hxx expect_fallback.hxx) + +target_include_directories(testsuite PRIVATE + mimicssl-md5 ${CMAKE_SOURCE_DIR}/lighter/include) + +target_link_libraries(testsuite mimicssl-md5) + +include(GoogleTest) +gtest_discover_tests(testsuite) diff --git a/testsuite/expect.hxx b/testsuite/expect.hxx new file mode 100644 index 0000000..35851b3 --- /dev/null +++ b/testsuite/expect.hxx @@ -0,0 +1,24 @@ +#ifndef expect_HXX +#define expect_HXX + +#include + +#if __has_include() +#include + +#include "maroontress/lighter/Flint.hxx" + +#define expect(x) maroontress::lighter::Flint {(x), #x, [](auto r) { \ + const auto& w = std::source_location::current(); \ + std::ostringstream out; \ + out \ + << w.file_name() << ":" << w.line() << ": error:" << std::endl \ + << " Expected: " << r.getExpected() << std::endl \ + << " Actual: " << r.getActual() << std::endl; \ + throw std::runtime_error(out.str()); \ + }} +#else +#include "expect_fallback.hxx" +#endif + +#endif diff --git a/testsuite/expect_fallback.hxx b/testsuite/expect_fallback.hxx new file mode 100644 index 0000000..19c0b6c --- /dev/null +++ b/testsuite/expect_fallback.hxx @@ -0,0 +1,16 @@ +#ifndef expect_fallback_HXX +#define expect_fallback_HXX + +#include "maroontress/lighter/Flint.hxx" + +#define expect(x) maroontress::lighter::Flint {(x), #x, [](auto r) { \ + auto fileName = __FILE__; \ + auto line = __LINE__; \ + std::ostringstream out; \ + out << fileName << ":" << line << ": error:" << std::endl \ + << " Expected: " << r.getExpected() << std::endl \ + << " Actual: " << r.getActual() << std::endl; \ + throw std::runtime_error(out.str()); \ + }} + +#endif diff --git a/testsuite/main.cxx b/testsuite/main.cxx new file mode 100644 index 0000000..e4b71c2 --- /dev/null +++ b/testsuite/main.cxx @@ -0,0 +1,213 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "md5.h" + +#include "expect.hxx" + +class Driver final { +private: + std::map> map; + bool list = false; + std::optional name; + +public: + Driver(const char* const* args) { + for (auto* a = args; *a != nullptr; ++a) { + if (std::strcmp(*a, "--gtest_list_tests") == 0) { + list = true; + return; + } + } + for (auto* a = args; *a != nullptr; ++a) { + auto o = std::string {*a}; + auto prefix = std::string {"--gtest_filter=main."}; + if (o.starts_with(prefix)) { + name = std::make_optional(o.substr(prefix.length())); + return; + } + } + } + + void add(const std::string& name, const std::function& testcase) { + map[name] = testcase; + } + + int run() const { + if (list) { + std::cout << "main.\n"; + for (auto i = map.cbegin(); i != map.cend(); ++i) { + auto [name, testcase] = *i; + std::cout << " " << name << "\n"; + } + return 0; + } + if (name.has_value()) { + std::string n = name.value(); + auto testcase = map.at(n); + try { + testcase(); + std::cout << n << ": succeeded\n"; + } catch (std::runtime_error& e) { + std::cout << n << ": failed\n"; + std::cout << " " << e.what(); + return 1; + } + return 0; + } + auto count = 0; + for (auto i = map.cbegin(); i != map.cend(); ++i) { + auto [name, testcase] = *i; + try { + testcase(); + std::cout << name << ": succeeded\n"; + } catch (std::runtime_error& e) { + std::cout << name << ": failed\n"; + std::cout << " " << e.what(); + ++count; + } + } + if (count == 0) { + std::cout << "all tests passed\n"; + return 0; + } + std::cout << count << " test(s) failed.\n"; + return 1; + } +}; + +namespace maroontress::lighter { + template <> + std::string toString(std::uint8_t b) { + auto v = (uint32_t)b; + std::ostringstream out; + out << std::dec << v << " (0x" << std::hex << v << ")"; + return out.str(); + } +} + +static auto +toArray(const std::string& m) -> std::array +{ + std::uint8_t array[16]; + + if (m.length() != 32) { + throw std::runtime_error("invalid length"); + } + for (auto k = 0; k < 16; ++k) { + auto p = m.substr(k * 2, 2); + array[k] = (std::uint8_t)std::stoull(p, nullptr, 16); + } + return std::to_array(array); +} + +static auto +withString(const std::string& m, const std::array& expected) + -> void +{ + MD5_CTX ctx; + uint8_t actual[16]; + + MD5_Init(&ctx); + MD5_Update(&ctx, m.c_str(), m.length()); + MD5_Final(actual, &ctx); + for (auto k = 0; k < 16; ++k) { + expect(actual[k]) == expected[k]; + } +} + +int +main(int ac, char** av) { + auto driver = Driver {av}; + driver.add("endian", [] { + expect(std::endian::native) == std::endian::little; + }); + driver.add("empty", [] { + MD5_CTX ctx; + uint8_t actual[16]; + + MD5_Init(&ctx); + MD5_Final(actual, &ctx); + + // MD5 ("") = d41d8cd98f00b204e9800998ecf8427e + auto expected = toArray("d41d8cd98f00b204e9800998ecf8427e"); + for (auto k = 0; k < 16; ++k) { + expect(actual[k]) == expected[k]; + } + }); + driver.add("a", [] { + // MD5 ("a") = 0cc175b9c0f1b6a831c399e269772661 + auto expected = toArray("0cc175b9c0f1b6a831c399e269772661"); + withString("a", expected); + }); + driver.add("abc", [] { + // MD5 ("abc") = 900150983cd24fb0d6963f7d28e17f72 + auto expected = toArray("900150983cd24fb0d6963f7d28e17f72"); + withString("abc", expected); + }); + driver.add("message digest", [] { + // MD5 ("message digest") = f96b697d7cb7938d525a2f31aaf161d0 + auto expected = toArray("f96b697d7cb7938d525a2f31aaf161d0"); + withString("message digest", expected); + }); + driver.add("a-z", [] { + // MD5 ("abcdefghijklmnopqrstuvwxyz") + // = c3fcd3d76192e4007dfb496cca67e13b + auto expected = toArray("c3fcd3d76192e4007dfb496cca67e13b"); + withString("abcdefghijklmnopqrstuvwxyz", expected); + }); + driver.add("A-Za-z0-9", [] { + // MD5 ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz + // 0123456789") = d174ab98d277d9f5a5611c2c9f419d9f + auto expected = toArray("d174ab98d277d9f5a5611c2c9f419d9f"); + withString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789", expected); + }); + driver.add("1-90-9...0-90", [] { + // MD5 ("12345678901234567890123456789012345678901234567890123456789 + // 012345678901234567890") = 57edf4a22be3c955ac49da2e2107b67a + auto expected = toArray("57edf4a22be3c955ac49da2e2107b67a"); + withString("1234567890123456789012345678901234567890123456789" + "0123456789012345678901234567890", expected); + }); + + driver.add("process one byte at a time", [] { + MD5_CTX ctx; + uint8_t actual[16]; + uint8_t expected[16]; + + MD5_Init(&ctx); + std::string m { + "Alice was beginning to get very tired of sitting by her sister " + "on the bank, and of having nothing to do: once or twice she had " + "peeped into the book her sister was reading, but it had no " + "pictures or conversations in it, and where is the use of a " + "book, thought Alice, without pictures or conversations? So she " + "was considering in her own mind, (as well as she could, for the " + "hot day made her feel very sleepy and stupid,) whether the " + "pleasure of making a daisy-chain would be worth the trouble of " + "getting up and picking the daisies, when suddenly a white " + "rabbit with pink eyes ran close by her."}; + std::size_t length = m.length(); + const char* top = m.c_str(); + for (auto k = 0; k < length; ++k) { + MD5_Update(&ctx, top + k, 1); + } + MD5_Final(actual, &ctx); + + MD5_Init(&ctx); + MD5_Update(&ctx, m.c_str(), length); + MD5_Final(expected, &ctx); + + for (auto k = 0; k < 16; ++k) { + expect(actual[k]) == expected[k]; + } + }); + return driver.run(); +}