/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * Logging in with smartcards
 *
 * Copyright 2022 David Fort <contact@hardening-consulting.com>
 *
 * 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 <string.h>

#include <winpr/error.h>
#include <winpr/ncrypt.h>
#include <winpr/string.h>
#include <winpr/wlog.h>
#include <winpr/crypto.h>
#include <winpr/path.h>

#include <freerdp/log.h>
#include <freerdp/freerdp.h>
#include <winpr/print.h>

#include <freerdp/utils/smartcardlogon.h>
#include <freerdp/crypto/crypto.h>
#include <freerdp/utils/helpers.h>

#include <openssl/obj_mac.h>

#define TAG FREERDP_TAG("smartcardlogon")

struct SmartcardKeyInfo_st
{
	char* certPath;
	char* keyPath;
};

static void delete_file(char* path)
{
	if (!path)
		return;

	/* Overwrite data in files before deletion */
	{
		FILE* fp = winpr_fopen(path, "r+");
		if (fp)
		{
			const char buffer[8192] = WINPR_C_ARRAY_INIT;
			INT64 size = 0;
			int rs = _fseeki64(fp, 0, SEEK_END);
			if (rs == 0)
				size = _ftelli64(fp);
			if (_fseeki64(fp, 0, SEEK_SET) == 0)
			{
				for (INT64 x = 0; x < size; x += sizeof(buffer))
				{
					const size_t dnmemb = (size_t)(size - x);
					const size_t nmemb = MIN(sizeof(buffer), dnmemb);
					const size_t count = fwrite(buffer, nmemb, 1, fp);
					if (count != 1)
						break;
				}
			}

			(void)fclose(fp);
		}
	}

	winpr_DeleteFile(path);
	free(path);
}

static void smartcardKeyInfo_Free(SmartcardKeyInfo* key_info)
{
	if (!key_info)
		return;

	delete_file(key_info->certPath);
	delete_file(key_info->keyPath);

	free(key_info);
}

void smartcardCertInfo_Free(SmartcardCertInfo* scCert)
{
	if (!scCert)
		return;

	free(scCert->csp);
	free(scCert->reader);
	freerdp_certificate_free(scCert->certificate);
	free(scCert->pkinitArgs);
	free(scCert->keyName);
	free(scCert->containerName);
	free(scCert->upn);
	free(scCert->userHint);
	free(scCert->domainHint);
	free(scCert->subject);
	free(scCert->issuer);
	smartcardKeyInfo_Free(scCert->key_info);

	free(scCert);
}

void smartcardCertList_Free(SmartcardCertInfo** pscCert, size_t count)
{
	if (!pscCert)
		return;

	for (size_t i = 0; i < count; i++)
	{
		SmartcardCertInfo* cert = pscCert[i];
		smartcardCertInfo_Free(cert);
	}

	free((void*)pscCert);
}

static BOOL add_cert_to_list(SmartcardCertInfo*** certInfoList, size_t* count,
                             SmartcardCertInfo* certInfo)
{
	size_t curCount = *count;
	SmartcardCertInfo** curInfoList = *certInfoList;

	/* Check if the certificate is already in the list */
	for (size_t i = 0; i < curCount; ++i)
	{
		if (_wcscmp(curInfoList[i]->containerName, certInfo->containerName) == 0)
		{
			smartcardCertInfo_Free(certInfo);
			return TRUE;
		}
	}

	{
		SmartcardCertInfo** tmpInfoList = (SmartcardCertInfo**)realloc(
		    (void*)curInfoList, sizeof(SmartcardCertInfo*) * (curCount + 1));
		if (!tmpInfoList)
		{
			WLog_ERR(TAG, "unable to reallocate certs");
			return FALSE;
		}
		curInfoList = tmpInfoList;
	}

	curInfoList[curCount++] = certInfo;
	*certInfoList = curInfoList;
	*count = curCount;
	return TRUE;
}

static BOOL treat_sc_cert(SmartcardCertInfo* scCert)
{
	WINPR_ASSERT(scCert);

	scCert->upn = freerdp_certificate_get_upn(scCert->certificate);
	if (!scCert->upn)
	{
		WLog_DBG(TAG, "%s has no UPN, trying emailAddress", scCert->keyName);
		scCert->upn = freerdp_certificate_get_email(scCert->certificate);
	}

	if (scCert->upn)
	{
		size_t userLen = 0;
		const char* atPos = strchr(scCert->upn, '@');

		if (!atPos)
		{
			WLog_ERR(TAG, "invalid UPN, for key %s (no @)", scCert->keyName);
			return FALSE;
		}

		userLen = (size_t)(atPos - scCert->upn);
		scCert->userHint = malloc(userLen + 1);
		scCert->domainHint = _strdup(atPos + 1);

		if (!scCert->userHint || !scCert->domainHint)
		{
			WLog_ERR(TAG, "error allocating userHint or domainHint, for key %s", scCert->keyName);
			return FALSE;
		}

		memcpy(scCert->userHint, scCert->upn, userLen);
		scCert->userHint[userLen] = 0;
	}

	scCert->subject = freerdp_certificate_get_subject(scCert->certificate);
	scCert->issuer = freerdp_certificate_get_issuer(scCert->certificate);
	return TRUE;
}

static BOOL set_info_certificate(SmartcardCertInfo* cert, BYTE* certBytes, DWORD cbCertBytes,
                                 const char* userFilter, const char* domainFilter)
{
	if (!winpr_Digest(WINPR_MD_SHA1, certBytes, cbCertBytes, cert->sha1Hash,
	                  sizeof(cert->sha1Hash)))
	{
		WLog_ERR(TAG, "unable to compute certificate sha1 for key %s", cert->keyName);
		return FALSE;
	}

	cert->certificate = freerdp_certificate_new_from_der(certBytes, cbCertBytes);
	if (!cert->certificate)
	{
		WLog_ERR(TAG, "unable to parse X509 certificate for key %s", cert->keyName);
		return FALSE;
	}

	if (!freerdp_certificate_check_eku(cert->certificate, NID_ms_smartcard_login))
	{
		WLog_DBG(TAG, "discarding certificate without Smartcard Login EKU for key %s",
		         cert->keyName);
		return FALSE;
	}

	if (!treat_sc_cert(cert))
	{
		WLog_DBG(TAG, "error treating cert");
		return FALSE;
	}

	if (userFilter && (!cert->upn || (strcmp(cert->upn, userFilter) != 0)))
	{
		if (cert->userHint && strcmp(cert->userHint, userFilter) != 0)
		{
			WLog_DBG(TAG, "discarding non matching cert by user %s@%s", cert->userHint,
			         cert->domainHint);
			return FALSE;
		}
	}

	if (domainFilter && cert->domainHint && strcmp(cert->domainHint, domainFilter) != 0)
	{
		WLog_DBG(TAG, "discarding non matching cert by domain(%s) %s@%s", domainFilter,
		         cert->userHint, cert->domainHint);
		return FALSE;
	}

	return TRUE;
}

#ifndef _WIN32
static BOOL build_pkinit_args(NCRYPT_PROV_HANDLE provider, SmartcardCertInfo* scCert)
{
	/* pkinit args only under windows
	 * 		PKCS11:module_name=opensc-pkcs11.so
	 */
	const char* pkModule = winpr_NCryptGetModulePath(provider);
	size_t size = 0;

	/* Extract the PKCS#11 CKA_ID from keyName (format: \<slotIdHex>\<certIdHex>)
	 * and pass it as certid= so that MIT krb5's PKINIT module can select the
	 * correct certificate when multiple certificates are present on the token.
	 */
	const char* certId = nullptr;
	if (scCert->keyName)
	{
		const char* secondBackslash = strchr(scCert->keyName + 1, '\\');
		if (secondBackslash)
			certId = secondBackslash + 1;
	}

	if (certId && *certId)
		return (winpr_asprintf(&scCert->pkinitArgs, &size,
		                       "PKCS11:module_name=%s:slotid=%" PRIu16 ":certid=%s", pkModule,
		                       (UINT16)scCert->slotId, certId) > 0);

	return (winpr_asprintf(&scCert->pkinitArgs, &size, "PKCS11:module_name=%s:slotid=%" PRIu16,
	                       pkModule, (UINT16)scCert->slotId) > 0);
}
#endif /* _WIN32 */

