diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2012-01-29 14:32:45 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2012-01-29 14:32:45 -0800 |
commit | 5fec515095ee10b522f414a03e78f282aaf520dc (patch) | |
tree | 204c75486639c23cdda2ef38b34d7e5050a1a2e3 /src/DotNetOpenAuth.Core/Messaging/Bindings | |
parent | f1a4155398635a4fd9f485eec817152627682704 (diff) | |
parent | 8f4165ee515728aca3faaa26e8354a40612e85e4 (diff) | |
download | DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.zip DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.tar.gz DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.tar.bz2 |
Merge branch 'splitDlls'.
DNOA now builds and (in some cases) ships as many distinct assemblies.
Diffstat (limited to 'src/DotNetOpenAuth.Core/Messaging/Bindings')
15 files changed, 1276 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/AsymmetricCryptoKeyStoreWrapper.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/AsymmetricCryptoKeyStoreWrapper.cs new file mode 100644 index 0000000..2691202 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/AsymmetricCryptoKeyStoreWrapper.cs @@ -0,0 +1,163 @@ +//----------------------------------------------------------------------- +// <copyright file="AsymmetricCryptoKeyStoreWrapper.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + 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 + /// the persistent store. + /// </summary> + public class AsymmetricCryptoKeyStoreWrapper : ICryptoKeyStore { + /// <summary> + /// The persistent store for asymmetrically encrypted symmetric keys. + /// </summary> + private readonly ICryptoKeyStore dataStore; + + /// <summary> + /// The memory cache of decrypted keys. + /// </summary> + private readonly MemoryCryptoKeyStore cache = new MemoryCryptoKeyStore(); + + /// <summary> + /// The asymmetric algorithm to use encrypting/decrypting the symmetric keys. + /// </summary> + private readonly RSACryptoServiceProvider asymmetricCrypto; + + /// <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) { + Requires.NotNull(dataStore, "dataStore"); + Requires.NotNull(asymmetricCrypto, "asymmetricCrypto"); + Requires.True(!asymmetricCrypto.PublicOnly, "asymmetricCrypto"); + 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> + [SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", MessageId = "2#", Justification = "Helps readability because multiple keys are involved.")] + 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.cache.StoreKey(bucket, handle, new CachedCryptoKey(encryptedCryptoKey, decryptedCryptoKey)); + } + + /// <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); + this.cache.RemoveKey(bucket, handle); + } + + /// <summary> + /// Decrypts the specified key. + /// </summary> + /// <param name="bucket">The bucket.</param> + /// <param name="handle">The handle.</param> + /// <param name="encryptedCryptoKey">The encrypted key.</param> + /// <returns> + /// The decrypted key. + /// </returns> + [SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", MessageId = "2#", Justification = "Helps readability because multiple keys are involved.")] + 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 = (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.cache.StoreKey(bucket, handle, new CachedCryptoKey(encryptedCryptoKey, decryptedCryptoKey)); + + return decryptedCryptoKey; + } + + /// <summary> + /// An encrypted key and its decrypted equivalent. + /// </summary> + 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) + : base(decrypted.Key, decrypted.ExpiresUtc) { + Contract.Requires(encrypted != null); + Contract.Requires(decrypted != null); + Contract.Requires(encrypted.ExpiresUtc == decrypted.ExpiresUtc); + + this.EncryptedKey = encrypted.Key; + } + + /// <summary> + /// Gets the encrypted key. + /// </summary> + internal byte[] EncryptedKey { get; private set; } + + /// <summary> + /// Invariant conditions. + /// </summary> + [ContractInvariantMethod] + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")] + private void ObjectInvariant() { + Contract.Invariant(this.EncryptedKey != null); + } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/Bindings.cd b/src/DotNetOpenAuth.Core/Messaging/Bindings/Bindings.cd new file mode 100644 index 0000000..e52e81e --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/Bindings.cd @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="utf-8"?> +<ClassDiagram MajorVersion="1" MinorVersion="1"> + <Class Name="DotNetOpenAuth.Messaging.Bindings.InvalidSignatureException" Collapsed="true"> + <Position X="8.25" Y="2" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>Messaging\Bindings\InvalidSignatureException.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.Messaging.Bindings.ReplayedMessageException" Collapsed="true"> + <Position X="6" Y="2" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>Messaging\Bindings\ReplayedMessageException.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.Messaging.Bindings.ExpiredMessageException" Collapsed="true"> + <Position X="3.75" Y="2" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>Messaging\Bindings\ExpiredMessageException.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.Messaging.ProtocolException" Collapsed="true"> + <Position X="6" Y="0.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>ICAMAAAAQAAAgAEAAIBAAAYgCgAAIAAAIACAACAAAAA=</HashCode> + <FileName>Messaging\ProtocolException.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.Messaging.Bindings.StandardExpirationBindingElement" Collapsed="true"> + <Position X="1" Y="3" Width="2.75" /> + <TypeIdentifier> + <HashCode>AAAAAAAgAAAARAAEAAAAAAAAAAIAAAAAAEAAAAAAAAA=</HashCode> + <FileName>Messaging\Bindings\StandardExpirationBindingElement.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Interface Name="DotNetOpenAuth.Messaging.IProtocolMessage" Collapsed="true"> + <Position X="6" Y="3.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>Messaging\IProtocolMessage.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="DotNetOpenAuth.Messaging.Bindings.IExpiringProtocolMessage" Collapsed="true"> + <Position X="3.75" Y="4.75" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>Messaging\Bindings\IExpiringProtocolMessage.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="DotNetOpenAuth.Messaging.Bindings.IReplayProtectedProtocolMessage" Collapsed="true"> + <Position X="6" Y="4.75" Width="2.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAA=</HashCode> + <FileName>Messaging\Bindings\IReplayProtectedProtocolMessage.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="DotNetOpenAuth.Messaging.IChannelBindingElement"> + <Position X="0.5" Y="0.5" Width="2" /> + <TypeIdentifier> + <HashCode>BAAAAAAgAAAAAAAEAAAAAAAAAAAAAAAAAEAAAAAAAAA=</HashCode> + <FileName>Messaging\IChannelBindingElement.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="DotNetOpenAuth.OAuth.ChannelElements.ITamperResistantOAuthMessage" Collapsed="true"> + <Position X="8.75" Y="4.75" Width="2.5" /> + <TypeIdentifier> + <HashCode>AIAAAAAAAAAAgAAAAIAAAgAAAAAAIAQAAAAAAAAAAAA=</HashCode> + <FileName>OAuth\ChannelElements\ITamperResistantOAuthMessage.cs</FileName> + </TypeIdentifier> + </Interface> + <Font Name="Segoe UI" Size="9" /> +</ClassDiagram>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKey.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKey.cs new file mode 100644 index 0000000..7160014 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKey.cs @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------- +// <copyright file="CryptoKey.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + 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"/> class. + /// </summary> + /// <param name="key">The cryptographic key.</param> + /// <param name="expiresUtc">The expires UTC.</param> + public CryptoKey(byte[] key, DateTime expiresUtc) { + Requires.NotNull(key, "key"); + Requires.True(expiresUtc.Kind == DateTimeKind.Utc, "expiresUtc"); + this.key = key; + this.expiresUtc = expiresUtc; + } + + /// <summary> + /// Gets the key. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It's a buffer")] + 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.Core/Messaging/Bindings/CryptoKeyCollisionException.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKeyCollisionException.cs new file mode 100644 index 0000000..ebd29de --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKeyCollisionException.cs @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------- +// <copyright file="CryptoKeyCollisionException.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + using System.Diagnostics.CodeAnalysis; + using System.Security.Permissions; + + /// <summary> + /// Thrown by a hosting application or web site when a cryptographic key is created with a + /// bucket and handle that conflicts with a previously stored and unexpired key. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", Justification = "Specialized exception has no need of a message parameter.")] + [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="inner">The inner exception to include.</param> + public CryptoKeyCollisionException(Exception inner) : base(null, 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 + [System.Security.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.Core/Messaging/Bindings/ExpiredMessageException.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/ExpiredMessageException.cs new file mode 100644 index 0000000..196946d --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/ExpiredMessageException.cs @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------- +// <copyright file="ExpiredMessageException.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + using System.Diagnostics.Contracts; + using System.Globalization; + + /// <summary> + /// An exception thrown when a message is received that exceeds the maximum message age limit. + /// </summary> + [Serializable] + internal class ExpiredMessageException : ProtocolException { + /// <summary> + /// Initializes a new instance of the <see cref="ExpiredMessageException"/> class. + /// </summary> + /// <param name="utcExpirationDate">The date the message expired.</param> + /// <param name="faultedMessage">The expired message.</param> + public ExpiredMessageException(DateTime utcExpirationDate, IProtocolMessage faultedMessage) + : base(string.Format(CultureInfo.CurrentCulture, MessagingStrings.ExpiredMessage, utcExpirationDate.ToLocalTime(), DateTime.Now), faultedMessage) { + Requires.True(utcExpirationDate.Kind == DateTimeKind.Utc, "utcExpirationDate"); + Requires.NotNull(faultedMessage, "faultedMessage"); + } + + /// <summary> + /// Initializes a new instance of the <see cref="ExpiredMessageException"/> 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 ExpiredMessageException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) { } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/ICryptoKeyStore.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/ICryptoKeyStore.cs new file mode 100644 index 0000000..861ba89 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/ICryptoKeyStore.cs @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------- +// <copyright file="ICryptoKeyStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + 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> + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Important for scalability")] + 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> + /// <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> + /// 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> + /// 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 ICryptoKeyStore.GetKey(string bucket, string handle) { + Requires.NotNullOrEmpty(bucket, "bucket"); + Requires.NotNullOrEmpty(handle, "handle"); + throw new NotImplementedException(); + } + + /// <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>> ICryptoKeyStore.GetKeys(string bucket) { + Requires.NotNullOrEmpty(bucket, "bucket"); + Contract.Ensures(Contract.Result<IEnumerable<KeyValuePair<string, CryptoKey>>>() != null); + throw new NotImplementedException(); + } + + /// <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> + void ICryptoKeyStore.StoreKey(string bucket, string handle, CryptoKey key) { + Requires.NotNullOrEmpty(bucket, "bucket"); + Requires.NotNullOrEmpty(handle, "handle"); + Requires.NotNull(key, "key"); + throw new NotImplementedException(); + } + + /// <summary> + /// Removes the key. + /// </summary> + /// <param name="bucket">The bucket name. Case sensitive.</param> + /// <param name="handle">The key handle. Case sensitive.</param> + void ICryptoKeyStore.RemoveKey(string bucket, string handle) { + Requires.NotNullOrEmpty(bucket, "bucket"); + Requires.NotNullOrEmpty(handle, "handle"); + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/IExpiringProtocolMessage.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/IExpiringProtocolMessage.cs new file mode 100644 index 0000000..fc43ae6 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/IExpiringProtocolMessage.cs @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------- +// <copyright file="IExpiringProtocolMessage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + + /// <summary> + /// The contract a message that has an allowable time window for processing must implement. + /// </summary> + /// <remarks> + /// All expiring messages must also be signed to prevent tampering with the creation date. + /// </remarks> + internal interface IExpiringProtocolMessage : IProtocolMessage { + /// <summary> + /// Gets or sets the UTC date/time the message was originally sent onto the network. + /// </summary> + /// <remarks> + /// The property setter should ensure a UTC date/time, + /// and throw an exception if this is not possible. + /// </remarks> + /// <exception cref="ArgumentException"> + /// Thrown when a DateTime that cannot be converted to UTC is set. + /// </exception> + DateTime UtcCreationDate { get; set; } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/INonceStore.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/INonceStore.cs new file mode 100644 index 0000000..7a3e8bb --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/INonceStore.cs @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------- +// <copyright file="INonceStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + + /// <summary> + /// Describes the contract a nonce store must fulfill. + /// </summary> + public interface INonceStore { + /// <summary> + /// Stores a given nonce and timestamp. + /// </summary> + /// <param name="context">The context, or namespace, within which the + /// <paramref name="nonce"/> must be unique. + /// The context SHOULD be treated as case-sensitive. + /// The value will never be <c>null</c> but may be the empty string.</param> + /// <param name="nonce">A series of random characters.</param> + /// <param name="timestampUtc">The UTC timestamp that together with the nonce string make it unique + /// within the given <paramref name="context"/>. + /// The timestamp may also be used by the data store to clear out old nonces.</param> + /// <returns> + /// True if the context+nonce+timestamp (combination) was not previously in the database. + /// False if the nonce was stored previously with the same timestamp and context. + /// </returns> + /// <remarks> + /// The nonce must be stored for no less than the maximum time window a message may + /// be processed within before being discarded as an expired message. + /// This maximum message age can be looked up via the + /// <see cref="DotNetOpenAuth.Configuration.MessagingElement.MaximumMessageLifetime"/> + /// property, accessible via the <see cref="DotNetOpenAuth.Configuration.MessagingElement.Configuration"/> + /// property. + /// </remarks> + bool StoreNonce(string context, string nonce, DateTime timestampUtc); + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/IReplayProtectedProtocolMessage.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/IReplayProtectedProtocolMessage.cs new file mode 100644 index 0000000..1edf934 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/IReplayProtectedProtocolMessage.cs @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------- +// <copyright file="IReplayProtectedProtocolMessage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The contract a message that has an allowable time window for processing must implement. + /// </summary> + /// <remarks> + /// All replay-protected messages must also be set to expire so the nonces do not have + /// to be stored indefinitely. + /// </remarks> + internal interface IReplayProtectedProtocolMessage : IExpiringProtocolMessage, IDirectedProtocolMessage { + /// <summary> + /// Gets the context within which the nonce must be unique. + /// </summary> + /// <value> + /// The value of this property must be a value assigned by the nonce consumer + /// to represent the entity that generated the nonce. The value must never be + /// <c>null</c> but may be the empty string. + /// This value is treated as case-sensitive. + /// </value> + string NonceContext { get; } + + /// <summary> + /// Gets or sets the nonce that will protect the message from replay attacks. + /// </summary> + string Nonce { get; set; } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/InvalidSignatureException.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/InvalidSignatureException.cs new file mode 100644 index 0000000..28b7e96 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/InvalidSignatureException.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// <copyright file="InvalidSignatureException.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + + /// <summary> + /// An exception thrown when a signed message does not pass signature validation. + /// </summary> + [Serializable] + internal class InvalidSignatureException : ProtocolException { + /// <summary> + /// Initializes a new instance of the <see cref="InvalidSignatureException"/> class. + /// </summary> + /// <param name="faultedMessage">The message with the invalid signature.</param> + public InvalidSignatureException(IProtocolMessage faultedMessage) + : base(MessagingStrings.SignatureInvalid, faultedMessage) { } + + /// <summary> + /// Initializes a new instance of the <see cref="InvalidSignatureException"/> 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 InvalidSignatureException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) { } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/MemoryCryptoKeyStore.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/MemoryCryptoKeyStore.cs new file mode 100644 index 0000000..63d1953 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/MemoryCryptoKeyStore.cs @@ -0,0 +1,156 @@ +//----------------------------------------------------------------------- +// <copyright file="MemoryCryptoKeyStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. 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; + } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/NonceMemoryStore.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/NonceMemoryStore.cs new file mode 100644 index 0000000..6e64acc --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/NonceMemoryStore.cs @@ -0,0 +1,136 @@ +//----------------------------------------------------------------------- +// <copyright file="NonceMemoryStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// An in-memory nonce store. Useful for single-server web applications. + /// NOT for web farms. + /// </summary> + internal class NonceMemoryStore : INonceStore { + /// <summary> + /// How frequently we should take time to clear out old nonces. + /// </summary> + private const int AutoCleaningFrequency = 10; + + /// <summary> + /// The maximum age a message can be before it is discarded. + /// </summary> + /// <remarks> + /// This is useful for knowing how long used nonces must be retained. + /// </remarks> + private readonly TimeSpan maximumMessageAge; + + /// <summary> + /// A list of the consumed nonces. + /// </summary> + private readonly SortedDictionary<DateTime, List<string>> usedNonces = new SortedDictionary<DateTime, List<string>>(); + + /// <summary> + /// A lock object used around accesses to the <see cref="usedNonces"/> field. + /// </summary> + private object nonceLock = new object(); + + /// <summary> + /// Where we're currently at in our periodic nonce cleaning cycle. + /// </summary> + private int nonceClearingCounter; + + /// <summary> + /// Initializes a new instance of the <see cref="NonceMemoryStore"/> class. + /// </summary> + internal NonceMemoryStore() + : this(StandardExpirationBindingElement.MaximumMessageAge) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="NonceMemoryStore"/> class. + /// </summary> + /// <param name="maximumMessageAge">The maximum age a message can be before it is discarded.</param> + internal NonceMemoryStore(TimeSpan maximumMessageAge) { + this.maximumMessageAge = maximumMessageAge; + } + + #region INonceStore Members + + /// <summary> + /// Stores a given nonce and timestamp. + /// </summary> + /// <param name="context">The context, or namespace, within which the <paramref name="nonce"/> must be unique.</param> + /// <param name="nonce">A series of random characters.</param> + /// <param name="timestamp">The timestamp that together with the nonce string make it unique. + /// The timestamp may also be used by the data store to clear out old nonces.</param> + /// <returns> + /// True if the nonce+timestamp (combination) was not previously in the database. + /// False if the nonce was stored previously with the same timestamp. + /// </returns> + /// <remarks> + /// The nonce must be stored for no less than the maximum time window a message may + /// be processed within before being discarded as an expired message. + /// If the binding element is applicable to your channel, this expiration window + /// is retrieved or set using the + /// <see cref="StandardExpirationBindingElement.MaximumMessageAge"/> property. + /// </remarks> + public bool StoreNonce(string context, string nonce, DateTime timestamp) { + if (timestamp.ToUniversalTimeSafe() + this.maximumMessageAge < DateTime.UtcNow) { + // The expiration binding element should have taken care of this, but perhaps + // it's at the boundary case. We should fail just to be safe. + return false; + } + + // We just concatenate the context with the nonce to form a complete, namespace-protected nonce. + string completeNonce = context + "\0" + nonce; + + lock (this.nonceLock) { + List<string> nonces; + if (!this.usedNonces.TryGetValue(timestamp, out nonces)) { + this.usedNonces[timestamp] = nonces = new List<string>(4); + } + + if (nonces.Contains(completeNonce)) { + return false; + } + + nonces.Add(completeNonce); + + // Clear expired nonces if it's time to take a moment to do that. + // Unchecked so that this can int overflow without an exception. + unchecked { + this.nonceClearingCounter++; + } + if (this.nonceClearingCounter % AutoCleaningFrequency == 0) { + this.ClearExpiredNonces(); + } + + return true; + } + } + + #endregion + + /// <summary> + /// Clears consumed nonces from the cache that are so old they would be + /// rejected if replayed because it is expired. + /// </summary> + public void ClearExpiredNonces() { + lock (this.nonceLock) { + var oldNonceLists = this.usedNonces.Keys.Where(time => time.ToUniversalTimeSafe() + this.maximumMessageAge < DateTime.UtcNow).ToList(); + foreach (DateTime time in oldNonceLists) { + this.usedNonces.Remove(time); + } + + // Reset the auto-clean counter so that if this method was called externally + // we don't auto-clean right away. + this.nonceClearingCounter = 0; + } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/ReplayedMessageException.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/ReplayedMessageException.cs new file mode 100644 index 0000000..2b8df9d --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/ReplayedMessageException.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// <copyright file="ReplayedMessageException.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + + /// <summary> + /// An exception thrown when a message is received for the second time, signalling a possible + /// replay attack. + /// </summary> + [Serializable] + internal class ReplayedMessageException : ProtocolException { + /// <summary> + /// Initializes a new instance of the <see cref="ReplayedMessageException"/> class. + /// </summary> + /// <param name="faultedMessage">The replayed message.</param> + public ReplayedMessageException(IProtocolMessage faultedMessage) : base(MessagingStrings.ReplayAttackDetected, faultedMessage) { } + + /// <summary> + /// Initializes a new instance of the <see cref="ReplayedMessageException"/> 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 ReplayedMessageException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) { } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs new file mode 100644 index 0000000..f8c8c6a --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs @@ -0,0 +1,107 @@ +//----------------------------------------------------------------------- +// <copyright file="StandardExpirationBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + using DotNetOpenAuth.Configuration; + + /// <summary> + /// A message expiration enforcing binding element that supports messages + /// implementing the <see cref="IExpiringProtocolMessage"/> interface. + /// </summary> + internal class StandardExpirationBindingElement : IChannelBindingElement { + /// <summary> + /// Initializes a new instance of the <see cref="StandardExpirationBindingElement"/> class. + /// </summary> + internal StandardExpirationBindingElement() { + } + + #region IChannelBindingElement Properties + + /// <summary> + /// Gets the protection offered by this binding element. + /// </summary> + /// <value><see cref="MessageProtections.Expiration"/></value> + MessageProtections IChannelBindingElement.Protection { + get { return MessageProtections.Expiration; } + } + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + public Channel Channel { get; set; } + + #endregion + + /// <summary> + /// Gets the maximum age a message implementing the + /// <see cref="IExpiringProtocolMessage"/> interface can be before + /// being discarded as too old. + /// </summary> + protected internal static TimeSpan MaximumMessageAge { + get { return Configuration.DotNetOpenAuthSection.Messaging.MaximumMessageLifetime; } + } + + #region IChannelBindingElement Methods + + /// <summary> + /// Sets the timestamp on an outgoing message. + /// </summary> + /// <param name="message">The outgoing message.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage; + if (expiringMessage != null) { + expiringMessage.UtcCreationDate = DateTime.UtcNow; + return MessageProtections.Expiration; + } + + return null; + } + + /// <summary> + /// Reads the timestamp on a message and throws an exception if the message is too old. + /// </summary> + /// <param name="message">The incoming message.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ExpiredMessageException">Thrown if the given message has already expired.</exception> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage; + if (expiringMessage != null) { + // Yes the UtcCreationDate is supposed to always be in UTC already, + // but just in case a given message failed to guarantee that, we do it here. + DateTime creationDate = expiringMessage.UtcCreationDate.ToUniversalTimeSafe(); + DateTime expirationDate = creationDate + MaximumMessageAge; + if (expirationDate < DateTime.UtcNow) { + throw new ExpiredMessageException(expirationDate, expiringMessage); + } + + // Mitigate HMAC attacks (just guessing the signature until they get it) by + // disallowing post-dated messages. + ErrorUtilities.VerifyProtocol( + creationDate <= DateTime.UtcNow + DotNetOpenAuthSection.Messaging.MaximumClockSkew, + MessagingStrings.MessageTimestampInFuture, + creationDate); + + return MessageProtections.Expiration; + } + + return null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs new file mode 100644 index 0000000..78fd1d5 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs @@ -0,0 +1,148 @@ +//----------------------------------------------------------------------- +// <copyright file="StandardReplayProtectionBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + using System.Diagnostics; + using System.Diagnostics.Contracts; + + /// <summary> + /// A binding element that checks/verifies a nonce message part. + /// </summary> + internal class StandardReplayProtectionBindingElement : IChannelBindingElement { + /// <summary> + /// These are the characters that may be chosen from when forming a random nonce. + /// </summary> + private const string AllowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + /// <summary> + /// The persistent store for nonces received. + /// </summary> + private INonceStore nonceStore; + + /// <summary> + /// The length of generated nonces. + /// </summary> + private int nonceLength = 8; + + /// <summary> + /// Initializes a new instance of the <see cref="StandardReplayProtectionBindingElement"/> class. + /// </summary> + /// <param name="nonceStore">The store where nonces will be persisted and checked.</param> + internal StandardReplayProtectionBindingElement(INonceStore nonceStore) + : this(nonceStore, false) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="StandardReplayProtectionBindingElement"/> class. + /// </summary> + /// <param name="nonceStore">The store where nonces will be persisted and checked.</param> + /// <param name="allowEmptyNonces">A value indicating whether zero-length nonces will be allowed.</param> + internal StandardReplayProtectionBindingElement(INonceStore nonceStore, bool allowEmptyNonces) { + Requires.NotNull(nonceStore, "nonceStore"); + + this.nonceStore = nonceStore; + this.AllowZeroLengthNonce = allowEmptyNonces; + } + + #region IChannelBindingElement Properties + + /// <summary> + /// Gets the protection that this binding element provides messages. + /// </summary> + public MessageProtections Protection { + get { return MessageProtections.ReplayProtection; } + } + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + public Channel Channel { get; set; } + + #endregion + + /// <summary> + /// Gets or sets the strength of the nonce, which is measured by the number of + /// nonces that could theoretically be generated. + /// </summary> + /// <remarks> + /// The strength of the nonce is equal to the number of characters that might appear + /// in the nonce to the power of the length of the nonce. + /// </remarks> + internal double NonceStrength { + get { + return Math.Pow(AllowedCharacters.Length, this.nonceLength); + } + + set { + value = Math.Max(value, AllowedCharacters.Length); + this.nonceLength = (int)Math.Log(value, AllowedCharacters.Length); + Debug.Assert(this.nonceLength > 0, "Nonce length calculated to be below 1!"); + } + } + + /// <summary> + /// Gets or sets a value indicating whether empty nonces are allowed. + /// </summary> + /// <value>Default is <c>false</c>.</value> + internal bool AllowZeroLengthNonce { get; set; } + + #region IChannelBindingElement Methods + + /// <summary> + /// Applies a nonce to the message. + /// </summary> + /// <param name="message">The message to apply replay protection to.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + IReplayProtectedProtocolMessage nonceMessage = message as IReplayProtectedProtocolMessage; + if (nonceMessage != null) { + nonceMessage.Nonce = this.GenerateUniqueFragment(); + return MessageProtections.ReplayProtection; + } + + return null; + } + + /// <summary> + /// Verifies that the nonce in an incoming message has not been seen before. + /// </summary> + /// <param name="message">The incoming message.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ReplayedMessageException">Thrown when the nonce check revealed a replayed message.</exception> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + IReplayProtectedProtocolMessage nonceMessage = message as IReplayProtectedProtocolMessage; + if (nonceMessage != null && nonceMessage.Nonce != null) { + ErrorUtilities.VerifyProtocol(nonceMessage.Nonce.Length > 0 || this.AllowZeroLengthNonce, MessagingStrings.InvalidNonceReceived); + + if (!this.nonceStore.StoreNonce(nonceMessage.NonceContext, nonceMessage.Nonce, nonceMessage.UtcCreationDate)) { + Logger.OpenId.ErrorFormat("Replayed nonce detected ({0} {1}). Rejecting message.", nonceMessage.Nonce, nonceMessage.UtcCreationDate); + throw new ReplayedMessageException(message); + } + + return MessageProtections.ReplayProtection; + } + + return null; + } + + #endregion + + /// <summary> + /// Generates a string of random characters for use as a nonce. + /// </summary> + /// <returns>The nonce string.</returns> + private string GenerateUniqueFragment() { + return MessagingUtilities.GetRandomString(this.nonceLength, AllowedCharacters); + } + } +} |