/*
 * gnome-keyring
 *
 * Copyright (C) 2011 Collabora Ltd.
 *
 * 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/>.
 *
 * Author: Stef Walter <stefw@collabora.co.uk>
 */

#include "config.h"

#include "gcr-openpgp.h"
#include "gcr-internal.h"
#include "gcr-record.h"
#include "gcr-types.h"

#include "egg/egg-hex.h"

#include <gcrypt.h>

#include <string.h>

typedef enum {
	OPENPGP_PKT_RESERVED = 0,
	OPENPGP_PKT_PUBKEY_ENC = 1,
	OPENPGP_PKT_SIGNATURE = 2,
	OPENPGP_PKT_ONEPASS_SIG = 4,
	OPENPGP_PKT_SECRET_KEY = 5,
	OPENPGP_PKT_PUBLIC_KEY = 6,
	OPENPGP_PKT_SECRET_SUBKEY = 7,
	OPENPGP_PKT_COMPRESSED = 8,
	OPENPGP_PKT_MARKER = 10,
	OPENPGP_PKT_LITERAL = 11,
	OPENPGP_PKT_RING_TRUST = 12,
	OPENPGP_PKT_USER_ID = 13,
	OPENPGP_PKT_PUBLIC_SUBKEY = 14,
	OPENPGP_PKT_OLD_COMMENT = 16,
	OPENPGP_PKT_ATTRIBUTE = 17,
	OPENPGP_PKT_MDC = 19
} OpenpgpPktType;

typedef enum {
	OPENPGP_SIG_CREATION = 2,
	OPENPGP_SIG_EXPIRY = 3,
	OPENPGP_SIG_EXPORTABLE = 4,
	OPENPGP_SIG_TRUST = 5,
	OPENPGP_SIG_REGULAR_EXPRESSION = 6,
	OPENPGP_SIG_REVOCABLE = 7,
	OPENPGP_SIG_KEY_EXPIRY = 9,
	OPENPGP_SIG_SYMMETRIC_ALGOS = 11,
	OPENPGP_SIG_REVOCATION_KEY = 12,
	OPENPGP_SIG_ISSUER = 16,
	OPENPGP_SIG_NOTATION_DATA = 20,
	OPENPGP_SIG_HASH_ALGOS = 21,
	OPENPGP_SIG_COMPRESSION_ALGOS = 22,
	OPENPGP_SIG_KEYSERVER_PREFS = 23,
	OPENPGP_SIG_PREFERRED_KEYSERVER = 24,
	OPENPGP_SIG_PRIMARY_USERID = 25,
	OPENPGP_SIG_POLICY_URI = 26,
	OPENPGP_SIG_KEY_FLAGS = 27,
	OPENPGP_SIG_SIGNER_USERID = 28,
	OPENPGP_SIG_REVOCATION_REASON = 29,
	OPENPGP_SIG_FEATURES = 30,
	OPENPGP_SIG_TARGET = 31,
	OPENPGP_SIG_EMBEDDED_SIGNATURE = 32,
} OpenpgpSigPacket;

static gboolean
read_byte (const guchar **at,
           const guchar *end,
           guint8 *result)
{
	g_assert (at);
	if (*at == end)
		*at = NULL;
	if (*at == NULL)
		return FALSE;
	if (result)
		*result = *(*at);
	(*at)++;
	return TRUE;
}

static gboolean
read_bytes (const guchar **at,
            const guchar *end,
            gpointer buffer,
            gsize length)
{
	g_assert (at);
	if (*at + length > end)
		*at = NULL;
	if (*at == NULL)
		return FALSE;
	if (buffer != NULL)
		memcpy (buffer, *at, length);
	(*at) += length;
	return TRUE;
}

static gboolean
read_uint32 (const guchar **at,
             const guchar *end,
             guint32 *value)
{
	guchar buf[4];
	g_assert (at);
	if (!read_bytes (at, end, buf, 4))
		return FALSE;
	if (value)
		*value = buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3];
	return TRUE;
}

static gboolean
read_uint16 (const guchar **at,
             const guchar *end,
             guint16 *value)
{
	guchar buf[2];
	g_assert (at);
	if (!read_bytes (at, end, buf, 2))
		return FALSE;
	if (value)
		*value = buf[0] << 8 | buf[1];
	return TRUE;
}

static gboolean
read_mpi (const guchar **at,
          const guchar *end,
          guint16 *bits,
          guchar **value)
{
	gsize bytes;
	guint16 b;
	g_assert (at);
	if (!bits)
		bits = &b;
	if (!read_uint16 (at, end, bits))
		return FALSE;
	bytes = (*bits + 7) / 8;
	if (bytes == 0)
		return FALSE;
	if (value)
		*value = g_malloc (bytes);
	if (!read_bytes (at, end, value ? *value : NULL, bytes)) {
		if (value)
			g_free (*value);
		return FALSE;
	}
	return TRUE;
}