#ifdef _WIN32
static BOOL list_capi_provider_keys(const rdpSettings* settings, LPCWSTR csp, LPCWSTR scope,
                                    const char* userFilter, const char* domainFilter,
                                    SmartcardCertInfo*** pcerts, size_t* pcount)
{
	BOOL ret = FALSE;
	HCRYPTKEY hKey = 0;
	HCRYPTPROV hProvider = 0;
	SmartcardCertInfo* cert = nullptr;
	BYTE* certBytes = nullptr;
	CHAR* readerName = nullptr;

	if (!CryptAcquireContextW(&hProvider, scope, csp, PROV_RSA_FULL, CRYPT_SILENT))
	{
		WLog_DBG(TAG, "Unable to acquire context: %d", GetLastError());
		goto out;
	}

	cert = calloc(1, sizeof(SmartcardCertInfo));
	if (!cert)
		goto out;

	cert->csp = _wcsdup(csp);
	if (!cert->csp)
		goto out;

	/* ====== retrieve key's reader ====== */
	DWORD dwDataLen = 0;
	if (!CryptGetProvParam(hProvider, PP_SMARTCARD_READER, nullptr, &dwDataLen, 0))
	{
		WLog_DBG(TAG, "Unable to get provider param: %d", GetLastError());
		goto out;
	}

	readerName = malloc(dwDataLen);
	if (!readerName)
		goto out;

	if (!CryptGetProvParam(hProvider, PP_SMARTCARD_READER, readerName, &dwDataLen, 0))
	{
		WLog_DBG(TAG, "Unable to get reader name: %d", GetLastError());
		goto out;
	}

	cert->reader = ConvertUtf8ToWCharAlloc(readerName, nullptr);
	if (!cert->reader)
		goto out;

	/* ====== retrieve key container name ====== */
	dwDataLen = 0;
	if (!CryptGetProvParam(hProvider, PP_CONTAINER, nullptr, &dwDataLen, 0))
	{
		WLog_DBG(TAG, "Unable to get provider param: %d", GetLastError());
		goto out;
	}

	cert->keyName = malloc(dwDataLen);
	if (!cert->keyName)
		goto out;

	if (!CryptGetProvParam(hProvider, PP_CONTAINER, cert->keyName, &dwDataLen, 0))
	{
		WLog_DBG(TAG, "Unable to get container name: %d", GetLastError());
		goto out;
	}

	cert->containerName = ConvertUtf8ToWCharAlloc(cert->keyName, nullptr);
	if (!cert->containerName)
		goto out;

	/* ========= retrieve the certificate ===============*/
	if (!CryptGetUserKey(hProvider, AT_KEYEXCHANGE, &hKey))
	{
		WLog_DBG(TAG, "Unable to get user key for %s: %d", cert->keyName, GetLastError());
		goto out;
	}

	dwDataLen = 0;
	if (!CryptGetKeyParam(hKey, KP_CERTIFICATE, nullptr, &dwDataLen, 0))
	{
		WLog_DBG(TAG, "Unable to get key param for key %s: %d", cert->keyName, GetLastError());
		goto out;
	}

	certBytes = malloc(dwDataLen);
	if (!certBytes)
	{
		WLog_ERR(TAG, "unable to allocate %" PRIu32 " certBytes for key %s", dwDataLen,
		         cert->keyName);
		goto out;
	}

	if (!CryptGetKeyParam(hKey, KP_CERTIFICATE, certBytes, &dwDataLen, 0))
	{
		WLog_ERR(TAG, "unable to retrieve certificate for key %s", cert->keyName);
		goto out;
	}

	if (!set_info_certificate(cert, certBytes, dwDataLen, userFilter, domainFilter))
		goto out;

	if (!add_cert_to_list(pcerts, pcount, cert))
		goto out;

	ret = TRUE;

out:
	free(readerName);
	free(certBytes);
	if (hKey)
		CryptDestroyKey(hKey);
	if (hProvider)
		CryptReleaseContext(hProvider, 0);
	if (!ret)
		smartcardCertInfo_Free(cert);
	return ret;
}
#endif /* _WIN32 */

