module std::hash::hmac <HashAlg, HASH_BYTES, BLOCK_BYTES>;
import std::crypto;

struct Hmac
{
	HashAlg a, b;
}

fn char[HASH_BYTES] hash(char[] key, char[] message)
{
	Hmac hmac @noinit;
	hmac.init(key);
	hmac.update(message);
	return hmac.final();
}

<*
 @require output.len > 0 : "Output must be greater than zero"
 @require output.len < int.max / HASH_BYTES : "Output is too large"
*>
fn void pbkdf2(char[] pw, char[] salt, uint iterations, char[] output)
{
	usz l = output.len / HASH_BYTES;
	usz r = output.len % HASH_BYTES;

	Hmac hmac;
	hmac.init(pw);

	char[] dst_curr = output;
	for (usz i = 1; i <= l; i++)
	{
		@derive(&hmac, salt, iterations, i, dst_curr[:HASH_BYTES]);
		dst_curr = dst_curr[HASH_BYTES..];
	}

	if (r > 0)
	{
		char[HASH_BYTES] tmp;
		@derive(&hmac, salt, iterations, l + 1, &tmp);
		dst_curr[..] = tmp[:dst_curr.len];
		mem::zero_volatile(&tmp);
	}
}

fn void Hmac.init(&self, char[] key)
{
	char[BLOCK_BYTES] buffer;
	if (key.len > BLOCK_BYTES)
	{
		self.a.init();
		self.a.update(key);
		buffer[:HASH_BYTES] = self.a.final()[..];
	}
	else
	{
		buffer[:key.len] = key[..];
	}

	foreach (&b : buffer) *b ^= IPAD;

	self.a.init();
	self.a.update(&buffer);

	foreach (&b : buffer) *b ^= IPAD ^ OPAD;

	self.b.init();
	self.b.update(&buffer);

	mem::zero_volatile(&buffer);
}

fn void Hmac.update(&self, char[] data)
{
	self.a.update(data);
}

fn char[HASH_BYTES] Hmac.final(&self)
{
	self.b.update(&&self.a.final());
	return self.b.final();
}

const IPAD @private = 0x36;
const OPAD @private = 0x5C;

macro void @derive(Hmac *hmac_start, char[] salt, uint iterations, usz index, char[] out)
{
	assert(out.len == HASH_BYTES);
	char[HASH_BYTES] tmp @noinit;
	defer mem::zero_volatile(&tmp);
	Hmac hmac  = *hmac_start;
	hmac.update(salt);
	UIntBE be = { (uint)index };
	hmac.update(&&bitcast(be, char[4]));
	tmp = hmac.final();
	out[..] = tmp[..];
	for (int it = 1; it < iterations; it++)
	{
		hmac = *hmac_start;
		hmac.update(&tmp);
		tmp = hmac.final();
		foreach (i, v : tmp)
		{
			out[i] ^= v;
		}
	}
}
