diff --git a/MimeKit/Cryptography/SQLServerCertificateDatabase.cs b/MimeKit/Cryptography/SQLServerCertificateDatabase.cs new file mode 100644 index 0000000000..10f87bb811 --- /dev/null +++ b/MimeKit/Cryptography/SQLServerCertificateDatabase.cs @@ -0,0 +1,223 @@ +// +// SQLServerCertificateDatabase.cs +// +// Author: Rob Blackin +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// 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.Text; +using System.Data; +using System.Data.Common; +using System.Collections.Generic; + +using MimeKit.Utils; + +using Org.BouncyCastle.X509; + +namespace MimeKit.Cryptography { + public class SQLServerCertificateDatabase : SqlCertificateDatabase + { + public SQLServerCertificateDatabase (DbConnection connection, string password) : base (connection, password) + { + } + + protected override void AddTableColumn (DbConnection connection, DataTable table, DataColumn column) + { + var statement = new StringBuilder ("ALTER TABLE "); + int primaryKeys = table.PrimaryKey?.Length ?? 0; + + statement.Append (table.TableName); + statement.Append (" ADD COLUMN "); + Build (statement, table, column, ref primaryKeys); + + using (var command = connection.CreateCommand ()) { + command.CommandText = statement.ToString (); + command.CommandType = CommandType.Text; + command.ExecuteNonQuery (); + } + } + + protected override void CreateTable (DbConnection connection, DataTable table) + { + var statement = new StringBuilder ($"if not exists (select * from sysobjects where name='{table.TableName}' and xtype='U') "); + int primaryKeys = 0; + + statement.Append ($"Create table {table.TableName} ("); + + foreach (DataColumn column in table.Columns) { + Build (statement, table, column, ref primaryKeys); + statement.Append (", "); + } + + if (table.Columns.Count > 0) + statement.Length -= 2; + + statement.Append (')'); + + using (var command = connection.CreateCommand ()) { + command.CommandText = statement.ToString (); + command.CommandType = CommandType.Text; + command.ExecuteNonQuery (); + } + } + + static void Build (StringBuilder statement, DataTable table, DataColumn column, ref int primaryKeys) + { + statement.Append (column.ColumnName); + statement.Append (' '); + + if (column.DataType == typeof (long) || column.DataType == typeof (int)) { + if (column.AutoIncrement) + statement.Append ("int identity(1,1)"); + else if (column.DataType == typeof (long)) + statement.Append ("DateTime"); + else + statement.Append ("int"); + } else if (column.DataType == typeof (bool)) { + statement.Append ("bit"); + } else if (column.DataType == typeof (byte[])) { + statement.Append ($"varbinary(4096)"); + } else if (column.DataType == typeof (string)) { + statement.Append ("varchar(256)"); + } else { + throw new NotImplementedException (); + } + + if (table != null && table.PrimaryKey != null && primaryKeys < table.PrimaryKey.Length) { + for (int i = 0; i < table.PrimaryKey.Length; i++) { + if (column == table.PrimaryKey[i]) { + statement.Append (" PRIMARY KEY Clustered"); + primaryKeys++; + break; + } + } + } + + if (!column.AllowDBNull) + statement.Append (" NOT NULL"); + } + + protected override IList GetTableColumns (DbConnection connection, string tableName) + { + using (var command = connection.CreateCommand ()) { + command.CommandText = $"select top 1 * from {tableName}"; + using (var reader = command.ExecuteReader ()) { + var columns = new List (); + var table = reader.GetSchemaTable (); + + foreach (DataRow row in table.Rows) + columns.Add (new DataColumn { ColumnName = row.Field ("ColumnName") }); + + return columns; + } + } + } + + protected override void CreateIndex (DbConnection connection, string tableName, string[] columnNames) + { + var indexName = GetIndexName (tableName, columnNames); + var query = string.Format ("IF NOT EXISTS (Select 8 from sys.indexes where name='{0}' and object_id=OBJECT_ID('{1}')) CREATE INDEX {0} ON {1}({2})", indexName, tableName, string.Join(", ", columnNames)); + + using (var command = connection.CreateCommand ()) { + command.CommandText = query; + command.ExecuteNonQuery (); + } + } + + protected override void RemoveIndex (DbConnection connection, string tableName, string[] columnNames) + { + var indexName = GetIndexName (tableName, columnNames); + var query = string.Format ("IF EXISTS (Select 8 from sys.indexes where name='{0}' and object_id=OBJECT_ID('{1}')) DROP INDEX {0} ON {1}", indexName, tableName); + + using (var command = connection.CreateCommand ()) { + command.CommandText = query; + command.ExecuteNonQuery (); + } + } + + /// + /// Gets the database command to select the record matching the specified certificate. + /// + /// + /// Gets the database command to select the record matching the specified certificate. + /// + /// The database command. + /// The certificate. + /// The fields to return. + protected override DbCommand GetSelectCommand (X509Certificate certificate, X509CertificateRecordFields fields) + { + var fingerprint = certificate.GetFingerprint ().ToLowerInvariant (); + var serialNumber = certificate.SerialNumber.ToString (); + var issuerName = certificate.IssuerDN.ToString (); + var command = connection.CreateCommand (); + var query = CreateSelectQuery (fields).Replace ("SELECT", "SELECT top 1"); + + // FIXME: Is this really the best way to query for an exact match of a certificate? + query = query.Append (" WHERE ISSUERNAME = @ISSUERNAME AND SERIALNUMBER = @SERIALNUMBER AND FINGERPRINT = @FINGERPRINT"); + command.AddParameterWithValue ("@ISSUERNAME", issuerName); + command.AddParameterWithValue ("@SERIALNUMBER", serialNumber); + command.AddParameterWithValue ("@FINGERPRINT", fingerprint); + + command.CommandText = query.ToString (); + command.CommandType = CommandType.Text; + + return command; + } + + protected override DbCommand GetInsertCommand (X509CertificateRecord record) + { + var statement = new StringBuilder ("INSERT INTO CERTIFICATES("); + var variables = new StringBuilder ("VALUES("); + var command = connection.CreateCommand (); + var columns = certificatesTable.Columns; + + for (int i = 1; i < columns.Count; i++) { + if (i > 1) { + statement.Append (", "); + variables.Append (", "); + } + + var value = GetValue (record, columns[i].ColumnName); + if (value.GetType () == typeof (DateTime)) + value = ((DateTime) value < DateUtils.UnixEpoch) ? DateUtils.UnixEpoch : (DateTime) value; + + if (columns[i].ColumnName == "PRIVATEKEY" && value.GetType () == typeof (DBNull)) + value = new byte[0]; + + var variable = "@" + columns[i]; + + command.AddParameterWithValue (variable, value); + statement.Append (columns[i]); + variables.Append (variable); + } + + statement.Append (')'); + variables.Append (')'); + + command.CommandText = statement + " " + variables; + command.CommandType = CommandType.Text; + + return command; + } + } +} diff --git a/MimeKit/Cryptography/SqlCertificateDatabase.cs b/MimeKit/Cryptography/SqlCertificateDatabase.cs index 482b6466c3..cd5979ce32 100644 --- a/MimeKit/Cryptography/SqlCertificateDatabase.cs +++ b/MimeKit/Cryptography/SqlCertificateDatabase.cs @@ -53,8 +53,8 @@ namespace MimeKit.Cryptography { /// public abstract class SqlCertificateDatabase : X509CertificateDatabase { - readonly DataTable certificatesTable, crlsTable; - readonly DbConnection connection; + protected readonly DataTable certificatesTable, crlsTable; + protected readonly DbConnection connection; bool disposed; /// @@ -229,12 +229,12 @@ static DataTable CreateCrlsDataTable (string tableName) /// The column to add. protected abstract void AddTableColumn (DbConnection connection, DataTable table, DataColumn column); - static string GetIndexName (string tableName, string[] columnNames) + protected string GetIndexName (string tableName, string[] columnNames) { return string.Format ("{0}_{1}_INDEX", tableName, string.Join ("_", columnNames)); } - static void CreateIndex (DbConnection connection, string tableName, string[] columnNames) + protected virtual void CreateIndex (DbConnection connection, string tableName, string[] columnNames) { var indexName = GetIndexName (tableName, columnNames); var query = string.Format ("CREATE INDEX IF NOT EXISTS {0} ON {1}({2})", indexName, tableName, string.Join (", ", columnNames)); @@ -245,7 +245,7 @@ static void CreateIndex (DbConnection connection, string tableName, string[] col } } - static void RemoveIndex (DbConnection connection, string tableName, string[] columnNames) + protected virtual void RemoveIndex (DbConnection connection, string tableName, string[] columnNames) { var indexName = GetIndexName (tableName, columnNames); var query = string.Format ("DROP INDEX IF EXISTS {0}", indexName); @@ -337,7 +337,7 @@ void CreateCrlsTable (DataTable table) CreateIndex (connection, table.TableName, new [] { "DELTA", "ISSUERNAME", "THISUPDATE" }); } - static StringBuilder CreateSelectQuery (X509CertificateRecordFields fields) + protected StringBuilder CreateSelectQuery (X509CertificateRecordFields fields) { var query = new StringBuilder ("SELECT "); var columns = GetColumnNames (fields); diff --git a/MimeKit/MimeKit.csproj b/MimeKit/MimeKit.csproj index e29d780a7f..a72895be6c 100644 --- a/MimeKit/MimeKit.csproj +++ b/MimeKit/MimeKit.csproj @@ -67,6 +67,7 @@ + @@ -134,6 +135,7 @@ +