How the HMAC Encryption Algorithm Works and How to Implement Variants on Android

Copyright belongs to the author. If reposting, please indicate the source of the article: https://cyrus-studio.github.io/blog/

HMAC

HMAC (Hash-based Message Authentication Code) is a message authentication code based on a hash function, used to verify the integrity and authenticity of data.

HMAC mainly relies on the following elements:

  1. 1. Message: The data that needs to be verified.
  2. 2. Key: A shared secret key used to protect the message.
  3. 3. Hash Function: Such as MD5, SHA-256, etc.

The calculation formula for HMAC is as follows:

How the HMAC Encryption Algorithm Works and How to Implement Variants on Android
word/media/image1.png

Where:

  • • H is the hash function.
  • • K is the key.
  • • M is the message.
  • • opad is the outer padding (0x5c).
  • • ipad is the inner padding (0x36).
  • • ∥ denotes concatenation.
  • • ⊕ denotes bitwise XOR.

Thus, the length of the string returned by HMAC depends on the specific hash function.

For example, if the hash function is MD5, the returned string is 16 bytes (128 bits), typically represented as a hexadecimal string of length 32.

For example, if the hash function is SHA256, the returned string is 32 bytes (256 bits), typically represented as a hexadecimal string of length 64.

HMAC MD5

The standard HMAC MD5 algorithm implementation is as follows:

1. Key processing:

  • • If the key is too long, compress it to 16 bytes using MD5.
  • • If the key is less than 64 bytes, pad it with 0x00 to 64 bytes.

2. Data processing: Use ipad and opad for XOR operations and concatenate the data.

3. MD5 calculation: Use the md5() function to calculate the Inner Hash and Outer Hash.

4. Return result: Return as a hexadecimal string.

The C++ code is as follows:

#include <string>
#include <vector>
#include <cstring>
#include <iomanip>
#include <sstream>
#include "md5.h"

// Define block size and output length
constexpr size_t BLOCK_SIZE = 64;
constexpr size_t MD5_DIGEST_LENGTH = 16;

// HMAC-MD5 implementation
void hmacMd5(const std::vector<uint8_t>& key, const std::vector<uint8_t>& data, uint8_t* outDigest) {
    std::vector<uint8_t> modifiedKey = key;

    // 1. Key processing
    if (modifiedKey.size() > BLOCK_SIZE) {
        uint8_t hash[MD5_DIGEST_LENGTH];
        MD5_CTX ctx;
        MD5_Init(&ctx);
        MD5_Update(&ctx, modifiedKey.data(), modifiedKey.size());
        MD5_Final(hash, &ctx);
        modifiedKey.assign(hash, hash + MD5_DIGEST_LENGTH);
    }
    if (modifiedKey.size() < BLOCK_SIZE) {
        modifiedKey.resize(BLOCK_SIZE, 0x00); // Pad with zeros
    }

    // 2. Generate ipad and opad
    std::vector<uint8_t> ipad(BLOCK_SIZE, 0x36);
    std::vector<uint8_t> opad(BLOCK_SIZE, 0x5c);

    for (size_t i = 0; i < BLOCK_SIZE; i++) {
        ipad[i] ^= modifiedKey[i];
        opad[i] ^= modifiedKey[i];
    }

    // 3. Inner Hash: MD5(ipad + data)
    uint8_t innerDigest[MD5_DIGEST_LENGTH];
    MD5_CTX innerCtx;
    MD5_Init(&innerCtx);
    MD5_Update(&innerCtx, ipad.data(), BLOCK_SIZE);
    MD5_Update(&innerCtx, data.data(), data.size());
    MD5_Final(innerDigest, &innerCtx);

    // 4. Outer Hash: MD5(opad + Inner Hash)
    MD5_CTX outerCtx;
    MD5_Init(&outerCtx);
    MD5_Update(&outerCtx, opad.data(), BLOCK_SIZE);
    MD5_Update(&outerCtx, innerDigest, MD5_DIGEST_LENGTH);
    MD5_Final(outDigest, &outerCtx);
}

The implementation of the MD5 algorithm refers to this article:

How to Morph an MD5 Algorithm? Android Implementation + OLLVM Protection Practical Analysis[1]

HMAC-MD5 calls MD5 a total of 2 times: once to calculate the Inner Hash and once to calculate the final HMAC.

Implementing JNI methods in Android to call the HMAC-MD5 algorithm to encrypt strings:

#include <jni.h>