static BOOL list_provider_keys(WINPR_ATTR_UNUSED const rdpSettings* settings,
                               NCRYPT_PROV_HANDLE provider, LPCWSTR csp, LPCWSTR scope,
                               const char* userFilter, const char* domainFilter,
                               SmartcardCertInfo*** pcerts, size_t* pcount)
{
	BOOL ret = FALSE;
	NCryptKeyName* keyName = nullptr;
	PVOID enumState = nullptr;
	SmartcardCertInfo** cert_list = *pcerts;
	size_t count = *pcount;

	while (NCryptEnumKeys(provider, scope, &keyName, &enumState, NCRYPT_SILENT_FLAG) ==
	       ERROR_SUCCESS)
	{
		NCRYPT_KEY_HANDLE phKey = 0;
		PBYTE certBytes = nullptr;
		DWORD dwFlags = NCRYPT_SILENT_FLAG;
		DWORD cbOutput = 0;
		SmartcardCertInfo* cert = nullptr;
		BOOL haveError = TRUE;
		SECURITY_STATUS status = 0;

		cert = calloc(1, sizeof(SmartcardCertInfo));
		if (!cert)
			goto out;

		cert->keyName = ConvertWCharToUtf8Alloc(keyName->pszName, nullptr);
		if (!cert->keyName)
			goto endofloop;

		WLog_DBG(TAG, "opening key %s", cert->keyName);

		status =
		    NCryptOpenKey(provider, &phKey, keyName->pszName, keyName->dwLegacyKeySpec, dwFlags);
		if (status != ERROR_SUCCESS)
		{
			WLog_DBG(TAG,
			         "unable to NCryptOpenKey(dwLegacyKeySpec=0x%08" PRIx32 " dwFlags=0x%08" PRIx32
			         "), status=%s, skipping",
			         keyName->dwLegacyKeySpec, keyName->dwFlags,
			         winpr_NCryptSecurityStatusError(status));
			goto endofloop;
		}

		cert->csp = _wcsdup(csp);
		if (!cert->csp)
			goto endofloop;

#ifndef _WIN32
		status = NCryptGetProperty(phKey, NCRYPT_WINPR_SLOTID, (PBYTE)&cert->slotId, 4, &cbOutput,
		                           dwFlags);
		if (status != ERROR_SUCCESS)
		{
			WLog_ERR(TAG, "unable to retrieve slotId for key %s, status=%s", cert->keyName,
			         winpr_NCryptSecurityStatusError(status));
			goto endofloop;
		}
#endif /* _WIN32 */

		/* ====== retrieve key's reader ====== */
		cbOutput = 0;
		status = NCryptGetProperty(phKey, NCRYPT_READER_PROPERTY, nullptr, 0, &cbOutput, dwFlags);
		if (status != ERROR_SUCCESS)
		{
			WLog_DBG(TAG, "unable to retrieve reader's name length for key %s", cert->keyName);
			goto endofloop;
		}

		cert->reader = calloc(1, cbOutput + 2);
		if (!cert->reader)
		{
			WLog_ERR(TAG, "unable to allocate reader's name for key %s", cert->keyName);
			goto endofloop;
		}

		status = NCryptGetProperty(phKey, NCRYPT_READER_PROPERTY, (PBYTE)cert->reader, cbOutput + 2,
		                           &cbOutput, dwFlags);
		if (status != ERROR_SUCCESS)
		{
			WLog_ERR(TAG, "unable to retrieve reader's name for key %s", cert->keyName);
			goto endofloop;
		}

		/* ====== retrieve key container name ====== */
		/* When using PKCS11, this will try to return what Windows would use for the key's name */
		cbOutput = 0;
		status = NCryptGetProperty(phKey, NCRYPT_NAME_PROPERTY, nullptr, 0, &cbOutput, dwFlags);
		if (status == ERROR_SUCCESS)
		{
			cert->containerName = calloc(1, cbOutput + sizeof(WCHAR));
			if (!cert->containerName)
			{
				WLog_ERR(TAG, "unable to allocate key container name for key %s", cert->keyName);
				goto endofloop;
			}

			status = NCryptGetProperty(phKey, NCRYPT_NAME_PROPERTY, (BYTE*)cert->containerName,
			                           cbOutput, &cbOutput, dwFlags);
		}

		if (status != ERROR_SUCCESS)
		{
			WLog_ERR(TAG, "unable to retrieve key container name for key %s", cert->keyName);
			goto endofloop;
		}

		/* ========= retrieve the certificate ===============*/
		cbOutput = 0;
		status =
		    NCryptGetProperty(phKey, NCRYPT_CERTIFICATE_PROPERTY, nullptr, 0, &cbOutput, dwFlags);
		if (status != ERROR_SUCCESS)
		{
			/* can happen that key don't have certificates */
			WLog_DBG(TAG, "unable to retrieve certificate property len, status=%s, skipping",
			         winpr_NCryptSecurityStatusError(status));
			goto endofloop;
		}

		certBytes = calloc(1, cbOutput);
		if (!certBytes)
		{
			WLog_ERR(TAG, "unable to allocate %" PRIu32 " certBytes for key %s", cbOutput,
			         cert->keyName);
			goto endofloop;
		}

		status = NCryptGetProperty(phKey, NCRYPT_CERTIFICATE_PROPERTY, certBytes, cbOutput,
		                           &cbOutput, dwFlags);
		if (status != ERROR_SUCCESS)
		{
			WLog_ERR(TAG, "unable to retrieve certificate for key %s", cert->keyName);
			goto endofloop;
		}

		if (!set_info_certificate(cert, certBytes, cbOutput, userFilter, domainFilter))
			goto endofloop;

#ifndef _WIN32
		if (!build_pkinit_args(provider, cert))
		{
			WLog_ERR(TAG, "error build pkinit args");
			goto endofloop;
		}
#endif
		haveError = FALSE;

	endofloop:
		free(certBytes);
		NCryptFreeBuffer(keyName);
		if (phKey)
			NCryptFreeObject((NCRYPT_HANDLE)phKey);

		if (haveError)
			smartcardCertInfo_Free(cert);
		else
		{
			if (!add_cert_to_list(&cert_list, &count, cert))
				goto out;
		}
	}

	ret = TRUE;
out:
	if (count == 0)
	{
		char cspa[128] = WINPR_C_ARRAY_INIT;

		(void)ConvertWCharToUtf8(csp, cspa, sizeof(cspa));
		char scopea[128] = WINPR_C_ARRAY_INIT;
		(void)ConvertWCharToUtf8(scope, scopea, sizeof(scopea));
		WLog_WARN(TAG, "%s [%s] no certificates found", cspa, scopea);
	}
	*pcount = count;
	*pcerts = cert_list;
	NCryptFreeBuffer(enumState);
	return ret;
}

