/*-------------------------------------------------------------------------
 *
 * hmac.c
 *	  Implements Keyed-Hashing for Message Authentication (HMAC)
 *
 * Fallback implementation of HMAC, as specified in RFC 2104.
 *
 * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/common/hmac.c
 *
 *-------------------------------------------------------------------------
 */

#ifndef FRONTEND
#include "postgres.h"
#else
#include "postgres_fe.h"
#endif

#include "od_c.h"
#include "pg_compat.h"

#ifdef __GNUC__
#pragma GCC diagnostic ignored "-Wsign-compare"
#endif

#include "common/cryptohash.h"
#include "common/hmac.h"
#include "common/md5.h"
#include "common/sha1.h"
#include "common/sha2.h"

/*
 * In backend, use palloc/pfree to ease the error handling.  In frontend,
 * use malloc to be able to return a failure status back to the caller.
 */
#ifndef FRONTEND
#define ALLOC(size) palloc(size)
#define FREE(ptr) pfree(ptr)
#else
#define ALLOC(size) malloc(size)
#define FREE(ptr) free(ptr)
#endif

/* Set of error states */
typedef enum pg_hmac_errno {
	PG_HMAC_ERROR_NONE = 0,
	PG_HMAC_ERROR_OOM,
	PG_HMAC_ERROR_INTERNAL,
} pg_hmac_errno;

/* Internal pg_hmac_ctx structure */
struct pg_hmac_ctx {
	pg_cryptohash_ctx *hash;
	pg_cryptohash_type type;
	pg_hmac_errno error;
	const char *errreason;
	int block_size;
	int digest_size;

	/*
	 * Use the largest block size among supported options.  This wastes some
	 * memory but simplifies the allocation logic.
	 */
	uint8 k_ipad[PG_SHA512_BLOCK_LENGTH];
	uint8 k_opad[PG_SHA512_BLOCK_LENGTH];
};

#define HMAC_IPAD 0x36
#define HMAC_OPAD 0x5C

/*
 * pg_hmac_create
 *
 * Allocate a hash context.  Returns NULL on failure for an OOM.  The
 * backend issues an error, without returning.
 */
pg_hmac_ctx *pg_hmac_create(pg_cryptohash_type type)
{
	pg_hmac_ctx *ctx;

	ctx = ALLOC(sizeof(pg_hmac_ctx));
	if (ctx == NULL) {
		return NULL;
	}
	memset(ctx, 0, sizeof(pg_hmac_ctx));
	ctx->type = type;
	ctx->error = PG_HMAC_ERROR_NONE;
	ctx->errreason = NULL;

	/*
	 * Initialize the context data.  This requires to know the digest and
	 * block lengths, that depend on the type of hash used.
	 */
	switch (type) {
	case PG_MD5:
		ctx->digest_size = MD5_DIGEST_LENGTH;
		ctx->block_size = MD5_BLOCK_SIZE;
		break;
	case PG_SHA1:
		ctx->digest_size = SHA1_DIGEST_LENGTH;
		ctx->block_size = SHA1_BLOCK_SIZE;
		break;
	case PG_SHA224:
		ctx->digest_size = PG_SHA224_DIGEST_LENGTH;
		ctx->block_size = PG_SHA224_BLOCK_LENGTH;
		break;
	case PG_SHA256:
		ctx->digest_size = PG_SHA256_DIGEST_LENGTH;
		ctx->block_size = PG_SHA256_BLOCK_LENGTH;
		break;
	case PG_SHA384:
		ctx->digest_size = PG_SHA384_DIGEST_LENGTH;
		ctx->block_size = PG_SHA384_BLOCK_LENGTH;
		break;
	case PG_SHA512:
		ctx->digest_size = PG_SHA512_DIGEST_LENGTH;
		ctx->block_size = PG_SHA512_BLOCK_LENGTH;
		break;
	}

	ctx->hash = pg_cryptohash_create(type);
	if (ctx->hash == NULL) {
		explicit_bzero(ctx, sizeof(pg_hmac_ctx));
		FREE(ctx);
		return NULL;
	}

	return ctx;
}

/*
 * pg_hmac_init
 *
 * Initialize a HMAC context.  Returns 0 on success, -1 on failure.
 */
