//
// X509CertificateDatabase.cs
//
// Author: Jeffrey Stedfast <[email protected]>
//
// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

using System;
using System.IO;
using System.Data;
using System.Data.Common;
using System.Collections;
using System.Collections.Generic;

using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Asn1.BC;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.X509.Store;

namespace MimeKit.Cryptography {
	/// <summary>
	/// An X.509 certificate database.
	/// </summary>
	/// <remarks>
	/// An X.509 certificate database is used for storing certificates, metadata related to the certificates
	/// (such as encryption algorithms supported by the associated client), certificate revocation lists (CRLs),
	/// and private keys.
	/// </remarks>
	public abstract class X509CertificateDatabase : IX509CertificateDatabase
	{
		const X509CertificateRecordFields PrivateKeyFields = X509CertificateRecordFields.Certificate | X509CertificateRecordFields.PrivateKey;
		static readonly DerObjectIdentifier DefaultEncryptionAlgorithm = BCObjectIdentifiers.bc_pbe_sha256_pkcs12_aes256_cbc;
		const int DefaultMinIterations = 1024;
		const int DefaultSaltSize = 20;

		readonly char[] passwd;

		/// <summary>
		/// Initializes a new instance of the <see cref="MimeKit.Cryptography.X509CertificateDatabase"/> class.
		/// </summary>
		/// <remarks>
		/// The password is used to encrypt and decrypt private keys in the database and cannot be null.
		/// </remarks>
		/// <param name="password">The password used for encrypting and decrypting the private keys.</param>
		/// <exception cref="System.ArgumentNullException">
		/// <paramref name="password"/> is <c>null.
		/// </exception>
		protected X509CertificateDatabase (string password)
		{
			if (password == null)
				throw new ArgumentNullException (nameof (password));

			EncryptionAlgorithm = DefaultEncryptionAlgorithm;
			MinIterations = DefaultMinIterations;
			SaltSize = DefaultSaltSize;

			passwd = password.ToCharArray ();
		}

		/// <summary>
		/// Releases unmanaged resources and performs other cleanup operations before the
		/// <see cref="MimeKit.Cryptography.X509CertificateDatabase"/> is reclaimed by garbage collection.
		/// </summary>
		/// <remarks>
		/// Releases unmanaged resources and performs other cleanup operations before the
		/// <see cref="MimeKit.Cryptography.X509CertificateDatabase"/> is reclaimed by garbage collection.
		/// </remarks>
		~X509CertificateDatabase ()
		{
			Dispose (false);
		}

		/// <summary>
		/// Gets or sets the algorithm used for encrypting the private keys.
		/// </summary>
		/// <remarks>
		/// <para>The encryption algorithm should be one of the PBE (password-based encryption) algorithms
		/// supported by Bouncy Castle.</para>
		/// <para>The default algorithm is SHA-256 + AES256.</para>
		/// </remarks>
		/// <value>The encryption algorithm.</value>
		protected DerObjectIdentifier EncryptionAlgorithm {
			get; set;
		}

		/// <summary>
		/// Gets or sets the minimum iterations.
		/// </summary>
		/// <remarks>
		/// The default minimum number of iterations is <c>1024</c>.
		/// </remarks>
		/// <value>The minimum iterations.</value>
		protected int MinIterations {
			get; set;
		}

		/// <summary>
		/// Gets or sets the size of the salt.
		/// </summary>
		/// <remarks>
		/// The default salt size is <c>20</c>.
		/// </remarks>
		/// <value>The size of the salt.</value>
		protected int SaltSize {
			get; set;
		}

		static int ReadBinaryBlob (DbDataReader reader, int column, ref byte[] buffer)
		{
#if NETSTANDARD
			buffer = reader.GetFieldValue<byte[]> (column);
			return (int) buffer.Length;
#else
			long nread;

			// first, get the length of the buffer needed
			if ((nread = reader.GetBytes (column, 0, null, 0, buffer.Length)) > buffer.Length)
				Array.Resize (ref buffer, (int) nread);

			// read the certificate data
			return (int) reader.GetBytes (column, 0, buffer, 0, (int) nread);
#endif
		}

		static X509Certificate DecodeCertificate (DbDataReader reader, X509CertificateParser parser, int column, ref byte[] buffer)
		{
			int nread = ReadBinaryBlob (reader, column, ref buffer);

			using (var memory = new MemoryStream (buffer, 0, nread, false)) {
				return parser.ReadCertificate (memory);
			}
		}