static BOOL smartcard_hw_enumerateCerts(const rdpSettings* settings, LPCWSTR csp,
                                        const char* reader, const char* userFilter,
                                        const char* domainFilter, SmartcardCertInfo*** scCerts,
                                        size_t* retCount)
{
	BOOL ret = FALSE;
	LPWSTR scope = nullptr;
	NCRYPT_PROV_HANDLE provider = 0;
	SECURITY_STATUS status = 0;
	size_t count = 0;
	SmartcardCertInfo** cert_list = nullptr;
	const char* Pkcs11Module = freerdp_settings_get_string(settings, FreeRDP_Pkcs11Module);

	WINPR_ASSERT(scCerts);
	WINPR_ASSERT(retCount);

	if (reader)
	{
		char* scopeStr = nullptr;
		size_t scopeStrLen = 0;
		winpr_asprintf(&scopeStr, &scopeStrLen, "\\\\.\\%s\\", reader);
		if (!scopeStr)
			goto out;

		scope = ConvertUtf8NToWCharAlloc(scopeStr, scopeStrLen, nullptr);
		free(scopeStr);

		if (!scope)
			goto out;
	}

	if (Pkcs11Module)
	{
		/* load a unique CSP by pkcs11 module path */
		LPCSTR paths[] = { Pkcs11Module, nullptr };

		if (!csp)
			csp = MS_SMART_CARD_KEY_STORAGE_PROVIDER;

		status = winpr_NCryptOpenStorageProviderEx(&provider, csp, 0, paths);
		if (status != ERROR_SUCCESS)
		{
			WLog_ERR(TAG, "unable to open provider given by pkcs11 module");
			goto out;
		}

		status = list_provider_keys(settings, provider, csp, scope, userFilter, domainFilter,
		                            &cert_list, &count);
		NCryptFreeObject((NCRYPT_HANDLE)provider);
		if (!status)
		{
			WLog_ERR(TAG, "error listing keys from CSP loaded from %s", Pkcs11Module);
			goto out;
		}
	}
	else
	{
		NCryptProviderName* names = nullptr;
		DWORD nproviders = 0;

#ifdef _WIN32
		/* On Windows, mstsc first enumerates the legacy CAPI providers for usable certificates. */
		DWORD provType, cbProvName = 0;
		for (DWORD i = 0; CryptEnumProvidersW(i, nullptr, 0, &provType, nullptr, &cbProvName); ++i)
		{
			char providerNameStr[256] = WINPR_C_ARRAY_INIT;
			LPWSTR szProvName = malloc(cbProvName * sizeof(WCHAR));
			if (!CryptEnumProvidersW(i, nullptr, 0, &provType, szProvName, &cbProvName))
			{
				free(szProvName);
				break;
			}

			if (ConvertWCharToUtf8(szProvName, providerNameStr, ARRAYSIZE(providerNameStr)) < 0)
			{
				_snprintf(providerNameStr, sizeof(providerNameStr), "<unknown>");
				WLog_ERR(TAG, "unable to convert provider name to char*, will show it as '%s'",
				         providerNameStr);
			}

			WLog_DBG(TAG, "exploring CSP '%s'", providerNameStr);
			if (provType != PROV_RSA_FULL || (csp && _wcscmp(szProvName, csp) != 0))
			{
				WLog_DBG(TAG, "CSP '%s' filtered out", providerNameStr);
				goto end_of_loop;
			}

			if (!list_capi_provider_keys(settings, szProvName, scope, userFilter, domainFilter,
			                             &cert_list, &count))
				WLog_INFO(TAG, "error when retrieving keys in CSP '%s'", providerNameStr);

		end_of_loop:
			free(szProvName);
		}
#endif

		status = NCryptEnumStorageProviders(&nproviders, &names, NCRYPT_SILENT_FLAG);
		if (status != ERROR_SUCCESS)
		{
			WLog_ERR(TAG, "error listing providers");
			goto out;
		}

		for (DWORD i = 0; i < nproviders; i++)
		{
			char providerNameStr[256] = WINPR_C_ARRAY_INIT;
			const NCryptProviderName* name = &names[i];

			if (ConvertWCharToUtf8(name->pszName, providerNameStr, ARRAYSIZE(providerNameStr)) < 0)
			{
				(void)_snprintf(providerNameStr, sizeof(providerNameStr), "<unknown>");
				WLog_ERR(TAG, "unable to convert provider name to char*, will show it as '%s'",
				         providerNameStr);
			}

			WLog_DBG(TAG, "exploring CSP '%s'", providerNameStr);
			if (csp && _wcscmp(name->pszName, csp) != 0)
			{
				WLog_DBG(TAG, "CSP '%s' filtered out", providerNameStr);
				continue;
			}

			status = NCryptOpenStorageProvider(&provider, name->pszName, 0);
			if (status != ERROR_SUCCESS)
				continue;

			if (!list_provider_keys(settings, provider, name->pszName, scope, userFilter,
			                        domainFilter, &cert_list, &count))
				WLog_INFO(TAG, "error when retrieving keys in CSP '%s'", providerNameStr);

			NCryptFreeObject((NCRYPT_HANDLE)provider);
		}

		NCryptFreeBuffer(names);
	}

	*scCerts = cert_list;
	*retCount = count;
	ret = TRUE;

out:
	if (!ret)
		smartcardCertList_Free(cert_list, count);
	free(scope);
	return ret;
}

