diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs | 254 | ||||
-rw-r--r-- | src/DotNetOpenAuth/CryptoKey.cs | 91 | ||||
-rw-r--r-- | src/DotNetOpenAuth/DotNetOpenAuth.csproj | 3 | ||||
-rw-r--r-- | src/DotNetOpenAuth/ICryptoKeyStore.cs | 100 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs | 9 |
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"); |