Added MessagingUtilities.AreEquivalentUnordered.
[dotnetoauth.git] / src / DotNetOpenAuth / OpenId / Association.cs
blob562826b1cd32feba656f4a8a820cc7c410eff31e
1 //-----------------------------------------------------------------------
2 // <copyright file="Association.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
4 // </copyright>
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth.OpenId {
8 using System;
9 using System.Diagnostics;
10 using System.Diagnostics.CodeAnalysis;
11 using System.IO;
12 using System.Security.Cryptography;
13 using System.Text;
14 using DotNetOpenAuth.Configuration;
15 using DotNetOpenAuth.Messaging;
17 /// <summary>
18 /// Stores a secret used in signing and verifying messages.
19 /// </summary>
20 /// <remarks>
21 /// OpenID associations may be shared between Provider and Relying Party (smart
22 /// associations), or be a way for a Provider to recall its own secret for later
23 /// (dumb associations).
24 /// </remarks>
25 [DebuggerDisplay("Handle = {Handle}, Expires = {Expires}")]
26 public abstract class Association {
27 /// <summary>
28 /// Initializes a new instance of the <see cref="Association"/> class.
29 /// </summary>
30 /// <param name="handle">The handle.</param>
31 /// <param name="secret">The secret.</param>
32 /// <param name="totalLifeLength">How long the association will be useful.</param>
33 /// <param name="issued">When this association was originally issued by the Provider.</param>
34 protected Association(string handle, byte[] secret, TimeSpan totalLifeLength, DateTime issued) {
35 ErrorUtilities.VerifyNonZeroLength(handle, "handle");
36 ErrorUtilities.VerifyArgumentNotNull(secret, "secret");
38 this.Handle = handle;
39 this.SecretKey = secret;
40 this.TotalLifeLength = totalLifeLength;
41 this.Issued = CutToSecond(issued);
44 /// <summary>
45 /// Gets a unique handle by which this <see cref="Association"/> may be stored or retrieved.
46 /// </summary>
47 public string Handle { get; private set; }
49 /// <summary>
50 /// Gets the time when this <see cref="Association"/> will expire.
51 /// </summary>
52 public DateTime Expires {
53 get { return this.Issued + this.TotalLifeLength; }
56 /// <summary>
57 /// Gets a value indicating whether this <see cref="Association"/> has already expired.
58 /// </summary>
59 public bool IsExpired {
60 get { return this.Expires < DateTime.UtcNow; }
63 /// <summary>
64 /// Gets a value indicating whether this instance has useful life remaining.
65 /// </summary>
66 /// <value>
67 /// <c>true</c> if this instance has useful life remaining; otherwise, <c>false</c>.
68 /// </value>
69 internal bool HasUsefulLifeRemaining {
70 get { return this.TimeTillExpiration >= MinimumUsefulAssociationLifetime; }
73 /// <summary>
74 /// Gets or sets the time that this <see cref="Association"/> was first created.
75 /// </summary>
76 internal DateTime Issued { get; set; }
78 /// <summary>
79 /// Gets the number of seconds until this <see cref="Association"/> expires.
80 /// Never negative (counter runs to zero).
81 /// </summary>
82 protected internal long SecondsTillExpiration {
83 get { return Math.Max(0, (long)this.TimeTillExpiration.TotalSeconds); }
86 /// <summary>
87 /// Gets the shared secret key between the consumer and provider.
88 /// </summary>
89 [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It is a buffer.")]
90 protected internal byte[] SecretKey { get; private set; }
92 /// <summary>
93 /// Gets the duration a secret key used for signing dumb client requests will be good for.
94 /// </summary>
95 protected static TimeSpan DumbSecretLifetime {
96 get { return DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime; }
99 /// <summary>
100 /// Gets the lifetime the OpenID provider permits this <see cref="Association"/>.
101 /// </summary>
102 protected TimeSpan TotalLifeLength { get; private set; }
104 /// <summary>
105 /// Gets the minimum lifetime an association must still be good for in order for it to be used for a future authentication.
106 /// </summary>
107 /// <remarks>
108 /// Associations that are not likely to last the duration of a user login are not worth using at all.
109 /// </remarks>
110 private static TimeSpan MinimumUsefulAssociationLifetime {
111 get { return DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime; }
114 /// <summary>
115 /// Gets the TimeSpan till this association expires.
116 /// </summary>
117 private TimeSpan TimeTillExpiration {
118 get { return this.Expires - DateTime.UtcNow; }
121 /// <summary>
122 /// Re-instantiates an <see cref="Association"/> previously persisted in a database or some
123 /// other shared store.
124 /// </summary>
125 /// <param name="handle">
126 /// The <see cref="Handle"/> property of the previous <see cref="Association"/> instance.
127 /// </param>
128 /// <param name="expires">
129 /// The value of the <see cref="Expires"/> property of the previous <see cref="Association"/> instance.
130 /// </param>
131 /// <param name="privateData">
132 /// The byte array returned by a call to <see cref="SerializePrivateData"/> on the previous
133 /// <see cref="Association"/> instance.
134 /// </param>
135 /// <returns>
136 /// The newly dehydrated <see cref="Association"/>, which can be returned
137 /// from a custom association store's
138 /// <see cref="IAssociationStore&lt;TKey&gt;.GetAssociation(TKey)"/> method.
139 /// </returns>
140 public static Association Deserialize(string handle, DateTime expires, byte[] privateData) {
141 if (string.IsNullOrEmpty(handle)) {
142 throw new ArgumentNullException("handle");
144 if (privateData == null) {
145 throw new ArgumentNullException("privateData");
147 expires = expires.ToUniversalTime();
148 TimeSpan remainingLifeLength = expires - DateTime.UtcNow;
149 byte[] secret = privateData; // the whole of privateData is the secret key for now.
150 // We figure out what derived type to instantiate based on the length of the secret.
151 try {
152 return HmacShaAssociation.Create(handle, secret, remainingLifeLength);
153 } catch (ArgumentException ex) {
154 throw new ArgumentException(OpenIdStrings.BadAssociationPrivateData, "privateData", ex);
158 /// <summary>
159 /// Returns private data required to persist this <see cref="Association"/> in
160 /// permanent storage (a shared database for example) for deserialization later.
161 /// </summary>
162 /// <returns>
163 /// An opaque byte array that must be stored and returned exactly as it is provided here.
164 /// The byte array may vary in length depending on the specific type of <see cref="Association"/>,
165 /// but in current versions are no larger than 256 bytes.
166 /// </returns>
167 /// <remarks>
168 /// Values of public properties on the base class <see cref="Association"/> are not included
169 /// in this byte array, as they are useful for fast database lookup and are persisted separately.
170 /// </remarks>
171 public byte[] SerializePrivateData() {
172 // We may want to encrypt this secret using the machine.config private key,
173 // and add data regarding which Association derivative will need to be
174 // re-instantiated on deserialization.
175 // For now, we just send out the secret key. We can derive the type from the length later.
176 byte[] secretKeyCopy = new byte[this.SecretKey.Length];
177 this.SecretKey.CopyTo(secretKeyCopy, 0);
178 return secretKeyCopy;
181 /// <summary>
182 /// Tests equality of two <see cref="Association"/> objects.
183 /// </summary>
184 /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
185 /// <returns>
186 /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
187 /// </returns>
188 public override bool Equals(object obj) {
189 Association a = obj as Association;
190 if (a == null) {
191 return false;
193 if (a.GetType() != GetType()) {
194 return false;
197 if (a.Handle != this.Handle ||
198 a.Issued != this.Issued ||
199 a.TotalLifeLength != this.TotalLifeLength) {
200 return false;
203 if (!MessagingUtilities.AreEquivalent(a.SecretKey, this.SecretKey)) {
204 return false;
207 return true;
210 /// <summary>
211 /// Returns the hash code.
212 /// </summary>
213 /// <returns>
214 /// A hash code for the current <see cref="T:System.Object"/>.
215 /// </returns>
216 public override int GetHashCode() {
217 HMACSHA1 hmac = new HMACSHA1(this.SecretKey);
218 CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write);
220 byte[] hbytes = ASCIIEncoding.ASCII.GetBytes(this.Handle);
222 cs.Write(hbytes, 0, hbytes.Length);
223 cs.Close();
225 byte[] hash = hmac.Hash;
226 hmac.Clear();
228 long val = 0;
229 for (int i = 0; i < hash.Length; i++) {
230 val = val ^ (long)hash[i];
233 val = val ^ this.Expires.ToFileTimeUtc();
235 return (int)val;
238 /// <summary>
239 /// The string to pass as the assoc_type value in the OpenID protocol.
240 /// </summary>
241 /// <param name="protocol">The protocol version of the message that the assoc_type value will be included in.</param>
242 /// <returns>The value that should be used for the openid.assoc_type parameter.</returns>
243 internal abstract string GetAssociationType(Protocol protocol);
245 /// <summary>
246 /// Generates a signature from a given blob of data.
247 /// </summary>
248 /// <param name="data">The data to sign. This data will not be changed (the signature is the return value).</param>
249 /// <returns>The calculated signature of the data.</returns>
250 protected internal byte[] Sign(byte[] data) {
251 using (HashAlgorithm hasher = this.CreateHasher()) {
252 return hasher.ComputeHash(data);
256 /// <summary>
257 /// Returns the specific hash algorithm used for message signing.
258 /// </summary>
259 /// <returns>The hash algorithm used for message signing.</returns>
260 protected abstract HashAlgorithm CreateHasher();
262 /// <summary>
263 /// Rounds the given <see cref="DateTime"/> downward to the whole second.
264 /// </summary>
265 /// <param name="dateTime">The DateTime object to adjust.</param>
266 /// <returns>The new <see cref="DateTime"/> value.</returns>
267 private static DateTime CutToSecond(DateTime dateTime) {
268 return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond));