//-----------------------------------------------------------------------
//
// 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
}
}