/**
 * WinPR: Windows Portable Runtime
 * Virtual GIDS implementation
 *
 * Copyright 2021 Martin Fleisz <martin.fleisz@thincast.com>
 * Copyright 2023 Armin Novak <anovak@thincast.com>
 * Copyright 2021,2023 Thincast Technologies GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <freerdp/config.h>

#include <winpr/wlog.h>
#include <winpr/stream.h>
#include <winpr/collections.h>

#include <freerdp/crypto/crypto.h>

#include <zlib.h>

#include "../../crypto/certificate.h"
#include "../../crypto/privatekey.h"
#include "smartcard_virtual_gids.h"

#define TAG CHANNELS_TAG("smartcard.vgids")

#define VGIDS_EFID_MASTER 0xA000
#define VGIDS_EFID_COMMON 0xA010
// #define VGIDS_EFID_CARDCF VGIDS_EFID_COMMON
// #define VGIDS_EFID_CARDAPPS VGIDS_EFID_COMMON
// #define VGIDS_EFID_CMAPFILE VGIDS_EFID_COMMON
#define VGIDS_EFID_CARDID 0xA012
// #define VGIDS_EFID_KXC00 VGIDS_EFID_COMMON
#define VGIDS_EFID_CURRENTDF 0x3FFF

#define VGIDS_DO_FILESYSTEMTABLE 0xDF1F
#define VGIDS_DO_KEYMAP 0xDF20
#define VGIDS_DO_CARDID 0xDF20
#define VGIDS_DO_CARDAPPS 0xDF21
#define VGIDS_DO_CARDCF 0xDF22
#define VGIDS_DO_CMAPFILE 0xDF23
#define VGIDS_DO_KXC00 0xDF24

#define VGIDS_CARDID_SIZE 16
#define VGIDS_MAX_PIN_SIZE 127

#define VGIDS_DEFAULT_RETRY_COUNTER 3

#define VGIDS_KEY_TYPE_KEYEXCHANGE 0x9A
// #define VGIDS_KEY_TYPE_SIGNATURE 0x9C

#define VGIDS_ALGID_RSA_1024 0x06
#define VGIDS_ALGID_RSA_2048 0x07
#define VGIDS_ALGID_RSA_3072 0x08
#define VGIDS_ALGID_RSA_4096 0x09

// #define VGIDS_SE_CRT_AUTH 0xA4
#define VGIDS_SE_CRT_SIGN 0xB6
#define VGIDS_SE_CRT_CONF 0xB8

#define VGIDS_SE_ALGOID_CT_PAD_PKCS1 0x40
#define VGIDS_SE_ALGOID_CT_PAD_OAEP 0x80
// #define VGIDS_SE_ALGOID_CT_RSA_1024 0x06
// #define VGIDS_SE_ALGOID_CT_RSA_2048 0x07
// #define VGIDS_SE_ALGOID_CT_RSA_3072 0x08
// #define VGIDS_SE_ALGOID_CT_RSA_4096 0x09

#define VGIDS_SE_ALGOID_DST_PAD_PKCS1 0x40
// #define VGIDS_SE_ALGOID_DST_RSA_1024 0x06
// #define VGIDS_SE_ALGOID_DST_RSA_2048 0x07
// #define VGIDS_SE_ALGOID_DST_RSA_3072 0x08
// #define VGIDS_SE_ALGOID_DST_RSA_4096 0x09
// #define VGIDS_SE_ALGOID_DST_ECDSA_P192 0x0A
// #define VGIDS_SE_ALGOID_DST_ECDSA_P224 0x0B
// #define VGIDS_SE_ALGOID_DST_ECDSA_P256 0x0C
// #define VGIDS_SE_ALGOID_DST_ECDSA_P384 0x0D
// #define VGIDS_SE_ALGOID_DST_ECDSA_P512 0x0E

#define VGIDS_DEFAULT_KEY_REF 0x81

#define ISO_INS_SELECT 0xA4
#define ISO_INS_GETDATA 0xCB
#define ISO_INS_GETRESPONSE 0xC0
#define ISO_INS_MSE 0x22
#define ISO_INS_PSO 0x2A
#define ISO_INS_VERIFY 0x20

#define ISO_STATUS_MORE_DATA 0x6100
#define ISO_STATUS_VERIFYFAILED 0x6300
#define ISO_STATUS_WRONGLC 0x6700
#define ISO_STATUS_COMMANDNOTALLOWED 0x6900
#define ISO_STATUS_SECURITYSTATUSNOTSATISFIED 0x6982
#define ISO_STATUS_AUTHMETHODBLOCKED 0x6983
#define ISO_STATUS_INVALIDCOMMANDDATA 0x6A80
#define ISO_STATUS_FILENOTFOUND 0x6A82
#define ISO_STATUS_INVALIDP1P2 0x6A86
#define ISO_STATUS_INVALIDLC 0x6A87
#define ISO_STATUS_REFERENCEDATANOTFOUND 0x6A88
#define ISO_STATUS_SUCCESS 0x9000

#define ISO_AID_MAX_SIZE 16

#define ISO_FID_MF 0x3F00

struct vgids_ef
{
	UINT16 id;
	UINT16 dirID;
	wStream* data;
};
typedef struct vgids_ef vgidsEF;

struct vgids_se
{
	BYTE crt;    /* control reference template tag */
	BYTE algoId; /* Algorithm ID */
	BYTE keyRef; /* Key reference */
};
typedef struct vgids_se vgidsSE;

struct vgids_context
{
	UINT16 currentDF;
	char* pin;
	UINT16 curRetryCounter;
	UINT16 retryCounter;
	wStream* commandData;
	wStream* responseData;
	BOOL pinVerified;
	vgidsSE currentSE;

	rdpCertificate* certificate;
	rdpPrivateKey* privateKey;

	wArrayList* files;
};

/* PKCS 1.5 DER encoded digest information */
#define VGIDS_MAX_DIGEST_INFO 7

static const BYTE g_PKCS1_SHA1[] = { 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e,
	                                 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14 };
static const BYTE g_PKCS1_SHA224[] = { 0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
	                                   0x65, 0x03, 0x04, 0x02, 0x04, 0x05, 0x00, 0x04, 0x1c };
static const BYTE g_PKCS1_SHA256[] = { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
	                                   0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20 };
static const BYTE g_PKCS1_SHA384[] = { 0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
	                                   0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30 };
static const BYTE g_PKCS1_SHA512[] = { 0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
	                                   0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40 };
static const BYTE g_PKCS1_SHA512_224[] = { 0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60,
	                                       0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02,
	                                       0x05, 0x05, 0x00, 0x04, 0x1c };
static const BYTE g_PKCS1_SHA512_256[] = { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60,
	                                       0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02,
	                                       0x06, 0x05, 0x00, 0x04, 0x20 };

/* Helper struct to map PKCS1.5 digest info to OpenSSL EVP_MD */
struct vgids_digest_info_map
{
	const BYTE* info;
	size_t infoSize;
	const EVP_MD* digest;
};
typedef struct vgids_digest_info_map vgidsDigestInfoMap;

/* MS GIDS AID */
/* xx: Used by the Windows smart card framework for the GIDS version number. This byte must be set
 * to the GIDS specification revision number which is either 0x01 or 0x02.
 * yy: Reserved for use by the card application (set to 01)
 */
static const BYTE g_MsGidsAID[] = {
	0xA0, 0x00, 0x00, 0x03, 0x97, 0x42, 0x54, 0x46, 0x59, 0x02, 0x01
};

/* GIDS APP File Control Parameter:
   FD-Byte (82): 38 (not shareable-DF)
   Sec Attr (8C): 03 30 30  Create/Delete File(03) Ext/User-Auth (30)
*/
static const BYTE g_GidsAppFCP[] = { 0x62, 0x08, 0x82, 0x01, 0x38, 0x8C, 0x03, 0x03, 0x30, 0x30 };
/* GIDS APP File Control Information:
   AppID (4F, Len 0B): A0 00 00 03 97 42 54 46 59 02 01
   Discretionary DOs (73, Len 03): 40 01 C0
      Supported Auth Protocols (40, Len 01): C0 Mutual/External-Auth
 */
static const BYTE g_GidsAppFCI[] = { 0x61, 0x12, 0x4F, 0x0B, 0xA0, 0x00, 0x00, 0x03, 0x97, 0x42,
	                                 0x54, 0x46, 0x59, 0x02, 0x01, 0x73, 0x03, 0x40, 0x01, 0xC0 };

/*
typedef struct
{
    BYTE bVersion; // Cache version
    BYTE bPinsFreshness; // Card PIN
    WORD wContainersFreshness;
    WORD wFilesFreshness;
} CARD_CACHE_FILE_FORMAT, *PCARD_CACHE_FILE_FORMAT; */
static const BYTE g_CardCFContents[] = { 0x00, 0x00, 0x01, 0x00, 0x04, 0x00 };

/* {mscp,0,0,0,0} */
static const BYTE g_CardAppsContents[] = { 0x6d, 0x73, 0x63, 0x70, 0x00, 0x00, 0x00, 0x00 };

#pragma pack(push, 1)

/* Type: CONTAINER_MAP_RECORD (taken from Windows Smart Card Minidriver Specification)

   This structure describes the format of the Base CSP's
   container map file, stored on the card. This is wellknown
   logical file wszCONTAINER_MAP_FILE. The file consists of
   zero or more of these records. */
#define MAX_CONTAINER_NAME_LEN 39

/* This flag is set in the CONTAINER_MAP_RECORD bFlags
   member if the corresponding container is valid and currently
   exists on the card. // If the container is deleted, its
   bFlags field must be cleared. */
#define CONTAINER_MAP_VALID_CONTAINER 1

/* This flag is set in the CONTAINER_MAP_RECORD bFlags
   member if the corresponding container is the default
   container on the card. */
#define CONTAINER_MAP_DEFAULT_CONTAINER 2

struct vgids_container_map_entry
{
	WCHAR wszGuid[MAX_CONTAINER_NAME_LEN + 1];
	BYTE bFlags;
	BYTE bReserved;
	WORD wSigKeySizeBits;
	WORD wKeyExchangeKeySizeBits;
};
typedef struct vgids_container_map_entry vgidsContainerMapEntry;

struct vgids_filesys_table_entry
{
	char directory[9];
	char filename[9];
	UINT16 pad0;
	UINT16 dataObjectIdentifier;
	UINT16 pad1;
	UINT16 fileIdentifier;
	UINT16 unknown;
};
typedef struct vgids_filesys_table_entry vgidsFilesysTableEntry;

struct vgids_keymap_record
{
	UINT32 state;
	BYTE algid;
	BYTE keytype;
	UINT16 keyref;
	UINT16 unknownWithFFFF;
	UINT16 unknownWith0000;
};
typedef struct vgids_keymap_record vgidsKeymapRecord;

#pragma pack(pop)

static void vgids_ef_free(void* ptr);

static vgidsEF* vgids_ef_new(vgidsContext* ctx, USHORT id)
{
	vgidsEF* ef = calloc(1, sizeof(vgidsEF));

	ef->id = id;
	ef->data = Stream_New(nullptr, 1024);
	if (!ef->data)
	{
		WLog_ERR(TAG, "Failed to create file data stream");
		goto create_failed;
	}
	if (!Stream_SetLength(ef->data, 0))
		goto create_failed;

	if (!ArrayList_Append(ctx->files, ef))
	{
		WLog_ERR(TAG, "Failed to add new ef to file list");
		goto create_failed;
	}

	return ef;

create_failed:
	vgids_ef_free(ef);
	return nullptr;
}

static BOOL vgids_write_tlv(wStream* s, UINT16 tag, const void* data, size_t dataSize)
{
	WINPR_ASSERT(dataSize <= UINT16_MAX);

	/* A maximum of 5 additional bytes is needed */
	if (!Stream_EnsureRemainingCapacity(s, dataSize + 5))
	{
		WLog_ERR(TAG, "Failed to ensure capacity of DO stream");
		return FALSE;
	}

	/* BER encoding: If the most-significant bit is set (0x80) the length is encoded in the
	 * remaining bits. So lengths < 128 bytes can be set directly, all others are encoded */
	if (tag > 0xFF)
		Stream_Write_UINT16_BE(s, tag);
	else
		Stream_Write_UINT8(s, (BYTE)tag);
	if (dataSize < 128)
	{
		Stream_Write_UINT8(s, (BYTE)dataSize);
	}
	else if (dataSize < 256)
	{
		Stream_Write_UINT8(s, 0x81);
		Stream_Write_UINT8(s, (BYTE)dataSize);
	}
	else
	{
		Stream_Write_UINT8(s, 0x82);
		Stream_Write_UINT16_BE(s, (UINT16)dataSize);
	}
	Stream_Write(s, data, dataSize);
	Stream_SealLength(s);
	return TRUE;
}

static BOOL vgids_ef_write_do(vgidsEF* ef, UINT16 doID, const void* data, DWORD dataSize)
{
	/* Write DO to end of file: 2-Byte ID, 1-Byte Len, Data */
	return vgids_write_tlv(ef->data, doID, data, dataSize);
}

static BOOL vgids_ef_read_do(vgidsEF* ef, UINT16 doID, BYTE** data, DWORD* dataSize)
{
	/* Read the given DO from the file: 2-Byte ID, 1-Byte Len, Data */
	Stream_ResetPosition(ef->data);

	/* Look for the requested DO */
	while (Stream_GetRemainingLength(ef->data) > 3)
	{
		BYTE len = 0;
		size_t curPos = 0;
		UINT16 doSize = 0;
		UINT16 nextDOID = 0;

		curPos = Stream_GetPosition(ef->data);
		Stream_Read_UINT16_BE(ef->data, nextDOID);
		Stream_Read_UINT8(ef->data, len);
		if ((len & 0x80))
		{
			BYTE lenSize = len & 0x7F;
			if (!Stream_CheckAndLogRequiredLength(TAG, ef->data, lenSize))
				return FALSE;

			switch (lenSize)
			{
				case 1:
					Stream_Read_UINT8(ef->data, doSize);
					break;
				case 2:
					Stream_Read_UINT16_BE(ef->data, doSize);
					break;
				default:
					WLog_ERR(TAG, "Unexpected tag length %" PRIu8, lenSize);
					return FALSE;
			}
		}
		else
			doSize = len;

		if (!Stream_CheckAndLogRequiredLength(TAG, ef->data, doSize))
			return FALSE;

		if (nextDOID == doID)
		{
			BYTE* outData = nullptr;

			/* Include Tag and length in result */
			doSize += (UINT16)(Stream_GetPosition(ef->data) - curPos);
			if (!Stream_SetPosition(ef->data, curPos))
				return FALSE;

			outData = malloc(doSize);
			if (!outData)
			{
				WLog_ERR(TAG, "Failed to allocate output buffer");
				return FALSE;
			}

			Stream_Read(ef->data, outData, doSize);
			*data = outData;
			*dataSize = doSize;
			return TRUE;
		}
		else
		{
			/* Skip DO */
			if (!Stream_SafeSeek(ef->data, doSize))
				return FALSE;
		}
	}

	return FALSE;
}

void vgids_ef_free(void* ptr)
{
	vgidsEF* ef = ptr;
	if (ef)
	{
		Stream_Free(ef->data, TRUE);
		free(ef);
	}
}

static BOOL vgids_prepare_fstable(const vgidsFilesysTableEntry* fstable, DWORD numEntries,
                                  BYTE** outData, DWORD* outDataSize)
{
	/* Filesystem table:
	    BYTE unknown: 0x01
	    Array of vgidsFilesysTableEntry
	*/
	BYTE* data = malloc(sizeof(vgidsFilesysTableEntry) * numEntries + 1);
	if (!data)
	{
		WLog_ERR(TAG, "Failed to allocate filesystem table data blob");
		return FALSE;
	}

	*data = 0x01;
	for (UINT32 i = 0; i < numEntries; ++i)
		memcpy(data + 1 + (sizeof(vgidsFilesysTableEntry) * i), &fstable[i],
		       sizeof(vgidsFilesysTableEntry));

	*outData = data;
	*outDataSize = sizeof(vgidsFilesysTableEntry) * numEntries + 1;

	return TRUE;
}

static BOOL vgids_prepare_certificate(const rdpCertificate* cert, BYTE** kxc, DWORD* kxcSize)
{
	/* Key exchange container:
	    UINT16 compression version: 0001
	    UINT16 source size
	    ZLIB compressed cert
	*/
	uLongf destSize = 0;
	wStream* s = nullptr;
	BYTE* comprData = nullptr;

	WINPR_ASSERT(cert);

	size_t certSize = 0;
	BYTE* certData = freerdp_certificate_get_der(cert, &certSize);
	if (!certData || (certSize == 0) || (certSize > UINT16_MAX))
	{
		WLog_ERR(TAG, "Failed to get certificate size");
		goto handle_error;
	}

	comprData = malloc(certSize);
	if (!comprData)
	{
		WLog_ERR(TAG, "Failed to allocate certificate buffer");
		goto handle_error;
	}

	/* compress certificate data */
	destSize = WINPR_ASSERTING_INT_CAST(uint16_t, certSize);
	if (compress(comprData, &destSize, certData, WINPR_ASSERTING_INT_CAST(uint16_t, certSize)) !=
	    Z_OK)
	{
		WLog_ERR(TAG, "Failed to compress certificate data");
		goto handle_error;
	}

	/* Write container data */
	s = Stream_New(nullptr, destSize + 4);
	Stream_Write_UINT16(s, 0x0001);
	Stream_Write_UINT16(s, WINPR_ASSERTING_INT_CAST(uint16_t, certSize));
	Stream_Write(s, comprData, destSize);
	Stream_SealLength(s);

	*kxc = Stream_Buffer(s);
	*kxcSize = (DWORD)Stream_Length(s);

	Stream_Free(s, FALSE);
	free(certData);
	free(comprData);
	return TRUE;

handle_error:
	Stream_Free(s, TRUE);
	free(certData);
	free(comprData);
	return FALSE;
}

static size_t get_rsa_key_size(const rdpPrivateKey* privateKey)
{
	WINPR_ASSERT(privateKey);

	return freerdp_key_get_bits(privateKey) / 8;
}

static BYTE vgids_get_algid(vgidsContext* p_Ctx)
{
	WINPR_ASSERT(p_Ctx);

	switch (get_rsa_key_size(p_Ctx->privateKey))
	{
		case (1024 / 8):
			return VGIDS_ALGID_RSA_1024;
		case (2048 / 8):
			return VGIDS_ALGID_RSA_2048;
		case (3072 / 8):
			return VGIDS_ALGID_RSA_3072;
		case (4096 / 8):
			return VGIDS_ALGID_RSA_4096;
		default:
			WLog_ERR(TAG, "Failed to determine algid for private key");
			break;
	}

	return 0;
}

static BOOL vgids_prepare_keymap(vgidsContext* context, BYTE** outData, DWORD* outDataSize)
{
	/* Key map record table:
	    BYTE unknown (count?): 0x01
	    Array of vgidsKeymapRecord
	*/
	BYTE* data = nullptr;
	vgidsKeymapRecord record = {
		1,                                /* state */
		0,                                /* algo */
		VGIDS_KEY_TYPE_KEYEXCHANGE,       /* keytpe */
		(0xB000 | VGIDS_DEFAULT_KEY_REF), /* keyref */
		0xFFFF,                           /* unknown FFFF */
		0x0000                            /* unknown 0000 */
	};

	/* Determine algo */
	BYTE algid = vgids_get_algid(context);
	if (algid == 0)
		return FALSE;

	data = malloc(sizeof(record) + 1);
	if (!data)
	{
		WLog_ERR(TAG, "Failed to allocate filesystem table data blob");
		return FALSE;
	}

	*data = 0x01;
	record.algid = algid;
	memcpy(data + 1, &record, sizeof(record));

	*outData = data;
	*outDataSize = sizeof(record) + 1;

	return TRUE;
}

static BOOL vgids_parse_apdu_header(wStream* s, BYTE* cla, BYTE* ins, BYTE* p1, BYTE* p2, BYTE* lc,
                                    BYTE* le)
{
	if (!Stream_CheckAndLogRequiredLength(TAG, s, 4))
		return FALSE;

	/* Read and verify APDU data */
	if (cla)
		Stream_Read_UINT8(s, *cla);
	else
		Stream_Seek(s, 1);
	if (ins)
		Stream_Read_UINT8(s, *ins);
	else
		Stream_Seek(s, 1);
	if (p1)
		Stream_Read_UINT8(s, *p1);
	else
		Stream_Seek(s, 1);
	if (p2)
		Stream_Read_UINT8(s, *p2);
	else
		Stream_Seek(s, 1);

	/* If LC is requested - check remaining length and read as well */
	if (lc)
	{
		if (!Stream_CheckAndLogRequiredLength(TAG, s, 1))
			return FALSE;

		Stream_Read_UINT8(s, *lc);
		if (!Stream_CheckAndLogRequiredLength(TAG, s, *lc))
			return FALSE;
	}

	/* read LE */
	if (le)
	{
		if (!Stream_CheckAndLogRequiredLength(TAG, s, 1))
			return FALSE;
		Stream_Read_UINT8(s, *le);
	}

	return TRUE;
}

static BOOL vgids_create_response(UINT16 status, const BYTE* answer, DWORD answerSize,
                                  BYTE** outData, DWORD* outDataSize)
{
	BYTE* out = malloc(answerSize + 2);
	if (!out)
	{
		WLog_ERR(TAG, "Failed to allocate memory for response data");
		return FALSE;
	}

	*outData = out;
	if (answer)
	{
		memcpy(out, answer, answerSize);
		out += answerSize;
	}

	*out = (BYTE)((status >> 8) & 0xFF);
	*(out + 1) = (BYTE)(status & 0xFF);
	*outDataSize = answerSize + 2;
	return TRUE;
}

static BOOL vgids_read_do_fkt(void* data, size_t index, va_list ap)
{
	BYTE* response = nullptr;
	DWORD responseSize = 0;
	vgidsEF* file = (vgidsEF*)data;
	vgidsContext* context = va_arg(ap, vgidsContext*);
	UINT16 efID = (UINT16)va_arg(ap, unsigned);
	UINT16 doID = (UINT16)va_arg(ap, unsigned);
	WINPR_UNUSED(index);

	if (efID == 0x3FFF || efID == file->id)
	{
		/* If the DO was successfully read - abort file enum */
		if (vgids_ef_read_do(file, doID, &response, &responseSize))
		{
			context->responseData = Stream_New(response, (size_t)responseSize);
			if (!context->responseData)
				free(response);
			return FALSE;
		}
	}

	return TRUE;
}

static BOOL vgids_read_do(vgidsContext* context, UINT16 efID, UINT16 doID)
{
	return ArrayList_ForEach(context->files, vgids_read_do_fkt, context, efID, doID);
}

static void vgids_reset_context_response(vgidsContext* context)
{
	Stream_Free(context->responseData, TRUE);
	context->responseData = nullptr;
}

static void vgids_reset_context_command_data(vgidsContext* context)
{
	Stream_Free(context->commandData, TRUE);
	context->commandData = nullptr;
}

static BOOL vgids_ins_select(vgidsContext* context, wStream* s, BYTE** response,
                             DWORD* responseSize)
{
	BYTE p1 = 0;
	BYTE p2 = 0;
	BYTE lc = 0;
	DWORD resultDataSize = 0;
	const BYTE* resultData = nullptr;
	UINT16 status = ISO_STATUS_SUCCESS;

	/* The only select operations performed are either select by AID or select 3FFF (return
	 * information about the currently selected DF) */
	if (!vgids_parse_apdu_header(s, nullptr, nullptr, &p1, &p2, &lc, nullptr))
		return FALSE;

	/* Check P1 for selection mode */
	switch (p1)
	{
		/* Select by AID */
		case 0x04:
		{
			/* read AID from APDU */
			BYTE aid[ISO_AID_MAX_SIZE] = WINPR_C_ARRAY_INIT;
			if (lc > ISO_AID_MAX_SIZE)
			{
				WLog_ERR(TAG, "The LC byte is greater than the maximum AID length");
				status = ISO_STATUS_INVALIDLC;
				break;
			}

			/* Check if we select MS GIDS App (only one we know) */
			Stream_Read(s, aid, lc);
			if ((lc > sizeof(g_MsGidsAID)) || (memcmp(aid, g_MsGidsAID, lc) != 0))
			{
				status = ISO_STATUS_FILENOTFOUND;
				break;
			}

			/* Return FCI or FCP for MsGids App */
			switch (p2)
			{
				/* Return FCI information */
				case 0x00:
				{
					resultData = g_GidsAppFCI;
					resultDataSize = sizeof(g_GidsAppFCI);
					break;
				}
				/* Return FCP information */
				case 0x04:
				{
					resultData = g_GidsAppFCP;
					resultDataSize = sizeof(g_GidsAppFCP);
					break;
				}
				default:
					status = ISO_STATUS_INVALIDP1P2;
					break;
			}

			if (resultData)
				context->currentDF = ISO_FID_MF;
			break;
		}
		/* Select by FID */
		case 0x00:
		{
			/* read FID from APDU */
			UINT16 fid = 0;
			if (lc > 2)
			{
				WLog_ERR(TAG, "The LC byte for the file ID is greater than 2");
				status = ISO_STATUS_INVALIDLC;
				break;
			}

			Stream_Read_UINT16_BE(s, fid);
			if (fid != VGIDS_EFID_CURRENTDF || context->currentDF == 0)
			{
				status = ISO_STATUS_FILENOTFOUND;
				break;
			}
			break;
		}
		default:
		{
			/* P1 P2 combination not supported */
			status = ISO_STATUS_INVALIDP1P2;
			break;
		}
	}

	return vgids_create_response(status, resultData, resultDataSize, response, responseSize);
}

static UINT16 vgids_handle_chained_response(vgidsContext* context, const BYTE** response,
                                            DWORD* responseSize)
{
	/* Cap to a maximum of 256 bytes and set status to more data */
	UINT16 status = ISO_STATUS_SUCCESS;
	DWORD remainingBytes = (DWORD)Stream_Length(context->responseData);
	if (remainingBytes > 256)
	{
		status = ISO_STATUS_MORE_DATA;
		remainingBytes = 256;
	}

	*response = Stream_Buffer(context->responseData);
	*responseSize = remainingBytes;
	Stream_Seek(context->responseData, remainingBytes);

	/* Check if there are more than 256 bytes left or if we can already provide the remaining length
	 * in the status word */
	remainingBytes = (DWORD)(Stream_Length(context->responseData) - remainingBytes);
	if (remainingBytes < 256 && remainingBytes != 0)
		status |= (remainingBytes & 0xFF);
	return status;
}

static BOOL vgids_get_public_key(vgidsContext* context, UINT16 doTag)
{
	BOOL rc = FALSE;
	wStream* pubKey = nullptr;
	wStream* response = nullptr;

	WINPR_ASSERT(context);

	/* Get key components */
	size_t nSize = 0;
	size_t eSize = 0;

	char* n = freerdp_certificate_get_param(context->certificate, FREERDP_CERT_RSA_N, &nSize);
	char* e = freerdp_certificate_get_param(context->certificate, FREERDP_CERT_RSA_E, &eSize);

	if (!n || !e)
		goto handle_error;

	pubKey = Stream_New(nullptr, nSize + eSize + 0x10);
	if (!pubKey)
	{
		WLog_ERR(TAG, "Failed to allocate public key stream");
		goto handle_error;
	}

	response = Stream_New(nullptr, Stream_Capacity(pubKey) + 0x10);
	if (!response)
	{
		WLog_ERR(TAG, "Failed to allocate response stream");
		goto handle_error;
	}

	/* write modulus and exponent DOs */
	if (!vgids_write_tlv(pubKey, 0x81, n, nSize))
		goto handle_error;

	if (!vgids_write_tlv(pubKey, 0x82, e, eSize))
		goto handle_error;

	/* write ISO public key template */
	if (!vgids_write_tlv(response, doTag, Stream_Buffer(pubKey), (DWORD)Stream_Length(pubKey)))
		goto handle_error;

	/* set response data */
	Stream_ResetPosition(response);
	context->responseData = response;
	response = nullptr;

	rc = TRUE;
handle_error:
	free(n);
	free(e);
	Stream_Free(pubKey, TRUE);
	Stream_Free(response, TRUE);
	return rc;
}