static gboolean
read_new_length (const guchar **at,
                 const guchar *end,
                 gsize *pkt_len)
{
	guint8 c, c1;
	guint32 val;

	if (!read_byte (at, end, &c))
		return FALSE;
	if (c < 192) {
		*pkt_len = c;
	} else if (c >= 192 && c <= 223) {
		if (!read_byte (at, end, &c1))
			return FALSE;
		*pkt_len = ((c - 192) << 8) + c1 + 192;
	} else if (c == 255) {
		if (!read_uint32 (at, end, &val))
			return FALSE;
		*pkt_len = val;
	} else {
		/* We don't support partial length */
		return FALSE;
	}

	return TRUE;
}

static gboolean
read_old_length (const guchar **at,
                 const guchar *end,
                 guchar ctb,
                 gsize *pkt_len)
{
	gsize llen = ctb & 0x03;
	guint16 v16;
	guint32 v32;
	guint8 c;

	if (llen == 0) {
		if (!read_byte (at, end, &c))
			return FALSE;
		*pkt_len = c;
	} else if (llen == 1) {
		if (!read_uint16 (at, end, &v16))
			return FALSE;
		*pkt_len = v16;
	} else if (llen == 2) {
		if (!read_uint32 (at, end, &v32))
			return FALSE;
		*pkt_len = v32;
	} else {
		*pkt_len = end - *at;
	}

	return TRUE;
}

static GcrDataError
read_openpgp_packet (const guchar **at,
                     const guchar *end,
                     guint8 *pkt_type,
                     gsize *length)
{
	gboolean new_ctb;
	guint8 ctb;
	gboolean ret;

	if (!read_byte (at, end, &ctb))
		return GCR_ERROR_UNRECOGNIZED;
	if (!(ctb & 0x80))
		return GCR_ERROR_UNRECOGNIZED;

	/* RFC2440 packet format. */
	if (ctb & 0x40) {
		*pkt_type = ctb & 0x3f;
		new_ctb = TRUE;

	/* the old RFC1991 packet format. */
	} else {
		*pkt_type = ctb & 0x3f;
		*pkt_type >>= 2;
		new_ctb = FALSE;
	}

	if (*pkt_type > 63)
		return GCR_ERROR_UNRECOGNIZED;

	if (new_ctb)
		ret = read_new_length (at, end, length);
	else
		ret = read_old_length (at, end, ctb, length);
	if (!ret)
		return GCR_ERROR_UNRECOGNIZED;

	if ((*at) + *length > end)
		return GCR_ERROR_FAILURE;

	return GCR_SUCCESS;
}

static gchar *
hash_user_id_or_attribute (const guchar *beg,
                           const guchar *end)
{
	guint8 digest[20] = { 0, };

	g_assert (beg != NULL);
	g_assert (end > beg);

	gcry_md_hash_buffer (GCRY_MD_RMD160, digest, beg, end - beg);
	return egg_hex_encode_full (digest, sizeof (digest), TRUE, NULL, 0);
}

static gboolean
parse_v3_rsa_bits_and_keyid (const guchar **at,
                             const guchar *end,
                             guint16 *bits,
                             gchar **keyid)
{
	guchar *n;
	gsize bytes;

	g_assert (bits);
	g_assert (keyid);

	/* Read in the modulus */
	if (!read_mpi (at, end, bits, &n))
		return FALSE;

	/* Last 64-bits of modulus are keyid */
	bytes = (*bits + 7) / 8;
	if (bytes < 8) {
		g_free (n);
		return FALSE;
	}

	*keyid = egg_hex_encode_full (n + (bytes - 8), 8, TRUE, NULL, 0);
	return TRUE;
}

static gchar *
hash_v4_keyid (const guchar *data,
               const guchar *end,
               gchar **fingerprint)
{
	gcry_md_hd_t mdh;
	gcry_error_t gcry;
	guchar header[3];
	guint8 *digest;
	gchar *keyid;
	gsize len;

	/*
	 * Both primary and subkeys use the public key tag byte
	 * 0x99 to construct the hash. So we skip over that here.
	 */

	g_assert (data != NULL);
	g_assert (end > data);

	len = end - data;
	g_return_val_if_fail (len < G_MAXUSHORT, NULL);

	header[0] = 0x99;
	header[1] = len >> 8 & 0xff;
	header[2] = len & 0xff;

	gcry = gcry_md_open (&mdh, GCRY_MD_SHA1, 0);
	g_return_val_if_fail (gcry == 0, NULL);

	gcry_md_write (mdh, header, 3);
	gcry_md_write (mdh, data, len);

	digest = gcry_md_read (mdh, 0);
	keyid = egg_hex_encode_full (digest + 12, 8, TRUE, NULL, 0);
	if (fingerprint)
		*fingerprint = egg_hex_encode_full (digest, 20, TRUE, NULL, 0);
	gcry_md_close (mdh);

	return keyid;
}

static gboolean
parse_v4_algo_bits (const guchar **at,
                    const guchar *end,
                    guint8 algo,
                    guint16 *bits)
{
	switch (algo) {
	case GCR_OPENPGP_ALGO_RSA:
	case GCR_OPENPGP_ALGO_RSA_E:
	case GCR_OPENPGP_ALGO_RSA_S:
		if (!read_mpi (at, end, bits, NULL) ||
		    !read_mpi (at, end, NULL, NULL))
			return FALSE;
		return TRUE;
	case GCR_OPENPGP_ALGO_DSA:
		if (!read_mpi (at, end, bits, NULL) ||
		    !read_mpi (at, end, NULL, NULL) ||
		    !read_mpi (at, end, NULL, NULL) ||
		    !read_mpi (at, end, NULL, NULL))
			return FALSE;
		return TRUE;
	case GCR_OPENPGP_ALGO_ELG_E:
		if (!read_mpi (at, end, bits, NULL) ||
		    !read_mpi (at, end, NULL, NULL) ||
		    !read_mpi (at, end, NULL, NULL))
			return FALSE;
		return TRUE;
	default: /* Unsupported key */
		return FALSE;
	}
}

static const gchar *
default_caps_for_algo (guint8 algo)
{
	switch (algo) {
	case GCR_OPENPGP_ALGO_RSA:
		return "cse";
	case GCR_OPENPGP_ALGO_RSA_E:
		return "e";
	case GCR_OPENPGP_ALGO_RSA_S:
		return "s";
	case GCR_OPENPGP_ALGO_ELG_E:
		return "e";
	case GCR_OPENPGP_ALGO_DSA:
		return "sca";
	default:
		return "";
	}
}

static gboolean
parse_public_key_or_subkey (GQuark schema,
                            guint n_columns,
                            const guchar *beg,
                            const guchar **at,
                            const guchar *end,
                            GcrOpenpgpParseFlags flags,
                            GPtrArray *records)
{
	gchar *fingerprint = NULL;
	gchar *keyid;
	GcrRecord *record;
	guint8 version;
	guint32 timestamp;
	guint16 ndays = 0;
	guint8 algo;
	guint16 bits;
	gulong expiry;
	const guchar *data;

	/* Start of actual key data in packet */
	data = *at;

	/* First byte is version */
	if (!read_byte (at, end, &version))
		return FALSE;
	if (version < 2 || version > 4)
		return FALSE;

	/* Next a 4 byte create date */
	if (!read_uint32 (at, end, &timestamp))
		return FALSE;
	/* If version 2 or 3, validity days comes next */
	if (version < 4) {
		if (!read_uint16 (at, end, &ndays))
			return FALSE;
	}

	/* Algorithm */
	if (!read_byte (at, end, &algo))
		return FALSE;

	/* For version 2 and 3, only RSA, keyid is low 64-bits of modulus */
	if (version < 4) {
		if (!parse_v3_rsa_bits_and_keyid (at, end, &bits, &keyid))
			return FALSE;

	/* For version 4 */
	} else {
		if (!parse_v4_algo_bits (at, end, algo, &bits))
			return FALSE;
		keyid = hash_v4_keyid (data, *at, &fingerprint);
	}

	record = _gcr_record_new (schema, n_columns, ':');
	_gcr_record_set_uint (record, GCR_RECORD_KEY_BITS, bits);
	_gcr_record_set_uint (record, GCR_RECORD_KEY_ALGO, algo);
	_gcr_record_take_raw (record, GCR_RECORD_KEY_KEYID, keyid);
	_gcr_record_set_ulong (record, GCR_RECORD_KEY_TIMESTAMP, timestamp);
	if (schema != GCR_RECORD_SCHEMA_SEC && schema != GCR_RECORD_SCHEMA_SSB)
		_gcr_record_set_raw (record, GCR_RECORD_PUB_CAPS, default_caps_for_algo (algo));

	if (ndays > 0) {
		expiry = (gulong)timestamp + ((gulong)ndays * 86400);
		_gcr_record_set_ulong (record, GCR_RECORD_KEY_EXPIRY, expiry);
	}

	g_ptr_array_add (records, record);

	if (fingerprint && (schema == GCR_RECORD_SCHEMA_PUB || schema == GCR_RECORD_SCHEMA_SEC)) {
		record = _gcr_record_new (GCR_RECORD_SCHEMA_FPR, GCR_RECORD_FPR_MAX, ':');
		_gcr_record_take_raw (record, GCR_RECORD_FPR_FINGERPRINT, fingerprint);
		g_ptr_array_add (records, record);
		fingerprint = NULL;
	}

	g_free (fingerprint);
	return TRUE;
}

static gboolean
parse_secret_key_or_subkey (GQuark schema,
                            const guchar *beg,
                            const guchar **at,
                            const guchar *end,
                            GcrOpenpgpParseFlags flags,
                            GPtrArray *records)
{
	/*
	 * Identical to a public key, with extra crap after it. The
	 * extra crap is hard to parse and doesn't add anything to
	 * the records, so just skip over it.
	 *
	 * Also don't print out trust, that doesn't make sense for
	 * secret keys.
	 */

	if (!parse_public_key_or_subkey (schema, GCR_RECORD_SEC_MAX,
	                                 beg, at, end, flags, records))
		return FALSE;

	*at = end;
	return TRUE;
}

static gboolean
parse_user_id (const guchar *beg,
               const guchar **at,
               const guchar *end,
               GcrOpenpgpParseFlags flags,
               GPtrArray *records)
{
	gchar *string;
	GcrRecord *record;
	gchar *fingerprint;

	g_assert (at);
	if (!*at || !end || *at > end)
		return FALSE;

	string = g_strndup ((gchar *)*at, end - *at);

	fingerprint = hash_user_id_or_attribute (*at, end);
	record = _gcr_record_new (GCR_RECORD_SCHEMA_UID, GCR_RECORD_UID_MAX, ':');
	_gcr_record_take_raw (record, GCR_RECORD_UID_FINGERPRINT, fingerprint);
	_gcr_record_set_string (record, GCR_RECORD_UID_USERID, string);
	g_free (string);

	g_ptr_array_add (records, record);

	*at = end;
	return TRUE;
}

