1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . Diagnostics . CodeAnalysis ;
4+ using System . IO ;
5+ using System . Linq ;
6+ using System . Text ;
7+ using System . Text . RegularExpressions ;
8+ using System . Xml . Linq ;
9+ using Microsoft . AspNetCore . DataProtection . Repositories ;
10+ using Microsoft . Extensions . Logging ;
11+ using StackExchange . Redis ;
12+
13+ namespace DataProtectionExtensions
14+ {
15+ /// <summary>
16+ /// Key repository that stores XML encrypted keys in a Redis distributed cache.
17+ /// </summary>
18+ /// <remarks>
19+ /// <para>
20+ /// The values stored in Redis are XML documents that contain encrypted session
21+ /// keys used for the protection of things like session state. The document contents
22+ /// are double-encrypted - first with a changing session key; then by a master key.
23+ /// As such, there's no risk in storing the keys in Redis - even if someone can crack
24+ /// the master key, they still need to also crack the session key. (Other solutions
25+ /// for sharing keys across a farm environment include writing them to files
26+ /// on a file share.)
27+ /// </para>
28+ /// <para>
29+ /// While the repository uses a hash to keep the set of encrypted keys separate, you
30+ /// can further separate these items from other items in Redis by specifying a unique
31+ /// database in the connection string.
32+ /// </para>
33+ /// <para>
34+ /// Consumers of the repository are responsible for caching the XML items as needed.
35+ /// Typically repositories are consumed by things like <see cref="Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingProvider"/>
36+ /// which generates <see cref="Microsoft.AspNetCore.DataProtection.KeyManagement.Internal.CacheableKeyRing"/>
37+ /// values that get cached. The mechanism is already optimized for caching so there's
38+ /// no need to create a redundant cache.
39+ /// </para>
40+ /// </remarks>
41+ /// <seealso cref="Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository" />
42+ /// <seealso cref="System.IDisposable" />
43+ public class RedisXmlRepository : IXmlRepository , IDisposable
44+ {
45+ /// <summary>
46+ /// The root cache key for XML items stored in Redis
47+ /// </summary>
48+ public static readonly string RedisHashKey = "DataProtectionXmlRepository" ;
49+
50+ /// <summary>
51+ /// The connection to the Redis backing store.
52+ /// </summary>
53+ private IConnectionMultiplexer _connection ;
54+
55+ /// <summary>
56+ /// Flag indicating whether the object has been disposed.
57+ /// </summary>
58+ private bool _disposed = false ;
59+
60+ /// <summary>
61+ /// Initializes a new instance of the <see cref="RedisXmlRepository"/> class.
62+ /// </summary>
63+ /// <param name="connectionString">
64+ /// The Redis connection string.
65+ /// </param>
66+ /// <param name="logger">
67+ /// The <see cref="ILogger{T}"/> used to log diagnostic messages.
68+ /// </param>
69+ /// <exception cref="System.ArgumentNullException">
70+ /// Thrown if <paramref name="connectionString" /> or <paramref name="logger" /> is <see langword="null" />.
71+ /// </exception>
72+ public RedisXmlRepository ( string connectionString , ILogger < RedisXmlRepository > logger )
73+ : this ( ConnectionMultiplexer . Connect ( connectionString ) , logger )
74+ {
75+ }
76+
77+ /// <summary>
78+ /// Initializes a new instance of the <see cref="RedisXmlRepository"/> class.
79+ /// </summary>
80+ /// <param name="connection">
81+ /// The Redis database connection.
82+ /// </param>
83+ /// <param name="logger">
84+ /// The <see cref="ILogger{T}"/> used to log diagnostic messages.
85+ /// </param>
86+ /// <exception cref="System.ArgumentNullException">
87+ /// Thrown if <paramref name="connection" /> or <paramref name="logger" /> is <see langword="null" />.
88+ /// </exception>
89+ public RedisXmlRepository ( IConnectionMultiplexer connection , ILogger < RedisXmlRepository > logger )
90+ {
91+ if ( connection == null )
92+ {
93+ throw new ArgumentNullException ( nameof ( connection ) ) ;
94+ }
95+
96+ if ( logger == null )
97+ {
98+ throw new ArgumentNullException ( nameof ( logger ) ) ;
99+ }
100+
101+ this . _connection = connection ;
102+ this . Logger = logger ;
103+
104+ // Mask the password so it doesn't get logged.
105+ var configuration = Regex . Replace ( this . _connection . Configuration , @"password\s*=\s*[^,]*" , "password=****" , RegexOptions . IgnoreCase ) ;
106+ this . Logger . LogDebug ( "Storing data protection keys in Redis: {RedisConfiguration}" , configuration ) ;
107+ }
108+
109+ /// <summary>
110+ /// Gets the logger.
111+ /// </summary>
112+ /// <value>
113+ /// The <see cref="ILogger{T}"/> used to log diagnostic messages.
114+ /// </value>
115+ public ILogger < RedisXmlRepository > Logger { get ; private set ; }
116+
117+ /// <summary>
118+ /// Performs application-defined tasks associated with freeing, releasing,
119+ /// or resetting unmanaged resources.
120+ /// </summary>
121+ public void Dispose ( )
122+ {
123+ this . Dispose ( true ) ;
124+ }
125+
126+ /// <summary>
127+ /// Gets all top-level XML elements in the repository.
128+ /// </summary>
129+ /// <returns>
130+ /// An <see cref="IReadOnlyCollection{T}"/> with the set of elements
131+ /// stored in the repository.
132+ /// </returns>
133+ public IReadOnlyCollection < XElement > GetAllElements ( )
134+ {
135+ var database = this . _connection . GetDatabase ( ) ;
136+ var hash = database . HashGetAll ( RedisHashKey ) ;
137+ var elements = new List < XElement > ( ) ;
138+
139+ if ( hash == null || hash . Length == 0 )
140+ {
141+ return elements . AsReadOnly ( ) ;
142+ }
143+
144+ foreach ( var item in hash . ToStringDictionary ( ) )
145+ {
146+ elements . Add ( XElement . Parse ( item . Value ) ) ;
147+ }
148+
149+ this . Logger . LogDebug ( "Read {XmlElementCount} XML elements from Redis." , elements . Count ) ;
150+ return elements . AsReadOnly ( ) ;
151+ }
152+
153+ /// <summary>
154+ /// Adds a top-level XML element to the repository.
155+ /// </summary>
156+ /// <param name="element">The element to add.</param>
157+ /// <param name="friendlyName">
158+ /// An optional name to be associated with the XML element.
159+ /// For instance, if this repository stores XML files on disk, the friendly name may
160+ /// be used as part of the file name. Repository implementations are not required to
161+ /// observe this parameter even if it has been provided by the caller.
162+ /// </param>
163+ /// <remarks>
164+ /// The <paramref name="friendlyName" /> parameter must be unique if specified.
165+ /// For instance, it could be the ID of the key being stored.
166+ /// </remarks>
167+ /// <exception cref="System.ArgumentNullException">
168+ /// Thrown if <paramref name="element" /> is <see langword="null" />.
169+ /// </exception>
170+ public void StoreElement ( XElement element , string friendlyName )
171+ {
172+ if ( element == null )
173+ {
174+ throw new ArgumentNullException ( nameof ( element ) ) ;
175+ }
176+
177+ if ( string . IsNullOrEmpty ( friendlyName ) )
178+ {
179+ // The framework always passes in a name, but
180+ // the contract indicates this may be null or empty.
181+ friendlyName = Guid . NewGuid ( ) . ToString ( ) ;
182+ }
183+
184+ this . Logger . LogDebug ( "Storing XML element with friendly name {XmlElementFriendlyName}." , friendlyName ) ;
185+
186+ this . _connection . GetDatabase ( ) . HashSet ( RedisHashKey , friendlyName , element . ToString ( ) ) ;
187+ }
188+
189+ /// <summary>
190+ /// Releases unmanaged and - optionally - managed resources.
191+ /// </summary>
192+ /// <param name="disposing">
193+ /// <see langword="true" /> to release both managed and unmanaged resources;
194+ /// <see langword="false" /> to release only unmanaged resources.
195+ /// </param>
196+ protected virtual void Dispose ( bool disposing )
197+ {
198+ if ( ! this . _disposed )
199+ {
200+ if ( disposing )
201+ {
202+ if ( this . _connection != null )
203+ {
204+ this . _connection . Close ( ) ;
205+ this . _connection . Dispose ( ) ;
206+ }
207+ }
208+
209+ this . _connection = null ;
210+ this . _disposed = true ;
211+ }
212+ }
213+ }
214+ }
0 commit comments