//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- 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; /// /// Stores a secret used in signing and verifying messages. /// /// /// 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). /// [DebuggerDisplay("Handle = {Handle}, Expires = {Expires}")] [ContractVerification(true)] [ContractClass(typeof(AssociationContract))] public abstract class Association { /// /// Initializes a new instance of the class. /// /// The handle. /// The secret. /// How long the association will be useful. /// The UTC time of when this association was originally issued by the Provider. protected Association(string handle, byte[] secret, TimeSpan totalLifeLength, DateTime issued) { Requires.NotNullOrEmpty(handle, "handle"); Requires.NotNull(secret, "secret"); Requires.InRange(totalLifeLength > TimeSpan.Zero, "totalLifeLength"); Requires.True(issued.Kind == DateTimeKind.Utc, "issued"); Requires.InRange(issued <= DateTime.UtcNow, "issued"); Contract.Ensures(this.TotalLifeLength == totalLifeLength); this.Handle = handle; this.SecretKey = secret; this.TotalLifeLength = totalLifeLength; this.Issued = OpenIdUtilities.CutToSecond(issued); } /// /// Gets a unique handle by which this may be stored or retrieved. /// public string Handle { get; internal set; } /// /// Gets the UTC time when this will expire. /// public DateTime Expires { get { return this.Issued + this.TotalLifeLength; } } /// /// Gets a value indicating whether this has already expired. /// public bool IsExpired { get { return this.Expires < DateTime.UtcNow; } } /// /// Gets the length (in bits) of the hash this association creates when signing. /// public abstract int HashBitLength { get; } /// /// Gets a value indicating whether this instance has useful life remaining. /// /// /// true if this instance has useful life remaining; otherwise, false. /// internal bool HasUsefulLifeRemaining { get { return this.TimeTillExpiration >= MinimumUsefulAssociationLifetime; } } /// /// Gets or sets the UTC time that this was first created. /// [MessagePart] internal DateTime Issued { get; set; } /// /// Gets the duration a secret key used for signing dumb client requests will be good for. /// protected internal static TimeSpan DumbSecretLifetime { get { Contract.Ensures(Contract.Result() > TimeSpan.Zero); return OpenIdElement.Configuration.MaxAuthenticationTime; } } /// /// Gets the number of seconds until this expires. /// Never negative (counter runs to zero). /// protected internal long SecondsTillExpiration { get { Contract.Ensures(Contract.Result() >= 0); return Math.Max(0, (long)this.TimeTillExpiration.TotalSeconds); } } /// /// Gets the shared secret key between the consumer and provider. /// [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It is a buffer.")] [MessagePart("key")] protected internal byte[] SecretKey { get; private set; } /// /// Gets the lifetime the OpenID provider permits this . /// [MessagePart("ttl")] protected TimeSpan TotalLifeLength { get; private set; } /// /// Gets the minimum lifetime an association must still be good for in order for it to be used for a future authentication. /// /// /// Associations that are not likely to last the duration of a user login are not worth using at all. /// private static TimeSpan MinimumUsefulAssociationLifetime { get { Contract.Ensures(Contract.Result() > TimeSpan.Zero); return OpenIdElement.Configuration.MaxAuthenticationTime; } } /// /// Gets the TimeSpan till this association expires. /// private TimeSpan TimeTillExpiration { get { return this.Expires - DateTime.UtcNow; } } /// /// Re-instantiates an previously persisted in a database or some /// other shared store. /// /// /// The property of the previous instance. /// /// /// The UTC value of the property of the previous instance. /// /// /// The byte array returned by a call to on the previous /// instance. /// /// /// The newly dehydrated , which can be returned /// from a custom association store's /// IRelyingPartyAssociationStore.GetAssociation method. /// public static Association Deserialize(string handle, DateTime expiresUtc, byte[] privateData) { Requires.NotNullOrEmpty(handle, "handle"); Requires.NotNull(privateData, "privateData"); Contract.Ensures(Contract.Result() != 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); } } /// /// Returns private data required to persist this in /// permanent storage (a shared database for example) for deserialization later. /// /// /// 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 , /// but in current versions are no larger than 256 bytes. /// /// /// Values of public properties on the base class are not included /// in this byte array, as they are useful for fast database lookup and are persisted separately. /// public byte[] SerializePrivateData() { Contract.Ensures(Contract.Result() != 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; } /// /// Tests equality of two objects. /// /// The to compare with the current . /// /// true if the specified is equal to the current ; otherwise, false. /// 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; } /// /// Returns the hash code. /// /// /// A hash code for the current . /// 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(); } } /// /// The string to pass as the assoc_type value in the OpenID protocol. /// /// The protocol version of the message that the assoc_type value will be included in. /// The value that should be used for the openid.assoc_type parameter. internal abstract string GetAssociationType(Protocol protocol); /// /// Generates a signature from a given blob of data. /// /// The data to sign. This data will not be changed (the signature is the return value). /// The calculated signature of the data. protected internal byte[] Sign(byte[] data) { Requires.NotNull(data, "data"); using (HashAlgorithm hasher = this.CreateHasher()) { return hasher.ComputeHash(data); } } /// /// Returns the specific hash algorithm used for message signing. /// /// The hash algorithm used for message signing. protected abstract HashAlgorithm CreateHasher(); #if CONTRACTS_FULL /// /// Verifies conditions that should be true for any valid state of this object. /// [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 } }