static char* create_temporary_file(void)
{
	BYTE buffer[32] = WINPR_C_ARRAY_INIT;
	char* path = nullptr;

	if (winpr_RAND(buffer, sizeof(buffer)) < 0)
		return nullptr;

	char* hex = winpr_BinToHexString(buffer, sizeof(buffer), FALSE);
	if (hex)
		path = GetKnownSubPath(KNOWN_PATH_TEMP, hex);
	free(hex);
	return path;
}

static SmartcardCertInfo* smartcardCertInfo_New(const char* privKeyPEM, const char* certPEM)
{
	size_t size = 0;

	WINPR_ASSERT(privKeyPEM);
	WINPR_ASSERT(certPEM);

	SmartcardCertInfo* cert = calloc(1, sizeof(SmartcardCertInfo));
	if (!cert)
		goto fail;

	{
		SmartcardKeyInfo* info = cert->key_info = calloc(1, sizeof(SmartcardKeyInfo));
		if (!info)
			goto fail;

		cert->certificate = freerdp_certificate_new_from_pem(certPEM);
		if (!cert->certificate)
		{
			WLog_ERR(TAG, "unable to read smartcard certificate");
			goto fail;
		}

		if (!treat_sc_cert(cert))
		{
			WLog_ERR(TAG, "unable to treat smartcard certificate");
			goto fail;
		}

		{
			char* str = nullptr;
			size_t len = 0;
			(void)winpr_asprintf(&str, &len, "%s Emulator", freerdp_getApplicationDetailsString());
			if (str)
				cert->reader = ConvertUtf8NToWCharAlloc(str, len, nullptr);
			free(str);
		}
		if (!cert->reader)
			goto fail;

		cert->containerName = ConvertUtf8ToWCharAlloc("Private Key 00", nullptr);
		if (!cert->containerName)
			goto fail;

		/* compute PKINIT args FILE:<cert file>,<key file>
		 *
		 * We need files for PKINIT to read, so write the certificate to some
		 * temporary location and use that.
		 */
		info->keyPath = create_temporary_file();
		WLog_DBG(TAG, "writing PKINIT key to %s", info->keyPath);
		if (!crypto_write_pem(info->keyPath, privKeyPEM, strlen(privKeyPEM)))
			goto fail;

		info->certPath = create_temporary_file();
		WLog_DBG(TAG, "writing PKINIT cert to %s", info->certPath);
		if (!crypto_write_pem(info->certPath, certPEM, strlen(certPEM)))
			goto fail;

		{
			const int res = winpr_asprintf(&cert->pkinitArgs, &size, "FILE:%s,%s", info->certPath,
			                               info->keyPath);
			if (res <= 0)
				goto fail;
		}
	}

	return cert;
fail:
	smartcardCertInfo_Free(cert);
	return nullptr;
}