int pg_hmac_init(pg_hmac_ctx *ctx, const uint8 *key, size_t len)
{
	int i;
	int digest_size;
	int block_size;
	uint8 *shrinkbuf = NULL;

	if (ctx == NULL) {
		return -1;
	}

	digest_size = ctx->digest_size;
	block_size = ctx->block_size;

	memset(ctx->k_opad, HMAC_OPAD, ctx->block_size);
	memset(ctx->k_ipad, HMAC_IPAD, ctx->block_size);

	/*
	 * If the key is longer than the block size, pass it through the hash once
	 * to shrink it down.
	 */
	if (len > block_size) {
		pg_cryptohash_ctx *hash_ctx;

		/* temporary buffer for one-time shrink */
		shrinkbuf = ALLOC(digest_size);
		if (shrinkbuf == NULL) {
			ctx->error = PG_HMAC_ERROR_OOM;
			return -1;
		}
		memset(shrinkbuf, 0, digest_size);

		hash_ctx = pg_cryptohash_create(ctx->type);
		if (hash_ctx == NULL) {
			ctx->error = PG_HMAC_ERROR_OOM;
			FREE(shrinkbuf);
			return -1;
		}

		if (pg_cryptohash_init(hash_ctx) < 0 ||
		    pg_cryptohash_update(hash_ctx, key, len) < 0 ||
		    pg_cryptohash_final(hash_ctx, shrinkbuf, digest_size) < 0) {
			ctx->error = PG_HMAC_ERROR_INTERNAL;
			ctx->errreason = pg_cryptohash_error(hash_ctx);
			pg_cryptohash_free(hash_ctx);
			FREE(shrinkbuf);
			return -1;
		}

		key = shrinkbuf;
		len = digest_size;
		pg_cryptohash_free(hash_ctx);
	}

	for (i = 0; i < len; i++) {
		ctx->k_ipad[i] ^= key[i];
		ctx->k_opad[i] ^= key[i];
	}

	/* tmp = H(K XOR ipad, text) */
	if (pg_cryptohash_init(ctx->hash) < 0 ||
	    pg_cryptohash_update(ctx->hash, ctx->k_ipad, ctx->block_size) < 0) {
		ctx->error = PG_HMAC_ERROR_INTERNAL;
		ctx->errreason = pg_cryptohash_error(ctx->hash);
		if (shrinkbuf) {
			FREE(shrinkbuf);
		}
		return -1;
	}

	if (shrinkbuf) {
		FREE(shrinkbuf);
	}
	return 0;
}

/*
 * pg_hmac_update
 *
 * Update a HMAC context.  Returns 0 on success, -1 on failure.
 */
int pg_hmac_update(pg_hmac_ctx *ctx, const uint8 *data, size_t len)
{
	if (ctx == NULL) {
		return -1;
	}

	if (pg_cryptohash_update(ctx->hash, data, len) < 0) {
		ctx->error = PG_HMAC_ERROR_INTERNAL;
		ctx->errreason = pg_cryptohash_error(ctx->hash);
		return -1;
	}

	return 0;
}

/*
 * pg_hmac_final
 *
 * Finalize a HMAC context.  Returns 0 on success, -1 on failure.
 */
int pg_hmac_final(pg_hmac_ctx *ctx, uint8 *dest, size_t len)
{
	uint8 *h;

	if (ctx == NULL) {
		return -1;
	}

	h = ALLOC(ctx->digest_size);
	if (h == NULL) {
		ctx->error = PG_HMAC_ERROR_OOM;
		return -1;
	}
	memset(h, 0, ctx->digest_size);

	if (pg_cryptohash_final(ctx->hash, h, ctx->digest_size) < 0) {
		ctx->error = PG_HMAC_ERROR_INTERNAL;
		ctx->errreason = pg_cryptohash_error(ctx->hash);
		FREE(h);
		return -1;
	}

	/* H(K XOR opad, tmp) */
	if (pg_cryptohash_init(ctx->hash) < 0 ||
	    pg_cryptohash_update(ctx->hash, ctx->k_opad, ctx->block_size) < 0 ||
	    pg_cryptohash_update(ctx->hash, h, ctx->digest_size) < 0 ||
	    pg_cryptohash_final(ctx->hash, dest, len) < 0) {
		ctx->error = PG_HMAC_ERROR_INTERNAL;
		ctx->errreason = pg_cryptohash_error(ctx->hash);
		FREE(h);
		return -1;
	}

	FREE(h);
	return 0;
}

/*
 * pg_hmac_free
 *
 * Free a HMAC context.
 */
void pg_hmac_free(pg_hmac_ctx *ctx)
{
	if (ctx == NULL) {
		return;
	}

	pg_cryptohash_free(ctx->hash);
	explicit_bzero(ctx, sizeof(pg_hmac_ctx));
	FREE(ctx);
}

/*
 * pg_hmac_error
 *
 * Returns a static string providing details about an error that happened
 * during a HMAC computation.
 */
const char *pg_hmac_error(pg_hmac_ctx *ctx)
{
	if (ctx == NULL) {
		return _("out of memory");
	}

	/*
	 * If a reason is provided, rely on it, else fallback to any error code
	 * set.
	 */
	if (ctx->errreason) {
		return ctx->errreason;
	}

	switch (ctx->error) {
	case PG_HMAC_ERROR_NONE:
		return _("success");
	case PG_HMAC_ERROR_INTERNAL:
		return _("internal error");
	case PG_HMAC_ERROR_OOM:
		return _("out of memory");
	}

	Assert(false); /* cannot be reached */
	return _("success");
}