		static X509Crl DecodeX509Crl (DbDataReader reader, X509CrlParser parser, int column, ref byte[] buffer)
		{
			int nread = ReadBinaryBlob (reader, column, ref buffer);

			using (var memory = new MemoryStream (buffer, 0, nread, false)) {
				return parser.ReadCrl (memory);
			}
		}

		byte[] EncryptAsymmetricKeyParameter (AsymmetricKeyParameter key)
		{
			var cipher = PbeUtilities.CreateEngine (EncryptionAlgorithm.Id) as IBufferedCipher;
			var keyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo (key);
			var random = new SecureRandom ();
			var salt = new byte[SaltSize];

			if (cipher == null)
				throw new Exception ("Unknown encryption algorithm: " + EncryptionAlgorithm.Id);

			random.NextBytes (salt);

			var pbeParameters = PbeUtilities.GenerateAlgorithmParameters (EncryptionAlgorithm.Id, salt, MinIterations);
			var algorithm = new AlgorithmIdentifier (EncryptionAlgorithm, pbeParameters);
			var cipherParameters = PbeUtilities.GenerateCipherParameters (algorithm, passwd);

			if (cipherParameters == null)
				throw new Exception ("BouncyCastle bug detected: Failed to generate cipher parameters.");

			cipher.Init (true, cipherParameters);

			var encoded = cipher.DoFinal (keyInfo.GetEncoded ());

			var encrypted = new EncryptedPrivateKeyInfo (algorithm, encoded);

			return encrypted.GetEncoded ();
		}

		AsymmetricKeyParameter DecryptAsymmetricKeyParameter (byte[] buffer, int length)
		{
			using (var memory = new MemoryStream (buffer, 0, length, false)) {
				using (var asn1 = new Asn1InputStream (memory)) {
					var sequence = asn1.ReadObject () as Asn1Sequence;
					if (sequence == null)
						return null;

					var encrypted = EncryptedPrivateKeyInfo.GetInstance (sequence);
					var algorithm = encrypted.EncryptionAlgorithm;
					var encoded = encrypted.GetEncryptedData ();

					var cipher = PbeUtilities.CreateEngine (algorithm) as IBufferedCipher;
					if (cipher == null)
						return null;

					var cipherParameters = PbeUtilities.GenerateCipherParameters (algorithm, passwd);

					if (cipherParameters == null)
						throw new Exception ("BouncyCastle bug detected: Failed to generate cipher parameters.");

					cipher.Init (false, cipherParameters);

					var decrypted = cipher.DoFinal (encoded);
					var keyInfo = PrivateKeyInfo.GetInstance (decrypted);

					return PrivateKeyFactory.CreateKey (keyInfo);
				}
			}
		}

		AsymmetricKeyParameter DecodePrivateKey (DbDataReader reader, int column, ref byte[] buffer)
		{
			if (reader.IsDBNull (column))
				return null;

			int nread = ReadBinaryBlob (reader, column, ref buffer);

			return DecryptAsymmetricKeyParameter (buffer, nread);
		}

		object EncodePrivateKey (AsymmetricKeyParameter key)
		{
			return key != null ? (object) EncryptAsymmetricKeyParameter (key) : DBNull.Value;
		}

		static EncryptionAlgorithm[] DecodeEncryptionAlgorithms (DbDataReader reader, int column)
		{
			if (reader.IsDBNull (column))
				return null;

			var algorithms = new List<EncryptionAlgorithm> ();
			var values = reader.GetString (column);

			foreach (var token in values.Split (new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)) {
				EncryptionAlgorithm algorithm;

#if NET_3_5
				try {
					algorithm = (EncryptionAlgorithm) Enum.Parse (typeof (EncryptionAlgorithm), token.Trim (), true);
					algorithms.Add (algorithm);
				} catch (ArgumentException) {
				} catch (OverflowException) {
				}
#else
				if (Enum.TryParse (token.Trim (), true, out algorithm))
					algorithms.Add (algorithm);
#endif
			}

			return algorithms.ToArray ();
		}

		static object EncodeEncryptionAlgorithms (EncryptionAlgorithm[] algorithms)
		{
			if (algorithms == null || algorithms.Length == 0)
				return DBNull.Value;

			var tokens = new string[algorithms.Length];
			for (int i = 0; i < algorithms.Length; i++)
				tokens[i] = algorithms[i].ToString ();

			return string.Join (",", tokens);
		}