static gboolean
parse_user_attribute_packet (const guchar *beg,
                             const guchar **at,
                             const guchar *end,
                             guchar subpkt_type,
                             GPtrArray *records)
{
	GcrRecord *record;
	gchar *fingerprint;

	record = _gcr_record_new (GCR_RECORD_SCHEMA_XA1, GCR_RECORD_XA1_MAX, ':');
	_gcr_record_set_uint (record, GCR_RECORD_XA1_LENGTH, end - *at);
	_gcr_record_set_uint (record, GCR_RECORD_XA1_TYPE, subpkt_type);
	fingerprint = hash_user_id_or_attribute (*at, end);
	_gcr_record_take_raw (record, GCR_RECORD_XA1_FINGERPRINT, fingerprint);
	_gcr_record_set_base64 (record, GCR_RECORD_XA1_DATA, *at, end - *at);

	g_ptr_array_add (records, record);

	*at = end;
	return TRUE;
}

static gboolean
parse_user_attribute (const guchar *beg,
                      const guchar **at,
                      const guchar *end,
                      GcrOpenpgpParseFlags flags,
                      GPtrArray *records)
{
	gsize subpkt_len;
	guint count = 0;
	const guchar *start;
	const guchar *subpkt_beg;
	guint8 subpkt_type;
	gchar *fingerprint;
	gchar *string;
	GcrRecord *record;

	start = *at;
	while (*at != end) {
		subpkt_beg = *at;

		if (!read_new_length (at, end, &subpkt_len) ||
		    !read_byte (at, end, &subpkt_type))
			return FALSE;

		count++;

		if (flags & GCR_OPENPGP_PARSE_ATTRIBUTES) {
			if (!parse_user_attribute_packet (subpkt_beg, at,
			                                  *at + (subpkt_len - 1),
			                                  subpkt_type, records))
				return FALSE;

		/* We already progressed one extra byte for the subpkt_type */
		} else {
			*at += (subpkt_len - 1);
		}
	}

	fingerprint = hash_user_id_or_attribute (start, end);
	string = g_strdup_printf ("%d %d", count, (guint)(*at - start));
	record = _gcr_record_new (GCR_RECORD_SCHEMA_UAT, GCR_RECORD_UAT_MAX, ':');
	_gcr_record_take_raw (record, GCR_RECORD_UAT_FINGERPRINT, fingerprint);
	_gcr_record_take_raw (record, GCR_RECORD_UAT_COUNT_SIZE, string);

	g_ptr_array_add (records, record);
	return TRUE;
}

static gboolean
skip_signature_mpis (const guchar **at,
                     const guchar *end,
                     guint8 algo)
{
	switch (algo) {

	/* RSA signature value */
	case GCR_OPENPGP_ALGO_RSA:
		return read_mpi (at, end, NULL, NULL);

	/* DSA values r and s */
	case GCR_OPENPGP_ALGO_DSA:
		return read_mpi (at, end, NULL, NULL) &&
		       read_mpi (at, end, NULL, NULL);
	default:
		return FALSE;
	}
}

static gboolean
parse_v3_signature (const guchar **at,
                    const guchar *end,
                    GcrOpenpgpParseFlags flags,
                    GPtrArray *records)
{
	guchar keyid[8];
	guint8 sig_type;
	guint8 sig_len;
	guint32 sig_time;
	guint8 key_algo;
	guint8 hash_algo;
	guint16 left_bits;
	GcrRecord *record;
	gchar *value;

	if (!read_byte (at, end, &sig_len) || sig_len != 5)
		return FALSE;

	if (!read_byte (at, end, &sig_type) ||
	    !read_uint32 (at, end, &sig_time) ||
	    !read_bytes (at, end, keyid, 8) ||
	    !read_byte (at, end, &key_algo) ||
	    !read_byte (at, end, &hash_algo) ||
	    !read_uint16 (at, end, &left_bits) ||
	    !skip_signature_mpis (at, end, key_algo))
		return FALSE;

	if (flags & GCR_OPENPGP_PARSE_SIGNATURES) {
		record = _gcr_record_new (GCR_RECORD_SCHEMA_SIG, GCR_RECORD_SIG_MAX, ':');
		_gcr_record_set_uint (record, GCR_RECORD_SIG_ALGO, key_algo);
		value = egg_hex_encode_full (keyid, sizeof (keyid), TRUE, NULL, 0);
		_gcr_record_take_raw (record, GCR_RECORD_SIG_KEYID, value);
		_gcr_record_set_ulong (record, GCR_RECORD_SIG_TIMESTAMP, sig_time);
		value = g_strdup_printf ("%02xx", (guint)sig_type);
		_gcr_record_take_raw (record, GCR_RECORD_SIG_CLASS, value);
		g_ptr_array_add (records, record);
	}

	return TRUE;
}

typedef struct {
	gulong key_expiry;
	gboolean exportable;
	gboolean primary;
	guint8 key_flags;
	GcrRecord *revocation;
} SigSubpacket;