static BOOL vgids_ins_getdata(vgidsContext* context, wStream* s, BYTE** response,
                              DWORD* responseSize)
{
	UINT16 doId = 0;
	UINT16 fileId = 0;
	BYTE p1 = 0;
	BYTE p2 = 0;
	BYTE lc = 0;
	DWORD resultDataSize = 0;
	const BYTE* resultData = nullptr;
	UINT16 status = ISO_STATUS_SUCCESS;

	/* GetData is called a lot!
	     - To retrieve DOs from files
	     - To retrieve public key information
	*/
	if (!vgids_parse_apdu_header(s, nullptr, nullptr, &p1, &p2, &lc, nullptr))
		return FALSE;

	/* free any previous queried data */
	vgids_reset_context_response(context);

	/* build up file identifier */
	fileId = (UINT16)(((UINT16)p1 << 8) | p2);

	/* Do we have a DO reference? */
	switch (lc)
	{
		case 4:
		{
			BYTE tag = 0;
			BYTE length = 0;
			Stream_Read_UINT8(s, tag);
			Stream_Read_UINT8(s, length);
			if (tag != 0x5C && length != 0x02)
			{
				status = ISO_STATUS_INVALIDCOMMANDDATA;
				break;
			}

			Stream_Read_UINT16_BE(s, doId);

			/* the function only returns if the ID was found in the list and iteration aborted early
			 * or not. we can ignore this here. */
			(void)vgids_read_do(context, fileId, doId);
			break;
		}
		case 0xA:
		{
			UINT16 pubKeyDO = 0;
			BYTE tag = 0;
			BYTE length = 0;
			BYTE keyRef = 0;

			/* We want to retrieve the public key? */
			if (p1 != 0x3F && p2 != 0xFF)
			{
				status = ISO_STATUS_INVALIDP1P2;
				break;
			}

			/* read parent tag/length */
			Stream_Read_UINT8(s, tag);
			Stream_Read_UINT8(s, length);
			if (tag != 0x70 || length != 0x08)
			{
				status = ISO_STATUS_INVALIDCOMMANDDATA;
				break;
			}

			/* read key reference TLV */
			Stream_Read_UINT8(s, tag);
			Stream_Read_UINT8(s, length);
			Stream_Read_UINT8(s, keyRef);
			if (tag != 0x84 || length != 0x01 || keyRef != VGIDS_DEFAULT_KEY_REF)
			{
				status = ISO_STATUS_INVALIDCOMMANDDATA;
				break;
			}

			/* read key value template TLV */
			Stream_Read_UINT8(s, tag);
			Stream_Read_UINT8(s, length);
			if (tag != 0xA5 || length != 0x03)
			{
				status = ISO_STATUS_INVALIDCOMMANDDATA;
				break;
			}

			Stream_Read_UINT16_BE(s, pubKeyDO);
			Stream_Read_UINT8(s, length);
			if (pubKeyDO != 0x7F49 || length != 0x80)
			{
				status = ISO_STATUS_INVALIDCOMMANDDATA;
				break;
			}

			if (!Stream_CheckAndLogRequiredLength(TAG, s, 1))
			{
				status = ISO_STATUS_INVALIDLC;
				break;
			}

			/* Return public key value */
			vgids_get_public_key(context, pubKeyDO);
			break;
		}
		default:
			status = ISO_STATUS_INVALIDCOMMANDDATA;
			break;
	}

	/* If we have response data, make it ready for return */
	if (context->responseData)
		status = vgids_handle_chained_response(context, &resultData, &resultDataSize);
	else if (status == ISO_STATUS_SUCCESS)
		status = ISO_STATUS_REFERENCEDATANOTFOUND;

	return vgids_create_response(status, resultData, resultDataSize, response, responseSize);
}

static BOOL vgids_ins_manage_security_environment(vgidsContext* context, wStream* s,
                                                  BYTE** response, DWORD* responseSize)
{
	BYTE tag = 0;
	BYTE length = 0;
	BYTE p1 = 0;
	BYTE p2 = 0;
	BYTE lc = 0;
	DWORD resultDataSize = 0;
	const BYTE* resultData = nullptr;
	UINT16 status = ISO_STATUS_SUCCESS;

	vgids_reset_context_command_data(context);
	vgids_reset_context_response(context);

	/* Manage security environment prepares the card for performing crypto operations. */
	if (!vgids_parse_apdu_header(s, nullptr, nullptr, &p1, &p2, &lc, nullptr))
		return FALSE;

	/* Check APDU params */
	/* P1: Set Computation, decipherment, Internal Auth */
	/* P2: Digital Signature (B6), Confidentiality (B8) */
	if (p1 != 0x41 && p2 != 0xB6 && p2 != 0xB8)
	{
		status = ISO_STATUS_INVALIDP1P2;
		goto create_response;
	}

	if (lc != 6)
	{
		status = ISO_STATUS_WRONGLC;
		goto create_response;
	}

	context->currentSE.crt = p2;

	/* parse command buffer */
	/* Read algo ID */
	Stream_Read_UINT8(s, tag);
	Stream_Read_UINT8(s, length);
	if (tag != 0x80 || length != 0x01)
	{
		status = ISO_STATUS_INVALIDCOMMANDDATA;
		goto create_response;
	}
	Stream_Read_UINT8(s, context->currentSE.algoId);

	/* Read private key reference */
	Stream_Read_UINT8(s, tag);
	Stream_Read_UINT8(s, length);
	if (tag != 0x84 || length != 0x01)
	{
		status = ISO_STATUS_INVALIDCOMMANDDATA;
		goto create_response;
	}
	Stream_Read_UINT8(s, context->currentSE.keyRef);

create_response:
	/* If an error occurred reset SE */
	if (status != ISO_STATUS_SUCCESS)
		memset(&context->currentSE, 0, sizeof(context->currentSE));
	return vgids_create_response(status, resultData, resultDataSize, response, responseSize);
}