		X509CertificateRecord LoadCertificateRecord (DbDataReader reader, X509CertificateParser parser, ref byte[] buffer)
		{
			var record = new X509CertificateRecord ();

			for (int i = 0; i < reader.FieldCount; i++) {
				switch (reader.GetName (i).ToUpperInvariant ()) {
				case "CERTIFICATE":
					record.Certificate = DecodeCertificate (reader, parser, i, ref buffer);
					break;
				case "PRIVATEKEY":
					record.PrivateKey = DecodePrivateKey (reader, i, ref buffer);
					break;
				case "ALGORITHMS":
					record.Algorithms = DecodeEncryptionAlgorithms (reader, i);
					break;
				case "ALGORITHMSUPDATED":
					record.AlgorithmsUpdated = DateTime.SpecifyKind (reader.GetDateTime (i), DateTimeKind.Utc);
					break;
				case "TRUSTED":
					record.IsTrusted = reader.GetBoolean (i);
					break;
				case "ID":
					record.Id = reader.GetInt32 (i);
					break;
				}
			}

			return record;
		}

		X509CrlRecord LoadCrlRecord (DbDataReader reader, X509CrlParser parser, ref byte[] buffer)
		{
			var record = new X509CrlRecord ();

			for (int i = 0; i < reader.FieldCount; i++) {
				switch (reader.GetName (i).ToUpperInvariant ()) {
				case "CRL":
					record.Crl = DecodeX509Crl (reader, parser, i, ref buffer);
					break;
				case "THISUPDATE":
					record.ThisUpdate = DateTime.SpecifyKind (reader.GetDateTime (i), DateTimeKind.Utc);
					break;
				case "NEXTUPDATE":
					record.NextUpdate = DateTime.SpecifyKind (reader.GetDateTime (i), DateTimeKind.Utc);
					break;
				case "DELTA":
					record.IsDelta = reader.GetBoolean (i);
					break;
				case "ID":
					record.Id = reader.GetInt32 (i);
					break;
				}
			}

			return record;
		}

		/// <summary>
		/// Gets the column names for the specified fields.
		/// </summary>
		/// <remarks>
		/// Gets the column names for the specified fields.
		/// </remarks>
		/// <returns>The column names.</returns>
		/// <param name="fields">The fields.</param>
		protected static string[] GetColumnNames (X509CertificateRecordFields fields)
		{
			var columns = new List<string> ();

			if ((fields & X509CertificateRecordFields.Id) != 0)
				columns.Add ("ID");
			if ((fields & X509CertificateRecordFields.Trusted) != 0)
				columns.Add ("TRUSTED");
			if ((fields & X509CertificateRecordFields.Algorithms) != 0)
				columns.Add ("ALGORITHMS");
			if ((fields & X509CertificateRecordFields.AlgorithmsUpdated) != 0)
				columns.Add ("ALGORITHMSUPDATED");
			if ((fields & X509CertificateRecordFields.Certificate) != 0)
				columns.Add ("CERTIFICATE");
			if ((fields & X509CertificateRecordFields.PrivateKey) != 0)
				columns.Add ("PRIVATEKEY");

			return columns.ToArray ();
		}

		/// <summary>
		/// Gets the database command to select the record matching the specified certificate.
		/// </summary>
		/// <remarks>
		/// Gets the database command to select the record matching the specified certificate.
		/// </remarks>
		/// <returns>The database command.</returns>
		/// <param name="certificate">The certificate.</param>
		/// <param name="fields">The fields to return.</param>
		protected abstract DbCommand GetSelectCommand (X509Certificate certificate, X509CertificateRecordFields fields);

		/// <summary>
		/// Gets the database command to select the certificate records for the specified mailbox.
		/// </summary>
		/// <remarks>
		/// Gets the database command to select the certificate records for the specified mailbox.
		/// </remarks>
		/// <returns>The database command.</returns>
		/// <param name="mailbox">The mailbox.</param>
		/// <param name="now">The date and time for which the certificate should be valid.</param>
		/// <param name="requirePrivateKey"><c>true if the certificate must have a private key.
		/// <param name="fields">The fields to return.</param>
		protected abstract DbCommand GetSelectCommand (MailboxAddress mailbox, DateTime now, bool requirePrivateKey, X509CertificateRecordFields fields);

