/* Jitter RNG: GCD health test
 *
 * Copyright (C) 2021 - 2022, Joshua E. Hill <josh@keypair.us>
 * Copyright (C) 2021 - 2022, Stephan Mueller <smueller@chronox.de>
 *
 * License: see LICENSE file in root directory
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ALL OF
 * WHICH ARE HEREBY DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR 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 NOT ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 */

#include "jitterentropy.h"
#include "jitterentropy-gcd.h"

/* The common divisor for all timestamp deltas */
static uint64_t jent_common_timer_gcd = 0;

static inline int jent_gcd_tested(void)
{
	return (jent_common_timer_gcd != 0);
}

/* A straight forward implementation of the Euclidean algorithm for GCD. */
static inline uint64_t jent_gcd64(uint64_t a, uint64_t b)
{
	/* Make a greater a than or equal b. */
	if (a < b) {
		uint64_t c = a;

		a = b;
		b = c;
	}

	/* Now perform the standard inner-loop for this algorithm.*/
	while (b != 0) {
		uint64_t r;

		r = a % b;

		a = b;
		b = r;
	}

	return a;
}

static int jent_gcd_analyze_internal(uint64_t *delta_history, size_t nelem,
				     uint64_t *running_gcd_out,
				     uint64_t *delta_sum_out)
{
	uint64_t running_gcd, delta_sum = 0;
	size_t i;

	if (!delta_history)
		return -EAGAIN;

	running_gcd = delta_history[0];

	/* Now perform the analysis on the accumulated delta data. */
	for (i = 1; i < nelem; i++) {
		/*
		 * ensure that we have a varying delta timer which is necessary
		 * for the calculation of entropy -- perform this check
		 * only after the first loop is executed as we need to prime
		 * the old_data value
		 */
		if (delta_history[i] >= delta_history[i - 1])
			delta_sum +=  delta_history[i] - delta_history[i - 1];
		else
			delta_sum +=  delta_history[i - 1] - delta_history[i];

		/*
		 * This calculates the gcd of all the delta values. that is
		 * gcd(delta_1, delta_2, ..., delta_nelem)

		 * Some timers increment by a fixed (non-1) amount each step.
		 * This code checks for such increments, and allows the library
		 * to output the number of such changes have occurred.
		 */
		running_gcd = jent_gcd64(delta_history[i], running_gcd);
	}

	*running_gcd_out = running_gcd;
	*delta_sum_out = delta_sum;

	return 0;
}

int jent_gcd_analyze(uint64_t *delta_history, size_t nelem)
{
	uint64_t running_gcd, delta_sum;
	int ret = jent_gcd_analyze_internal(delta_history, nelem, &running_gcd,
					    &delta_sum);

	if (ret == -EAGAIN)
		return 0;

	/*
	 * Variations of deltas of time must on average be larger than 1 to
	 * ensure the entropy estimation implied with 1 is preserved.
	 */
	if (delta_sum <= nelem - 1) {
		ret = EMINVARVAR;
		goto out;
	}

	/* Set a sensible maximum value. */
	if (running_gcd >= UINT32_MAX / 2) {
		ret = ECOARSETIME;
		goto out;
	}

	/*  Adjust all deltas by the observed (small) common factor. */
	if (!jent_gcd_tested())
		jent_common_timer_gcd = running_gcd;

out:
	return ret;
}

uint64_t *jent_gcd_init(size_t nelem)
{
	uint64_t *delta_history;

	delta_history = jent_zalloc(nelem * sizeof(uint64_t));
	if (!delta_history)
		return NULL;

	return delta_history;
}

void jent_gcd_fini(uint64_t *delta_history, size_t nelem)
{
	if (delta_history)
		jent_zfree(delta_history,
			   (unsigned int)(nelem * sizeof(uint64_t)));
}

int jent_gcd_get(uint64_t *value)
{
	if (!jent_gcd_tested())
		return 1;

	*value = jent_common_timer_gcd;
	return 0;
}

int jent_gcd_selftest(void)
{
#define JENT_GCD_SELFTEST_ELEM 10
#define JENT_GCD_SELFTEST_EXP 3ULL
	uint64_t *gcd = jent_gcd_init(JENT_GCD_SELFTEST_ELEM);
	uint64_t running_gcd, delta_sum;
	unsigned int i;
	int ret = EGCD;

	if (!gcd)
		return EMEM;

	for (i = 0; i < JENT_GCD_SELFTEST_ELEM; i++)
		jent_gcd_add_value(gcd, i * JENT_GCD_SELFTEST_EXP, i);

	if (jent_gcd_analyze_internal(gcd, JENT_GCD_SELFTEST_ELEM,
				      &running_gcd, &delta_sum))
		goto out;

	if (running_gcd != JENT_GCD_SELFTEST_EXP)
		goto out;

	ret = 0;

out:
	jent_gcd_fini(gcd, JENT_GCD_SELFTEST_ELEM);
	return ret;
}