static gboolean
parse_v4_signature_revocation (const guchar **at,
                               const guchar *end,
                               GcrRecord *revocation)
{
	guchar fingerprint[20];
	gchar *value;
	guint8 klass;
	guint8 algo;

	if (!read_byte (at, end, &klass) ||
	    !read_byte (at, end, &algo) ||
	    !read_bytes (at, end, fingerprint, 20))
		return FALSE;

	_gcr_record_set_uint (revocation, GCR_RECORD_RVK_ALGO, algo);
	value = egg_hex_encode_full (fingerprint, 20, TRUE, NULL, 0);
	_gcr_record_take_raw (revocation, GCR_RECORD_RVK_FINGERPRINT, value);
	value = g_strdup_printf ("%02X", (guint)klass);
	_gcr_record_take_raw (revocation, GCR_RECORD_RVK_CLASS, value);

	return TRUE;
}

static gboolean
parse_v4_signature_subpacket (const guchar **at,
                              const guchar *end,
                              guint8 sub_type,
                              GcrRecord *record,
                              SigSubpacket *subpkt)
{
	guchar keyid[8];
	guint32 when;
	guint8 byte;
	gboolean critical;
	gchar *value;

	critical = (sub_type & 0x80) ? TRUE : FALSE;
	sub_type &= ~0xC0;

	switch (sub_type) {
	case OPENPGP_SIG_CREATION:
		if (!read_uint32 (at, end, &when))
			return FALSE;
		_gcr_record_set_ulong (record, GCR_RECORD_SIG_TIMESTAMP, when);
		return TRUE;
	case OPENPGP_SIG_ISSUER:
		if (!read_bytes (at, end, keyid, 8))
			return FALSE;
		value = egg_hex_encode_full (keyid, 8, TRUE, NULL, 0);
		_gcr_record_take_raw (record, GCR_RECORD_SIG_KEYID, value);
		return TRUE;
	case OPENPGP_SIG_KEY_EXPIRY:
		if (!read_uint32 (at, end, &when))
			return FALSE;
		subpkt->key_expiry = when;
		return TRUE;
	case OPENPGP_SIG_EXPIRY:
		if (!read_uint32 (at, end, &when))
			return FALSE;
		_gcr_record_set_ulong (record, GCR_RECORD_SIG_EXPIRY, when);
		return TRUE;
	case OPENPGP_SIG_EXPORTABLE:
		if (!read_byte (at, end, &byte))
			return FALSE;
		if (byte != 0 && byte != 1)
			return FALSE;
		subpkt->exportable = (byte == 0 ? FALSE : TRUE);
		return TRUE;

	case OPENPGP_SIG_PRIMARY_USERID:
		if (!read_byte (at, end, &byte))
			return FALSE;
		if (byte != 0 && byte != 1)
			return FALSE;
		subpkt->primary = byte;
		return TRUE;

	case OPENPGP_SIG_KEY_FLAGS:
		if (!read_byte (at, end, &byte))
			return FALSE;
		*at = end; /* N octets of flags */
		subpkt->key_flags = byte;
		return TRUE;

	case OPENPGP_SIG_SIGNER_USERID:
		value = g_strndup ((gchar *)*at, end - *at);
		_gcr_record_set_string (record, GCR_RECORD_SIG_USERID, value);
		g_free (value);
		return TRUE;

	case OPENPGP_SIG_REVOCATION_KEY:
		_gcr_record_free (subpkt->revocation);
		subpkt->revocation = _gcr_record_new (GCR_RECORD_SCHEMA_RVK, GCR_RECORD_RVK_MAX, ':');
		return parse_v4_signature_revocation (at, end, subpkt->revocation);

	/* Ignored */
	case OPENPGP_SIG_SYMMETRIC_ALGOS:
	case OPENPGP_SIG_HASH_ALGOS:
	case OPENPGP_SIG_COMPRESSION_ALGOS:
	case OPENPGP_SIG_REVOCABLE:
	case OPENPGP_SIG_TRUST:
	case OPENPGP_SIG_REGULAR_EXPRESSION:
	case OPENPGP_SIG_NOTATION_DATA:
	case OPENPGP_SIG_KEYSERVER_PREFS:
	case OPENPGP_SIG_PREFERRED_KEYSERVER:
	case OPENPGP_SIG_POLICY_URI:
	case OPENPGP_SIG_REVOCATION_REASON:
	case OPENPGP_SIG_FEATURES:
	case OPENPGP_SIG_TARGET:
	case OPENPGP_SIG_EMBEDDED_SIGNATURE:
		*at = end;
		return TRUE;

	/* Unrecognized */
	default:
		/* Critical, but not recognized */
		if (critical)
			return FALSE;
		*at = end;
		return TRUE;
	}

}

static gboolean
parse_v4_signature_subpackets (const guchar **at,
                               const guchar *end,
                               GcrRecord *record,
                               SigSubpacket *subpkt)
{
	gsize length;
	guint8 sub_type;
	const guchar *stop;

	while (*at != end) {
		if (!read_new_length (at, end, &length) ||
		    !read_byte (at, end, &sub_type) ||
		    length == 0)
			return FALSE;

		/* The length includes the sub_type */
		length--;
		stop = *at + length;
		if (stop > end)
			return FALSE;

		/* Actually parse the sub packets */
		if (!parse_v4_signature_subpacket (at, stop, sub_type, record, subpkt))
			return FALSE;
		if (*at != stop)
			return FALSE;
	}

	return TRUE;
}