		/// <summary>
		/// Gets the database command to select certificate records matching the specified selector.
		/// </summary>
		/// <remarks>
		/// Gets the database command to select certificate records matching the specified selector.
		/// </remarks>
		/// <returns>The database command.</returns>
		/// <param name="selector">Selector.</param>
		/// <param name="trustedOnly"><c>true if only trusted certificates should be matched.
		/// <param name="requirePrivateKey"><c>true if the certificate must have a private key.
		/// <param name="fields">The fields to return.</param>
		protected abstract DbCommand GetSelectCommand (IX509Selector selector, bool trustedOnly, bool requirePrivateKey, X509CertificateRecordFields fields);

		/// <summary>
		/// Gets the column names for the specified fields.
		/// </summary>
		/// <remarks>
		/// Gets the column names for the specified fields.
		/// </remarks>
		/// <returns>The column names.</returns>
		/// <param name="fields">The fields.</param>
		protected static string[] GetColumnNames (X509CrlRecordFields fields)
		{
			const X509CrlRecordFields all = X509CrlRecordFields.Id | X509CrlRecordFields.IsDelta |
				X509CrlRecordFields.IssuerName | X509CrlRecordFields.ThisUpdate |
				X509CrlRecordFields.NextUpdate | X509CrlRecordFields.Crl;

			if (fields == all)
				return new [] { "*" };

			var columns = new List<string> ();

			if ((fields & X509CrlRecordFields.Id) != 0)
				columns.Add ("ID");
			if ((fields & X509CrlRecordFields.IsDelta) != 0)
				columns.Add ("DELTA");
			if ((fields & X509CrlRecordFields.IssuerName) != 0)
				columns.Add ("ISSUERNAME");
			if ((fields & X509CrlRecordFields.ThisUpdate) != 0)
				columns.Add ("THISUPDATE");
			if ((fields & X509CrlRecordFields.NextUpdate) != 0)
				columns.Add ("NEXTUPDATE");
			if ((fields & X509CrlRecordFields.Crl) != 0)
				columns.Add ("CRL");

			return columns.ToArray ();
		}

		/// <summary>
		/// Gets the database command to select the CRL records matching the specified issuer.
		/// </summary>
		/// <remarks>
		/// Gets the database command to select the CRL records matching the specified issuer.
		/// </remarks>
		/// <returns>The database command.</returns>
		/// <param name="issuer">The issuer.</param>
		/// <param name="fields">The fields to return.</param>
		protected abstract DbCommand GetSelectCommand (X509Name issuer, X509CrlRecordFields fields);

		/// <summary>
		/// Gets the database command to select the record for the specified CRL.
		/// </summary>
		/// <remarks>
		/// Gets the database command to select the record for the specified CRL.
		/// </remarks>
		/// <returns>The database command.</returns>
		/// <param name="crl">The X.509 CRL.</param>
		/// <param name="fields">The fields to return.</param>
		protected abstract DbCommand GetSelectCommand (X509Crl crl, X509CrlRecordFields fields);

		/// <summary>
		/// Gets the database command to select all CRLs in the table.
		/// </summary>
		/// <remarks>
		/// Gets the database command to select all CRLs in the table.
		/// </remarks>
		/// <returns>The database command.</returns>
		protected abstract DbCommand GetSelectAllCrlsCommand ();

		/// <summary>
		/// Gets the database command to delete the specified certificate record.
		/// </summary>
		/// <remarks>
		/// Gets the database command to delete the specified certificate record.
		/// </remarks>
		/// <returns>The database command.</returns>
		/// <param name="record">The certificate record.</param>
		protected abstract DbCommand GetDeleteCommand (X509CertificateRecord record);

		/// <summary>
		/// Gets the database command to delete the specified CRL record.
		/// </summary>
		/// <remarks>
		/// Gets the database command to delete the specified CRL record.
		/// </remarks>
		/// <returns>The database command.</returns>
		/// <param name="record">The record.</param>
		protected abstract DbCommand GetDeleteCommand (X509CrlRecord record);

