diff --git a/SyncPro.Certificates/CertificateHelper.cs b/SyncPro.Certificates/CertificateHelper.cs new file mode 100644 index 0000000..32c20ad --- /dev/null +++ b/SyncPro.Certificates/CertificateHelper.cs @@ -0,0 +1,83 @@ +namespace SyncPro.Certificates +{ + using System; + using System.Security.Cryptography.X509Certificates; + + using CERTENROLLLib; + + public static class CertificateHelper + { + public static X509Certificate2 CreateSelfSignedCertificate(string subjectName) + { + var distinguishedName = new CX500DistinguishedName(); + distinguishedName.Encode( + "CN=" + subjectName, + X500NameFlags.XCN_CERT_NAME_STR_NONE); + + CCspInformations objCSPs = new CCspInformations(); + CCspInformation objCSP = new CCspInformation(); + + objCSP.InitializeFromName( + "Microsoft Enhanced RSA and AES Cryptographic Provider"); + + objCSPs.Add(objCSP); + + // Build the private key + CX509PrivateKey privateKey = new CX509PrivateKey(); + + privateKey.MachineContext = false; + privateKey.Length = 2048; + privateKey.CspInformations = objCSPs; + privateKey.KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE; + privateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES; + privateKey.ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG; + + // Create the private key in the CSP's protected storage + privateKey.Create(); + + // Build the algorithm identifier + var hashobj = new CObjectId(); + hashobj.InitializeFromAlgorithmName( + ObjectIdGroupId.XCN_CRYPT_HASH_ALG_OID_GROUP_ID, + ObjectIdPublicKeyFlags.XCN_CRYPT_OID_INFO_PUBKEY_ANY, + AlgorithmFlags.AlgorithmFlagsNone, + "SHA256"); + + // Create the self-signing request from the private key + var certificateRequest = new CX509CertificateRequestCertificate(); + certificateRequest.InitializeFromPrivateKey( + X509CertificateEnrollmentContext.ContextUser, + privateKey, + string.Empty); + + certificateRequest.Subject = distinguishedName; + certificateRequest.Issuer = distinguishedName; + certificateRequest.NotBefore = DateTime.Now.AddDays(-1); + certificateRequest.NotAfter = DateTime.Now.AddYears(100); + certificateRequest.HashAlgorithm = hashobj; + + certificateRequest.Encode(); + + var enrollment = new CX509Enrollment(); + + // Load the certificate request + enrollment.InitializeFromRequest(certificateRequest); + enrollment.CertificateFriendlyName = subjectName; + + // Output the request in base64 and install it back as the response + string csr = enrollment.CreateRequest(); + + // Install the response + enrollment.InstallResponse( + InstallResponseRestrictionFlags.AllowUntrustedCertificate, + csr, + EncodingType.XCN_CRYPT_STRING_BASE64, + string.Empty); + + // Get the new certificate without the private key + byte[] certificateData = Convert.FromBase64String(enrollment.Certificate); + + return new X509Certificate2(certificateData); + } + } +} diff --git a/SyncPro.Certificates/Properties/AssemblyInfo.cs b/SyncPro.Certificates/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ded8f82 --- /dev/null +++ b/SyncPro.Certificates/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SyncPro.Certificates")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SyncPro.Certificates")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("11febc64-ee05-4095-bd49-9fc7fabcc1df")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SyncPro.Certificates/SyncPro.Certificates.csproj b/SyncPro.Certificates/SyncPro.Certificates.csproj new file mode 100644 index 0000000..b55bd45 --- /dev/null +++ b/SyncPro.Certificates/SyncPro.Certificates.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {11FEBC64-EE05-4095-BD49-9FC7FABCC1DF} + Library + Properties + SyncPro.Certificates + SyncPro.Certificates + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + {728AB348-217D-11DA-B2A4-000E7BBB2B09} + 1 + 0 + 0 + tlbimp + False + True + + + + + \ No newline at end of file diff --git a/SyncPro.Core/Configuration/TriggerConfiguration.cs b/SyncPro.Core/Configuration/TriggerConfiguration.cs index 089585a..cc5cfb1 100644 --- a/SyncPro.Core/Configuration/TriggerConfiguration.cs +++ b/SyncPro.Core/Configuration/TriggerConfiguration.cs @@ -11,9 +11,16 @@ public class TriggerConfiguration public int HourlyMinutesPastSyncTime { get; set; } } + public enum EncryptionMode + { + None = 0, + Encrypt = 1, + Decrypt = 2 + } + public class EncryptionConfiguration { - public bool IsEnabled { get; set; } + public EncryptionMode Mode { get; set; } public string CertificateThumbprint { get; set; } } diff --git a/SyncPro.Core/Runtime/EncryptionManager.cs b/SyncPro.Core/Runtime/EncryptionManager.cs index a66aef2..dbb89c4 100644 --- a/SyncPro.Core/Runtime/EncryptionManager.cs +++ b/SyncPro.Core/Runtime/EncryptionManager.cs @@ -6,12 +6,7 @@ namespace SyncPro.Runtime using System.Security.Cryptography.X509Certificates; using SyncPro.Adapters; - - public enum EncryptionMode - { - Encrypt, - Decrypt - } + using SyncPro.Configuration; public class EncryptionManager : IDisposable { @@ -54,6 +49,8 @@ public EncryptionManager( Pre.ThrowIfArgumentNull(encryptionCertificate, nameof(encryptionCertificate)); Pre.ThrowIfArgumentNull(outputStream, nameof(outputStream)); + Pre.ThrowIfTrue(mode == EncryptionMode.None, "Encryption mode cannot be None"); + this.encryptionCertificate = encryptionCertificate; this.Mode = mode; this.sourceFileSize = sourceFileSize; diff --git a/SyncPro.Core/Runtime/SyncRelationship.cs b/SyncPro.Core/Runtime/SyncRelationship.cs index e687f3d..5eebf49 100644 --- a/SyncPro.Core/Runtime/SyncRelationship.cs +++ b/SyncPro.Core/Runtime/SyncRelationship.cs @@ -5,10 +5,12 @@ using System.Diagnostics; using System.IO; using System.Linq; + using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using SyncPro.Adapters; + using SyncPro.Certificates; using SyncPro.Configuration; using SyncPro.Data; using SyncPro.Tracing; @@ -100,7 +102,7 @@ private SyncRelationship(RelationshipConfiguration configuration) this.ThrottlingValue = configuration.ThrottlingConfiguration.Value; this.ThrottlingScaleFactor = configuration.ThrottlingConfiguration.ScaleFactor; - this.EncryptionIsEnabled = configuration.EncryptionConfiguration.IsEnabled; + this.EncryptionMode = configuration.EncryptionConfiguration.Mode; this.EncryptionCertificateThumbprint = configuration.EncryptionConfiguration.CertificateThumbprint; this.State = SyncRelationshipState.NotInitialized; @@ -137,8 +139,8 @@ public async Task SaveAsync() this.Configuration.TriggerConfiguration.HourlyMinutesPastSyncTime = this.TriggerHourlyMinutesPastSyncTime; this.Configuration.TriggerConfiguration.ScheduleInterval = this.TriggerScheduleInterval; - this.Configuration.EncryptionConfiguration.IsEnabled = this.EncryptionIsEnabled; - this.Configuration.EncryptionConfiguration.CertificateThumbprint = this.EncryptionCertificateThumbprint; + // Set the encryption mode for adapters that may need it. Other encryption configuration is set below. + this.Configuration.EncryptionConfiguration.Mode = this.EncryptionMode; // If the relaionship contains adapters that arent in the configuration, add them foreach (AdapterConfiguration adapterConfig in this.Adapters.Select(a => a.Configuration)) @@ -208,9 +210,20 @@ public async Task SaveAsync() adapterBase.SaveConfiguration(); } - // Set the creation time of the adapter + // Check if we are creating this relationship for the first time if (this.Configuration.InitiallyCreatedUtc == DateTime.MinValue) { + // Create the encryption certificate if needed + if (this.EncryptionMode == EncryptionMode.Encrypt && this.EncryptionCreateCertificate) + { + string subjectName = "SyncProEncryption " + this.Configuration.RelationshipId.ToString("D").ToLowerInvariant(); + + X509Certificate2 encryptionCert = CertificateHelper.CreateSelfSignedCertificate(subjectName); + + this.EncryptionCertificateThumbprint = encryptionCert.Thumbprint; + this.Configuration.EncryptionConfiguration.CertificateThumbprint = encryptionCert.Thumbprint; + } + this.Configuration.InitiallyCreatedUtc = DateTime.UtcNow; } @@ -409,14 +422,20 @@ public TriggerScheduleInterval TriggerScheduleInterval #region Encryption Properties [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool encryptionIsEnabled; + private EncryptionMode encryptionMode; - public bool EncryptionIsEnabled + public EncryptionMode EncryptionMode { - get { return this.encryptionIsEnabled; } - set { this.SetProperty(ref this.encryptionIsEnabled, value); } + get { return this.encryptionMode; } + set { this.SetProperty(ref this.encryptionMode, value); } } + /// + /// Indicates whether the certificate should be created when the relationship is saved + /// for the first time. + /// + public bool EncryptionCreateCertificate { get; set; } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string encryptionCertificateThumbprint; diff --git a/SyncPro.Core/Runtime/SyncRun.cs b/SyncPro.Core/Runtime/SyncRun.cs index d4c7f92..a0c31c5 100644 --- a/SyncPro.Core/Runtime/SyncRun.cs +++ b/SyncPro.Core/Runtime/SyncRun.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using SyncPro.Adapters; + using SyncPro.Configuration; using SyncPro.Data; using SyncPro.Tracing; @@ -335,7 +336,7 @@ private async Task SyncInternalAsync() { "AnalyzeResultId", this.AnalyzeResult.Id }, }); - if (this.relationship.EncryptionIsEnabled) + if (this.relationship.EncryptionMode != Configuration.EncryptionMode.None) { X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadOnly); @@ -887,18 +888,25 @@ private async Task CopyFileAsync( { long writeStreamLength = updateInfo.Entry.SourceSize; - if (this.relationship.EncryptionIsEnabled) + if (this.relationship.EncryptionMode == EncryptionMode.Encrypt) { short padding; writeStreamLength = EncryptionManager.CalculateEncryptedFileSize( updateInfo.Entry.SourceSize, out padding); } + else if (this.relationship.EncryptionMode == EncryptionMode.Decrypt) + { + short padding; + writeStreamLength = EncryptionManager.CalculateDecryptedFileSize( + updateInfo.Entry.SourceSize, + out padding); + } fromStream = fromAdapter.GetReadStreamForEntry(updateInfo.Entry); toStream = toAdapter.GetWriteStreamForEntry(updateInfo.Entry, writeStreamLength); - if (this.relationship.EncryptionIsEnabled) + if (this.relationship.EncryptionMode != EncryptionMode.None) { // Create a copy of the certificate from the original cert's handle. A unique copy is required // because the encryption manager will dispose of the RSA CSP derived from the cert, and will diff --git a/SyncPro.Core/SyncPro.Core.csproj b/SyncPro.Core/SyncPro.Core.csproj index 3075a94..804f0e0 100644 --- a/SyncPro.Core/SyncPro.Core.csproj +++ b/SyncPro.Core/SyncPro.Core.csproj @@ -121,6 +121,10 @@ + + {11febc64-ee05-4095-bd49-9fc7fabcc1df} + SyncPro.Certificates + {CE6A7780-BC06-4818-B15F-8DAD91032A71} SyncPro.Tracing diff --git a/SyncPro.UI/Controls/EncryptionSettingsDialog.xaml b/SyncPro.UI/Controls/EncryptionSettingsDialog.xaml index c0859d1..cdeb57f 100644 --- a/SyncPro.UI/Controls/EncryptionSettingsDialog.xaml +++ b/SyncPro.UI/Controls/EncryptionSettingsDialog.xaml @@ -44,7 +44,8 @@ Text="Files can be encrypted/decrypted when synchronized. Select the way that encryption should be performed below. Encryption cannot be enabled or disabled once a relationship is created." /> - + diff --git a/SyncPro.UI/RelationshipEditor/Sections/SyncOptionsSection.xaml b/SyncPro.UI/RelationshipEditor/Sections/SyncOptionsSection.xaml index e23015e..4616369 100644 --- a/SyncPro.UI/RelationshipEditor/Sections/SyncOptionsSection.xaml +++ b/SyncPro.UI/RelationshipEditor/Sections/SyncOptionsSection.xaml @@ -98,7 +98,18 @@ + Text="{Binding Path=EncryptedSettingsStatus}"> + + + + diff --git a/SyncPro.UI/RelationshipEditor/SyncOptionsPageViewModel.cs b/SyncPro.UI/RelationshipEditor/SyncOptionsPageViewModel.cs index a85f719..e102c40 100644 --- a/SyncPro.UI/RelationshipEditor/SyncOptionsPageViewModel.cs +++ b/SyncPro.UI/RelationshipEditor/SyncOptionsPageViewModel.cs @@ -3,6 +3,7 @@ namespace SyncPro.UI.RelationshipEditor using System.Diagnostics; using System.Windows.Input; + using SyncPro.Configuration; using SyncPro.UI.Controls; using SyncPro.UI.Framework.MVVM; using SyncPro.UI.ViewModels; @@ -28,7 +29,12 @@ private bool CanShowEncryptionSettingsDialog(object obj) private void ShowEncryptionSettingsDialog(object obj) { - EncryptionSettingsDialogViewModel dialogViewModel = new EncryptionSettingsDialogViewModel(); + EncryptionSettingsDialogViewModel dialogViewModel = new EncryptionSettingsDialogViewModel + { + IsCreateMode = this.EditorViewModel.IsCreateMode, + IsEncryptionEnabled = this.EditorViewModel.Relationship.EncryptionMode != EncryptionMode.None, + CreateNewCertificate = this.EditorViewModel.Relationship.EncryptionCreateCertificate + }; EncryptionSettingsDialog dialog = new EncryptionSettingsDialog { @@ -47,20 +53,44 @@ private void ShowEncryptionSettingsDialog(object obj) private void SetEncryptedSettingsStatus() { + if (this.EditorViewModel.IsEditMode) + { + switch (this.EditorViewModel.Relationship.EncryptionMode) + { + case EncryptionMode.None: + this.EncryptedSettingsStatus = "Encryption is disabled"; + this.EncryptedSettingsStatusImportant = false; + break; + case EncryptionMode.Encrypt: + this.EncryptedSettingsStatus = "File encryption is enabled"; + this.EncryptedSettingsStatusImportant = true; + break; + case EncryptionMode.Decrypt: + this.EncryptedSettingsStatus = "File decryption is enabled"; + this.EncryptedSettingsStatusImportant = true; + break; + } + + return; + } + if (this.IsEncryptionEnabled) { if (this.CreateNewEncryptionCertificate) { this.EncryptedSettingsStatus = "File encryption will be enabled using a new certificate."; + this.EncryptedSettingsStatusImportant = true; } else { this.EncryptedSettingsStatus = "File encryption will be enabled using an existing certificate."; + this.EncryptedSettingsStatusImportant = true; } } else { this.EncryptedSettingsStatus = "Files will not be encrypted before copying."; + this.EncryptedSettingsStatusImportant = false; } } @@ -73,12 +103,23 @@ public override void LoadContext() this.SelectedScopeType = this.EditorViewModel.Relationship.Scope; } + this.IsEncryptionEnabled = + this.EditorViewModel.Relationship.EncryptionMode != EncryptionMode.None; + this.SetEncryptedSettingsStatus(); } public override void SaveContext() { this.EditorViewModel.Relationship.Scope = this.SelectedScopeType; + + if (this.EditorViewModel.IsCreateMode) + { + this.EditorViewModel.Relationship.EncryptionMode = + this.IsEncryptionEnabled ? EncryptionMode.Encrypt : EncryptionMode.None; + this.EditorViewModel.Relationship.EncryptionCreateCertificate = + this.CreateNewEncryptionCertificate; + } } public override string NavTitle => "Options"; @@ -142,5 +183,14 @@ public string EncryptedSettingsStatus get { return this.encryptedSettingsStatus; } set { this.SetProperty(ref this.encryptedSettingsStatus, value); } } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool encryptedSettingsStatusImportant; + + public bool EncryptedSettingsStatusImportant + { + get { return this.encryptedSettingsStatusImportant; } + set { this.SetProperty(ref this.encryptedSettingsStatusImportant, value); } + } } } \ No newline at end of file diff --git a/SyncPro.UI/ViewModels/EncryptionSettingsDialogViewModel.cs b/SyncPro.UI/ViewModels/EncryptionSettingsDialogViewModel.cs index f6ca827..2ba3d19 100644 --- a/SyncPro.UI/ViewModels/EncryptionSettingsDialogViewModel.cs +++ b/SyncPro.UI/ViewModels/EncryptionSettingsDialogViewModel.cs @@ -8,13 +8,6 @@ using SyncPro.UI.Framework; using SyncPro.UI.Framework.MVVM; - public enum EncryptionType - { - None, - Encrypt, - Decrypt - } - public class EncryptionSettingsDialogViewModel : ViewModelBase, IRequestClose { public ICommand OKCommand { get; } @@ -37,6 +30,15 @@ public EncryptionSettingsDialogViewModel() this.CreateNewCertificate = true; } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool isCreateMode; + + public bool IsCreateMode + { + get { return this.isCreateMode; } + set { this.SetProperty(ref this.isCreateMode, value); } + } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private bool isEncryptionEnabled; diff --git a/SyncPro.UI/ViewModels/SyncRelationshipViewModel.cs b/SyncPro.UI/ViewModels/SyncRelationshipViewModel.cs index 753e41e..f7fe4ae 100644 --- a/SyncPro.UI/ViewModels/SyncRelationshipViewModel.cs +++ b/SyncPro.UI/ViewModels/SyncRelationshipViewModel.cs @@ -10,6 +10,7 @@ using System.Windows.Input; using SyncPro.Adapters; + using SyncPro.Configuration; using SyncPro.Data; using SyncPro.Runtime; using SyncPro.Tracing; @@ -99,6 +100,20 @@ public int ThrottlingScaleFactor set { this.BaseModel.ThrottlingScaleFactor = value; } } + [BaseModelProperty(NotifyOnPropertyChange = true)] + public EncryptionMode EncryptionMode + { + get { return this.BaseModel.EncryptionMode; } + set { this.BaseModel.EncryptionMode = value; } + } + + [BaseModelProperty(NotifyOnPropertyChange = true)] + public bool EncryptionCreateCertificate + { + get { return this.BaseModel.EncryptionCreateCertificate; } + set { this.BaseModel.EncryptionCreateCertificate = value; } + } + #endregion #region ViewModel properties diff --git a/SyncPro.UnitTests/EncryptionTests.cs b/SyncPro.UnitTests/EncryptionTests.cs index d540656..8045f85 100644 --- a/SyncPro.UnitTests/EncryptionTests.cs +++ b/SyncPro.UnitTests/EncryptionTests.cs @@ -8,6 +8,7 @@ namespace SyncPro.UnitTests using Microsoft.VisualStudio.TestTools.UnitTesting; + using SyncPro.Configuration; using SyncPro.Runtime; [TestClass] @@ -42,7 +43,7 @@ public void SyncLocalFilesWithEncryption() .SaveRelationship() .CreateSimpleSourceStructure(); - wrapper.Relationship.EncryptionIsEnabled = true; + wrapper.Relationship.EncryptionMode = EncryptionMode.Encrypt; wrapper.Relationship.EncryptionCertificateThumbprint = "420fe8033179cfb0ef21862d24bf6a1ec7df6c6d"; wrapper.CreateSyncRun() diff --git a/SyncPro.sln b/SyncPro.sln index 9172149..33db5cd 100644 --- a/SyncPro.sln +++ b/SyncPro.sln @@ -34,6 +34,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncPro.Adapters.GoogleDriv EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncPro.Adapters.WindowsFileSystem", "SyncPro.Adapters.WindowsFileSystem\SyncPro.Adapters.WindowsFileSystem.csproj", "{3475A78F-4AEB-4070-BBBB-4A8EFEC03E64}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncPro.Certificates", "SyncPro.Certificates\SyncPro.Certificates.csproj", "{11FEBC64-EE05-4095-BD49-9FC7FABCC1DF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -92,6 +94,10 @@ Global {3475A78F-4AEB-4070-BBBB-4A8EFEC03E64}.Debug|Any CPU.Build.0 = Debug|Any CPU {3475A78F-4AEB-4070-BBBB-4A8EFEC03E64}.Release|Any CPU.ActiveCfg = Release|Any CPU {3475A78F-4AEB-4070-BBBB-4A8EFEC03E64}.Release|Any CPU.Build.0 = Release|Any CPU + {11FEBC64-EE05-4095-BD49-9FC7FABCC1DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11FEBC64-EE05-4095-BD49-9FC7FABCC1DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11FEBC64-EE05-4095-BD49-9FC7FABCC1DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11FEBC64-EE05-4095-BD49-9FC7FABCC1DF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE