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