summaryrefslogtreecommitdiffstats
path: root/src/main.lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.lib')
-rw-r--r--src/main.lib/DomainObjects/RenewResult.cs35
-rw-r--r--src/main.lib/DomainObjects/Renewal.cs175
-rw-r--r--src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs2
-rw-r--r--src/main.lib/Services/Interfaces/IInputService.cs1
-rw-r--r--src/main.lib/Services/Serialization/ProtectedString.cs192
-rw-r--r--src/main.lib/wacs.lib.csproj1
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>