diff options
13 files changed, 371 insertions, 417 deletions
diff --git a/src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs b/src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs index 28f65d9..203d6ab 100644 --- a/src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs +++ b/src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs @@ -12,6 +12,7 @@ namespace DotNetOpenAuth { using System.Linq; using System.Security.Cryptography; using System.Text; + using DotNetOpenAuth.Messaging; /// <summary> /// Provides RSA encryption of symmetric keys to protect them from a theft of @@ -19,32 +20,19 @@ namespace DotNetOpenAuth { /// </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. + /// The memory cache of decrypted 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); + private readonly MemoryCryptoKeyStore cache = new MemoryCryptoKeyStore(); /// <summary> - /// The last time the cache had expired keys removed from it. + /// The asymmetric algorithm to use encrypting/decrypting the symmetric keys. /// </summary> - private DateTime lastCleaning = DateTime.UtcNow; + private readonly RSACryptoServiceProvider asymmetricCrypto; /// <summary> /// Initializes a new instance of the <see cref="AsymmetricCryptoKeyStoreWrapper"/> class. @@ -95,9 +83,7 @@ namespace DotNetOpenAuth { var encryptedCryptoKey = new CryptoKey(encryptedKey, decryptedCryptoKey.ExpiresUtc); this.dataStore.StoreKey(bucket, handle, encryptedCryptoKey); - this.CacheKey(bucket, handle, decryptedCryptoKey, encryptedCryptoKey); - - this.CleanExpiredKeysFromMemoryCacheIfAppropriate(); + this.cache.StoreKey(bucket, handle, new CachedCryptoKey(encryptedCryptoKey, decryptedCryptoKey)); } /// <summary> @@ -107,36 +93,7 @@ namespace DotNetOpenAuth { /// <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); - } + this.cache.RemoveKey(bucket, handle); } /// <summary> @@ -150,95 +107,42 @@ namespace DotNetOpenAuth { } // 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; - } - } + CachedCryptoKey cached = (CachedCryptoKey)this.cache.GetKey(bucket, handle); + if (cached != null && MessagingUtilities.AreEquivalent(cached.EncryptedKey, encryptedCryptoKey.Key)) { + return cached; } 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); + this.cache.StoreKey(bucket, handle, new CachedCryptoKey(encryptedCryptoKey, decryptedCryptoKey)); 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 { + private class CachedCryptoKey : CryptoKey { /// <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) { + internal CachedCryptoKey(CryptoKey encrypted, CryptoKey decrypted) + : base(decrypted.Key, decrypted.ExpiresUtc) { Contract.Requires(encrypted != null); Contract.Requires(decrypted != null); + Contract.Requires(encrypted.ExpiresUtc == decrypted.ExpiresUtc); - this.Encrypted = encrypted; - this.Decrypted = decrypted; + this.EncryptedKey = encrypted.Key; } /// <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; } + internal byte[] EncryptedKey { get; private set; } /// <summary> /// Invariant conditions. @@ -246,8 +150,7 @@ namespace DotNetOpenAuth { [ContractInvariantMethod] [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")] private void ObjectInvariant() { - Contract.Invariant(this.Encrypted != null); - Contract.Invariant(this.Decrypted != null); + Contract.Invariant(this.EncryptedKey != null); } } } diff --git a/src/DotNetOpenAuth/CryptoKeyCollisionException.cs b/src/DotNetOpenAuth/CryptoKeyCollisionException.cs new file mode 100644 index 0000000..e68e022 --- /dev/null +++ b/src/DotNetOpenAuth/CryptoKeyCollisionException.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------- +// <copyright file="CryptoKeyCollisionException.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth { + using System; + using System.Security.Permissions; + + [Serializable] + public class CryptoKeyCollisionException : ArgumentException { + /// <summary> + /// Initializes a new instance of the <see cref="CryptoKeyCollisionException"/> class. + /// </summary> + public CryptoKeyCollisionException() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="CryptoKeyCollisionException"/> class. + /// </summary> + /// <param name="message">A message describing the specific error the occurred or was detected.</param> + public CryptoKeyCollisionException(string message) : base(message) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="CryptoKeyCollisionException"/> class. + /// </summary> + /// <param name="message">A message describing the specific error the occurred or was detected.</param> + /// <param name="inner">The inner exception to include.</param> + public CryptoKeyCollisionException(string message, Exception inner) : base(message, inner) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="CryptoKeyCollisionException"/> class. + /// </summary> + /// <param name="info">The <see cref="System.Runtime.Serialization.SerializationInfo"/> + /// that holds the serialized object data about the exception being thrown.</param> + /// <param name="context">The System.Runtime.Serialization.StreamingContext + /// that contains contextual information about the source or destination.</param> + protected CryptoKeyCollisionException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) { + throw new NotImplementedException(); + } + + /// <summary> + /// When overridden in a derived class, sets the <see cref="T:System.Runtime.Serialization.SerializationInfo"/> with information about the exception. + /// </summary> + /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param> + /// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext"/> that contains contextual information about the source or destination.</param> + /// <exception cref="T:System.ArgumentNullException"> + /// The <paramref name="info"/> parameter is a null reference (Nothing in Visual Basic). + /// </exception> + /// <PermissionSet> + /// <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Read="*AllFiles*" PathDiscovery="*AllFiles*"/> + /// <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="SerializationFormatter"/> + /// </PermissionSet> +#if CLR4 + [SecurityCritical] +#else + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] +#endif + public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { + base.GetObjectData(info, context); + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 2dc5653..c91f92b 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -307,6 +307,7 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="Configuration\HostNameElement.cs" /> <Compile Include="Configuration\XriResolverElement.cs" /> <Compile Include="CryptoKey.cs" /> + <Compile Include="CryptoKeyCollisionException.cs" /> <Compile Include="ICryptoKeyStore.cs" /> <Compile Include="IEmbeddedResourceRetrieval.cs" /> <Compile Include="InfoCard\ClaimType.cs" /> @@ -321,6 +322,7 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="InfoCard\Token\TokenUtility.cs" /> <Compile Include="InfoCard\Token\TokenDecryptor.cs" /> <Compile Include="InfoCard\WellKnownIssuers.cs" /> + <Compile Include="MemoryCryptoKeyStore.cs" /> <Compile Include="Messaging\BinaryDataBagFormatter.cs" /> <Compile Include="Messaging\CachedDirectWebResponse.cs" /> <Compile Include="Messaging\ChannelContract.cs" /> @@ -500,7 +502,6 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OpenId\Association.cs" /> <Compile Include="OpenId\Provider\AssociationDataBag.cs" /> <Compile Include="OpenId\Provider\IProviderAssociationStore.cs" /> - <Compile Include="OpenId\RelyingParty\AssociationMemoryStore.cs" /> <Compile Include="OpenId\Provider\ProviderAssociationHandleEncoder.cs" /> <Compile Include="OpenId\RelyingParty\CryptoKeyStoreAsRelyingPartyAssociationStore.cs" /> <Compile Include="OpenId\RelyingParty\IRelyingPartyAssociationStore.cs" /> @@ -660,7 +661,6 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OpenId\Provider\ProviderSecuritySettings.cs" /> <Compile Include="OpenId\RelyingParty\IRelyingPartyApplicationStore.cs" /> <Compile Include="OpenId\RelyingParty\PositiveAuthenticationResponseSnapshot.cs" /> - <Compile Include="OpenId\RelyingParty\PrivateSecretManager.cs" /> <Compile Include="OpenId\RelyingParty\RelyingPartySecuritySettings.cs" /> <Compile Include="OpenId\IdentifierDiscoveryResult.cs" /> <Compile Include="OpenId\OpenIdXrdsHelper.cs" /> diff --git a/src/DotNetOpenAuth/ICryptoKeyStore.cs b/src/DotNetOpenAuth/ICryptoKeyStore.cs index 4f221be..d2a5147 100644 --- a/src/DotNetOpenAuth/ICryptoKeyStore.cs +++ b/src/DotNetOpenAuth/ICryptoKeyStore.cs @@ -45,6 +45,7 @@ namespace DotNetOpenAuth { /// <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> void StoreKey(string bucket, string handle, CryptoKey key); /// <summary> diff --git a/src/DotNetOpenAuth/MemoryCryptoKeyStore.cs b/src/DotNetOpenAuth/MemoryCryptoKeyStore.cs new file mode 100644 index 0000000..fcf1f23 --- /dev/null +++ b/src/DotNetOpenAuth/MemoryCryptoKeyStore.cs @@ -0,0 +1,156 @@ +//----------------------------------------------------------------------- +// <copyright file="MemoryCryptoKeyStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth { + 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; + } + } + } +} diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index 2119d78..3062f23 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -27,6 +27,13 @@ namespace DotNetOpenAuth.Messaging { /// A grab-bag of utility methods useful for the channel stack of the protocol. /// </summary> public static class MessagingUtilities { + private const int SymmetricSecretHandleLength = 4; // TODO: replace this with an unnamed secret concept. + + /// <summary> + /// The default lifetime of a private secret. + /// </summary> + private static readonly TimeSpan SymmetricSecretKeyLifespan = Configuration.DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.SecuritySettings.PrivateSecretMaximumAge; + /// <summary> /// The cryptographically strong random data generator used for creating secrets. /// </summary> @@ -54,6 +61,11 @@ namespace DotNetOpenAuth.Messaging { internal const string Digits = "0123456789"; /// <summary> + /// The set of digits and alphabetic letters (upper and lowercase). + /// </summary> + internal const string AlphaNumeric = UppercaseLetters + LowercaseLetters + Digits; + + /// <summary> /// The set of digits, and alphabetic letters (upper and lowercase) that are clearly /// visually distinguishable. /// </summary> @@ -654,6 +666,45 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Gets a key from a given bucket with the longest remaining life, or creates a new one if necessary. + /// </summary> + /// <param name="cryptoKeyStore">The crypto key store.</param> + /// <param name="bucket">The bucket where the key should be found or stored.</param> + /// <param name="minimumRemainingLife">The minimum remaining life required on the returned key.</param> + /// <param name="keySize">The required size of the key, in bits.</param> + /// <returns> + /// A key-value pair whose key is the secret's handle and whose value is the cryptographic key. + /// </returns> + internal static KeyValuePair<string, CryptoKey> GetCurrentKey(this ICryptoKeyStore cryptoKeyStore, string bucket, TimeSpan minimumRemainingLife, int keySize = 256) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore"); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(bucket)); + Contract.Requires<ArgumentException>(keySize % 8 == 0); + + var cryptoKeyPair = cryptoKeyStore.GetKeys(bucket).FirstOrDefault(pair => pair.Value.Key.Length == keySize / 8); + if (cryptoKeyPair.Value == null || cryptoKeyPair.Value.ExpiresUtc < DateTime.UtcNow + minimumRemainingLife) { + // No key exists with enough remaining life for the required purpose. Create a new key. + ErrorUtilities.VerifyProtocol(minimumRemainingLife <= SymmetricSecretKeyLifespan, "Unable to create a new symmetric key with the required lifespan of {0} because it is beyond the limit of {1}.", minimumRemainingLife, SymmetricSecretKeyLifespan); + byte[] secret = GetCryptoRandomData(keySize / 8); + DateTime expires = DateTime.UtcNow + SymmetricSecretKeyLifespan; + var cryptoKey = new CryptoKey(secret, expires); + + // Store this key so we can find and use it later. + int failedAttempts = 0; + tryAgain: + try { + string handle = GetRandomString(SymmetricSecretHandleLength, AlphaNumeric); + cryptoKeyPair = new KeyValuePair<string, CryptoKey>(handle, cryptoKey); + cryptoKeyStore.StoreKey(bucket, handle, cryptoKey); + } catch (CryptoKeyCollisionException) { + ErrorUtilities.VerifyInternal(++failedAttempts < 3, "Unable to derive a unique handle to a private symmetric key."); + goto tryAgain; + } + } + + return cryptoKeyPair; + } + + /// <summary> /// Compresses a given buffer. /// </summary> /// <param name="buffer">The buffer to compress.</param> diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs index 0acd796..072ebe0 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs @@ -46,11 +46,11 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// Initializes a new instance of the <see cref="OpenIdChannel"/> class /// for use by a Relying Party. /// </summary> - /// <param name="associationStore">The association store to use.</param> + /// <param name="cryptoKeyStore">The association store to use.</param> /// <param name="nonceStore">The nonce store to use.</param> /// <param name="securitySettings">The security settings to apply.</param> - internal OpenIdChannel(IRelyingPartyAssociationStore associationStore, INonceStore nonceStore, RelyingPartySecuritySettings securitySettings) - : this(associationStore, nonceStore, new OpenIdMessageFactory(), securitySettings, false) { + internal OpenIdChannel(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore, RelyingPartySecuritySettings securitySettings) + : this(cryptoKeyStore, nonceStore, new OpenIdMessageFactory(), securitySettings, false) { Contract.Requires<ArgumentNullException>(securitySettings != null); } @@ -71,13 +71,13 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// Initializes a new instance of the <see cref="OpenIdChannel"/> class /// for use by a Relying Party. /// </summary> - /// <param name="associationStore">The association store to use.</param> + /// <param name="cryptoKeyStore">The association store to use.</param> /// <param name="nonceStore">The nonce store to use.</param> /// <param name="messageTypeProvider">An object that knows how to distinguish the various OpenID message types for deserialization purposes.</param> /// <param name="securitySettings">The security settings to apply.</param> /// <param name="nonVerifying">A value indicating whether the channel is set up with no functional security binding elements.</param> - private OpenIdChannel(IRelyingPartyAssociationStore associationStore, INonceStore nonceStore, IMessageFactory messageTypeProvider, RelyingPartySecuritySettings securitySettings, bool nonVerifying) : - this(messageTypeProvider, InitializeBindingElements(associationStore, nonceStore, securitySettings, nonVerifying)) { + private OpenIdChannel(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore, IMessageFactory messageTypeProvider, RelyingPartySecuritySettings securitySettings, bool nonVerifying) : + this(messageTypeProvider, InitializeBindingElements(cryptoKeyStore, nonceStore, securitySettings, nonVerifying)) { Contract.Requires<ArgumentNullException>(messageTypeProvider != null); Contract.Requires<ArgumentNullException>(securitySettings != null); Contract.Requires<ArgumentException>(!nonVerifying || securitySettings is RelyingPartySecuritySettings); @@ -311,11 +311,11 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// <returns> /// An array of binding elements which may be used to construct the channel. /// </returns> - private static IChannelBindingElement[] InitializeBindingElements(IRelyingPartyAssociationStore associationStore, INonceStore nonceStore, RelyingPartySecuritySettings securitySettings, bool nonVerifying) { + private static IChannelBindingElement[] InitializeBindingElements(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore, RelyingPartySecuritySettings securitySettings, bool nonVerifying) { Contract.Requires<ArgumentNullException>(securitySettings != null); SigningBindingElement signingElement; - signingElement = nonVerifying ? null : new SigningBindingElement(associationStore); + signingElement = nonVerifying ? null : new SigningBindingElement(new CryptoKeyStoreAsRelyingPartyAssociationStore(cryptoKeyStore)); var extensionFactory = OpenIdExtensionFactoryAggregator.LoadFromConfiguration(); @@ -325,7 +325,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { elements.Add(new BackwardCompatibilityBindingElement()); ReturnToNonceBindingElement requestNonceElement = null; - if (associationStore != null) { + if (cryptoKeyStore != null) { if (nonceStore != null) { // There is no point in having a ReturnToNonceBindingElement without // a ReturnToSignatureBindingElement because the nonce could be @@ -336,7 +336,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { // It is important that the return_to signing element comes last // so that the nonce is included in the signature. - elements.Add(new ReturnToSignatureBindingElement(associationStore, securitySettings)); + elements.Add(new ReturnToSignatureBindingElement(cryptoKeyStore)); } ErrorUtilities.VerifyOperation(!securitySettings.RejectUnsolicitedAssertions || requestNonceElement != null, OpenIdStrings.UnsolicitedAssertionRejectionRequiresNonceStore); diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs index eee7509..fea68d0 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs @@ -11,6 +11,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { using System.Diagnostics.Contracts; using System.Security.Cryptography; using System.Web; + using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId.Messages; using DotNetOpenAuth.OpenId.RelyingParty; @@ -43,19 +44,24 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { private const string ReturnToSignatureHandleParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig_handle"; /// <summary> - /// The hashing algorithm used to generate the private signature on the return_to parameter. + /// The URI to use for private associations at this RP. /// </summary> - private PrivateSecretManager secretManager; + private static readonly Uri SecretUri = new Uri("https://localhost/dnoa/secret"); + + /// <summary> + /// The key store used to generate the private signature on the return_to parameter. + /// </summary> + private ICryptoKeyStore cryptoKeyStore; /// <summary> /// Initializes a new instance of the <see cref="ReturnToSignatureBindingElement"/> class. /// </summary> /// <param name="secretStore">The secret store from which to retrieve the secret used for signing.</param> /// <param name="securitySettings">The security settings.</param> - internal ReturnToSignatureBindingElement(IRelyingPartyAssociationStore secretStore, RelyingPartySecuritySettings securitySettings) { - Contract.Requires<ArgumentNullException>(secretStore != null); + internal ReturnToSignatureBindingElement(ICryptoKeyStore cryptoKeyStore) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); - this.secretManager = new PrivateSecretManager(securitySettings, secretStore); + this.cryptoKeyStore = cryptoKeyStore; } #region IChannelBindingElement Members @@ -96,8 +102,10 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { SignedResponseRequest request = message as SignedResponseRequest; if (request != null && request.ReturnTo != null && request.SignReturnTo) { - request.AddReturnToArguments(ReturnToSignatureHandleParameterName, this.secretManager.CurrentHandle); - request.AddReturnToArguments(ReturnToSignatureParameterName, this.GetReturnToSignature(request.ReturnTo)); + var cryptoKeyPair = this.cryptoKeyStore.GetCurrentKey(SecretUri.AbsoluteUri, DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime); + request.AddReturnToArguments(ReturnToSignatureHandleParameterName, cryptoKeyPair.Key); + string signature = Convert.ToBase64String(this.GetReturnToSignature(request.ReturnTo, cryptoKeyPair.Value)); + request.AddReturnToArguments(ReturnToSignatureParameterName, signature); // We return none because we are not signing the entire message (only a part). return MessageProtections.None; @@ -134,10 +142,11 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { // Only check the return_to signature if one is present. if (returnToParameters[ReturnToSignatureHandleParameterName] != null) { // Set the safety flag showing whether the return_to url had a valid signature. - string expected = this.GetReturnToSignature(response.ReturnTo); + byte[] expectedBytes = this.GetReturnToSignature(response.ReturnTo); string actual = returnToParameters[ReturnToSignatureParameterName]; actual = OpenIdUtilities.FixDoublyUriDecodedBase64String(actual); - response.ReturnToParametersSignatureValidated = actual == expected; + byte[] actualBytes = Convert.FromBase64String(actual); + response.ReturnToParametersSignatureValidated = MessagingUtilities.AreEquivalentConstantTime(actualBytes, expectedBytes); if (!response.ReturnToParametersSignatureValidated) { Logger.Bindings.WarnFormat("The return_to signature failed verification."); } @@ -161,7 +170,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// itself, in order that OPs that might change the return_to's implicit port :80 part /// or other minor changes do not invalidate the signature. /// </remarks> - private string GetReturnToSignature(Uri returnTo) { + private byte[] GetReturnToSignature(Uri returnTo, CryptoKey cryptoKey = null) { Contract.Requires<ArgumentNullException>(returnTo != null); // Assemble the dictionary to sign, taking care to remove the signature itself @@ -182,12 +191,17 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { byte[] bytesToSign = KeyValueFormEncoding.GetBytes(sortedReturnToParameters); byte[] signature; try { - signature = this.secretManager.Sign(bytesToSign, returnToParameters[ReturnToSignatureHandleParameterName]); + if (cryptoKey == null) { + cryptoKey = this.cryptoKeyStore.GetKey(SecretUri.AbsoluteUri, returnToParameters[ReturnToSignatureHandleParameterName]); + } + + var signer = new HMACSHA256(cryptoKey.Key); + signature = signer.ComputeHash(bytesToSign); } catch (ProtocolException ex) { throw ErrorUtilities.Wrap(ex, OpenIdStrings.MaximumAuthenticationTimeExpired); } - string signatureBase64 = Convert.ToBase64String(signature); - return signatureBase64; + + return signature; } } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationMemoryStore.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationMemoryStore.cs deleted file mode 100644 index 9224a1a..0000000 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationMemoryStore.cs +++ /dev/null @@ -1,133 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="AssociationMemoryStore.cs" company="Andrew Arnott"> -// Copyright (c) Andrew Arnott. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.OpenId.RelyingParty { - using System; - using System.Collections.Generic; - using System.Linq; - - /// <summary> - /// Manages a set of associations in memory only (no database). - /// </summary> - /// <remarks> - /// This class should be used for low-to-medium traffic relying party sites that can afford to lose associations - /// if the app pool was ever restarted. High traffic relying parties and providers should write their own - /// implementation of <see cref="IRelyingPartyAssociationStore"/> that works against their own database schema - /// to allow for persistance and recall of associations across servers in a web farm and server restarts. - /// </remarks> - internal class AssociationMemoryStore : IRelyingPartyAssociationStore { - /// <summary> - /// How many association store requests should occur between each spring cleaning. - /// </summary> - private const int PeriodicCleaningFrequency = 10; - - /// <summary> - /// For Relying Parties, this maps OP Endpoints to a set of associations with that endpoint. - /// For Providers, this keeps smart and dumb associations in two distinct pools. - /// </summary> - private Dictionary<Uri, Associations> serverAssocsTable = new Dictionary<Uri, Associations>(); - - /// <summary> - /// A counter to track how close we are to an expired association cleaning run. - /// </summary> - private int periodicCleaning; - - /// <summary> - /// Stores a given association for later recall. - /// </summary> - /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param> - /// <param name="association">The association to store.</param> - public void StoreAssociation(Uri providerEndpoint, Association association) { - lock (this) { - if (!this.serverAssocsTable.ContainsKey(providerEndpoint)) { - this.serverAssocsTable.Add(providerEndpoint, new Associations()); - } - Associations server_assocs = this.serverAssocsTable[providerEndpoint]; - - server_assocs.Set(association); - - unchecked { - this.periodicCleaning++; - } - if (this.periodicCleaning % PeriodicCleaningFrequency == 0) { - this.ClearExpiredAssociations(); - } - } - } - - /// <summary> - /// Gets the best association (the one with the longest remaining life) for a given key. - /// </summary> - /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> - /// <param name="securitySettings">The security settings.</param> - /// <returns> - /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key. - /// </returns> - public Association GetAssociation(Uri providerEndpoint, SecuritySettings securitySettings) { - lock (this) { - return this.GetServerAssociations(providerEndpoint).Best.FirstOrDefault(assoc => securitySettings.IsAssociationInPermittedRange(assoc)); - } - } - - /// <summary> - /// Gets the association for a given key and handle. - /// </summary> - /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> - /// <param name="handle">The handle of the specific association that must be recalled.</param> - /// <returns> - /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key and handle. - /// </returns> - public Association GetAssociation(Uri providerEndpoint, string handle) { - lock (this) { - return this.GetServerAssociations(providerEndpoint).Get(handle); - } - } - - /// <summary> - /// Removes a specified handle that may exist in the store. - /// </summary> - /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> - /// <param name="handle">The handle of the specific association that must be deleted.</param> - /// <returns> - /// True if the association existed in this store previous to this call. - /// </returns> - /// <remarks> - /// No exception should be thrown if the association does not exist in the store - /// before this call. - /// </remarks> - public bool RemoveAssociation(Uri providerEndpoint, string handle) { - lock (this) { - return this.GetServerAssociations(providerEndpoint).Remove(handle); - } - } - - /// <summary> - /// Gets the server associations for a given OP Endpoint or dumb/smart mode. - /// </summary> - /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param> - /// <returns>The collection of associations that fit the <paramref name="providerEndpoint"/>.</returns> - internal Associations GetServerAssociations(Uri providerEndpoint) { - lock (this) { - if (!this.serverAssocsTable.ContainsKey(providerEndpoint)) { - this.serverAssocsTable.Add(providerEndpoint, new Associations()); - } - - return this.serverAssocsTable[providerEndpoint]; - } - } - - /// <summary> - /// Clears all expired associations from the store. - /// </summary> - private void ClearExpiredAssociations() { - lock (this) { - foreach (Associations assocs in this.serverAssocsTable.Values) { - assocs.ClearExpired(); - } - } - } - } -} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/IRelyingPartyApplicationStore.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/IRelyingPartyApplicationStore.cs index 0c80c0f..2943720 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/IRelyingPartyApplicationStore.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/IRelyingPartyApplicationStore.cs @@ -16,6 +16,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// A hybrid of all the store interfaces that a Relying Party requires in order /// to operate in "smart" mode. /// </summary> - public interface IRelyingPartyApplicationStore : IRelyingPartyAssociationStore, INonceStore { + public interface IRelyingPartyApplicationStore : ICryptoKeyStore, INonceStore { } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs index cf82cf3..6fa2194 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs @@ -107,14 +107,14 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class. /// </summary> - /// <param name="associationStore">The association store. If null, the relying party will always operate in "dumb mode".</param> + /// <param name="cryptoKeyStore">The association store. If null, the relying party will always operate in "dumb mode".</param> /// <param name="nonceStore">The nonce store to use. If null, the relying party will always operate in "dumb mode".</param> [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable")] - private OpenIdRelyingParty(IRelyingPartyAssociationStore associationStore, INonceStore nonceStore) { + private OpenIdRelyingParty(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore) { // If we are a smart-mode RP (supporting associations), then we MUST also be // capable of storing nonces to prevent replay attacks. // If we're a dumb-mode RP, then 2.0 OPs are responsible for preventing replays. - Contract.Requires<ArgumentException>(associationStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore); + Contract.Requires<ArgumentException>(cryptoKeyStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore); this.securitySettings = DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.SecuritySettings.CreateSecuritySettings(); @@ -137,10 +137,10 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20; } - this.channel = new OpenIdChannel(associationStore, nonceStore, this.SecuritySettings); - this.AssociationManager = new AssociationManager(this.Channel, associationStore, this.SecuritySettings); + this.channel = new OpenIdChannel(cryptoKeyStore, nonceStore, this.SecuritySettings); + this.AssociationManager = new AssociationManager(this.Channel, new CryptoKeyStoreAsRelyingPartyAssociationStore(cryptoKeyStore), this.SecuritySettings); - Reporting.RecordFeatureAndDependencyUse(this, associationStore, nonceStore); + Reporting.RecordFeatureAndDependencyUse(this, cryptoKeyStore, nonceStore); } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs deleted file mode 100644 index 02cf4c5..0000000 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs +++ /dev/null @@ -1,103 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="PrivateSecretManager.cs" company="Andrew Arnott"> -// Copyright (c) Andrew Arnott. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.OpenId.RelyingParty { - using System; - using System.Diagnostics.Contracts; - using DotNetOpenAuth.Configuration; - using DotNetOpenAuth.Messaging; - - /// <summary> - /// Manages signing at the RP using private secrets. - /// </summary> - internal class PrivateSecretManager { - /// <summary> - /// The URI to use for private associations at this RP. - /// </summary> - private static readonly Uri SecretUri = new Uri("https://localhost/dnoa/secret"); - - /// <summary> - /// The security settings that apply to this Relying Party. - /// </summary> - private RelyingPartySecuritySettings securitySettings; - - /// <summary> - /// The association store - /// </summary> - private IRelyingPartyAssociationStore store; - - /// <summary> - /// Initializes a new instance of the <see cref="PrivateSecretManager"/> class. - /// </summary> - /// <param name="securitySettings">The security settings.</param> - /// <param name="store">The association store.</param> - internal PrivateSecretManager(RelyingPartySecuritySettings securitySettings, IRelyingPartyAssociationStore store) { - Contract.Requires<ArgumentNullException>(securitySettings != null); - Contract.Requires<ArgumentNullException>(store != null); - - this.securitySettings = securitySettings; - this.store = store; - } - - /// <summary> - /// Gets the handle of the association to use for private signatures. - /// </summary> - /// <returns> - /// An string made up of plain ASCII characters. - /// </returns> - internal string CurrentHandle { - get { - Association association = this.GetAssociation(); - return association.Handle; - } - } - - /// <summary> - /// Used to verify a signature previously written. - /// </summary> - /// <param name="buffer">The data whose signature is to be verified.</param> - /// <param name="handle">The handle to the private association used to sign the data.</param> - /// <returns> - /// The signature for the given buffer using the provided handle. - /// </returns> - /// <exception cref="ProtocolException">Thrown when an association with the given handle could not be found. - /// This most likely happens if the association was near the end of its life and the user took too long to log in.</exception> - internal byte[] Sign(byte[] buffer, string handle) { - Contract.Requires<ArgumentNullException>(buffer != null); - Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(handle)); - - Association association = this.store.GetAssociation(SecretUri, handle); - ErrorUtilities.VerifyProtocol(association != null, OpenIdStrings.PrivateRPSecretNotFound, handle); - return association.Sign(buffer); - } - - /// <summary> - /// Gets an association to use for signing new data. - /// </summary> - /// <returns> - /// The association, which may have previously existed or - /// may have been created as a result of this call. - /// </returns> - private Association GetAssociation() { - Association privateAssociation = this.store.GetAssociation(SecretUri, this.securitySettings); - if (privateAssociation == null || !privateAssociation.HasUsefulLifeRemaining) { - int secretLength = HmacShaAssociation.GetSecretLength(Protocol.Default, Protocol.Default.Args.SignatureAlgorithm.Best); - byte[] secret = MessagingUtilities.GetCryptoRandomData(secretLength); - privateAssociation = HmacShaAssociation.Create(OpenIdUtilities.GenerateRandomAssociationHandle(), secret, this.securitySettings.PrivateSecretMaximumAge); - if (!privateAssociation.HasUsefulLifeRemaining) { - Logger.OpenId.WarnFormat( - "Brand new private association has a shorter lifespan ({0}) than the maximum allowed authentication time for a user ({1}). This may lead to login failures.", - this.securitySettings.PrivateSecretMaximumAge, - DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime); - } - - this.store.StoreAssociation(SecretUri, privateAssociation); - } - - return privateAssociation; - } - } -} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs index 8691662..c3a623b 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System; + using System.Collections.Generic; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OpenId.ChannelElements; @@ -23,65 +24,59 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// The association store to use. /// </summary> - private readonly IRelyingPartyAssociationStore associationStore; + private readonly ICryptoKeyStore keyStore; /// <summary> /// Initializes a new instance of the <see cref="StandardRelyingPartyApplicationStore"/> class. /// </summary> public StandardRelyingPartyApplicationStore() { this.nonceStore = new NonceMemoryStore(DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime); - this.associationStore = new AssociationMemoryStore(); + this.keyStore = new MemoryCryptoKeyStore(); } - #region IAssociationStore Members + #region ICryptoKeyStore Members /// <summary> - /// Saves an <see cref="Association"/> for later recall. + /// Gets the key in a given bucket and handle. /// </summary> - /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for providers).</param> - /// <param name="association">The association to store.</param> - public void StoreAssociation(Uri providerEndpoint, Association association) { - this.associationStore.StoreAssociation(providerEndpoint, association); + /// <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) { + return this.keyStore.GetKey(bucket, handle); } /// <summary> - /// Gets the best association (the one with the longest remaining life) for a given key. + /// Gets a sequence of existing keys within a given bucket. /// </summary> - /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> - /// <param name="securityRequirements">The security settings.</param> + /// <param name="bucket">The bucket name. Case sensitive.</param> /// <returns> - /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key. + /// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>. /// </returns> - public Association GetAssociation(Uri providerEndpoint, SecuritySettings securityRequirements) { - return this.associationStore.GetAssociation(providerEndpoint, securityRequirements); + public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) { + return this.keyStore.GetKeys(bucket); } /// <summary> - /// Gets the association for a given key and handle. + /// Stores a cryptographic key. /// </summary> - /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> - /// <param name="handle">The handle of the specific association that must be recalled.</param> - /// <returns> - /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key and handle. - /// </returns> - public Association GetAssociation(Uri providerEndpoint, string handle) { - return this.associationStore.GetAssociation(providerEndpoint, handle); + /// <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) { + this.keyStore.StoreKey(bucket, handle, key); } /// <summary> - /// Removes a specified handle that may exist in the store. + /// Removes the key. /// </summary> - /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> - /// <param name="handle">The handle of the specific association that must be deleted.</param> - /// <returns> - /// True if the association existed in this store previous to this call. - /// </returns> - /// <remarks> - /// No exception should be thrown if the association does not exist in the store - /// before this call. - /// </remarks> - public bool RemoveAssociation(Uri providerEndpoint, string handle) { - return this.associationStore.RemoveAssociation(providerEndpoint, handle); + /// <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.keyStore.RemoveKey(bucket, handle); } #endregion |