//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OpenId.Provider {
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using DotNetOpenAuth.Messaging;
using Validation;
///
/// Provides standard PPID Identifiers to users to protect their identity from individual relying parties
/// and from colluding groups of relying parties.
///
public abstract class PrivatePersonalIdentifierProviderBase : IDirectedIdentityIdentifierProvider {
///
/// The type of hash function to use for the property.
///
private const string HashAlgorithmName = "SHA256";
///
/// The length of the salt to generate for first time PPID-users.
///
private int newSaltLength = 20;
///
/// Initializes a new instance of the class.
///
/// The base URI on which to append the anonymous part.
protected PrivatePersonalIdentifierProviderBase(Uri baseIdentifier) {
Requires.NotNull(baseIdentifier, "baseIdentifier");
this.Hasher = HashAlgorithm.Create(HashAlgorithmName);
this.Encoder = Encoding.UTF8;
this.BaseIdentifier = baseIdentifier;
this.PairwiseUnique = AudienceScope.Realm;
}
///
/// A granularity description for who wide of an audience sees the same generated PPID.
///
[SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Breaking change")]
public enum AudienceScope {
///
/// A unique Identifier is generated for every realm. This is the highest security setting.
///
Realm,
///
/// Only the host name in the realm is used in calculating the PPID,
/// allowing for some level of sharing of the PPID Identifiers between RPs
/// that are able to share the same realm host value.
///
RealmHost,
///
/// Although the user's Identifier is still opaque to the RP so they cannot determine
/// who the user is at the OP, the same Identifier is used at all RPs so collusion
/// between the RPs is possible.
///
Global,
}
///
/// Gets the base URI on which to append the anonymous part.
///
public Uri BaseIdentifier { get; private set; }
///
/// Gets or sets a value indicating whether each Realm will get its own private identifier
/// for the authenticating uesr.
///
/// The default value is .
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Pairwise", Justification = "Meaningful word")]
public AudienceScope PairwiseUnique { get; set; }
///
/// Gets the hash function to use to perform the one-way transform of a personal identifier
/// to an "anonymous" looking one.
///
protected HashAlgorithm Hasher { get; private set; }
///
/// Gets the encoder to use for transforming the personal identifier into bytes for hashing.
///
protected Encoding Encoder { get; private set; }
///
/// Gets or sets the new length of the salt.
///
/// The new length of the salt.
protected int NewSaltLength {
get {
return this.newSaltLength;
}
set {
Requires.Range(value > 0, "value");
this.newSaltLength = value;
}
}
#region IDirectedIdentityIdentifierProvider Members
///
/// Gets the Identifier to use for the Claimed Identifier and Local Identifier of
/// an outgoing positive assertion.
///
/// The OP local identifier for the authenticating user.
/// The realm of the relying party receiving the assertion.
///
/// A valid, discoverable OpenID Identifier that should be used as the value for the
/// openid.claimed_id and openid.local_id parameters. Must not be null.
///
public Uri GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm) {
byte[] salt = this.GetHashSaltForLocalIdentifier(localIdentifier);
string valueToHash = localIdentifier + "#";
switch (this.PairwiseUnique) {
case AudienceScope.Realm:
valueToHash += relyingPartyRealm;
break;
case AudienceScope.RealmHost:
valueToHash += relyingPartyRealm.Host;
break;
case AudienceScope.Global:
break;
default:
throw new InvalidOperationException(
string.Format(
CultureInfo.CurrentCulture,
OpenIdStrings.UnexpectedEnumPropertyValue,
"PairwiseUnique",
this.PairwiseUnique));
}
byte[] valueAsBytes = this.Encoder.GetBytes(valueToHash);
byte[] bytesToHash = new byte[valueAsBytes.Length + salt.Length];
valueAsBytes.CopyTo(bytesToHash, 0);
salt.CopyTo(bytesToHash, valueAsBytes.Length);
byte[] hash = this.Hasher.ComputeHash(bytesToHash);
string base64Hash = Convert.ToBase64String(hash);
Uri anonymousIdentifier = this.AppendIdentifiers(base64Hash);
return anonymousIdentifier;
}
///
/// Determines whether a given identifier is the primary (non-PPID) local identifier for some user.
///
/// The identifier in question.
///
/// true if the given identifier is the valid, unique identifier for some uesr (and NOT a PPID); otherwise, false.
///
public virtual bool IsUserLocalIdentifier(Identifier identifier) {
return !identifier.ToString().StartsWith(this.BaseIdentifier.AbsoluteUri, StringComparison.Ordinal);
}
#endregion
///
/// Creates a new salt to assign to a user.
///
/// A non-null buffer of length filled with a random salt.
protected virtual byte[] CreateSalt() {
// We COULD use a crypto random function, but for a salt it seems overkill.
return MessagingUtilities.GetNonCryptoRandomData(this.NewSaltLength);
}
///
/// Creates a new PPID Identifier by appending a pseudonymous identifier suffix to
/// the .
///
/// The unique part of the Identifier to append to the common first part.
/// The full PPID Identifier.
[SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "NOT equivalent overload. The recommended one breaks on relative URIs.")]
protected virtual Uri AppendIdentifiers(string uriHash) {
Requires.NotNullOrEmpty(uriHash, "uriHash");
if (string.IsNullOrEmpty(this.BaseIdentifier.Query)) {
// The uriHash will appear on the path itself.
string pathEncoded = Uri.EscapeUriString(uriHash.Replace('/', '_'));
return new Uri(this.BaseIdentifier, pathEncoded);
} else {
// The uriHash will appear on the query string.
string dataEncoded = Uri.EscapeDataString(uriHash);
return new Uri(this.BaseIdentifier + dataEncoded);
}
}
///
/// Gets the salt to use for generating an anonymous identifier for a given OP local identifier.
///
/// The OP local identifier.
/// The salt to use in the hash.
///
/// It is important that this method always return the same value for a given
/// .
/// New salts can be generated for local identifiers without previously assigned salt
/// values by calling or by a custom method.
///
protected abstract byte[] GetHashSaltForLocalIdentifier(Identifier localIdentifier);
#if CONTRACTS_FULL
///
/// Verifies conditions that should be true for any valid state of this object.
///
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
[ContractInvariantMethod]
private void ObjectInvariant() {
Contract.Invariant(this.Hasher != null);
Contract.Invariant(this.Encoder != null);
Contract.Invariant(this.BaseIdentifier != null);
Contract.Invariant(this.NewSaltLength > 0);
}
#endif
}
}