/*
 * gnome-keyring
 *
 * Copyright (C) 2008 Stefan Walter
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, see
 * <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "egg-padding.h"

#include <gcrypt.h>

#include "egg/egg-secure-memory.h"

/* ----------------------------------------------------------------------------
 * INTERNAL
 */

static void
fill_random_nonzero (guchar *data, gsize n_data)
{
	guchar *rnd;
	guint n_zero, i, j;

	gcry_randomize (data, n_data, GCRY_STRONG_RANDOM);

	/* Find any zeros in random data */
	n_zero = 0;
	for (i = 0; i < n_data; ++i) {
		if (data[i] == 0x00)
			++n_zero;
	}

	while (n_zero > 0) {
		rnd = gcry_random_bytes (n_zero, GCRY_STRONG_RANDOM);
		n_zero = 0;
		for (i = 0, j = 0; i < n_data; ++i) {
			if (data[i] != 0x00)
				continue;

			/* Use some of the replacement data */
			data[i] = rnd[j];
			++j;

			/* It's zero again :( */
			if (data[i] == 0x00)
				n_zero++;
		}

		gcry_free (rnd);
	}
}

static gboolean
unpad_pkcs1 (guchar bt, EggAllocator alloc, gsize block, const guchar* padded,
             gsize n_padded, gpointer *raw, gsize *n_raw)
{
	const guchar *at;

	if (block && n_padded % block != 0)
		return FALSE;

	/* Check the header */
	if (padded[0] != 0x00 || padded[1] != bt)
		return FALSE;

	/* The first zero byte after the header */
	at = memchr (padded + 2, 0x00, n_padded - 2);
	if (!at)
		return FALSE;

	if (alloc == NULL)
		alloc = g_realloc;

	++at;
	*n_raw = n_padded - (at - padded);
	if (raw) {
		*raw = (alloc) (NULL, *n_raw + 1);
		if (*raw == NULL)
			return FALSE;
		memcpy (*raw, at, *n_raw);

		/* Convenience null terminate the result */
		memset (((guchar*)*raw) + *n_raw, 0, 1);
	}

	return TRUE;
}

/* ----------------------------------------------------------------------------
 * PUBLIC
 */

gboolean
egg_padding_zero_pad (EggAllocator alloc, gsize block, gconstpointer raw,
                      gsize n_raw, gpointer *padded, gsize *n_padded)
{
	guchar *pad;
	gsize n_pad;

	/*
	 * 0x00 0x00 0x00 ... 0x?? 0x?? 0x?? ...
	 *   padding               data
	 */

	g_return_val_if_fail (block != 0, FALSE);

	*n_padded = ((n_raw + (block - 1)) / block) * block;
	g_assert (n_raw <= *n_padded);
	n_pad = *n_padded - n_raw;
	g_assert (n_pad < block);

	if (alloc == NULL)
		alloc = g_realloc;

	if (padded) {
		*padded = pad = (alloc) (NULL, MAX (*n_padded, 1));
		if (pad == NULL)
			return FALSE;
		memset (pad, 0x00, n_pad);
		memcpy (pad + n_pad, raw, n_raw);
	}

	return TRUE;
}

gboolean
egg_padding_pkcs1_pad_01 (EggAllocator alloc, gsize block, gconstpointer raw,
                           gsize n_raw, gpointer *padded, gsize *n_padded)
{
	guchar *pad;
	gsize n_pad;

	/*
	 * 0x00 0x01 0xFF 0xFF ... 0x00 0x?? 0x?? 0x?? ...
	 *      type  padding              data
	 */

	g_return_val_if_fail (block != 0, FALSE);
	g_return_val_if_fail (block > 3, FALSE);

	*n_padded = ((n_raw + 3 + (block - 1)) / block) * block;
	g_assert (n_raw <= *n_padded);
	n_pad = *n_padded - n_raw;
	g_assert (n_pad <= block);
	g_assert (n_pad >= 3);

	if (alloc == NULL)
		alloc = g_realloc;

	if (padded) {
		*padded = pad = (alloc) (NULL, MAX (*n_padded, 1));
		if (pad == NULL)
			return FALSE;
		pad[0] = 0; /* Prefix */
		pad[1] = 1; /* Block type */
		memset (pad + 2, 0xFF, n_pad - 3);
		pad[n_pad - 1] = 0;
		memcpy (pad + n_pad, raw, n_raw);
	}

	return TRUE;
}