static BOOL vgids_perform_digital_signature(vgidsContext* context)
{
	size_t sigSize = 0;
	size_t msgSize = 0;
	EVP_PKEY_CTX* ctx = nullptr;
	EVP_PKEY* pk = freerdp_key_get_evp_pkey(context->privateKey);
	const vgidsDigestInfoMap gidsDigestInfo[VGIDS_MAX_DIGEST_INFO] = {
		{ g_PKCS1_SHA1, sizeof(g_PKCS1_SHA1), EVP_sha1() },
		{ g_PKCS1_SHA224, sizeof(g_PKCS1_SHA224), EVP_sha224() },
		{ g_PKCS1_SHA256, sizeof(g_PKCS1_SHA256), EVP_sha256() },
		{ g_PKCS1_SHA384, sizeof(g_PKCS1_SHA384), EVP_sha384() },
		{ g_PKCS1_SHA512, sizeof(g_PKCS1_SHA512), EVP_sha512() },
#if OPENSSL_VERSION_NUMBER >= 0x10101000L
		{ g_PKCS1_SHA512_224, sizeof(g_PKCS1_SHA512_224), EVP_sha512_224() },
		{ g_PKCS1_SHA512_256, sizeof(g_PKCS1_SHA512_256), EVP_sha512_256() }
#endif
	};

	if (!pk)
	{
		WLog_ERR(TAG, "Failed to create PKEY");
		return FALSE;
	}

	vgids_reset_context_response(context);

	/* for each digest info */
	Stream_ResetPosition(context->commandData);
	for (int i = 0; i < VGIDS_MAX_DIGEST_INFO; ++i)
	{
		/* have we found our digest? */
		const vgidsDigestInfoMap* digest = &gidsDigestInfo[i];
		if (Stream_Length(context->commandData) >= digest->infoSize &&
		    memcmp(Stream_Buffer(context->commandData), digest->info, digest->infoSize) == 0)
		{
			/* skip digest info and calculate message size */
			Stream_Seek(context->commandData, digest->infoSize);
			if (!Stream_CheckAndLogRequiredLength(TAG, context->commandData, 2))
				goto sign_failed;
			msgSize = Stream_GetRemainingLength(context->commandData);

			/* setup signing context */
			ctx = EVP_PKEY_CTX_new(pk, nullptr);
			if (!ctx)
			{
				WLog_ERR(TAG, "Failed to create signing context");
				goto sign_failed;
			}

			if (EVP_PKEY_sign_init(ctx) <= 0)
			{
				WLog_ERR(TAG, "Failed to init signing context");
				goto sign_failed;
			}

			/* set padding and signature algo */
			if (context->currentSE.algoId & VGIDS_SE_ALGOID_DST_PAD_PKCS1)
			{
				if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0)
				{
					WLog_ERR(TAG, "Failed to set padding mode");
					goto sign_failed;
				}
			}

			if (EVP_PKEY_CTX_set_signature_md(ctx, digest->digest) <= 0)
			{
				WLog_ERR(TAG, "Failed to set signing mode");
				goto sign_failed;
			}

			/* Determine buffer length */
			if (EVP_PKEY_sign(ctx, nullptr, &sigSize, Stream_Pointer(context->commandData),
			                  msgSize) <= 0)
			{
				WLog_ERR(TAG, "Failed to determine signature size");
				goto sign_failed;
			}

			context->responseData = Stream_New(nullptr, sigSize);
			if (!context->responseData)
			{
				WLog_ERR(TAG, "Failed to allocate signing buffer");
				goto sign_failed;
			}

			/* sign */
			if (EVP_PKEY_sign(ctx, Stream_Buffer(context->responseData), &sigSize,
			                  Stream_Pointer(context->commandData), msgSize) <= 0)
			{
				WLog_ERR(TAG, "Failed to create signature");
				goto sign_failed;
			}

			if (!Stream_SetLength(context->responseData, sigSize))
				goto sign_failed;

			EVP_PKEY_CTX_free(ctx);
			break;
		}
	}

	EVP_PKEY_free(pk);
	vgids_reset_context_command_data(context);
	return TRUE;

sign_failed:
	vgids_reset_context_command_data(context);
	vgids_reset_context_response(context);
	EVP_PKEY_CTX_free(ctx);
	EVP_PKEY_free(pk);
	return FALSE;
}

static BOOL vgids_perform_decrypt(vgidsContext* context)
{
	EVP_PKEY_CTX* ctx = nullptr;
	BOOL rc = FALSE;
	int res = 0;
	int padding = RSA_NO_PADDING;

	vgids_reset_context_response(context);

	/* determine padding */
	if (context->currentSE.algoId & VGIDS_SE_ALGOID_CT_PAD_PKCS1)
		padding = RSA_PKCS1_PADDING;
	else if (context->currentSE.algoId & VGIDS_SE_ALGOID_CT_PAD_OAEP)
		padding = RSA_PKCS1_OAEP_PADDING;

	/* init response buffer */
	EVP_PKEY* pkey = freerdp_key_get_evp_pkey(context->privateKey);
	if (!pkey)
		goto decrypt_failed;
	ctx = EVP_PKEY_CTX_new(pkey, nullptr);
	if (!ctx)
		goto decrypt_failed;
	if (EVP_PKEY_decrypt_init(ctx) <= 0)
		goto decrypt_failed;
	if (EVP_PKEY_CTX_set_rsa_padding(ctx, padding) <= 0)
		goto decrypt_failed;

	/* Determine buffer length */
	{
		const size_t inlen = Stream_Length(context->commandData);
		size_t outlen = 0;
		res = EVP_PKEY_decrypt(ctx, nullptr, &outlen, Stream_Buffer(context->commandData), inlen);
		if (res < 0)
		{
			WLog_ERR(TAG, "Failed to decrypt data");
			goto decrypt_failed;
		}

		/* Prepare output buffer */
		context->responseData = Stream_New(nullptr, outlen);

		if (!context->responseData)
		{
			WLog_ERR(TAG, "Failed to create decryption buffer");
			goto decrypt_failed;
		}

		/* Decrypt */
		res = EVP_PKEY_decrypt(ctx, Stream_Buffer(context->responseData), &outlen,
		                       Stream_Buffer(context->commandData), inlen);

		if (res < 0)
		{
			WLog_ERR(TAG, "Failed to decrypt data");
			goto decrypt_failed;
		}

		rc = Stream_SetLength(context->responseData, outlen);
	}

decrypt_failed:
	EVP_PKEY_CTX_free(ctx);
	EVP_PKEY_free(pkey);
	vgids_reset_context_command_data(context);
	if (!rc)
		vgids_reset_context_response(context);
	return rc;
}

