summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.Core/Messaging/Bindings
diff options
context:
space:
mode:
authorAndrew Arnott <andrewarnott@gmail.com>2012-01-29 14:32:45 -0800
committerAndrew Arnott <andrewarnott@gmail.com>2012-01-29 14:32:45 -0800
commit5fec515095ee10b522f414a03e78f282aaf520dc (patch)
tree204c75486639c23cdda2ef38b34d7e5050a1a2e3 /src/DotNetOpenAuth.Core/Messaging/Bindings
parentf1a4155398635a4fd9f485eec817152627682704 (diff)
parent8f4165ee515728aca3faaa26e8354a40612e85e4 (diff)
downloadDotNetOpenAuth-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')
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/AsymmetricCryptoKeyStoreWrapper.cs163
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/Bindings.cd76
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKey.cs93
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKeyCollisionException.cs68
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/ExpiredMessageException.cs40
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/ICryptoKeyStore.cs118
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/IExpiringProtocolMessage.cs29
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/INonceStore.cs39
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/IReplayProtectedProtocolMessage.cs35
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/InvalidSignatureException.cs34
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/MemoryCryptoKeyStore.cs156
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/NonceMemoryStore.cs136
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/ReplayedMessageException.cs34
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs107
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs148
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);
+ }
+ }
+}