static BOOL smartcard_sw_enumerateCerts(const rdpSettings* settings, SmartcardCertInfo*** scCerts,
                                        size_t* retCount)
{
	BOOL rc = FALSE;
	SmartcardCertInfo** cert_list = nullptr;

	WINPR_ASSERT(settings);
	WINPR_ASSERT(scCerts);
	WINPR_ASSERT(retCount);

	const char* privKeyPEM = freerdp_settings_get_string(settings, FreeRDP_SmartcardPrivateKey);
	const char* certPEM = freerdp_settings_get_string(settings, FreeRDP_SmartcardCertificate);
	if (!privKeyPEM)
	{
		WLog_ERR(TAG, "Invalid smartcard private key PEM, aborting");
		goto out_error;
	}
	if (!certPEM)
	{
		WLog_ERR(TAG, "Invalid smartcard certificate PEM, aborting");
		goto out_error;
	}

	cert_list = (SmartcardCertInfo**)calloc(1, sizeof(SmartcardCertInfo*));
	if (!cert_list)
		goto out_error;

	{
		SmartcardCertInfo* cert = smartcardCertInfo_New(privKeyPEM, certPEM);
		if (!cert)
			goto out_error;
		cert_list[0] = cert;
	}

	rc = TRUE;
	*scCerts = cert_list;
	*retCount = 1;

out_error:
	if (!rc)
		smartcardCertList_Free(cert_list, 1);
	return rc;
}