		/// <summary>
		/// Gets the value for the specified column.
		/// </summary>
		/// <remarks>
		/// Gets the value for the specified column.
		/// </remarks>
		/// <returns>The value.</returns>
		/// <param name="record">The certificate record.</param>
		/// <param name="columnName">The column name.</param>
		/// <exception cref="System.ArgumentException">
		/// <paramref name="columnName"/> is not a known column name.
		/// </exception>
		protected object GetValue (X509CertificateRecord record, string columnName)
		{
			switch (columnName) {
			case "ID": return record.Id;
			case "BASICCONSTRAINTS": return record.BasicConstraints;
			case "TRUSTED": return record.IsTrusted;
			case "KEYUSAGE": return (int) record.KeyUsage;
			case "NOTBEFORE": return record.NotBefore.ToUniversalTime ();
			case "NOTAFTER": return record.NotAfter.ToUniversalTime ();
			case "ISSUERNAME": return record.IssuerName;
			case "SERIALNUMBER": return record.SerialNumber;
			case "SUBJECTEMAIL": return record.SubjectEmail != null ? record.SubjectEmail.ToLowerInvariant () : string.Empty;
			case "FINGERPRINT": return record.Fingerprint.ToLowerInvariant ();
			case "ALGORITHMS": return EncodeEncryptionAlgorithms (record.Algorithms);
			case "ALGORITHMSUPDATED": return record.AlgorithmsUpdated;
			case "CERTIFICATE": return record.Certificate.GetEncoded ();
			case "PRIVATEKEY": return EncodePrivateKey (record.PrivateKey);
			default: throw new ArgumentException (string.Format ("Unknown column name: {0}", columnName), nameof (columnName));
			}
		}

		/// <summary>
		/// Gets the value for the specified column.
		/// </summary>
		/// <remarks>
		/// Gets the value for the specified column.
		/// </remarks>
		/// <returns>The value.</returns>
		/// <param name="record">The CRL record.</param>
		/// <param name="columnName">The column name.</param>
		/// <exception cref="System.ArgumentException">
		/// <paramref name="columnName"/> is not a known column name.
		/// </exception>
		protected static object GetValue (X509CrlRecord record, string columnName)
		{
			switch (columnName) {
			case "ID": return record.Id;
			case "DELTA": return record.IsDelta;
			case "ISSUERNAME": return record.IssuerName;
			case "THISUPDATE": return record.ThisUpdate;
			case "NEXTUPDATE": return record.NextUpdate;
			case "CRL": return record.Crl.GetEncoded ();
			default: throw new ArgumentException (string.Format ("Unknown column name: {0}", columnName), nameof (columnName));
			}
		}

		/// <summary>
		/// Gets the database command to insert the specified certificate record.
		/// </summary>
		/// <remarks>
		/// Gets the database command to insert the specified certificate record.
		/// </remarks>
		/// <returns>The database command.</returns>
		/// <param name="record">The certificate record.</param>
		protected abstract DbCommand GetInsertCommand (X509CertificateRecord record);

		/// <summary>
		/// Gets the database command to insert the specified CRL record.
		/// </summary>
		/// <remarks>
		/// Gets the database command to insert the specified CRL record.
		/// </remarks>
		/// <returns>The database command.</returns>
		/// <param name="record">The CRL record.</param>
		protected abstract DbCommand GetInsertCommand (X509CrlRecord record);

		/// <summary>
		/// Gets the database command to update the specified record.
		/// </summary>
		/// <remarks>
		/// Gets the database command to update the specified record.
		/// </remarks>
		/// <returns>The database command.</returns>
		/// <param name="record">The certificate record.</param>
		/// <param name="fields">The fields to update.</param>
		protected abstract DbCommand GetUpdateCommand (X509CertificateRecord record, X509CertificateRecordFields fields);

		/// <summary>
		/// Gets the database command to update the specified CRL record.
		/// </summary>
		/// <remarks>
		/// Gets the database command to update the specified CRL record.
		/// </remarks>
		/// <returns>The database command.</returns>
		/// <param name="record">The CRL record.</param>
		protected abstract DbCommand GetUpdateCommand (X509CrlRecord record);

		/// <summary>
		/// Find the specified certificate.
		/// </summary>
		/// <remarks>
		/// Searches the database for the specified certificate, returning the matching
		/// record with the desired fields populated.
		/// </remarks>
		/// <returns>The matching record if found; otherwise <c>null.
		/// <param name="certificate">The certificate.</param>
		/// <param name="fields">The desired fields.</param>
		/// <exception cref="System.ArgumentNullException">
		/// <paramref name="certificate"/> is <c>null.
		/// </exception>
		public X509CertificateRecord Find (X509Certificate certificate, X509CertificateRecordFields fields)
		{
			if (certificate == null)
				throw new ArgumentNullException (nameof (certificate));

			using (var command = GetSelectCommand (certificate, fields)) {
				var reader = command.ExecuteReader ();

				try {
					if (reader.Read ()) {
						var parser = new X509CertificateParser ();
						var buffer = new byte[4096];

						return LoadCertificateRecord (reader, parser, ref buffer);
					}
				} finally {
#if NETSTANDARD
					reader.Dispose ();
#else
					reader.Close ();
#endif
				}
			}

			return null;
		}

