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;
}
}
}
}