static BOOL vgids_ins_perform_security_operation(vgidsContext* context, wStream* s, BYTE** response,
                                                 DWORD* responseSize)
{
	BYTE cla = 0;
	BYTE p1 = 0;
	BYTE p2 = 0;
	BYTE lc = 0;
	DWORD resultDataSize = 0;
	const BYTE* resultData = nullptr;
	UINT16 status = ISO_STATUS_SUCCESS;

	/* Perform security operation */
	if (!vgids_parse_apdu_header(s, &cla, nullptr, &p1, &p2, &lc, nullptr))
		return FALSE;

	if (lc == 0)
	{
		status = ISO_STATUS_WRONGLC;
		goto create_response;
	}

	/* Is our default key referenced? */
	if (context->currentSE.keyRef != VGIDS_DEFAULT_KEY_REF)
	{
		status = ISO_STATUS_SECURITYSTATUSNOTSATISFIED;
		goto create_response;
	}

	/* is the pin protecting the key verified? */
	if (!context->pinVerified)
	{
		status = ISO_STATUS_SECURITYSTATUSNOTSATISFIED;
		goto create_response;
	}

	/* Append the data to the context command buffer (PSO might chain command data) */
	if (!context->commandData)
	{
		context->commandData = Stream_New(nullptr, lc);
		if (!context->commandData)
			return FALSE;
	}
	else if (!Stream_EnsureRemainingCapacity(context->commandData, lc))
		return FALSE;

	Stream_Write(context->commandData, Stream_Pointer(s), lc);
	Stream_SealLength(context->commandData);

	/* Check if the correct operation is requested for our current SE */
	switch (context->currentSE.crt)
	{
		case VGIDS_SE_CRT_SIGN:
		{
			if (p1 != 0x9E || p2 != 0x9A)
			{
				status = ISO_STATUS_INVALIDP1P2;
				break;
			}

			/* If chaining is over perform op */
			if (!(cla & 0x10))
				vgids_perform_digital_signature(context);
			break;
		}
		case VGIDS_SE_CRT_CONF:
		{
			if ((p1 != 0x86 || p2 != 0x80) && (p1 != 0x80 || p2 != 0x86))
			{
				status = ISO_STATUS_INVALIDP1P2;
				break;
			}

			/* If chaining is over perform op */
			if (!(cla & 0x10))
				vgids_perform_decrypt(context);
			break;
		}
		default:
			status = ISO_STATUS_INVALIDP1P2;
			break;
	}

	/* Do chaining of response data if necessary */
	if (status == ISO_STATUS_SUCCESS && context->responseData)
		status = vgids_handle_chained_response(context, &resultData, &resultDataSize);

	/* Check APDU params */
create_response:
	return vgids_create_response(status, resultData, resultDataSize, response, responseSize);
}

static BOOL vgids_ins_getresponse(vgidsContext* context, wStream* s, BYTE** response,
                                  DWORD* responseSize)
{
	BYTE p1 = 0;
	BYTE p2 = 0;
	BYTE le = 0;
	DWORD resultDataSize = 0;
	const BYTE* resultData = nullptr;
	DWORD expectedLen = 0;
	DWORD remainingSize = 0;
	UINT16 status = ISO_STATUS_SUCCESS;

	/* Get response continues data transfer after a previous get data command */
	/* Check if there is any data to transfer left */
	if (!context->responseData || !Stream_CheckAndLogRequiredLength(TAG, context->responseData, 1))
	{
		status = ISO_STATUS_COMMANDNOTALLOWED;
		goto create_response;
	}

	if (!vgids_parse_apdu_header(s, nullptr, nullptr, &p1, &p2, nullptr, &le))
		return FALSE;

	/* Check APDU params */
	if (p1 != 00 || p2 != 0x00)
	{
		status = ISO_STATUS_INVALIDP1P2;
		goto create_response;
	}

	/* LE = 0 means 256 bytes expected */
	expectedLen = le;
	if (expectedLen == 0)
		expectedLen = 256;

	/* prepare response size and update offset */
	remainingSize = (DWORD)Stream_GetRemainingLength(context->responseData);
	if (remainingSize < expectedLen)
		expectedLen = remainingSize;

	resultData = Stream_Pointer(context->responseData);
	resultDataSize = expectedLen;
	Stream_Seek(context->responseData, expectedLen);

	/* If more data is left return 61XX - otherwise 9000 */
	remainingSize = (DWORD)Stream_GetRemainingLength(context->responseData);
	if (remainingSize > 0)
	{
		status = ISO_STATUS_MORE_DATA;
		if (remainingSize < 256)
			status |= (remainingSize & 0xFF);
	}

create_response:
	return vgids_create_response(status, resultData, resultDataSize, response, responseSize);
}

static BOOL vgids_ins_verify(vgidsContext* context, wStream* s, BYTE** response,
                             DWORD* responseSize)
{
	BYTE ins = 0;
	BYTE p1 = 0;
	BYTE p2 = 0;
	BYTE lc = 0;
	UINT16 status = ISO_STATUS_SUCCESS;
	char pin[VGIDS_MAX_PIN_SIZE + 1] = WINPR_C_ARRAY_INIT;

	/* Verify is always called for the application password (PIN) P2=0x80 */
	if (!vgids_parse_apdu_header(s, nullptr, &ins, &p1, &p2, nullptr, nullptr))
		return FALSE;

	/* Check APDU params */
	if (p1 != 00 && p2 != 0x80 && p2 != 0x82)
	{
		status = ISO_STATUS_INVALIDP1P2;
		goto create_response;
	}

	/* shall we reset the security state? */
	if (p2 == 0x82)
	{
		context->pinVerified = FALSE;
		goto create_response;
	}

	/* Check if pin is not already blocked */
	if (context->curRetryCounter == 0)
	{
		status = ISO_STATUS_AUTHMETHODBLOCKED;
		goto create_response;
	}

	/* Read and verify LC */
	if (!Stream_CheckAndLogRequiredLength(TAG, s, 1))
	{
		status = ISO_STATUS_INVALIDLC;
		goto create_response;
	}

	Stream_Read_UINT8(s, lc);
	if (!Stream_CheckAndLogRequiredLength(TAG, s, lc) || (lc > VGIDS_MAX_PIN_SIZE))
	{
		status = ISO_STATUS_INVALIDLC;
		goto create_response;
	}

	/* read and verify pin */
	Stream_Read(s, pin, lc);
	if (strcmp(context->pin, pin) != 0)
	{
		/* retries are encoded in the lowest 4-bit of the status code */
		--context->curRetryCounter;
		context->pinVerified = FALSE;
		status = (ISO_STATUS_VERIFYFAILED | (context->curRetryCounter & 0xFF));
	}
	else
	{
		/* reset retry counter and mark pin as verified */
		context->curRetryCounter = context->retryCounter;
		context->pinVerified = TRUE;
	}

create_response:
	return vgids_create_response(status, nullptr, 0, response, responseSize);
}

vgidsContext* vgids_new(void)
{
	wObject* obj = nullptr;
	vgidsContext* ctx = calloc(1, sizeof(vgidsContext));

	ctx->files = ArrayList_New(FALSE);
	if (!ctx->files)
	{
		WLog_ERR(TAG, "Failed to create files array list");
		goto create_failed;
	}

	obj = ArrayList_Object(ctx->files);
	obj->fnObjectFree = vgids_ef_free;

	return ctx;

create_failed:
	vgids_free(ctx);
	return nullptr;
}

