1
//-----------------------------------------------------------------------
2 // <copyright file="Association.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth
.OpenId
{
9 using System
.Diagnostics
;
10 using System
.Diagnostics
.CodeAnalysis
;
12 using System
.Security
.Cryptography
;
14 using DotNetOpenAuth
.Configuration
;
15 using DotNetOpenAuth
.Messaging
;
18 /// Stores a secret used in signing and verifying messages.
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).
25 [DebuggerDisplay("Handle = {Handle}, Expires = {Expires}")]
26 public abstract class Association
{
28 /// Initializes a new instance of the <see cref="Association"/> class.
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");
39 this.SecretKey
= secret
;
40 this.TotalLifeLength
= totalLifeLength
;
41 this.Issued
= CutToSecond(issued
);
45 /// Gets a unique handle by which this <see cref="Association"/> may be stored or retrieved.
47 public string Handle { get; private set; }
50 /// Gets the time when this <see cref="Association"/> will expire.
52 public DateTime Expires
{
53 get { return this.Issued + this.TotalLifeLength; }
57 /// Gets a value indicating whether this <see cref="Association"/> has already expired.
59 public bool IsExpired
{
60 get { return this.Expires < DateTime.UtcNow; }
64 /// Gets a value indicating whether this instance has useful life remaining.
67 /// <c>true</c> if this instance has useful life remaining; otherwise, <c>false</c>.
69 internal bool HasUsefulLifeRemaining
{
70 get { return this.TimeTillExpiration >= MinimumUsefulAssociationLifetime; }
74 /// Gets or sets the time that this <see cref="Association"/> was first created.
76 internal DateTime Issued { get; set; }
79 /// Gets the number of seconds until this <see cref="Association"/> expires.
80 /// Never negative (counter runs to zero).
82 protected internal long SecondsTillExpiration
{
83 get { return Math.Max(0, (long)this.TimeTillExpiration.TotalSeconds); }
87 /// Gets the shared secret key between the consumer and provider.
89 [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification
= "It is a buffer.")]
90 protected internal byte[] SecretKey { get; private set; }
93 /// Gets the duration a secret key used for signing dumb client requests will be good for.
95 protected static TimeSpan DumbSecretLifetime
{
96 get { return DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime; }
100 /// Gets the lifetime the OpenID provider permits this <see cref="Association"/>.
102 protected TimeSpan TotalLifeLength { get; private set; }
105 /// Gets the minimum lifetime an association must still be good for in order for it to be used for a future authentication.
108 /// Associations that are not likely to last the duration of a user login are not worth using at all.
110 private static TimeSpan MinimumUsefulAssociationLifetime
{
111 get { return DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime; }
115 /// Gets the TimeSpan till this association expires.
117 private TimeSpan TimeTillExpiration
{
118 get { return this.Expires - DateTime.UtcNow; }
122 /// Re-instantiates an <see cref="Association"/> previously persisted in a database or some
123 /// other shared store.
125 /// <param name="handle">
126 /// The <see cref="Handle"/> property of the previous <see cref="Association"/> instance.
128 /// <param name="expires">
129 /// The value of the <see cref="Expires"/> property of the previous <see cref="Association"/> instance.
131 /// <param name="privateData">
132 /// The byte array returned by a call to <see cref="SerializePrivateData"/> on the previous
133 /// <see cref="Association"/> instance.
136 /// The newly dehydrated <see cref="Association"/>, which can be returned
137 /// from a custom association store's
138 /// <see cref="IAssociationStore<TKey>.GetAssociation(TKey)"/> method.
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.
152 return HmacShaAssociation
.Create(handle
, secret
, remainingLifeLength
);
153 } catch (ArgumentException ex
) {
154 throw new ArgumentException(OpenIdStrings
.BadAssociationPrivateData
, "privateData", ex
);
159 /// Returns private data required to persist this <see cref="Association"/> in
160 /// permanent storage (a shared database for example) for deserialization later.
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.
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.
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
;
182 /// Tests equality of two <see cref="Association"/> objects.
184 /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
186 /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
188 public override bool Equals(object obj
) {
189 Association a
= obj
as Association
;
193 if (a
.GetType() != GetType()) {
197 if (a
.Handle
!= this.Handle
||
198 a
.Issued
!= this.Issued
||
199 a
.TotalLifeLength
!= this.TotalLifeLength
) {
203 if (!MessagingUtilities
.AreEquivalent(a
.SecretKey
, this.SecretKey
)) {
211 /// Returns the hash code.
214 /// A hash code for the current <see cref="T:System.Object"/>.
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
);
225 byte[] hash
= hmac
.Hash
;
229 for (int i
= 0; i
< hash
.Length
; i
++) {
230 val
= val ^
(long)hash
[i
];
233 val
= val ^
this.Expires
.ToFileTimeUtc();
239 /// The string to pass as the assoc_type value in the OpenID protocol.
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
);
246 /// Generates a signature from a given blob of data.
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
);
257 /// Returns the specific hash algorithm used for message signing.
259 /// <returns>The hash algorithm used for message signing.</returns>
260 protected abstract HashAlgorithm
CreateHasher();
263 /// Rounds the given <see cref="DateTime"/> downward to the whole second.
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
));