gboolean
egg_padding_pkcs1_pad_02 (EggAllocator alloc, gsize block, gconstpointer raw,
                           gsize n_raw, gpointer *padded, gsize *n_padded)
{
	guchar *pad;
	gsize n_pad;

	/*
	 * 0x00 0x01 0x?? 0x?? ... 0x00 0x?? 0x?? 0x?? ...
	 *      type  padding              data
	 */

	g_return_val_if_fail (block != 0, FALSE);
	g_return_val_if_fail (block > 3, FALSE);

	*n_padded = ((n_raw + 3 + (block - 1)) / block) * block;
	g_assert (n_raw <= *n_padded);
	n_pad = *n_padded - n_raw;
	g_assert (n_pad <= block);
	g_assert (n_pad >= 3);

	if (alloc == NULL)
		alloc = g_realloc;

	if (padded) {
		*padded = pad = (alloc) (NULL, MAX (*n_padded, 1));
		if (pad == NULL)
			return FALSE;
		pad[0] = 0; /* Prefix */
		pad[1] = 2; /* Block type */
		fill_random_nonzero (pad + 2, n_pad - 3);
		pad[n_pad - 1] = 0;
		memcpy (pad + n_pad, raw, n_raw);
	}

	return TRUE;
}

gboolean
egg_padding_pkcs1_unpad_01 (EggAllocator alloc, gsize block, gconstpointer padded,
                             gsize n_padded, gpointer *raw, gsize *n_raw)
{
	return unpad_pkcs1 (0x01, alloc, block, padded, n_padded, raw, n_raw);
}

gboolean
egg_padding_pkcs1_unpad_02 (EggAllocator alloc, gsize block, gconstpointer padded,
                             gsize n_padded, gpointer *raw, gsize *n_raw)
{
	return unpad_pkcs1 (0x02, alloc, block, padded, n_padded, raw, n_raw);
}

gboolean
egg_padding_pkcs7_pad (EggAllocator alloc, gsize block, gconstpointer raw,
                       gsize n_raw, gpointer *padded, gsize *n_padded)
{
	guchar *pad;
	gsize n_pad;

	g_return_val_if_fail (block != 0, FALSE);
	g_return_val_if_fail (block < 256, FALSE);

	*n_padded = ((n_raw + block) / block) * block;
	g_assert (n_raw < *n_padded);
	n_pad = *n_padded - n_raw;
	g_assert (n_pad > 0 && n_pad <= block);

	if (alloc == NULL)
		alloc = g_realloc;

	if (padded) {
		*padded = pad = (alloc) (NULL, MAX (*n_padded, 1));
		if (pad == NULL)
			return FALSE;
		memcpy (pad, raw, n_raw);
		memset (pad + n_raw, n_pad, n_pad);
	}

	return TRUE;
}

gboolean
egg_padding_pkcs7_unpad (EggAllocator alloc, gsize block, gconstpointer padded,
                         gsize n_padded, gpointer *raw, gsize *n_raw)
{
	const guchar *pad;
	gsize n_pad, i;

	if (n_padded == 0)
		return FALSE;

	pad = padded;
	n_pad = pad[n_padded - 1];

	/* Validate the padding */
	if (n_pad == 0 || n_pad > 256)
		return FALSE;
	if (n_pad > n_padded)
		return FALSE;
	if (block && n_pad > block)
		return FALSE;
	for (i = n_padded - n_pad; i < n_padded; ++i) {
		if (pad[i] != n_pad)
			return FALSE;
	}

	*n_raw = n_padded - n_pad;

	if (alloc == NULL)
		alloc = g_realloc;

	if (raw) {
		*raw = (alloc) (NULL, *n_raw + 1);
		if (*raw == NULL)
			return FALSE;

		/* Output the result, null terminated */
		memcpy (*raw, pad, *n_raw);
		memset (((guchar*)*raw) + *n_raw, 0, 1);
	}

	return TRUE;
}