		/// <summary>
		/// Finds the certificates matching the specified selector.
		/// </summary>
		/// <remarks>
		/// Searches the database for certificates matching the selector, returning all
		/// matching certificates.
		/// </remarks>
		/// <returns>The matching certificates.</returns>
		/// <param name="selector">The match selector or <c>null to return all certificates.
		public IEnumerable<X509Certificate> FindCertificates (IX509Selector selector)
		{
			using (var command = GetSelectCommand (selector, false, false, X509CertificateRecordFields.Certificate)) {
				var reader = command.ExecuteReader ();

				try {
					var parser = new X509CertificateParser ();
					var buffer = new byte[4096];

					while (reader.Read ()) {
						var record = LoadCertificateRecord (reader, parser, ref buffer);
						if (selector == null || selector.Match (record.Certificate))
							yield return record.Certificate;
					}
				} finally {
#if NETSTANDARD
					reader.Dispose ();
#else
					reader.Close ();
#endif
				}
			}

			yield break;
		}

		/// <summary>
		/// Finds the private keys matching the specified selector.
		/// </summary>
		/// <remarks>
		/// Searches the database for certificate records matching the selector, returning the
		/// private keys for each matching record.
		/// </remarks>
		/// <returns>The matching certificates.</returns>
		/// <param name="selector">The match selector or <c>null to return all private keys.
		public IEnumerable<AsymmetricKeyParameter> FindPrivateKeys (IX509Selector selector)
		{
			using (var command = GetSelectCommand (selector, false, true, PrivateKeyFields)) {
				var reader = command.ExecuteReader ();

				try {
					var parser = new X509CertificateParser ();
					var buffer = new byte[4096];

					while (reader.Read ()) {
						var record = LoadCertificateRecord (reader, parser, ref buffer);

						if (selector == null || selector.Match (record.Certificate))
							yield return record.PrivateKey;
					}
				} finally {
#if NETSTANDARD
					reader.Dispose ();
#else
					reader.Close ();
#endif
				}
			}

			yield break;
		}

		/// <summary>
		/// Finds the certificate records for the specified mailbox.
		/// </summary>
		/// <remarks>
		/// Searches the database for certificates matching the specified mailbox that are valid
		/// for the date and time specified, returning all matching records populated with the
		/// desired fields.
		/// </remarks>
		/// <returns>The matching certificate records populated with the desired fields.</returns>
		/// <param name="mailbox">The mailbox.</param>
		/// <param name="now">The date and time.</param>
		/// <param name="requirePrivateKey"><c>true if a private key is required.
		/// <param name="fields">The desired fields.</param>
		/// <exception cref="System.ArgumentNullException">
		/// <paramref name="mailbox"/> is <c>null.
		/// </exception>
		public IEnumerable<X509CertificateRecord> Find (MailboxAddress mailbox, DateTime now, bool requirePrivateKey, X509CertificateRecordFields fields)
		{
			if (mailbox == null)
				throw new ArgumentNullException (nameof (mailbox));

			using (var command = GetSelectCommand (mailbox, now, requirePrivateKey, fields)) {
				var reader = command.ExecuteReader ();

				try {
					var parser = new X509CertificateParser ();
					var buffer = new byte[4096];

					while (reader.Read ()) {
						yield return LoadCertificateRecord (reader, parser, ref buffer);
					}
				} finally {
#if NETSTANDARD
					reader.Dispose ();
#else
					reader.Close ();
#endif
				}
			}

			yield break;
		}

		/// <summary>
		/// Finds the certificate records matching the specified selector.
		/// </summary>
		/// <remarks>
		/// Searches the database for certificate records matching the selector, returning all
		/// of the matching records populated with the desired fields.
		/// </remarks>
		/// <returns>The matching certificate records populated with the desired fields.</returns>
		/// <param name="selector">The match selector or <c>null to match all certificates.
		/// <param name="trustedOnly"><c>true if only trusted certificates should be returned.
		/// <param name="fields">The desired fields.</param>
		public IEnumerable<X509CertificateRecord> Find (IX509Selector selector, bool trustedOnly, X509CertificateRecordFields fields)
		{
			using (var command = GetSelectCommand (selector, trustedOnly, false, fields | X509CertificateRecordFields.Certificate)) {
				var reader = command.ExecuteReader ();

				try {
					var parser = new X509CertificateParser ();
					var buffer = new byte[4096];

					while (reader.Read ()) {
						var record = LoadCertificateRecord (reader, parser, ref buffer);

						if (selector == null || selector.Match (record.Certificate))
							yield return record;
					}
				} finally {
#if NETSTANDARD
					reader.Dispose ();
#else
					reader.Close ();
#endif
				}
			}

			yield break;
		}

