summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.Core/Messaging/Bindings/MemoryCryptoKeyStore.cs
blob: 53bce178aa599724737709c97cd5f10389e28311 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//-----------------------------------------------------------------------
// <copyright file="MemoryCryptoKeyStore.cs" company="Outercurve Foundation">
//     Copyright (c) Outercurve Foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace DotNetOpenAuth.Messaging.Bindings {
	using System;
	using System.Collections.Generic;
	using System.Linq;

	/// <summary>
	/// A in-memory store of crypto keys.
	/// </summary>
	internal class MemoryCryptoKeyStore : ICryptoKeyStore {
		/// <summary>
		/// How frequently to check for and remove expired secrets.
		/// </summary>
		private static readonly TimeSpan cleaningInterval = TimeSpan.FromMinutes(30);

		/// <summary>
		/// An in-memory cache of decrypted symmetric keys.
		/// </summary>
		/// <remarks>
		/// The key is the bucket name.  The value is a dictionary whose key is the handle and whose value is the cached key.
		/// </remarks>
		private readonly Dictionary<string, Dictionary<string, CryptoKey>> store = new Dictionary<string, Dictionary<string, CryptoKey>>(StringComparer.Ordinal);

		/// <summary>
		/// The last time the cache had expired keys removed from it.
		/// </summary>
		private DateTime lastCleaning = DateTime.UtcNow;

		/// <summary>
		/// Gets the key in a given bucket and handle.
		/// </summary>
		/// <param name="bucket">The bucket name.  Case sensitive.</param>
		/// <param name="handle">The key handle.  Case sensitive.</param>
		/// <returns>
		/// The cryptographic key, or <c>null</c> if no matching key was found.
		/// </returns>
		public CryptoKey GetKey(string bucket, string handle) {
			lock (this.store) {
				Dictionary<string, CryptoKey> cacheBucket;
				if (this.store.TryGetValue(bucket, out cacheBucket)) {
					CryptoKey key;
					if (cacheBucket.TryGetValue(handle, out key)) {
						return key;
					}
				}
			}

			return null;
		}

		/// <summary>
		/// Gets a sequence of existing keys within a given bucket.
		/// </summary>
		/// <param name="bucket">The bucket name.  Case sensitive.</param>
		/// <returns>
		/// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>.
		/// </returns>
		public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
			lock (this.store) {
				Dictionary<string, CryptoKey> cacheBucket;
				if (this.store.TryGetValue(bucket, out cacheBucket)) {
					return cacheBucket.ToList();
				} else {
					return Enumerable.Empty<KeyValuePair<string, CryptoKey>>();
				}
			}
		}

		/// <summary>
		/// Stores a cryptographic key.
		/// </summary>
		/// <param name="bucket">The name of the bucket to store the key in.  Case sensitive.</param>
		/// <param name="handle">The handle to the key, unique within the bucket.  Case sensitive.</param>
		/// <param name="key">The key to store.</param>
		/// <exception cref="CryptoKeyCollisionException">Thrown in the event of a conflict with an existing key in the same bucket and with the same handle.</exception>
		public void StoreKey(string bucket, string handle, CryptoKey key) {
			lock (this.store) {
				Dictionary<string, CryptoKey> cacheBucket;
				if (!this.store.TryGetValue(bucket, out cacheBucket)) {
					this.store[bucket] = cacheBucket = new Dictionary<string, CryptoKey>(StringComparer.Ordinal);
				}

				if (cacheBucket.ContainsKey(handle)) {
					throw new CryptoKeyCollisionException();
				}

				cacheBucket[handle] = key;

				this.CleanExpiredKeysFromMemoryCacheIfAppropriate();
			}
		}

		/// <summary>
		/// Removes the key.
		/// </summary>
		/// <param name="bucket">The bucket name.  Case sensitive.</param>
		/// <param name="handle">The key handle.  Case sensitive.</param>
		public void RemoveKey(string bucket, string handle) {
			lock (this.store) {
				Dictionary<string, CryptoKey> cacheBucket;
				if (this.store.TryGetValue(bucket, out cacheBucket)) {
					cacheBucket.Remove(handle);
				}
			}
		}

		/// <summary>
		/// Cleans the expired keys from memory cache if the cleaning interval has passed.
		/// </summary>
		private void CleanExpiredKeysFromMemoryCacheIfAppropriate() {
			if (DateTime.UtcNow > this.lastCleaning + cleaningInterval) {
				lock (this.store) {
					if (DateTime.UtcNow > this.lastCleaning + cleaningInterval) {
						this.ClearExpiredKeysFromMemoryCache();
					}
				}
			}
		}

		/// <summary>
		/// Weeds out expired keys from the in-memory cache.
		/// </summary>
		private void ClearExpiredKeysFromMemoryCache() {
			lock (this.store) {
				var emptyBuckets = new List<string>();
				foreach (var bucketPair in this.store) {
					var expiredKeys = new List<string>();
					foreach (var handlePair in bucketPair.Value) {
						if (handlePair.Value.ExpiresUtc < DateTime.UtcNow) {
							expiredKeys.Add(handlePair.Key);
						}
					}

					foreach (var expiredKey in expiredKeys) {
						bucketPair.Value.Remove(expiredKey);
					}

					if (bucketPair.Value.Count == 0) {
						emptyBuckets.Add(bucketPair.Key);
					}
				}

				foreach (string emptyBucket in emptyBuckets) {
					this.store.Remove(emptyBucket);
				}

				this.lastCleaning = DateTime.UtcNow;
			}
		}
	}
}