Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Sql server support #619

Merged
merged 9 commits into from
Nov 20, 2020
223 changes: 223 additions & 0 deletions MimeKit/Cryptography/SQLServerCertificateDatabase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
//
// SQLServerCertificateDatabase.cs
//
// Author: Rob Blackin <[email protected]>
//
// 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<DataColumn> 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<DataColumn> ();
var table = reader.GetSchemaTable ();

foreach (DataRow row in table.Rows)
columns.Add (new DataColumn { ColumnName = row.Field<string> ("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 ();
}
}

/// <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 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;
}
}
}
12 changes: 6 additions & 6 deletions MimeKit/Cryptography/SqlCertificateDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ namespace MimeKit.Cryptography {
/// </remarks>
public abstract class SqlCertificateDatabase : X509CertificateDatabase
{
readonly DataTable certificatesTable, crlsTable;
readonly DbConnection connection;
protected readonly DataTable certificatesTable, crlsTable;
protected readonly DbConnection connection;
bool disposed;

/// <summary>
Expand Down Expand Up @@ -229,12 +229,12 @@ static DataTable CreateCrlsDataTable (string tableName)
/// <param name="column">The column to add.</param>
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));
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions MimeKit/MimeKit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<ItemGroup>
<PackageReference Include="Portable.BouncyCastle" Version="1.8.8" />
<PackageReference Include="System.Buffers" Version="4.5.1" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
</ItemGroup>

<ItemGroup>
Expand Down Expand Up @@ -134,6 +135,7 @@
<Compile Include="Cryptography\SecureMimeType.cs" />
<Compile Include="Cryptography\SqlCertificateDatabase.cs" />
<Compile Include="Cryptography\SqliteCertificateDatabase.cs" />
<Compile Include="Cryptography\SQLServerCertificateDatabase.cs" />
<Compile Include="Cryptography\SubjectIdentifierType.cs" />
<Compile Include="Cryptography\TemporarySecureMimeContext.cs" />
<Compile Include="Cryptography\WindowsSecureMimeContext.cs" Condition="$(TargetFramework.StartsWith('net4')) Or $(TargetFramework.StartsWith('netstandard2.'))" />
Expand Down