BOOL vgids_init(vgidsContext* ctx, const char* cert, const char* privateKey, const char* pin)
{
	DWORD kxcSize = 0;
	DWORD keymapSize = 0;
	DWORD fsTableSize = 0;
	BOOL rc = FALSE;
	BYTE* kxc = nullptr;
	BYTE* keymap = nullptr;
	BYTE* fsTable = nullptr;
	vgidsEF* masterEF = nullptr;
	vgidsEF* cardidEF = nullptr;
	vgidsEF* commonEF = nullptr;
	BYTE cardid[VGIDS_CARDID_SIZE] = WINPR_C_ARRAY_INIT;
	vgidsContainerMapEntry cmrec = { { 'P', 'r', 'i', 'v', 'a', 't', 'e', ' ', 'K', 'e', 'y', ' ',
		                               '0', '0' },
		                             CONTAINER_MAP_VALID_CONTAINER |
		                                 CONTAINER_MAP_DEFAULT_CONTAINER,
		                             0,
		                             0,
		                             0x00 /* key-size in bits - filled out later */ };
	vgidsFilesysTableEntry filesys[] = {
		{ "mscp", "", 0, 0, 0, 0xA000, 0 },
		{ "", "cardid", 0, 0xDF20, 0, 0xA012, 0 },
		{ "", "cardapps", 0, 0xDF21, 0, 0xA010, 0 },
		{ "", "cardcf", 0, 0xDF22, 0, 0xA010, 0 },
		{ "mscp", "cmapfile", 0, 0xDF23, 0, 0xA010, 0 },
		{ "mscp", "kxc00", 0, 0xDF24, 0, 0xA010, 0 },
	};

	/* Check params */
	if (!cert || !privateKey || !pin)
	{
		WLog_DBG(TAG, "Passed invalid nullptr argument: cert=%p, privateKey=%p, pin=%p",
		         WINPR_CXX_COMPAT_CAST(const void*, cert),
		         WINPR_CXX_COMPAT_CAST(const void*, privateKey),
		         WINPR_CXX_COMPAT_CAST(const void*, pin));
		goto init_failed;
	}

	/* Convert PEM input to DER certificate/public key/private key */
	ctx->certificate = freerdp_certificate_new_from_pem(cert);
	if (!ctx->certificate)
		goto init_failed;

	ctx->privateKey = freerdp_key_new_from_pem_enc(privateKey, nullptr);
	if (!ctx->privateKey)
		goto init_failed;

	/* create masterfile */
	// NOLINTNEXTLINE(clang-analyzer-unix.Malloc)
	masterEF = vgids_ef_new(ctx, VGIDS_EFID_MASTER);
	if (!masterEF)
		goto init_failed;

	/* create cardid file with cardid DO */
	// NOLINTNEXTLINE(clang-analyzer-unix.Malloc)
	cardidEF = vgids_ef_new(ctx, VGIDS_EFID_CARDID);
	if (!cardidEF)
		goto init_failed;
	if (winpr_RAND(cardid, sizeof(cardid)) < 0)
		goto init_failed;
	if (!vgids_ef_write_do(cardidEF, VGIDS_DO_CARDID, cardid, sizeof(cardid)))
		goto init_failed;

	/* create user common file */
	// NOLINTNEXTLINE(clang-analyzer-unix.Malloc)
	commonEF = vgids_ef_new(ctx, VGIDS_EFID_COMMON);
	if (!commonEF)
		goto init_failed;

	/* write card cache DO */
	if (!vgids_ef_write_do(commonEF, VGIDS_DO_CARDCF, g_CardCFContents, sizeof(g_CardCFContents)))
		goto init_failed;

	/* write container map DO */
	{
		const size_t size = get_rsa_key_size(ctx->privateKey);
		if ((size == 0) || (size > UINT16_MAX / 8))
			goto init_failed;

		cmrec.wKeyExchangeKeySizeBits = (WORD)size * 8;
	}
	if (!vgids_ef_write_do(commonEF, VGIDS_DO_CMAPFILE, &cmrec, sizeof(cmrec)))
		goto init_failed;

	/* write cardapps DO */
	if (!vgids_ef_write_do(commonEF, VGIDS_DO_CARDAPPS, g_CardAppsContents,
	                       sizeof(g_CardAppsContents)))
		goto init_failed;

	/* convert and write certificate to key exchange container */
	if (!vgids_prepare_certificate(ctx->certificate, &kxc, &kxcSize))
		goto init_failed;
	if (!vgids_ef_write_do(commonEF, VGIDS_DO_KXC00, kxc, kxcSize))
		goto init_failed;

	/* prepare and write file system table */
	if (!vgids_prepare_fstable(filesys, ARRAYSIZE(filesys), &fsTable, &fsTableSize))
		goto init_failed;
	if (!vgids_ef_write_do(masterEF, VGIDS_DO_FILESYSTEMTABLE, fsTable, fsTableSize))
		goto init_failed;

	/* vgids_prepare_keymap and write to masterEF */
	if (!vgids_prepare_keymap(ctx, &keymap, &keymapSize))
		goto init_failed;
	if (!vgids_ef_write_do(masterEF, VGIDS_DO_KEYMAP, keymap, keymapSize))
		goto init_failed;

	/* store user pin */
	ctx->curRetryCounter = ctx->retryCounter = VGIDS_DEFAULT_RETRY_COUNTER;
	ctx->pin = _strdup(pin);
	if (!ctx->pin)
		goto init_failed;

	rc = TRUE;

init_failed:
	// ArrayList_Append in vgids_ef_new takes ownership
	// of cardidEF, commonEF, masterEF
	// NOLINTNEXTLINE(clang-analyzer-unix.Malloc)
	free(kxc);
	free(keymap);
	free(fsTable);
	return rc;
}

BOOL vgids_process_apdu(vgidsContext* context, const BYTE* data, DWORD dataSize, BYTE** response,
                        DWORD* responseSize)
{
	wStream s;
	static int x = 1;

	/* Check params */
	if (!context || !data || !response || !responseSize)
	{
		WLog_ERR(TAG, "Invalid nullptr pointer passed");
		return FALSE;
	}

	if (dataSize < 4)
	{
		WLog_ERR(TAG, "APDU buffer is less than 4 bytes: %" PRIu32, dataSize);
		return FALSE;
	}

	/* Examine INS byte */
	Stream_StaticConstInit(&s, data, dataSize);
	if (x++ == 0xe)
		x = 0xe + 1;
	switch (data[1])
	{
		case ISO_INS_SELECT:
			return vgids_ins_select(context, &s, response, responseSize);
		case ISO_INS_GETDATA:
			return vgids_ins_getdata(context, &s, response, responseSize);
		case ISO_INS_GETRESPONSE:
			return vgids_ins_getresponse(context, &s, response, responseSize);
		case ISO_INS_MSE:
			return vgids_ins_manage_security_environment(context, &s, response, responseSize);
		case ISO_INS_PSO:
			return vgids_ins_perform_security_operation(context, &s, response, responseSize);
		case ISO_INS_VERIFY:
			return vgids_ins_verify(context, &s, response, responseSize);
		default:
			break;
	}

	/* return command not allowed */
	return vgids_create_response(ISO_STATUS_COMMANDNOTALLOWED, nullptr, 0, response, responseSize);
}

void vgids_free(vgidsContext* context)
{
	if (context)
	{
		freerdp_key_free(context->privateKey);
		freerdp_certificate_free(context->certificate);
		Stream_Free(context->commandData, TRUE);
		Stream_Free(context->responseData, TRUE);
		free(context->pin);
		ArrayList_Free(context->files);
		free(context);
	}
}