		/// <summary>
		/// Add the specified certificate record.
		/// </summary>
		/// <remarks>
		/// Adds the specified certificate record to the database.
		/// </remarks>
		/// <param name="record">The certificate record.</param>
		/// <exception cref="System.ArgumentNullException">
		/// <paramref name="record"/> is <c>null.
		/// </exception>
		public void Add (X509CertificateRecord record)
		{
			if (record == null)
				throw new ArgumentNullException (nameof (record));

			using (var command = GetInsertCommand (record)) {
				command.ExecuteNonQuery ();
			}
		}

		/// <summary>
		/// Remove the specified certificate record.
		/// </summary>
		/// <remarks>
		/// Removes the specified certificate record from the database.
		/// </remarks>
		/// <param name="record">The certificate record.</param>
		/// <exception cref="System.ArgumentNullException">
		/// <paramref name="record"/> is <c>null.
		/// </exception>
		public void Remove (X509CertificateRecord record)
		{
			if (record == null)
				throw new ArgumentNullException (nameof (record));

			using (var command = GetDeleteCommand (record)) {
				command.ExecuteNonQuery ();
			}
		}

		/// <summary>
		/// Update the specified certificate record.
		/// </summary>
		/// <remarks>
		/// Updates the specified fields of the record in the database.
		/// </remarks>
		/// <param name="record">The certificate record.</param>
		/// <param name="fields">The fields to update.</param>
		/// <exception cref="System.ArgumentNullException">
		/// <paramref name="record"/> is <c>null.
		/// </exception>
		public void Update (X509CertificateRecord record, X509CertificateRecordFields fields)
		{
			if (record == null)
				throw new ArgumentNullException (nameof (record));

			using (var command = GetUpdateCommand (record, fields)) {
				command.ExecuteNonQuery ();
			}
		}

		/// <summary>
		/// Finds the CRL records for the specified issuer.
		/// </summary>
		/// <remarks>
		/// Searches the database for CRL records matching the specified issuer, returning
		/// all matching records populated with the desired fields.
		/// </remarks>
		/// <returns>The matching CRL records populated with the desired fields.</returns>
		/// <param name="issuer">The issuer.</param>
		/// <param name="fields">The desired fields.</param>
		/// <exception cref="System.ArgumentNullException">
		/// <paramref name="issuer"/> is <c>null.
		/// </exception>
		public IEnumerable<X509CrlRecord> Find (X509Name issuer, X509CrlRecordFields fields)
		{
			if (issuer == null)
				throw new ArgumentNullException (nameof (issuer));

			using (var command = GetSelectCommand (issuer, fields)) {
				var reader = command.ExecuteReader ();

				try {
					var parser = new X509CrlParser ();
					var buffer = new byte[4096];

					while (reader.Read ()) {
						yield return LoadCrlRecord (reader, parser, ref buffer);
					}
				} finally {
#if NETSTANDARD
					reader.Dispose ();
#else
					reader.Close ();
#endif
				}
			}

			yield break;
		}

		/// <summary>
		/// Finds the specified certificate revocation list.
		/// </summary>
		/// <remarks>
		/// Searches the database for the specified CRL, returning the matching record with
		/// the desired fields populated.
		/// </remarks>
		/// <returns>The matching record if found; otherwise <c>null.
		/// <param name="crl">The certificate revocation list.</param>
		/// <param name="fields">The desired fields.</param>
		/// <exception cref="System.ArgumentNullException">
		/// <paramref name="crl"/> is <c>null.
		/// </exception>
		public X509CrlRecord Find (X509Crl crl, X509CrlRecordFields fields)
		{
			if (crl == null)
				throw new ArgumentNullException (nameof (crl));

			using (var command = GetSelectCommand (crl, fields)) {
				var reader = command.ExecuteReader ();

				try {
					if (reader.Read ()) {
						var parser = new X509CrlParser ();
						var buffer = new byte[4096];

						return LoadCrlRecord (reader, parser, ref buffer);
					}
				} finally {
#if NETSTANDARD
					reader.Dispose ();
#else
					reader.Close ();
#endif
				}
			}

			return null;
		}