static GcrRecord *
uid_or_uat_find_for_self_signature (GPtrArray *records,
                                    guint8 sig_type)
{
	GcrRecord *record;
	GQuark schema;

	if (records->len == 0)
		return NULL;

	switch (sig_type) {
	/* Generic certification of a key or userid */
	case 0x10: case 0x11: case 0x12: case 0x13:
		record = records->pdata[records->len - 1];
		schema = _gcr_record_get_schema (record);
		if (schema == GCR_RECORD_SCHEMA_UID ||
		    schema == GCR_RECORD_SCHEMA_UAT)
			return record;
		return NULL;

	default:
		return NULL;
	}

}

static GcrRecord *
key_or_sub_find_for_self_signature (GPtrArray *records,
                                    guint8 sig_type,
                                    const gchar *keyid)
{
	GcrRecord *record;
	const gchar *check;
	GQuark schema;
	gint i;

	if (records->len == 0)
		return NULL;

	switch (sig_type) {
	/* Generic certification of a key or userid */
	case 0x10: case 0x11: case 0x12: case 0x13:
		for (i = records->len - 1; i >= 0; i--) {
			record = records->pdata[i];
			schema = _gcr_record_get_schema (record);
			if (schema == GCR_RECORD_SCHEMA_PUB || schema == GCR_RECORD_SCHEMA_SEC) {
				check = _gcr_record_get_raw (record, GCR_RECORD_KEY_KEYID);
				return (check != NULL && g_str_equal (check, keyid)) ? record : NULL;
			}
		}
		return NULL;

	/* (Primary) Subkey Binding Signature */
	case 0x18: case 0x19:
		record = records->pdata[records->len - 1];
		schema = _gcr_record_get_schema (record);
		if (schema == GCR_RECORD_SCHEMA_SUB)
			return record;
		return NULL;

	default:
		return NULL;
	}
}

static void
pub_or_sub_set_key_caps (GcrRecord *record,
                         guint8 key_flags)
{
	GString *string;
	GQuark schema;

	schema = _gcr_record_get_schema (record);
	if (schema == GCR_RECORD_SCHEMA_SEC || schema == GCR_RECORD_SCHEMA_SSB)
		return;

	string = g_string_sized_new (8);
	if (key_flags & 0x02)
		g_string_append_c (string, 's');
	if (key_flags & 0x01)
		g_string_append_c (string, 'c');
	if (key_flags & 0x04 || key_flags & 0x08)
		g_string_append_c (string, 'e');
	if (key_flags & 0x20)
		g_string_append_c (string, 'a');

	_gcr_record_take_raw (record, GCR_RECORD_PUB_CAPS,
	                      g_string_free (string, FALSE));
}

static gboolean
parse_v4_signature (const guchar **at,
                    const guchar *end,
                    GcrOpenpgpParseFlags flags,
                    GPtrArray *records)
{
	guint8 sig_type;
	guint8 key_algo;
	guint8 hash_algo;
	guint16 hashed_len;
	guint16 unhashed_len;
	guint16 left_bits;
	GcrRecord *record;
	GcrRecord *key, *uid;
	const gchar *keyid;
	gchar *value;
	const guchar *stop;
	gulong timestamp;

	/* Information to transfer back onto the key record */
	SigSubpacket subpkt = { 0, };
	subpkt.exportable = 1;

	if (!read_byte (at, end, &sig_type) ||
	    !read_byte (at, end, &key_algo) ||
	    !read_byte (at, end, &hash_algo) ||
	    !read_uint16 (at, end, &hashed_len))
		return FALSE;

	/* Hashed subpackets which we use */
	record = _gcr_record_new (GCR_RECORD_SCHEMA_SIG, GCR_RECORD_SIG_MAX, ':');
	stop = *at + hashed_len;
	if (stop > end ||
	    !parse_v4_signature_subpackets (at, stop, record, &subpkt)) {
		_gcr_record_free (record);
		_gcr_record_free (subpkt.revocation);
		return FALSE;
	}

	/* Includes unhashed subpackets, which we skip over */
	if (!read_uint16 (at, end, &unhashed_len)) {
		_gcr_record_free (record);
		_gcr_record_free (subpkt.revocation);
		return FALSE;
	}

	stop = *at + unhashed_len;
	if (stop > end ||
	    !parse_v4_signature_subpackets (at, stop, record, &subpkt) ||
	    !read_uint16 (at, end, &left_bits) ||
	    !skip_signature_mpis (at, end, key_algo)) {
		_gcr_record_free (record);
		_gcr_record_free (subpkt.revocation);
		return FALSE;
	}

	if (subpkt.revocation) {
		g_ptr_array_add (records, subpkt.revocation);
		subpkt.revocation = NULL;
	}

	/* Fill in information on previous key or subkey */
	keyid = _gcr_record_get_raw (record, GCR_RECORD_SIG_KEYID);
	key = key_or_sub_find_for_self_signature (records, sig_type, keyid);
	if (key != NULL) {
		if (subpkt.key_expiry != 0) {
			if (_gcr_record_get_ulong (key, GCR_RECORD_KEY_TIMESTAMP, &timestamp))
				_gcr_record_set_ulong (key, GCR_RECORD_KEY_EXPIRY, timestamp + subpkt.key_expiry);
		}
		if (subpkt.key_flags != 0)
			pub_or_sub_set_key_caps (key, subpkt.key_flags);
	}

	if (key && _gcr_record_get_schema (key) == GCR_RECORD_SCHEMA_PUB) {
		uid = uid_or_uat_find_for_self_signature (records, sig_type);
		if (uid != NULL) {
			if (_gcr_record_get_ulong (record, GCR_RECORD_SIG_TIMESTAMP, &timestamp))
				_gcr_record_set_ulong (uid, GCR_RECORD_UID_TIMESTAMP, timestamp);
		}
	}

	if (flags & GCR_OPENPGP_PARSE_SIGNATURES) {
		_gcr_record_set_uint (record, GCR_RECORD_SIG_ALGO, key_algo);
		value = g_strdup_printf ("%02x%s", (guint)sig_type,
		                         subpkt.exportable ? "x" : "l");
		_gcr_record_take_raw (record, GCR_RECORD_SIG_CLASS, value);
		g_ptr_array_add (records, record);
	} else {
		_gcr_record_free (record);
	}

	return TRUE;
}

