using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Security.Cryptography;
using System.IO;
using System.Text;
using System.Globalization;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics;
namespace DotNetOpenId {
///
/// 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}")]
public abstract class Association {
///
/// Instantiates an object.
///
protected Association(string handle, byte[] secret, TimeSpan totalLifeLength, DateTime issued) {
if (string.IsNullOrEmpty(handle)) throw new ArgumentNullException("handle");
if (secret == null) throw new ArgumentNullException("secret");
Handle = handle;
SecretKey = secret;
TotalLifeLength = totalLifeLength;
Issued = cutToSecond(issued);
}
///
/// Re-instantiates an previously persisted in a database or some
/// other shared store.
///
///
/// The property of the previous instance.
///
///
/// The 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
/// method.
///
public static Association Deserialize(string handle, DateTime expires, byte[] privateData) {
if (string.IsNullOrEmpty(handle)) throw new ArgumentNullException("handle");
if (privateData == null) throw new ArgumentNullException("privateData");
expires = expires.ToUniversalTime();
TimeSpan remainingLifeLength = expires - 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(secret.Length, handle, secret, remainingLifeLength);
} catch (ArgumentException ex) {
throw new ArgumentException(Strings.BadAssociationPrivateData, "privateData", ex);
}
}
static TimeSpan minimumUsefulAssociationLifetime {
get { return Protocol.MaximumUserAgentAuthenticationTime; }
}
internal bool HasUsefulLifeRemaining {
get { return timeTillExpiration >= minimumUsefulAssociationLifetime; }
}
///
/// Represents January 1, 1970 12 AM.
///
protected internal readonly static DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
///
/// A unique handle by which this may be stored or retrieved.
///
public string Handle { get; private set; }
///
/// Gets the time that this was first created
/// and the issued.
///
internal DateTime Issued { get; set; }
///
/// The lifetime the OpenID provider permits this .
///
protected TimeSpan TotalLifeLength { get; private set; }
///
/// The shared secret key between the consumer and provider.
///
[SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
protected internal byte[] SecretKey { get; private set; }
///
/// 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() {
// 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[SecretKey.Length];
SecretKey.CopyTo(secretKeyCopy, 0);
return secretKeyCopy;
}
///
/// Gets the time when this will expire.
///
public DateTime Expires {
get { return Issued + TotalLifeLength; }
}
///
/// Gets whether this has already expired.
///
public bool IsExpired {
get { return Expires < DateTime.UtcNow; }
}
///
/// Gets the TimeSpan till this association expires.
///
TimeSpan timeTillExpiration {
get { return Expires - DateTime.UtcNow; }
}
///
/// The number of seconds until this expires.
/// Never negative (counter runs to zero).
///
protected internal long SecondsTillExpiration {
get { return Math.Max(0, (long)timeTillExpiration.TotalSeconds); }
}
///
/// The string to pass as the assoc_type value in the OpenID protocol.
///
internal abstract string GetAssociationType(Protocol protocol);
///
/// Signs certain given key/value pairs in a supplied dictionary.
///
///
/// A dictionary with key/value pairs, at least some of which you want to include in the signature.
///
///
/// A list of the keys in the supplied dictionary you wish to sign.
///
///
/// An optional prefix to use in front of a given name in
/// when looking up the value from .
///
/// The signature of the key-value pairs.
internal byte[] Sign(IDictionary data, IList keysToSign, string keyLookupPrefix) {
var nvc = new Dictionary();
foreach (string field in keysToSign) {
nvc.Add(field, data[keyLookupPrefix + field]);
}
return Sign(nvc, keysToSign);
}
///
/// Generates a signature from a given dictionary.
///
/// The dictionary. This dictionary will not be changed.
/// The order that the data in the dictionary must be encoded in for the signature to be valid.
/// The calculated signature of the data in the dictionary.
protected internal byte[] Sign(IDictionary data, IList keyOrder) {
using (HashAlgorithm hasher = CreateHasher()) {
return hasher.ComputeHash(ProtocolMessages.KeyValueForm.GetBytes(data, keyOrder));
}
}
///
/// Returns the specific hash algorithm used for message signing.
///
protected abstract HashAlgorithm CreateHasher();
///
/// Rounds the given downward to the whole second.
///
static DateTime cutToSecond(DateTime dateTime) {
return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond));
}
///
/// Tests equality of two objects.
///
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 ||
a.TotalLifeLength != this.TotalLifeLength)
return false;
if (!Util.ArrayEquals(a.SecretKey, this.SecretKey))
return false;
return true;
}
///
/// Returns the hash code.
///
public override int GetHashCode() {
HMACSHA1 hmac = new HMACSHA1(SecretKey);
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;
}
}
}