// Convert string to byte array
std::vector<uint8_t> toBytes(const std::string& str) {
    return std::vector<uint8_t>(str.begin(), str.end());
}

// Convert byte array to hexadecimal string
std::string bytesToHex(const std::vector<uint8_t>& bytes) {
    std::ostringstream oss;
    for (uint8_t byte : bytes) {
        oss << std::hex << std::setw(2) << std::setfill('0') << (int)byte;
    }
    return oss.str();
}

// JNI interface implementation
extern "C"
JNIEXPORT jstring JNICALL
Java_com_cyrus_example_hmac_HMACUtils_hmacMD5(
        JNIEnv* env,
        jclass,
        jstring data) {

    // Convert jstring to std::string
    const char* dataStr = env->GetStringUTFChars(data, nullptr);
    const char* keyStr = "CYRUS STUDIO";

    std::vector<uint8_t> dataBytes = toBytes(dataStr);
    std::vector<uint8_t> keyBytes = toBytes(keyStr);

    // Calculate HMAC-MD5
    uint8_t resultDigest[MD5_DIGEST_LENGTH];
    hmacMd5(keyBytes, dataBytes, resultDigest);

    // Convert result to hexadecimal string
    std::string hexResult = bytesToHex(std::vector<uint8_t>(resultDigest, resultDigest + MD5_DIGEST_LENGTH));

    // Release resources
    env->ReleaseStringUTFChars(data, dataStr);

    return env->NewStringUTF(hexResult.c_str());
}

The effect is as follows:

How the HMAC Encryption Algorithm Works and How to Implement Variants on Android
word/media/image2.png

HMAC MD5 Morphing

HMAC MD5 can be morphed by modifying the initial constants in MD5Init, MD5_Update, macro constants, etc.

For specific implementations, refer to this article:How to Morph an MD5 Algorithm? Android Implementation + OLLVM Protection Practical Analysis[2]

HMAC SHA256

HMAC SHA256 is implemented similarly to HMAC MD5, except that the hash function is changed to SHA256.

The C++ code implementation is as follows:

#include <jni.h>
#include <string>
#include <vector>
#include <cstring>
#include <iomanip>
#include <sstream>
#include "sha256.h"

// Define block size and output length
constexpr size_t BLOCK_SIZE = 64;
constexpr size_t SHA256_DIGEST_LENGTH = 32;

// HMAC-SHA256 implementation
void hmacSha256(const std::vector<uint8_t>& key, const std::vector<uint8_t>& data, uint8_t* outDigest) {
    std::vector<uint8_t> modifiedKey = key;

    // 1. Key processing
    if (modifiedKey.size() > BLOCK_SIZE) {
        uint8_t hash[SHA256_DIGEST_LENGTH];
        SHA256_hash(modifiedKey.data(), modifiedKey.size(), hash);
        modifiedKey.assign(hash, hash + SHA256_DIGEST_LENGTH);
    }
    if (modifiedKey.size() < BLOCK_SIZE) {
        modifiedKey.resize(BLOCK_SIZE, 0x00); // Pad with zeros
    }

    // 2. Generate ipad and opad
    std::vector<uint8_t> ipad(BLOCK_SIZE, 0x36);
    std::vector<uint8_t> opad(BLOCK_SIZE, 0x5c);

    for (size_t i = 0; i < BLOCK_SIZE; i++) {
        ipad[i] ^= modifiedKey[i];
        opad[i] ^= modifiedKey[i];
    }

    // 3. Inner Hash: SHA256(ipad + data)
    uint8_t innerDigest[SHA256_DIGEST_LENGTH];
    SHA256_CTX innerCtx;
    SHA256_init(&innerCtx);
    SHA256_update(&innerCtx, ipad.data(), BLOCK_SIZE);
    SHA256_update(&innerCtx, data.data(), data.size());
    memcpy(innerDigest, SHA256_final(&innerCtx), SHA256_DIGEST_LENGTH);

    // 4. Outer Hash: SHA256(opad + Inner Hash)
    SHA256_CTX outerCtx;
    SHA256_init(&outerCtx);
    SHA256_update(&outerCtx, opad.data(), BLOCK_SIZE);
    SHA256_update(&outerCtx, innerDigest, SHA256_DIGEST_LENGTH);
    memcpy(outDigest, SHA256_final(&outerCtx), SHA256_DIGEST_LENGTH);
}

The SHA256 algorithm implementation refers to:

  • sha256.h[3]
  • hash-internal.h[4]
  • sha256.c[5]