static gboolean
parse_signature (const guchar *beg,
                 const guchar **at,
                 const guchar *end,
                 GcrOpenpgpParseFlags flags,
                 GPtrArray *records)
{
	guint8 version;

	if (!read_byte (at, end, &version))
		return FALSE;

	if (version == 3)
		return parse_v3_signature (at, end, flags, records);
	else if (version == 4)
		return parse_v4_signature (at, end, flags, records);
	else
		return FALSE;
}

static GcrDataFormat
parse_openpgp_packet (const guchar *beg,
                      const guchar *at,
                      const guchar *end,
                      guint8 pkt_type,
                      GcrOpenpgpParseFlags flags,
                      GPtrArray *records)
{
	gboolean ret;

	switch (pkt_type) {
	case OPENPGP_PKT_PUBLIC_KEY:
		ret = parse_public_key_or_subkey (GCR_RECORD_SCHEMA_PUB, GCR_RECORD_PUB_MAX,
		                                  beg, &at, end, flags, records);
		break;
	case OPENPGP_PKT_PUBLIC_SUBKEY:
		ret = parse_public_key_or_subkey (GCR_RECORD_SCHEMA_SUB, GCR_RECORD_PUB_MAX,
		                                  beg, &at, end, flags, records);
		break;
	case OPENPGP_PKT_USER_ID:
		ret = parse_user_id (beg, &at, end, flags, records);
		break;
	case OPENPGP_PKT_ATTRIBUTE:
		ret = parse_user_attribute (beg, &at, end, flags, records);
		break;
	case OPENPGP_PKT_SIGNATURE:
		ret = parse_signature (beg, &at, end, flags, records);
		break;
	case OPENPGP_PKT_SECRET_KEY:
		ret = parse_secret_key_or_subkey (GCR_RECORD_SCHEMA_SEC,
		                                  beg, &at, end, flags, records);
		break;
	case OPENPGP_PKT_SECRET_SUBKEY:
		ret = parse_secret_key_or_subkey (GCR_RECORD_SCHEMA_SSB,
		                                  beg, &at, end, flags, records);
		break;

	/* Stuff we don't want to be meddling with right now */
	case OPENPGP_PKT_RING_TRUST:
		return GCR_SUCCESS;

	/* Ignore packets we don't understand */
	default:
		return GCR_SUCCESS;
	}

	/* Key packet had extra data */
	if (ret == TRUE && at != end)
		ret = FALSE;

	return ret ? GCR_SUCCESS : GCR_ERROR_FAILURE;
}

static void
append_key_capabilities (GString *string,
                         const gchar *caps)
{
	guint i;
	gchar cap;

	for (i = 0; caps[i] != 0; i++) {
		cap = g_ascii_toupper (caps[i]);
		if (!strchr (string->str, cap))
			g_string_append_c (string, cap);
	}
}

static void
normalize_capabilities (GPtrArray *records)
{
	GString *string;
	GQuark schema;
	const gchar *caps;
	guint i;

	/* Gather the capabilities of all subkeys into the primary key */
	string = g_string_new (_gcr_record_get_raw (records->pdata[0], GCR_RECORD_PUB_CAPS));
	for (i = 0; i < records->len; i++) {
		schema = _gcr_record_get_schema (records->pdata[i]);
		if (schema == GCR_RECORD_SCHEMA_PUB || schema == GCR_RECORD_SCHEMA_SUB) {
			caps = _gcr_record_get_raw (records->pdata[i], GCR_RECORD_PUB_CAPS);
			append_key_capabilities (string, caps);
		}
	}
	_gcr_record_take_raw (records->pdata[0], GCR_RECORD_PUB_CAPS,
	                      g_string_free (string, FALSE));
}

