diff options
Diffstat (limited to 'src/DotNetOpenAuth.OpenId/OpenId/Association.cs')
-rw-r--r-- | src/DotNetOpenAuth.OpenId/OpenId/Association.cs | 308 |
1 files changed, 308 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Association.cs b/src/DotNetOpenAuth.OpenId/OpenId/Association.cs new file mode 100644 index 0000000..5b97ad4 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Association.cs @@ -0,0 +1,308 @@ +//----------------------------------------------------------------------- +// <copyright file="Association.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.IO; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Stores a secret used in signing and verifying messages. + /// </summary> + /// <remarks> + /// OpenID associations may be shared between Provider and Relying Party (smart + /// associations), or be a way for a Provider to recall its own secret for later + /// (dumb associations). + /// </remarks> + [DebuggerDisplay("Handle = {Handle}, Expires = {Expires}")] + [ContractVerification(true)] + [ContractClass(typeof(AssociationContract))] + public abstract class Association { + /// <summary> + /// Initializes a new instance of the <see cref="Association"/> class. + /// </summary> + /// <param name="handle">The handle.</param> + /// <param name="secret">The secret.</param> + /// <param name="totalLifeLength">How long the association will be useful.</param> + /// <param name="issued">The UTC time of when this association was originally issued by the Provider.</param> + protected Association(string handle, byte[] secret, TimeSpan totalLifeLength, DateTime issued) { + Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(handle)); + Contract.Requires<ArgumentNullException>(secret != null); + Contract.Requires<ArgumentOutOfRangeException>(totalLifeLength > TimeSpan.Zero); + Contract.Requires<ArgumentException>(issued.Kind == DateTimeKind.Utc); + Contract.Requires<ArgumentOutOfRangeException>(issued <= DateTime.UtcNow); + Contract.Ensures(this.TotalLifeLength == totalLifeLength); + + this.Handle = handle; + this.SecretKey = secret; + this.TotalLifeLength = totalLifeLength; + this.Issued = OpenIdUtilities.CutToSecond(issued); + } + + /// <summary> + /// Gets a unique handle by which this <see cref="Association"/> may be stored or retrieved. + /// </summary> + public string Handle { get; internal set; } + + /// <summary> + /// Gets the UTC time when this <see cref="Association"/> will expire. + /// </summary> + public DateTime Expires { + get { return this.Issued + this.TotalLifeLength; } + } + + /// <summary> + /// Gets a value indicating whether this <see cref="Association"/> has already expired. + /// </summary> + public bool IsExpired { + get { return this.Expires < DateTime.UtcNow; } + } + + /// <summary> + /// Gets the length (in bits) of the hash this association creates when signing. + /// </summary> + public abstract int HashBitLength { get; } + + /// <summary> + /// Gets a value indicating whether this instance has useful life remaining. + /// </summary> + /// <value> + /// <c>true</c> if this instance has useful life remaining; otherwise, <c>false</c>. + /// </value> + internal bool HasUsefulLifeRemaining { + get { return this.TimeTillExpiration >= MinimumUsefulAssociationLifetime; } + } + + /// <summary> + /// Gets or sets the UTC time that this <see cref="Association"/> was first created. + /// </summary> + [MessagePart] + internal DateTime Issued { get; set; } + + /// <summary> + /// Gets the number of seconds until this <see cref="Association"/> expires. + /// Never negative (counter runs to zero). + /// </summary> + protected internal long SecondsTillExpiration { + get { + Contract.Ensures(Contract.Result<long>() >= 0); + return Math.Max(0, (long)this.TimeTillExpiration.TotalSeconds); + } + } + + /// <summary> + /// Gets the shared secret key between the consumer and provider. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It is a buffer.")] + [MessagePart("key")] + protected internal byte[] SecretKey { get; private set; } + + /// <summary> + /// Gets the duration a secret key used for signing dumb client requests will be good for. + /// </summary> + protected static TimeSpan DumbSecretLifetime { + get { + Contract.Ensures(Contract.Result<TimeSpan>() > TimeSpan.Zero); + return DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime; + } + } + + /// <summary> + /// Gets the lifetime the OpenID provider permits this <see cref="Association"/>. + /// </summary> + [MessagePart("ttl")] + protected TimeSpan TotalLifeLength { get; private set; } + + /// <summary> + /// Gets the minimum lifetime an association must still be good for in order for it to be used for a future authentication. + /// </summary> + /// <remarks> + /// Associations that are not likely to last the duration of a user login are not worth using at all. + /// </remarks> + private static TimeSpan MinimumUsefulAssociationLifetime { + get { + Contract.Ensures(Contract.Result<TimeSpan>() > TimeSpan.Zero); + return DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime; + } + } + + /// <summary> + /// Gets the TimeSpan till this association expires. + /// </summary> + private TimeSpan TimeTillExpiration { + get { return this.Expires - DateTime.UtcNow; } + } + + /// <summary> + /// Re-instantiates an <see cref="Association"/> previously persisted in a database or some + /// other shared store. + /// </summary> + /// <param name="handle"> + /// The <see cref="Handle"/> property of the previous <see cref="Association"/> instance. + /// </param> + /// <param name="expiresUtc"> + /// The UTC value of the <see cref="Expires"/> property of the previous <see cref="Association"/> instance. + /// </param> + /// <param name="privateData"> + /// The byte array returned by a call to <see cref="SerializePrivateData"/> on the previous + /// <see cref="Association"/> instance. + /// </param> + /// <returns> + /// The newly dehydrated <see cref="Association"/>, which can be returned + /// from a custom association store's + /// <see cref="IRelyingPartyAssociationStore.GetAssociation(Uri, SecuritySettings)"/> method. + /// </returns> + public static Association Deserialize(string handle, DateTime expiresUtc, byte[] privateData) { + Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(handle)); + Contract.Requires<ArgumentNullException>(privateData != null); + Contract.Ensures(Contract.Result<Association>() != null); + + expiresUtc = expiresUtc.ToUniversalTimeSafe(); + TimeSpan remainingLifeLength = expiresUtc - DateTime.UtcNow; + byte[] secret = privateData; // the whole of privateData is the secret key for now. + // We figure out what derived type to instantiate based on the length of the secret. + try { + return HmacShaAssociation.Create(handle, secret, remainingLifeLength); + } catch (ArgumentException ex) { + throw new ArgumentException(OpenIdStrings.BadAssociationPrivateData, "privateData", ex); + } + } + + /// <summary> + /// Returns private data required to persist this <see cref="Association"/> in + /// permanent storage (a shared database for example) for deserialization later. + /// </summary> + /// <returns> + /// An opaque byte array that must be stored and returned exactly as it is provided here. + /// The byte array may vary in length depending on the specific type of <see cref="Association"/>, + /// but in current versions are no larger than 256 bytes. + /// </returns> + /// <remarks> + /// Values of public properties on the base class <see cref="Association"/> are not included + /// in this byte array, as they are useful for fast database lookup and are persisted separately. + /// </remarks> + public byte[] SerializePrivateData() { + Contract.Ensures(Contract.Result<byte[]>() != null); + + // We may want to encrypt this secret using the machine.config private key, + // and add data regarding which Association derivative will need to be + // re-instantiated on deserialization. + // For now, we just send out the secret key. We can derive the type from the length later. + byte[] secretKeyCopy = new byte[this.SecretKey.Length]; + if (this.SecretKey.Length > 0) { + this.SecretKey.CopyTo(secretKeyCopy, 0); + } + return secretKeyCopy; + } + + /// <summary> + /// Tests equality of two <see cref="Association"/> objects. + /// </summary> + /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param> + /// <returns> + /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false. + /// </returns> + public override bool Equals(object obj) { + Association a = obj as Association; + if (a == null) { + return false; + } + if (a.GetType() != GetType()) { + return false; + } + + if (a.Handle != this.Handle || + a.Issued != this.Issued || + !MessagingUtilities.Equals(a.TotalLifeLength, this.TotalLifeLength, TimeSpan.FromSeconds(1))) { + return false; + } + + if (!MessagingUtilities.AreEquivalent(a.SecretKey, this.SecretKey)) { + return false; + } + + return true; + } + + /// <summary> + /// Returns the hash code. + /// </summary> + /// <returns> + /// A hash code for the current <see cref="T:System.Object"/>. + /// </returns> + public override int GetHashCode() { + HMACSHA1 hmac = new HMACSHA1(this.SecretKey); + try { + CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write); + + byte[] hbytes = ASCIIEncoding.ASCII.GetBytes(this.Handle); + + cs.Write(hbytes, 0, hbytes.Length); + cs.Close(); + + byte[] hash = hmac.Hash; + hmac.Clear(); + + long val = 0; + for (int i = 0; i < hash.Length; i++) { + val = val ^ (long)hash[i]; + } + + val = val ^ this.Expires.ToFileTimeUtc(); + + return (int)val; + } finally { + ((IDisposable)hmac).Dispose(); + } + } + + /// <summary> + /// The string to pass as the assoc_type value in the OpenID protocol. + /// </summary> + /// <param name="protocol">The protocol version of the message that the assoc_type value will be included in.</param> + /// <returns>The value that should be used for the openid.assoc_type parameter.</returns> + internal abstract string GetAssociationType(Protocol protocol); + + /// <summary> + /// Generates a signature from a given blob of data. + /// </summary> + /// <param name="data">The data to sign. This data will not be changed (the signature is the return value).</param> + /// <returns>The calculated signature of the data.</returns> + protected internal byte[] Sign(byte[] data) { + Contract.Requires<ArgumentNullException>(data != null); + using (HashAlgorithm hasher = this.CreateHasher()) { + return hasher.ComputeHash(data); + } + } + + /// <summary> + /// Returns the specific hash algorithm used for message signing. + /// </summary> + /// <returns>The hash algorithm used for message signing.</returns> + protected abstract HashAlgorithm CreateHasher(); + +#if CONTRACTS_FULL + /// <summary> + /// Verifies conditions that should be true for any valid state of this object. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] + [ContractInvariantMethod] + private void ObjectInvariant() { + Contract.Invariant(!string.IsNullOrEmpty(this.Handle)); + Contract.Invariant(this.TotalLifeLength > TimeSpan.Zero); + Contract.Invariant(this.SecretKey != null); + } +#endif + } +} |