using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Xml.Linq; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.Extensions.Logging; using StackExchange.Redis; namespace DataProtectionExtensions { /// /// Key repository that stores XML encrypted keys in a Redis distributed cache. /// /// /// /// The values stored in Redis are XML documents that contain encrypted session /// keys used for the protection of things like session state. The document contents /// are double-encrypted - first with a changing session key; then by a master key. /// As such, there's no risk in storing the keys in Redis - even if someone can crack /// the master key, they still need to also crack the session key. (Other solutions /// for sharing keys across a farm environment include writing them to files /// on a file share.) /// /// /// While the repository uses a hash to keep the set of encrypted keys separate, you /// can further separate these items from other items in Redis by specifying a unique /// database in the connection string. /// /// /// Consumers of the repository are responsible for caching the XML items as needed. /// Typically repositories are consumed by things like /// which generates /// values that get cached. The mechanism is already optimized for caching so there's /// no need to create a redundant cache. /// /// /// /// public class RedisXmlRepository : IXmlRepository, IDisposable { /// /// The root cache key for XML items stored in Redis /// public static readonly string RedisHashKey = "DataProtectionXmlRepository"; /// /// The connection to the Redis backing store. /// private IConnectionMultiplexer _connection; /// /// Flag indicating whether the object has been disposed. /// private bool _disposed = false; /// /// Initializes a new instance of the class. /// /// /// The Redis connection string. /// /// /// The used to log diagnostic messages. /// /// /// Thrown if or is . /// public RedisXmlRepository(string connectionString, ILogger logger) : this(ConnectionMultiplexer.Connect(connectionString), logger) { } /// /// Initializes a new instance of the class. /// /// /// The Redis database connection. /// /// /// The used to log diagnostic messages. /// /// /// Thrown if or is . /// public RedisXmlRepository(IConnectionMultiplexer connection, ILogger logger) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } if (logger == null) { throw new ArgumentNullException(nameof(logger)); } this._connection = connection; this.Logger = logger; // Mask the password so it doesn't get logged. var configuration = Regex.Replace(this._connection.Configuration, @"password\s*=\s*[^,]*", "password=****", RegexOptions.IgnoreCase); this.Logger.LogDebug("Storing data protection keys in Redis: {RedisConfiguration}", configuration); } /// /// Gets the logger. /// /// /// The used to log diagnostic messages. /// public ILogger Logger { get; private set; } /// /// Performs application-defined tasks associated with freeing, releasing, /// or resetting unmanaged resources. /// public void Dispose() { this.Dispose(true); } /// /// Gets all top-level XML elements in the repository. /// /// /// An with the set of elements /// stored in the repository. /// public IReadOnlyCollection GetAllElements() { var database = this._connection.GetDatabase(); var hash = database.HashGetAll(RedisHashKey); var elements = new List(); if (hash == null || hash.Length == 0) { return elements.AsReadOnly(); } foreach (var item in hash.ToStringDictionary()) { elements.Add(XElement.Parse(item.Value)); } this.Logger.LogDebug("Read {XmlElementCount} XML elements from Redis.", elements.Count); return elements.AsReadOnly(); } /// /// Adds a top-level XML element to the repository. /// /// The element to add. /// /// An optional name to be associated with the XML element. /// For instance, if this repository stores XML files on disk, the friendly name may /// be used as part of the file name. Repository implementations are not required to /// observe this parameter even if it has been provided by the caller. /// /// /// The parameter must be unique if specified. /// For instance, it could be the ID of the key being stored. /// /// /// Thrown if is . /// public void StoreElement(XElement element, string friendlyName) { if (element == null) { throw new ArgumentNullException(nameof(element)); } if (string.IsNullOrEmpty(friendlyName)) { // The framework always passes in a name, but // the contract indicates this may be null or empty. friendlyName = Guid.NewGuid().ToString(); } this.Logger.LogDebug("Storing XML element with friendly name {XmlElementFriendlyName}.", friendlyName); this._connection.GetDatabase().HashSet(RedisHashKey, friendlyName, element.ToString()); } /// /// Releases unmanaged and - optionally - managed resources. /// /// /// to release both managed and unmanaged resources; /// to release only unmanaged resources. /// protected virtual void Dispose(bool disposing) { if (!this._disposed) { if (disposing) { if (this._connection != null) { this._connection.Close(); this._connection.Dispose(); } } this._connection = null; this._disposed = true; } } } }