BOOL smartcard_enumerateCerts(const rdpSettings* settings, SmartcardCertInfo*** scCerts,
                              size_t* retCount, BOOL gateway)
{
	BOOL ret = 0;
	LPWSTR csp = nullptr;
	const char* ReaderName = freerdp_settings_get_string(settings, FreeRDP_ReaderName);
	const char* CspName = freerdp_settings_get_string(settings, FreeRDP_CspName);
	const char* Username = nullptr;
	const char* Domain = nullptr;

	if (gateway)
	{
		Username = freerdp_settings_get_string(settings, FreeRDP_GatewayUsername);
		Domain = freerdp_settings_get_string(settings, FreeRDP_GatewayDomain);
	}
	else
	{
		Username = freerdp_settings_get_string(settings, FreeRDP_Username);
		Domain = freerdp_settings_get_string(settings, FreeRDP_Domain);
	}

	WINPR_ASSERT(settings);
	WINPR_ASSERT(scCerts);
	WINPR_ASSERT(retCount);

	if (Domain && !strlen(Domain))
		Domain = nullptr;

	if (freerdp_settings_get_bool(settings, FreeRDP_SmartcardEmulation))
		return smartcard_sw_enumerateCerts(settings, scCerts, retCount);

	if (CspName && (!(csp = ConvertUtf8ToWCharAlloc(CspName, nullptr))))
	{
		WLog_ERR(TAG, "error while converting CSP to WCHAR");
		return FALSE;
	}

	ret =
	    smartcard_hw_enumerateCerts(settings, csp, ReaderName, Username, Domain, scCerts, retCount);
	free(csp);
	return ret;
}

static BOOL set_settings_from_smartcard(rdpSettings* settings, FreeRDP_Settings_Keys_String id,
                                        const char* value)
{
	WINPR_ASSERT(settings);

	if (!freerdp_settings_get_string(settings, id) && value)
		if (!freerdp_settings_set_string(settings, id, value))
			return FALSE;

	return TRUE;
}

BOOL smartcard_getCert(const rdpContext* context, SmartcardCertInfo** cert, BOOL gateway)
{
	WINPR_ASSERT(context);

	const freerdp* instance = context->instance;
	rdpSettings* settings = context->settings;
	SmartcardCertInfo** cert_list = nullptr;
	size_t count = 0;

	WINPR_ASSERT(instance);
	WINPR_ASSERT(settings);

	if (!smartcard_enumerateCerts(settings, &cert_list, &count, gateway))
		return FALSE;

	if (count < 1)
	{
		WLog_ERR(TAG, "no suitable smartcard certificates were found");
		return FALSE;
	}

	if (count > UINT32_MAX)
	{
		WLog_ERR(TAG, "smartcard certificate count %" PRIuz " exceeds UINT32_MAX", count);
		return FALSE;
	}

	if (count > 1)
	{
		DWORD index = 0;

		if (!instance->ChooseSmartcard ||
		    !instance->ChooseSmartcard(context->instance, cert_list, (UINT32)count, &index,
		                               gateway))
		{
			WLog_ERR(TAG, "more than one suitable smartcard certificate was found");
			smartcardCertList_Free(cert_list, count);
			return FALSE;
		}
		*cert = cert_list[index];

		for (DWORD i = 0; i < index; i++)
			smartcardCertInfo_Free(cert_list[i]);
		for (DWORD i = index + 1; i < count; i++)
			smartcardCertInfo_Free(cert_list[i]);
	}
	else
		*cert = cert_list[0];

	FreeRDP_Settings_Keys_String username_setting =
	    gateway ? FreeRDP_GatewayUsername : FreeRDP_Username;
	FreeRDP_Settings_Keys_String domain_setting = gateway ? FreeRDP_GatewayDomain : FreeRDP_Domain;

	free((void*)cert_list);

	if (!set_settings_from_smartcard(settings, username_setting, (*cert)->userHint) ||
	    !set_settings_from_smartcard(settings, domain_setting, (*cert)->domainHint))
	{
		WLog_ERR(TAG, "unable to set settings from smartcard!");
		smartcardCertInfo_Free(*cert);
		return FALSE;
	}

	return TRUE;
}
