summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs254
-rw-r--r--src/DotNetOpenAuth/CryptoKey.cs91
-rw-r--r--src/DotNetOpenAuth/DotNetOpenAuth.csproj3
-rw-r--r--src/DotNetOpenAuth/ICryptoKeyStore.cs100
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs9
5 files changed, 448 insertions, 9 deletions
diff --git a/src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs b/src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs
new file mode 100644
index 0000000..28f65d9
--- /dev/null
+++ b/src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs
@@ -0,0 +1,254 @@
+//-----------------------------------------------------------------------
+// <copyright file="AsymmetricCryptoKeyStoreWrapper.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Security.Cryptography;
+ using System.Text;
+
+ /// <summary>
+ /// Provides RSA encryption of symmetric keys to protect them from a theft of
+ /// the persistent store.
+ /// </summary>
+ public class AsymmetricCryptoKeyStoreWrapper : ICryptoKeyStore {
+ /// <summary>
+ /// How frequently to check for and remove expired secrets.
+ /// </summary>
+ private static readonly TimeSpan cleaningInterval = TimeSpan.FromMinutes(30);
+
+ /// <summary>
+ /// The persistent store for asymmetrically encrypted symmetric keys.
+ /// </summary>
+ private readonly ICryptoKeyStore dataStore;
+
+ /// <summary>
+ /// The asymmetric algorithm to use encrypting/decrypting the symmetric keys.
+ /// </summary>
+ private readonly RSACryptoServiceProvider asymmetricCrypto;
+
+ /// <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, CachedCryptoKey>> decryptedKeyCache = new Dictionary<string, Dictionary<string, CachedCryptoKey>>(StringComparer.Ordinal);
+
+ /// <summary>
+ /// The last time the cache had expired keys removed from it.
+ /// </summary>
+ private DateTime lastCleaning = DateTime.UtcNow;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AsymmetricCryptoKeyStoreWrapper"/> class.
+ /// </summary>
+ /// <param name="dataStore">The data store.</param>
+ /// <param name="asymmetricCrypto">The asymmetric protection to apply to symmetric keys. Must include the private key.</param>
+ public AsymmetricCryptoKeyStoreWrapper(ICryptoKeyStore dataStore, RSACryptoServiceProvider asymmetricCrypto) {
+ Contract.Requires<ArgumentNullException>(dataStore != null, "dataStore");
+ Contract.Requires<ArgumentNullException>(asymmetricCrypto != null, "asymmetricCrypto");
+ Contract.Requires<ArgumentException>(!asymmetricCrypto.PublicOnly);
+ this.dataStore = dataStore;
+ this.asymmetricCrypto = asymmetricCrypto;
+ }
+
+ /// <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) {
+ var key = this.dataStore.GetKey(bucket, handle);
+ return this.Decrypt(bucket, handle, key);
+ }
+
+ /// <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) {
+ return this.dataStore.GetKeys(bucket)
+ .Select(pair => new KeyValuePair<string, CryptoKey>(pair.Key, this.Decrypt(bucket, pair.Key, pair.Value)));
+ }
+
+ /// <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="decryptedCryptoKey">The key to store.</param>
+ public void StoreKey(string bucket, string handle, CryptoKey decryptedCryptoKey) {
+ byte[] encryptedKey = this.asymmetricCrypto.Encrypt(decryptedCryptoKey.Key, true);
+ var encryptedCryptoKey = new CryptoKey(encryptedKey, decryptedCryptoKey.ExpiresUtc);
+ this.dataStore.StoreKey(bucket, handle, encryptedCryptoKey);
+
+ this.CacheKey(bucket, handle, decryptedCryptoKey, encryptedCryptoKey);
+
+ 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) {
+ this.dataStore.RemoveKey(bucket, handle);
+
+ lock (this.decryptedKeyCache) {
+ Dictionary<string, CachedCryptoKey> cacheBucket;
+ if (this.decryptedKeyCache.TryGetValue(bucket, out cacheBucket)) {
+ cacheBucket.Remove(handle);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Caches an encrypted/decrypted key pair.
+ /// </summary>
+ /// <param name="bucket">The bucket.</param>
+ /// <param name="handle">The handle.</param>
+ /// <param name="decryptedCryptoKey">The decrypted crypto key.</param>
+ /// <param name="encryptedCryptoKey">The encrypted crypto key.</param>
+ private void CacheKey(string bucket, string handle, CryptoKey decryptedCryptoKey, CryptoKey encryptedCryptoKey) {
+ Contract.Requires(!String.IsNullOrEmpty(bucket));
+ Contract.Requires(!String.IsNullOrEmpty(handle));
+ Contract.Requires(decryptedKeyCache != null);
+ Contract.Requires(encryptedCryptoKey != null);
+
+ lock (this.decryptedKeyCache) {
+ Dictionary<string, CachedCryptoKey> cacheBucket;
+ if (!this.decryptedKeyCache.TryGetValue(bucket, out cacheBucket)) {
+ this.decryptedKeyCache[bucket] = cacheBucket = new Dictionary<string, CachedCryptoKey>(StringComparer.Ordinal);
+ }
+
+ cacheBucket[handle] = new CachedCryptoKey(encryptedCryptoKey, decryptedCryptoKey);
+ }
+ }
+
+ /// <summary>
+ /// Decrypts the specified key.
+ /// </summary>
+ /// <param name="encryptedCryptoKey">The encrypted key.</param>
+ /// <returns>The decrypted key.</returns>
+ private CryptoKey Decrypt(string bucket, string handle, CryptoKey encryptedCryptoKey) {
+ if (encryptedCryptoKey == null) {
+ return null;
+ }
+
+ // Avoid the asymmetric decryption if possible by looking up whether we have that in our cache.
+ CachedCryptoKey cached;
+ lock (this.decryptedKeyCache) {
+ Dictionary<string, CachedCryptoKey> cacheBucket;
+ if (this.decryptedKeyCache.TryGetValue(bucket, out cacheBucket)) {
+ if (cacheBucket.TryGetValue(handle, out cached) && encryptedCryptoKey.Equals(cached.Encrypted)) {
+ return cached.Decrypted;
+ }
+ }
+ }
+
+ byte[] decryptedKey = this.asymmetricCrypto.Decrypt(encryptedCryptoKey.Key, true);
+ var decryptedCryptoKey = new CryptoKey(decryptedKey, encryptedCryptoKey.ExpiresUtc);
+
+ // Store the decrypted version in the cache to save time next time.
+ this.CacheKey(bucket, handle, decryptedCryptoKey, encryptedCryptoKey);
+
+ return decryptedCryptoKey;
+ }
+
+ /// <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.decryptedKeyCache) {
+ if (DateTime.UtcNow > this.lastCleaning + cleaningInterval) {
+ this.ClearExpiredKeysFromMemoryCache();
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Weeds out expired keys from the in-memory cache.
+ /// </summary>
+ private void ClearExpiredKeysFromMemoryCache() {
+ lock (this.decryptedKeyCache) {
+ var emptyBuckets = new List<string>();
+ foreach (var bucketPair in this.decryptedKeyCache) {
+ var expiredKeys = new List<string>();
+ foreach (var handlePair in bucketPair.Value) {
+ if (handlePair.Value.Encrypted.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.decryptedKeyCache.Remove(emptyBucket);
+ }
+
+ this.lastCleaning = DateTime.UtcNow;
+ }
+ }
+
+ /// <summary>
+ /// An encrypted key and its decrypted equivalent.
+ /// </summary>
+ private class CachedCryptoKey {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CachedCryptoKey"/> class.
+ /// </summary>
+ /// <param name="encrypted">The encrypted key.</param>
+ /// <param name="decrypted">The decrypted key.</param>
+ internal CachedCryptoKey(CryptoKey encrypted, CryptoKey decrypted) {
+ Contract.Requires(encrypted != null);
+ Contract.Requires(decrypted != null);
+
+ this.Encrypted = encrypted;
+ this.Decrypted = decrypted;
+ }
+
+ /// <summary>
+ /// Gets or sets the encrypted key.
+ /// </summary>
+ internal CryptoKey Encrypted { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the decrypted key.
+ /// </summary>
+ internal CryptoKey Decrypted { get; private set; }
+
+ /// <summary>
+ /// Invariant conditions.
+ /// </summary>
+ [ContractInvariantMethod]
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
+ private void ObjectInvariant() {
+ Contract.Invariant(this.Encrypted != null);
+ Contract.Invariant(this.Decrypted != null);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/CryptoKey.cs b/src/DotNetOpenAuth/CryptoKey.cs
new file mode 100644
index 0000000..7a4f788
--- /dev/null
+++ b/src/DotNetOpenAuth/CryptoKey.cs
@@ -0,0 +1,91 @@
+//-----------------------------------------------------------------------
+// <copyright file="CryptoKey.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A cryptographic key and metadata concerning it.
+ /// </summary>
+ public class CryptoKey {
+ /// <summary>
+ /// Backing field for the <see cref="Key"/> property.
+ /// </summary>
+ private readonly byte[] key;
+
+ /// <summary>
+ /// Backing field for the <see cref="ExpiresUtc"/> property.
+ /// </summary>
+ private readonly DateTime expiresUtc;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CryptoKey"/> struct.
+ /// </summary>
+ /// <param name="key">The cryptographic key.</param>
+ /// <param name="expiresUtc">The expires UTC.</param>
+ public CryptoKey(byte[] key, DateTime expiresUtc) {
+ Contract.Requires<ArgumentNullException>(key != null, "key");
+ Contract.Requires<ArgumentException>(expiresUtc.Kind == DateTimeKind.Utc);
+ this.key = key;
+ this.expiresUtc = expiresUtc;
+ }
+
+ /// <summary>
+ /// Gets the key.
+ /// </summary>
+ public byte[] Key {
+ get {
+ Contract.Ensures(Contract.Result<byte[]>() != null);
+ return this.key;
+ }
+ }
+
+ /// <summary>
+ /// Gets the expiration date of this key (UTC time).
+ /// </summary>
+ public DateTime ExpiresUtc {
+ get {
+ Contract.Ensures(Contract.Result<DateTime>().Kind == DateTimeKind.Utc);
+ return this.expiresUtc;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
+ /// </summary>
+ /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
+ /// <returns>
+ /// <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ var other = obj as CryptoKey;
+ if (other == null) {
+ return false;
+ }
+
+ return this.ExpiresUtc == other.ExpiresUtc
+ && MessagingUtilities.AreEquivalent(this.Key, other.Key);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ /// </returns>
+ public override int GetHashCode() {
+ return this.ExpiresUtc.GetHashCode();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
index 931366c..32285a3 100644
--- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj
+++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
@@ -275,6 +275,7 @@ http://opensource.org/licenses/ms-pl.html
<Reference Include="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
</ItemGroup>
<ItemGroup>
+ <Compile Include="AsymmetricCryptoKeyStoreWrapper.cs" />
<Compile Include="ComponentModel\ClaimTypeSuggestions.cs" />
<Compile Include="ComponentModel\ConverterBase.cs" />
<Compile Include="ComponentModel\SuggestedStringsConverterContract.cs" />
@@ -305,6 +306,8 @@ http://opensource.org/licenses/ms-pl.html
<Compile Include="Configuration\HostNameOrRegexCollection.cs" />
<Compile Include="Configuration\HostNameElement.cs" />
<Compile Include="Configuration\XriResolverElement.cs" />
+ <Compile Include="CryptoKey.cs" />
+ <Compile Include="ICryptoKeyStore.cs" />
<Compile Include="IEmbeddedResourceRetrieval.cs" />
<Compile Include="InfoCard\ClaimType.cs" />
<Compile Include="InfoCard\InfoCardImage.cs" />
diff --git a/src/DotNetOpenAuth/ICryptoKeyStore.cs b/src/DotNetOpenAuth/ICryptoKeyStore.cs
new file mode 100644
index 0000000..4f221be
--- /dev/null
+++ b/src/DotNetOpenAuth/ICryptoKeyStore.cs
@@ -0,0 +1,100 @@
+//-----------------------------------------------------------------------
+// <copyright file="ICryptoKeyStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A persistent store for rotating symmetric cryptographic keys.
+ /// </summary>
+ /// <remarks>
+ /// Implementations should persist it in such a way that the keys are shared across all servers
+ /// on a web farm, where applicable.
+ /// The store should consider protecting the persistent store against theft resulting in the loss
+ /// of the confidentiality of the keys. One possible mitigation is to asymmetrically encrypt
+ /// each key using a certificate installed in the server's certificate store.
+ /// </remarks>
+ [ContractClass(typeof(ICryptoKeyStoreContract))]
+ public interface ICryptoKeyStore {
+ /// <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>
+ CryptoKey GetKey(string bucket, string handle);
+
+ /// <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>
+ IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket);
+
+ /// <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>
+ void StoreKey(string bucket, string handle, CryptoKey key);
+
+ /// <summary>
+ /// Removes the key.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ void RemoveKey(string bucket, string handle);
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="ICryptoKeyStore"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(ICryptoKeyStore))]
+ internal abstract class ICryptoKeyStoreContract : ICryptoKeyStore {
+ /// <summary>
+ /// See the <see cref="ICryptoKeyStore"/> interface.
+ /// </summary>
+ CryptoKey ICryptoKeyStore.GetKey(string bucket, string handle) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(bucket));
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(handle));
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// See the <see cref="ICryptoKeyStore"/> interface.
+ /// </summary>
+ IEnumerable<KeyValuePair<string, CryptoKey>> ICryptoKeyStore.GetKeys(string bucket) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(bucket));
+ Contract.Ensures(Contract.Result<IEnumerable<KeyValuePair<string, CryptoKey>>>() != null);
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// See the <see cref="ICryptoKeyStore"/> interface.
+ /// </summary>
+ void ICryptoKeyStore.StoreKey(string bucket, string handle, CryptoKey key) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(bucket));
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(handle));
+ Contract.Requires<ArgumentNullException>(key != null, "key");
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// See the <see cref="ICryptoKeyStore"/> interface.
+ /// </summary>
+ void ICryptoKeyStore.RemoveKey(string bucket, string handle) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(bucket));
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(handle));
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs
index 348c8fb..02cf4c5 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs
@@ -15,15 +15,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
/// </summary>
internal class PrivateSecretManager {
/// <summary>
- /// The optimal length for a private secret used for signing using the HMACSHA256 class.
- /// </summary>
- /// <remarks>
- /// The 64-byte length is optimized for highest security when used with HMACSHA256.
- /// See HMACSHA256.HMACSHA256(byte[]) documentation for more information.
- /// </remarks>
- private const int OptimalPrivateSecretLength = 64;
-
- /// <summary>
/// The URI to use for private associations at this RP.
/// </summary>
private static readonly Uri SecretUri = new Uri("https://localhost/dnoa/secret");