static gboolean
check_key_expiry (GcrRecord *record)
{
	gulong expiry;
	time_t current;

	if (_gcr_record_get_ulong (record, GCR_RECORD_KEY_EXPIRY, &expiry)) {
		if (expiry == 0)
			return FALSE;
		current = time (NULL);
		if (current > expiry)
			return TRUE;
	}

	return FALSE;
}

static void
normalize_key_records (GPtrArray *records)
{
	GQuark schema;
	guchar trust = 0;
	const gchar *prev;
	gboolean force = FALSE;
	guint i;

	if (records->len == 0)
		return;

	schema = _gcr_record_get_schema (records->pdata[0]);
	if (schema == GCR_RECORD_SCHEMA_PUB) {

		if (check_key_expiry (records->pdata[0])) {
			trust = 'e';
			force = TRUE;

		/* Mark public keys as unknown trust */
		} else {
			normalize_capabilities (records);
			trust = 'o';
			force = FALSE;
		}

		/* Ownertrust unknown, new to system */
		_gcr_record_set_char (records->pdata[0], GCR_RECORD_KEY_OWNERTRUST, 'o');

	} else if (schema == GCR_RECORD_SCHEMA_SEC) {

		/* Trust doesn't make sense for secret keys */
		trust = 0;
		force = FALSE;
	}


	/* Setup default trust if necessary */
	if (trust != 0) {
		for (i = 0; i < records->len; i++) {
			if (!force) {
				prev = _gcr_record_get_raw (records->pdata[i], GCR_RECORD_TRUST);
				if (prev != NULL && prev[0])
					continue;
			}
			schema = _gcr_record_get_schema (records->pdata[i]);
			if (schema != GCR_RECORD_SCHEMA_SIG && schema != GCR_RECORD_SCHEMA_FPR)
				_gcr_record_set_char (records->pdata[i], GCR_RECORD_TRUST, trust);
		}
	}
}

typedef struct {
	GcrOpenpgpCallback callback;
	gpointer user_data;
	guint count;
	GBytes *backing;
	GPtrArray *records;
} openpgp_parse_closure;

static void
openpgp_parse_free (gpointer data)
{
	openpgp_parse_closure *closure = data;
	g_ptr_array_unref (closure->records);
	g_bytes_unref (closure->backing);
	g_free (closure);
}

static void
maybe_emit_openpgp_block (openpgp_parse_closure *closure,
                          const guchar *block,
                          const guchar *end)
{
	GBytes *outer;
	gsize length;
	GPtrArray *records;

	if (block == NULL || block == end)
		return;

	g_assert (end != NULL);
	g_assert (end > block);

	length = end - block;
	closure->count++;

	records = closure->records;
	closure->records = g_ptr_array_new_with_free_func (_gcr_record_free);

	outer = g_bytes_new_with_free_func (block, length, (GDestroyNotify)g_bytes_unref,
	                                      g_bytes_ref (closure->backing));
	if (closure->callback)
		(closure->callback) (records, outer, closure->user_data);
	g_bytes_unref (outer);

	g_ptr_array_unref (records);
}

guint
_gcr_openpgp_parse (GBytes *data,
                    GcrOpenpgpParseFlags flags,
                    GcrOpenpgpCallback callback,
                    gpointer user_data)
{
	openpgp_parse_closure *closure;
	const guchar *at;
	const guchar *beg;
	const guchar *end;
	const guchar *block;
	guint8 pkt_type;
	GcrDataError res;
	gsize length;
	gboolean new_key;
	guint ret;

	g_return_val_if_fail (data != NULL, 0);

	/* For libgcrypt */
	_gcr_initialize_library ();

	at = g_bytes_get_data (data, NULL);
	end = at + g_bytes_get_size (data);
	block = NULL;

	closure = g_new0 (openpgp_parse_closure, 1);
	closure->callback = callback;
	closure->user_data = user_data;
	closure->backing = g_bytes_ref (data);
	closure->records = g_ptr_array_new_with_free_func (_gcr_record_free);

	while (at != NULL && at != end) {
		beg = at;
		res = read_openpgp_packet (&at, end, &pkt_type, &length);

		if (res == GCR_SUCCESS) {
			new_key = (pkt_type == OPENPGP_PKT_PUBLIC_KEY ||
			           pkt_type == OPENPGP_PKT_SECRET_KEY);
			if (flags & GCR_OPENPGP_PARSE_KEYS && new_key)
				normalize_key_records (closure->records);
			/* Start of a new set of packets, per key */
			if (!(flags & GCR_OPENPGP_PARSE_KEYS) || new_key) {
				maybe_emit_openpgp_block (closure, block, beg);
				block = beg;
			}
			if (!(flags & GCR_OPENPGP_PARSE_NO_RECORDS))
				parse_openpgp_packet (beg, at, at + length, pkt_type,
				                      flags, closure->records);
		}

		if (res != GCR_SUCCESS) {
			if (block != NULL && block != beg)
				maybe_emit_openpgp_block (closure, block, beg);
			block = NULL;
			break;
		}

		at += length;
	}

	if (flags & GCR_OPENPGP_PARSE_KEYS)
		normalize_key_records (closure->records);
	maybe_emit_openpgp_block (closure, block, at);
	ret = closure->count;
	openpgp_parse_free (closure);
	return ret;
}