Implementing JNI methods in Android to call the HMAC SHA256 algorithm to encrypt strings:

#include <jni.h>

// Convert string to byte array
extern std::vector<uint8_t> toBytes(const std::string& str);

// Convert byte array to hexadecimal string
extern std::string bytesToHex(const std::vector<uint8_t>& bytes);

// JNI interface implementation
extern "C"
JNIEXPORT jstring JNICALL
Java_com_cyrus_example_hmac_HMACUtils_hmacSHA256(
        JNIEnv* env,
        jclass,
        jstring data) {

    // Convert jstring to std::string
    const char* dataStr = env->GetStringUTFChars(data, nullptr);
    const char* keyStr = "CYRUS STUDIO";

    std::vector<uint8_t> dataBytes = toBytes(dataStr);
    std::vector<uint8_t> keyBytes = toBytes(keyStr);

    // Calculate HMAC-SHA256
    uint8_t resultDigest[SHA256_DIGEST_LENGTH];
    hmacSha256(keyBytes, dataBytes, resultDigest);

    // Convert result to hexadecimal string
    std::string hexResult = bytesToHex(std::vector<uint8_t>(resultDigest, resultDigest + SHA256_DIGEST_LENGTH));

    // Release resources
    env->ReleaseStringUTFChars(data, dataStr);

    return env->NewStringUTF(hexResult.c_str());
}

The effect is as follows:

How the HMAC Encryption Algorithm Works and How to Implement Variants on Android
word/media/image3.png

HMAC SHA256 Morphing

HMAC SHA256 can be morphed by modifying the initial constants in SHA256_init.

How the HMAC Encryption Algorithm Works and How to Implement Variants on Android
word/media/image4.png

Call SHA256_Update to add custom data.

How the HMAC Encryption Algorithm Works and How to Implement Variants on Android
word/media/image5.png

Modify the K constant and other methods to achieve algorithm morphing.

How the HMAC Encryption Algorithm Works and How to Implement Variants on Android
word/media/image6.png

For specific implementations, refer to this article:How to Morph SHA-1 Algorithm? From Standard Implementation to Android Custom Encryption Practice[6]

Complete Source Code

Complete source code address: https://github.com/CYRUS-STUDIO/AndroidExample

Reference Links

<span>[1]</span> How to Morph an MD5 Algorithm? Android Implementation + OLLVM Protection Practical Analysis:https://cyrus-studio.github.io/blog/posts/%E5%A6%82%E4%BD%95%E5%8F%98%E5%BD%A2%E4%B8%80%E4%B8%AA-md5-%E7%AE%97%E6%B3%95android-%E5%AE%9E%E7%8E%B0-+-ollvm-%E9%98%B2%E6%8A%A4%E5%AE%9E%E6%88%98%E8%A7%A3%E6%9E%90/<span>[2]</span>How to Morph an MD5 Algorithm? Android Implementation + OLLVM Protection Practical Analysis:https://cyrus-studio.github.io/blog/posts/%E5%A6%82%E4%BD%95%E5%8F%98%E5%BD%A2%E4%B8%80%E4%B8%AA-md5-%E7%AE%97%E6%B3%95android-%E5%AE%9E%E7%8E%B0-+-ollvm-%E9%98%B2%E6%8A%A4%E5%AE%9E%E6%88%98%E8%A7%A3%E6%9E%90/<span>[3]</span>sha256.h:https://android.googlesource.com/platform/system/core/+/l-preview/include/mincrypt/sha256.h<span>[4]</span>hash-internal.h:https://android.googlesource.com/platform/system/core/+/l-preview/include/mincrypt/hash-internal.h<span>[5]</span>sha256.c:https://android.googlesource.com/platform/system/core/+/669ecc2f5e80ff924fa20ce7445354a7c5bcfd98/libmincrypt/sha256.c<span>[6]</span>How to Morph SHA-1 Algorithm? From Standard Implementation to Android Custom Encryption Practice:https://cyrus-studio.github.io/blog/posts/%E5%A6%82%E4%BD%95%E5%8F%98%E5%BD%A2-sha-1-%E7%AE%97%E6%B3%95%E4%BB%8E%E6%A0%87%E5%87%86%E5%AE%9E%E7%8E%B0%E5%88%B0-android-%E5%AE%9A%E5%88%B6%E5%8A%A0%E5%AF%86%E5%AE%9E%E8%B7%B5/

Leave a Comment