		/// <summary>
		/// Add the specified CRL record.
		/// </summary>
		/// <remarks>
		/// Adds the specified CRL record to the database.
		/// </remarks>
		/// <param name="record">The CRL record.</param>
		/// <exception cref="System.ArgumentNullException">
		/// <paramref name="record"/> is <c>null.
		/// </exception>
		public void Add (X509CrlRecord record)
		{
			if (record == null)
				throw new ArgumentNullException (nameof (record));

			using (var command = GetInsertCommand (record)) {
				command.ExecuteNonQuery ();
			}
		}

		/// <summary>
		/// Remove the specified CRL record.
		/// </summary>
		/// <remarks>
		/// Removes the specified CRL record from the database.
		/// </remarks>
		/// <param name="record">The CRL record.</param>
		/// <exception cref="System.ArgumentNullException">
		/// <paramref name="record"/> is <c>null.
		/// </exception>
		public void Remove (X509CrlRecord record)
		{
			if (record == null)
				throw new ArgumentNullException (nameof (record));

			using (var command = GetDeleteCommand (record)) {
				command.ExecuteNonQuery ();
			}
		}

		/// <summary>
		/// Update the specified CRL record.
		/// </summary>
		/// <remarks>
		/// Updates the specified fields of the record in the database.
		/// </remarks>
		/// <param name="record">The CRL record.</param>
		/// <exception cref="System.ArgumentNullException">
		/// <paramref name="record"/> is <c>null.
		/// </exception>
		public void Update (X509CrlRecord record)
		{
			if (record == null)
				throw new ArgumentNullException (nameof (record));

			using (var command = GetUpdateCommand (record)) {
				command.ExecuteNonQuery ();
			}
		}

		/// <summary>
		/// Gets a certificate revocation list store.
		/// </summary>
		/// <remarks>
		/// Gets a certificate revocation list store.
		/// </remarks>
		/// <returns>A certificate recovation list store.</returns>
		public IX509Store GetCrlStore ()
		{
			var crls = new List<X509Crl> ();

			using (var command = GetSelectAllCrlsCommand ()) {
				var reader = command.ExecuteReader ();

				try {
					var parser = new X509CrlParser ();
					var buffer = new byte[4096];

					while (reader.Read ()) {
						var record = LoadCrlRecord (reader, parser, ref buffer);
						crls.Add (record.Crl);
					}
				} finally {
#if NETSTANDARD
					reader.Dispose ();
#else
					reader.Close ();
#endif
				}
			}

			return X509StoreFactory.Create ("Crl/Collection", new X509CollectionStoreParameters (crls));
		}

#region IX509Store implementation

		/// <summary>
		/// Gets a collection of matching certificates matching the specified selector.
		/// </summary>
		/// <remarks>
		/// Gets a collection of matching certificates matching the specified selector.
		/// </remarks>
		/// <returns>The matching certificates.</returns>
		/// <param name="selector">The match criteria.</param>
		ICollection IX509Store.GetMatches (IX509Selector selector)
		{
			return new List<X509Certificate> (FindCertificates (selector));
		}

#endregion

#region IDisposable implementation

		/// <summary>
		/// Releases the unmanaged resources used by the <see cref="X509CertificateDatabase"/> and
		/// optionally releases the managed resources.
		/// </summary>
		/// <remarks>
		/// Releases the unmanaged resources used by the <see cref="X509CertificateDatabase"/> and
		/// optionally releases the managed resources.
		/// </remarks>
		/// <param name="disposing"><c>true to release both managed and unmanaged resources;
		/// <c>false</c> to release only the unmanaged resources.
		protected virtual void Dispose (bool disposing)
		{
			for (int i = 0; i < passwd.Length; i++)
				passwd[i] = '\0';
		}

		/// <summary>
		/// Releases all resource used by the <see cref="MimeKit.Cryptography.X509CertificateDatabase"/> object.
		/// </summary>
		/// <remarks>Call <see cref="Dispose()"/> when you are finished using the
		/// <see cref="MimeKit.Cryptography.X509CertificateDatabase"/>. The <see cref="Dispose()"/> method leaves the
		/// <see cref="MimeKit.Cryptography.X509CertificateDatabase"/> in an unusable state. After calling
		/// <see cref="Dispose()"/>, you must release all references to the
		/// <see cref="MimeKit.Cryptography.X509CertificateDatabase"/> so the garbage collector can reclaim the memory that
		/// the <see cref="MimeKit.Cryptography.X509CertificateDatabase"/> was occupying.</remarks>
		public void Dispose ()
		{
			Dispose (true);
			GC.SuppressFinalize (this);
		}

#endregion
	}
}