diff options
Diffstat (limited to 'src/main.lib')
-rw-r--r-- | src/main.lib/DomainObjects/RenewResult.cs | 35 | ||||
-rw-r--r-- | src/main.lib/DomainObjects/Renewal.cs | 175 | ||||
-rw-r--r-- | src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs | 2 | ||||
-rw-r--r-- | src/main.lib/Services/Interfaces/IInputService.cs | 1 | ||||
-rw-r--r-- | src/main.lib/Services/Serialization/ProtectedString.cs | 192 | ||||
-rw-r--r-- | src/main.lib/wacs.lib.csproj | 1 |
6 files changed, 405 insertions, 1 deletions
diff --git a/src/main.lib/DomainObjects/RenewResult.cs b/src/main.lib/DomainObjects/RenewResult.cs new file mode 100644 index 0000000..6d751b0 --- /dev/null +++ b/src/main.lib/DomainObjects/RenewResult.cs @@ -0,0 +1,35 @@ +using PKISharp.WACS.Extensions; +using System; + +namespace PKISharp.WACS.DomainObjects +{ + public class RenewResult + { + public DateTime Date { get; set; } + public bool Success { get; set; } + public string ErrorMessage { get; set; } + public string Thumbprint { get; set; } + + private RenewResult() + { + Date = DateTime.UtcNow; + } + + public RenewResult(CertificateInfo certificate) : this() + { + Success = true; + Thumbprint = certificate.Certificate.Thumbprint; + } + + public RenewResult(string error) : this() + { + Success = false; + ErrorMessage = error; + } + + public override string ToString() => $"{Date} " + + $"- {(Success ? "Success" : "Error")}" + + $"{(string.IsNullOrEmpty(Thumbprint) ? "" : $" - Thumbprint {Thumbprint}")}" + + $"{(string.IsNullOrEmpty(ErrorMessage) ? "" : $" - {ErrorMessage.ReplaceNewLines()}")}"; + } +} diff --git a/src/main.lib/DomainObjects/Renewal.cs b/src/main.lib/DomainObjects/Renewal.cs new file mode 100644 index 0000000..5ee7a2f --- /dev/null +++ b/src/main.lib/DomainObjects/Renewal.cs @@ -0,0 +1,175 @@ +using Newtonsoft.Json; +using PKISharp.WACS.Extensions; +using PKISharp.WACS.Plugins.Base.Options; +using PKISharp.WACS.Services; +using PKISharp.WACS.Services.Serialization; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace PKISharp.WACS.DomainObjects +{ + /// <summary> + /// Main unit of work for the program, contains all the information + /// required to generate a target, do the validation, store the resulting + /// certificate somewhere and finally run installation steps to update + /// software. + /// </summary> + [DebuggerDisplay("Renewal {Id}: {FriendlyName}")] + public class Renewal + { + internal static Renewal Create(string id, int renewalDays, PasswordGenerator generator) + { + var ret = new Renewal + { + New = true, + Id = string.IsNullOrEmpty(id) ? ShortGuid.NewGuid().ToString() : id, + PfxPassword = new ProtectedString(generator.Generate()), + RenewalDays = renewalDays + }; + return ret; + } + + /// <summary> + /// Is this renewal a test? + /// </summary> + [JsonIgnore] + internal bool Test { get; set; } + + /// <summary> + /// Has this renewal been changed? + /// </summary> + [JsonIgnore] + internal bool Updated { get; set; } + + /// <summary> + /// Has this renewal been deleted? + /// </summary> + [JsonIgnore] + internal bool Deleted { get; set; } + + /// <summary> + /// Current renewal days setting, stored + /// here as a shortcut because its not + /// otherwise available to the instance + /// </summary> + [JsonIgnore] + internal int RenewalDays { get; set; } + + /// <summary> + /// Is this renewal new + /// </summary> + [JsonIgnore] + internal bool New { get; set; } + + /// <summary> + /// Unique identifer for the renewal + /// </summary> + public string Id { get; set; } + + /// <summary> + /// Friendly name for the certificate. If left + /// blank or empty, the CommonName will be used. + /// </summary> + public string FriendlyName { get; set; } + + /// <summary> + /// Display name, as the program shows this certificate + /// in the interface. This is set to the most recently + /// used FriendlyName + /// </summary> + public string LastFriendlyName { get; set; } + + /// <summary> + /// Plain text readable version of the PfxFile password + /// </summary> + [JsonProperty(PropertyName = "PfxPasswordProtected")] + public ProtectedString PfxPassword { get; set; } + + public DateTime? GetDueDate() { + var lastSuccess = History.LastOrDefault(x => x.Success)?.Date; + if (lastSuccess.HasValue) + { + return lastSuccess. + Value. + AddDays(RenewalDays). + ToLocalTime(); + } + else + { + return null; + } + } + + public bool IsDue() + { + return GetDueDate() == null || GetDueDate() < DateTime.Now; + } + + /// <summary> + /// Store information about TargetPlugin + /// </summary> + public TargetPluginOptions TargetPluginOptions { get; set; } + + /// <summary> + /// Store information about ValidationPlugin + /// </summary> + public ValidationPluginOptions ValidationPluginOptions { get; set; } + + /// <summary> + /// Store information about CsrPlugin + /// </summary> + public CsrPluginOptions CsrPluginOptions { get; set; } + + /// <summary> + /// Store information about StorePlugin + /// </summary> + public List<StorePluginOptions> StorePluginOptions { get; set; } = new List<StorePluginOptions>(); + + /// <summary> + /// Store information about InstallationPlugins + /// </summary> + public List<InstallationPluginOptions> InstallationPluginOptions { get; set; } = new List<InstallationPluginOptions>(); + + /// <summary> + /// History for this renewal + /// </summary> + public List<RenewResult> History { get; set; } = new List<RenewResult>(); + + /// <summary> + /// Pretty format + /// </summary> + /// <returns></returns> + public override string ToString() { + return ToString(null); + } + + /// <summary> + /// Pretty format + /// </summary> + /// <returns></returns> + public string ToString(IInputService inputService) + { + var success = History.FindAll(x => x.Success).Count; + var errors = History.AsEnumerable().Reverse().TakeWhile(x => !x.Success); + var ret = $"{LastFriendlyName} - renewed {success} time{(success != 1 ? "s" : "")}"; + var due = IsDue(); + var dueDate = GetDueDate(); + if (inputService == null) + { + ret += due ? ", due now" : dueDate == null ? "" : $", due after {dueDate}"; + } + else + { + ret += due ? ", due now" : dueDate == null ? "" : $", due after {inputService.FormatDate(dueDate.Value)}"; + } + + if (errors.Count() > 0) + { + ret += $", {errors.Count()} error{(errors.Count() != 1 ? "s" : "")} like \"{errors.First().ErrorMessage}\""; + } + return ret; + } + } +} diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs index db1e411..5998e2a 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs @@ -30,7 +30,7 @@ namespace PKISharp.WACS.Plugins.Base.Factories.Null public override string Description => "Do not run any (extra) installation steps"; } - internal class NullInstallation : IInstallationPlugin + class NullInstallation : IInstallationPlugin { void IInstallationPlugin.Install(IEnumerable<IStorePlugin> stores, CertificateInfo newCertificateInfo, CertificateInfo oldCertificateInfo) { } } diff --git a/src/main.lib/Services/Interfaces/IInputService.cs b/src/main.lib/Services/Interfaces/IInputService.cs index aaabc8f..4e96f8c 100644 --- a/src/main.lib/Services/Interfaces/IInputService.cs +++ b/src/main.lib/Services/Interfaces/IInputService.cs @@ -14,6 +14,7 @@ namespace PKISharp.WACS.Services void Show(string label, string value = null, bool first = false, int level = 0); bool Wait(string message = ""); void WritePagedList(IEnumerable<Choice> listItems); + string FormatDate(DateTime date); } diff --git a/src/main.lib/Services/Serialization/ProtectedString.cs b/src/main.lib/Services/Serialization/ProtectedString.cs new file mode 100644 index 0000000..95a2227 --- /dev/null +++ b/src/main.lib/Services/Serialization/ProtectedString.cs @@ -0,0 +1,192 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace PKISharp.WACS.Services.Serialization +{ + /// <summary> + /// Wrapper to handle string encryption and encoding + /// Strings can be in three forms: + /// - Clear, prefixed by ClearPrefix + /// - Base64 encoded, without any prefix + /// - Base64 encoded *with* encryption, prefixed by EncryptedPrefix + /// </summary> + public class ProtectedString + { + + /// <summary> + /// Indicates encryption + /// </summary> + internal const string EncryptedPrefix = "enc-"; + + /// <summary> + /// Indicates clear text + /// </summary> + internal const string ClearPrefix = "clear-"; + + /// <summary> + /// Logging service, used only by the JsonConverter + /// </summary> + private readonly ILogService _log; + + /// <summary> + /// Indicates if there was an error decoding or decrypting the string + /// </summary> + public bool Error { get; private set; } = false; + + /// <summary> + /// Clear value, should be used for operations + /// </summary> + public string Value { get; private set; } + + /// <summary> + /// Value to save to disk, based on the setting + /// </summary> + public string DiskValue(bool encrypt) + { + return encrypt ? ProtectedValue : EncodedValue; + } + + /// <summary> + /// Constructor for user input, always starting with clear text + /// </summary> + /// <param name="clearValue"></param> + public ProtectedString(string clearValue) + { + Value = clearValue; + } + + /// <summary> + /// Constructor for deserialisation, may be any format + /// </summary> + /// <param name="rawValue"></param> + /// <param name="log"></param> + public ProtectedString(string rawValue, ILogService log) + { + _log = log; + Value = rawValue; + + if (!string.IsNullOrEmpty(rawValue)) + { + if (rawValue.StartsWith(EncryptedPrefix)) + { + // Sure to be encrypted + try + { + Value = Unprotect(rawValue.Substring(EncryptedPrefix.Length)); + } + catch + { + _log.Error("Unable to decrypt configuration value, may have been written by a different machine."); + Error = true; + } + } + else if (rawValue.StartsWith(ClearPrefix)) + { + // Sure to be clear/unencoded + Value = rawValue.Substring(ClearPrefix.Length); + } + else + { + // Should be Base64 + try + { + var clearBytes = Convert.FromBase64String(rawValue); + Value = Encoding.UTF8.GetString(clearBytes); + } + catch + { + _log.Error("Unable to decode configuration value, use the prefix {prefix} to input clear text", ClearPrefix); + Error = true; + } + } + } + } + + /// <summary> + /// Encrypted value should be used when the "EncryptConfig" setting is true + /// </summary> + internal string ProtectedValue + { + get + { + if (string.IsNullOrEmpty(Value) || Error) + { + return Value; + } + else + { + return EncryptedPrefix + Protect(Value); + } + } + } + + /// <summary> + /// Encoded value should be used when the "EncryptConfig" setting is false + /// </summary> + internal string EncodedValue + { + get + { + if (string.IsNullOrEmpty(Value) || Error) + { + return Value; + } + else + { + return Encode(Value); + } + } + } + + /// <summary> + /// Base64 encode a string + /// </summary> + /// <param name="clearText"></param> + /// <returns></returns> + private string Encode(string clearText) + { + byte[] clearBytes = Encoding.UTF8.GetBytes(clearText); + return Convert.ToBase64String(clearBytes); + } + + /// <summary> + /// Encrypt and Base64-encode a string + /// </summary> + /// <param name="clearText"></param> + /// <param name="optionalEntropy"></param> + /// <param name="scope"></param> + /// <returns></returns> + private string Protect(string clearText, string optionalEntropy = null, DataProtectionScope scope = DataProtectionScope.LocalMachine) + { + byte[] clearBytes = Encoding.UTF8.GetBytes(clearText); + byte[] entropyBytes = string.IsNullOrEmpty(optionalEntropy) + ? null + : Encoding.UTF8.GetBytes(optionalEntropy); + byte[] encryptedBytes = ProtectedData.Protect(clearBytes, entropyBytes, scope); + return Convert.ToBase64String(encryptedBytes); + } + + /// <summary> + /// Base64-decode and decrypt a string + /// </summary> + /// <param name="clearText"></param> + /// <param name="optionalEntropy"></param> + /// <param name="scope"></param> + /// <returns></returns> + private string Unprotect(string encryptedText, string optionalEntropy = null, DataProtectionScope scope = DataProtectionScope.LocalMachine) + { + if (encryptedText == null) + { + return null; + } + byte[] clearBytes = null; + byte[] encryptedBytes = Convert.FromBase64String(encryptedText); + byte[] entropyBytes = string.IsNullOrEmpty(optionalEntropy) + ? null + : Encoding.UTF8.GetBytes(optionalEntropy); + clearBytes = ProtectedData.Unprotect(encryptedBytes, entropyBytes, scope); + return Encoding.UTF8.GetString(clearBytes); + } + } +} diff --git a/src/main.lib/wacs.lib.csproj b/src/main.lib/wacs.lib.csproj index 103aaec..d6b6af1 100644 --- a/src/main.lib/wacs.lib.csproj +++ b/src/main.lib/wacs.lib.csproj @@ -14,6 +14,7 @@ <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /> <PackageReference Include="Serilog.Sinks.EventLog" Version="3.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.0.0" /> + <PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.5.0" /> </ItemGroup> </Project> |