diff options
11 files changed, 359 insertions, 141 deletions
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj b/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj index 570d91f..b35fa29 100644 --- a/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj +++ b/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj @@ -62,7 +62,6 @@ <Compile Include="OAuthIdentity.cs" /> <Compile Include="OAuthPrincipal.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="Provider\AnonymousIdentifierProviderBase.cs" /> <Compile Include="Provider\AuthenticationRequestExtensions.cs" /> <Compile Include="TwitterConsumer.cs" /> <Compile Include="Util.cs" /> diff --git a/samples/DotNetOpenAuth.ApplicationBlock/Provider/AnonymousIdentifierProviderBase.cs b/samples/DotNetOpenAuth.ApplicationBlock/Provider/AnonymousIdentifierProviderBase.cs deleted file mode 100644 index 1df7267..0000000 --- a/samples/DotNetOpenAuth.ApplicationBlock/Provider/AnonymousIdentifierProviderBase.cs +++ /dev/null @@ -1,122 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="AnonymousIdentifierProviderBase.cs" company="Andrew Arnott"> -// Copyright (c) Andrew Arnott. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.ApplicationBlock.Provider { - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Security.Cryptography; - using System.Text; - using DotNetOpenAuth.Messaging; - using DotNetOpenAuth.OpenId; - - public abstract class AnonymousIdentifierProviderBase { - private int newSaltLength = 20; - - /// <summary> - /// Initializes a new instance of the <see cref="AnonymousIdentifierProviderBase"/> class. - /// </summary> - /// <param name="baseIdentifier">The base URI on which to append the anonymous part.</param> - public AnonymousIdentifierProviderBase(Uri baseIdentifier) { - if (baseIdentifier == null) { - throw new ArgumentNullException("baseIdentifier"); - } - - this.Hasher = HashAlgorithm.Create("SHA256"); - this.Encoder = Encoding.UTF8; - this.BaseIdentifier = baseIdentifier; - } - - public Uri BaseIdentifier { get; private set; } - - protected HashAlgorithm Hasher { get; private set; } - - protected Encoding Encoder { get; private set; } - - protected int NewSaltLength { - get { - return this.newSaltLength; - } - - set { - if (value <= 0) { - throw new ArgumentOutOfRangeException("value"); - } - - this.newSaltLength = value; - } - } - - #region IAnonymousIdentifierProvider Members - - public Uri GetAnonymousIdentifier(Identifier localIdentifier, Realm relyingPartyRealm) { - byte[] salt = this.GetHashSaltForLocalIdentifier(localIdentifier); - string valueToHash = localIdentifier + "#" + (relyingPartyRealm ?? string.Empty); - 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(this.BaseIdentifier, base64Hash); - return anonymousIdentifier; - } - - #endregion - - protected virtual byte[] GetNewSalt() { - // We COULD use a crypto random function, but for a salt it seems overkill. - return Util.GetNonCryptoRandomData(this.NewSaltLength); - } - - protected Uri AppendIdentifiers(Uri baseIdentifier, string uriHash) { - if (baseIdentifier == null) { - throw new ArgumentNullException("baseIdentifier"); - } - if (String.IsNullOrEmpty(uriHash)) { - throw new ArgumentNullException("uriHash"); - } - - if (string.IsNullOrEmpty(baseIdentifier.Query)) { - // The uriHash will appear on the path itself. - string pathEncoded = Uri.EscapeUriString(uriHash.Replace('/', '_')); - return new Uri(baseIdentifier, pathEncoded); - } else { - // The uriHash will appear on the query string. - string dataEncoded = Uri.EscapeDataString(uriHash); - return new Uri(baseIdentifier + dataEncoded); - } - } - - /// <summary> - /// Gets the salt to use for generating an anonymous identifier for a given OP local identifier. - /// </summary> - /// <param name="localIdentifier">The OP local identifier.</param> - /// <returns>The salt to use in the hash.</returns> - /// <remarks> - /// It is important that this method always return the same value for a given - /// <paramref name="localIdentifier"/>. - /// New salts can be generated for local identifiers without previously assigned salt - /// values by calling <see cref="GetNewSalt"/> or by a custom method. - /// </remarks> - protected abstract byte[] GetHashSaltForLocalIdentifier(Identifier localIdentifier); - -#if CONTRACTS_FULL - /// <summary> - /// Verifies conditions that should be true for any valid state of this object. - /// </summary> - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] - [ContractInvariantMethod] - protected void ObjectInvariant() { - Contract.Invariant(this.Hasher != null); - Contract.Invariant(this.Encoder != null); - Contract.Invariant(this.BaseIdentifier != null); - Contract.Invariant(this.NewHashLength > 0); - } -#endif - } -} diff --git a/samples/DotNetOpenAuth.ApplicationBlock/Provider/AuthenticationRequestExtensions.cs b/samples/DotNetOpenAuth.ApplicationBlock/Provider/AuthenticationRequestExtensions.cs index a737d30..8af72aa 100644 --- a/samples/DotNetOpenAuth.ApplicationBlock/Provider/AuthenticationRequestExtensions.cs +++ b/samples/DotNetOpenAuth.ApplicationBlock/Provider/AuthenticationRequestExtensions.cs @@ -10,11 +10,10 @@ /// <param name="request">The incoming authentication request.</param> /// <param name="localIdentifier">The OP local identifier, before the anonymous hash is applied to it.</param> /// <param name="anonymousIdentifierProvider">The anonymous identifier provider.</param> - /// <param name="pairwiseUnique">if set to <c>true</c> the anonymous identifier will be unique to the requesting relying party's realm.</param> /// <remarks> /// The openid.claimed_id and openid.identity values are hashed. /// </remarks> - public static void ScrubPersonallyIdentifiableInformation(this IAuthenticationRequest request, Identifier localIdentifier, AnonymousIdentifierProviderBase anonymousIdentifierProvider, bool pairwiseUnique) { + public static void ScrubPersonallyIdentifiableInformation(this IAuthenticationRequest request, Identifier localIdentifier, IDirectedIdentityIdentifierProvider anonymousIdentifierProvider) { if (request == null) { throw new ArgumentNullException("request"); } @@ -30,7 +29,7 @@ // When generating the anonymous identifiers, the openid.identity and openid.claimed_id // will always end up with matching values. - var anonymousIdentifier = anonymousIdentifierProvider.GetAnonymousIdentifier(localIdentifier, pairwiseUnique ? request.Realm : null); + var anonymousIdentifier = anonymousIdentifierProvider.GetIdentifier(localIdentifier, request.Realm); request.ClaimedIdentifier = anonymousIdentifier; request.LocalIdentifier = anonymousIdentifier; } diff --git a/samples/DotNetOpenAuth.ApplicationBlock/Util.cs b/samples/DotNetOpenAuth.ApplicationBlock/Util.cs index 8a188ac..ea7da97 100644 --- a/samples/DotNetOpenAuth.ApplicationBlock/Util.cs +++ b/samples/DotNetOpenAuth.ApplicationBlock/Util.cs @@ -5,8 +5,6 @@ using DotNetOpenAuth.Messaging; internal static class Util { - internal static readonly Random NonCryptoRandomDataGenerator = new Random(); - /// <summary> /// Enumerates through the individual set bits in a flag enum. /// </summary> @@ -30,17 +28,6 @@ } /// <summary> - /// Gets a buffer of random data (not cryptographically strong). - /// </summary> - /// <param name="length">The length of the sequence to generate.</param> - /// <returns>The generated values, which may contain zeros.</returns> - internal static byte[] GetNonCryptoRandomData(int length) { - byte[] buffer = new byte[length]; - NonCryptoRandomDataGenerator.NextBytes(buffer); - return buffer; - } - - /// <summary> /// Copies the contents of one stream to another. /// </summary> /// <param name="copyFrom">The stream to copy from, at the position where copying should begin.</param> diff --git a/samples/OpenIdProviderMvc/Code/AnonymousIdentifierProvider.cs b/samples/OpenIdProviderMvc/Code/AnonymousIdentifierProvider.cs index 2b9e01c..bed4e82 100644 --- a/samples/OpenIdProviderMvc/Code/AnonymousIdentifierProvider.cs +++ b/samples/OpenIdProviderMvc/Code/AnonymousIdentifierProvider.cs @@ -3,9 +3,10 @@ using System.Web.Security; using DotNetOpenAuth.ApplicationBlock.Provider; using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Provider; using OpenIdProviderMvc.Models; - internal class AnonymousIdentifierProvider : AnonymousIdentifierProviderBase { + internal class AnonymousIdentifierProvider : PrivatePersonalIdentifierProviderBase { internal AnonymousIdentifierProvider() : base(Util.GetAppPathRootedUri("anon?id=")) { } diff --git a/samples/OpenIdProviderMvc/Controllers/OpenIdController.cs b/samples/OpenIdProviderMvc/Controllers/OpenIdController.cs index e353268..8aad0ba 100644 --- a/samples/OpenIdProviderMvc/Controllers/OpenIdController.cs +++ b/samples/OpenIdProviderMvc/Controllers/OpenIdController.cs @@ -45,7 +45,7 @@ namespace OpenIdProviderMvc.Controllers { } var anonProvider = new AnonymousIdentifierProvider(); - authReq.ScrubPersonallyIdentifiableInformation(localIdentifier, anonProvider, true); + authReq.ScrubPersonallyIdentifiableInformation(localIdentifier, anonProvider); authReq.IsAuthenticated = true; } else { if (authReq.IsDirectedIdentity) { diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 02e093a..bc10d44 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -373,6 +373,7 @@ <Compile Include="OpenId\Messages\SignedResponseRequest.cs" /> <Compile Include="OpenId\NoDiscoveryIdentifier.cs" /> <Compile Include="OpenId\OpenIdUtilities.cs" /> + <Compile Include="OpenId\Provider\PrivatePersonalIdentifierProviderBase.cs" /> <Compile Include="OpenId\Provider\AnonymousRequest.cs" /> <Compile Include="OpenId\Provider\AnonymousRequestEventArgs.cs" /> <Compile Include="OpenId\Provider\AuthenticationChallengeEventArgs.cs" /> @@ -381,6 +382,7 @@ <Compile Include="OpenId\Provider\HostProcessedRequest.cs" /> <Compile Include="OpenId\Provider\IAnonymousRequest.cs" /> <Compile Include="OpenId\Provider\IAuthenticationRequest.cs" /> + <Compile Include="OpenId\Provider\IDirectedIdentityCustomIdentifier.cs" /> <Compile Include="OpenId\Provider\IHostProcessedRequest.cs" /> <Compile Include="OpenId\Provider\IdentityEndpoint.cs" /> <Compile Include="OpenId\Provider\IdentityEndpointNormalizationEventArgs.cs" /> diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index 10ddba0..b06502e 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -31,6 +31,11 @@ namespace DotNetOpenAuth.Messaging { internal static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider(); /// <summary> + /// A pseudo-random data generator (NOT cryptographically strong random data) + /// </summary> + internal static readonly Random NonCryptoRandomDataGenerator = new Random(); + + /// <summary> /// The set of characters that are unreserved in RFC 2396 but are NOT unreserved in RFC 3986. /// </summary> private static readonly string[] UriRfc3986CharsToEscape = new[] { "!", "*", "'", "(", ")" }; @@ -140,6 +145,17 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Gets a buffer of random data (not cryptographically strong). + /// </summary> + /// <param name="length">The length of the sequence to generate.</param> + /// <returns>The generated values, which may contain zeros.</returns> + internal static byte[] GetNonCryptoRandomData(int length) { + byte[] buffer = new byte[length]; + NonCryptoRandomDataGenerator.NextBytes(buffer); + return buffer; + } + + /// <summary> /// Gets a cryptographically strong random sequence of values. /// </summary> /// <param name="length">The length of the sequence to generate.</param> diff --git a/src/DotNetOpenAuth/OpenId/Provider/IDirectedIdentityCustomIdentifier.cs b/src/DotNetOpenAuth/OpenId/Provider/IDirectedIdentityCustomIdentifier.cs new file mode 100644 index 0000000..63f9cdf --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Provider/IDirectedIdentityCustomIdentifier.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------- +// <copyright file="IDirectedIdentityCustomIdentifier.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Diagnostics.Contracts; + + /// <summary> + /// An interface to provide custom identifiers for users logging into specific relying parties. + /// </summary> + /// <remarks> + /// This interface would allow, for example, the Provider to offer PPIDs to their users, + /// allowing the users to log into RPs without leaving any clue as to their true identity, + /// and preventing multiple RPs from colluding to track user activity across realms. + /// </remarks> + [ContractClass(typeof(IDirectedIdentityIdentifierProviderContract))] + public interface IDirectedIdentityIdentifierProvider { + /// <summary> + /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of + /// an outgoing positive assertion. + /// </summary> + /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param> + /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param> + /// <returns> + /// 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. + /// </returns> + Uri GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm); + } + + /// <summary> + /// Contract class for the <see cref="IDirectedIdentityIdentifierProvider"/> type. + /// </summary> + [ContractClassFor(typeof(IDirectedIdentityIdentifierProvider))] + internal abstract class IDirectedIdentityIdentifierProviderContract : IDirectedIdentityIdentifierProvider { + #region IDirectedIdentityIdentifierProvider Members + + /// <summary> + /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of + /// an outgoing positive assertion. + /// </summary> + /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param> + /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param> + /// <returns> + /// 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. + /// </returns> + Uri IDirectedIdentityIdentifierProvider.GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm) { + Contract.Requires(localIdentifier != null); + Contract.Requires(relyingPartyRealm != null); + + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth/OpenId/Provider/IHostProcessedRequest.cs b/src/DotNetOpenAuth/OpenId/Provider/IHostProcessedRequest.cs index 3e743ec..eee99f1 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/IHostProcessedRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/IHostProcessedRequest.cs @@ -5,12 +5,14 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.Provider { + using System.Diagnostics.Contracts; using DotNetOpenAuth.Messaging; /// <summary> /// Interface exposing incoming messages to the OpenID Provider that /// require interaction with the host site. /// </summary> + [ContractClass(typeof(IHostProcessedRequestContract))] public interface IHostProcessedRequest : IRequest { /// <summary> /// Gets the version of OpenID being used by the relying party that sent the request. @@ -42,4 +44,102 @@ namespace DotNetOpenAuth.OpenId.Provider { /// </remarks> RelyingPartyDiscoveryResult IsReturnUrlDiscoverable(OpenIdProvider provider); } + + /// <summary> + /// Contract class for the <see cref="IHostProcessedRequest"/> type. + /// </summary> + [ContractClassFor(typeof(IHostProcessedRequest))] + internal abstract class IHostProcessedRequestContract : IHostProcessedRequest { + #region IHostProcessedRequest Properties + + /// <summary> + /// Gets the version of OpenID being used by the relying party that sent the request. + /// </summary> + ProtocolVersion IHostProcessedRequest.RelyingPartyVersion { + get { throw new System.NotImplementedException(); } + } + + /// <summary> + /// Gets the URL the consumer site claims to use as its 'base' address. + /// </summary> + Realm IHostProcessedRequest.Realm { + get { throw new System.NotImplementedException(); } + } + + /// <summary> + /// Gets a value indicating whether the consumer demands an immediate response. + /// If false, the consumer is willing to wait for the identity provider + /// to authenticate the user. + /// </summary> + bool IHostProcessedRequest.Immediate { + get { throw new System.NotImplementedException(); } + } + + #endregion + + #region IRequest Members + + /// <summary> + /// Gets a value indicating whether the response is ready to be sent to the user agent. + /// </summary> + /// <remarks> + /// This property returns false if there are properties that must be set on this + /// request instance before the response can be sent. + /// </remarks> + bool IRequest.IsResponseReady { + get { throw new System.NotImplementedException(); } + } + + /// <summary> + /// Adds an extension to the response to send to the relying party. + /// </summary> + /// <param name="extension">The extension to add to the response message.</param> + void IRequest.AddResponseExtension(DotNetOpenAuth.OpenId.Messages.IOpenIdMessageExtension extension) { + throw new System.NotImplementedException(); + } + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <typeparam name="T">The type of the extension.</typeparam> + /// <returns> + /// An instance of the extension initialized with values passed in with the request. + /// </returns> + T IRequest.GetExtension<T>() { + throw new System.NotImplementedException(); + } + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <param name="extensionType">The type of the extension.</param> + /// <returns> + /// An instance of the extension initialized with values passed in with the request. + /// </returns> + DotNetOpenAuth.OpenId.Messages.IOpenIdMessageExtension IRequest.GetExtension(System.Type extensionType) { + throw new System.NotImplementedException(); + } + + #endregion + + #region IHostProcessedRequest Methods + + /// <summary> + /// Attempts to perform relying party discovery of the return URL claimed by the Relying Party. + /// </summary> + /// <param name="provider">The OpenIdProvider that is performing the RP discovery.</param> + /// <returns> + /// The details of how successful the relying party discovery was. + /// </returns> + /// <remarks> + /// <para>Return URL verification is only attempted if this method is called.</para> + /// <para>See OpenID Authentication 2.0 spec section 9.2.1.</para> + /// </remarks> + RelyingPartyDiscoveryResult IHostProcessedRequest.IsReturnUrlDiscoverable(OpenIdProvider provider) { + Contract.Requires(provider != null); + throw new System.NotImplementedException(); + } + + #endregion + } } diff --git a/src/DotNetOpenAuth/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs b/src/DotNetOpenAuth/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs new file mode 100644 index 0000000..a4b123b --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs @@ -0,0 +1,176 @@ +//----------------------------------------------------------------------- +// <copyright file="PrivatePersonalIdentifierProviderBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Provides standard PPID Identifiers to users to protect their identity from individual relying parties + /// and from colluding groups of relying parties. + /// </summary> + public abstract class PrivatePersonalIdentifierProviderBase : IDirectedIdentityIdentifierProvider { + /// <summary> + /// The type of hash function to use for the <see cref="Hasher"/> property. + /// </summary> + private const string HashAlgorithmName = "SHA256"; + + /// <summary> + /// The length of the salt to generate for first time PPID-users. + /// </summary> + private int newSaltLength = 20; + + /// <summary> + /// Initializes a new instance of the <see cref="PrivatePersonalIdentifierProviderBase"/> class. + /// </summary> + /// <param name="baseIdentifier">The base URI on which to append the anonymous part.</param> + public PrivatePersonalIdentifierProviderBase(Uri baseIdentifier) { + Contract.Requires(baseIdentifier != null); + ErrorUtilities.VerifyArgumentNotNull(baseIdentifier, "baseIdentifier"); + + this.Hasher = HashAlgorithm.Create(HashAlgorithmName); + this.Encoder = Encoding.UTF8; + this.BaseIdentifier = baseIdentifier; + this.PairwiseUnique = true; + } + + /// <summary> + /// Gets the base URI on which to append the anonymous part. + /// </summary> + public Uri BaseIdentifier { get; private set; } + + /// <summary> + /// Gets or sets a value indicating whether each Realm will get its own private identifier + /// for the authenticating uesr. + /// </summary> + /// <value>The default value is <c>true</c>.</value> + public bool PairwiseUnique { get; set; } + + /// <summary> + /// Gets the hash function to use to perform the one-way transform of a personal identifier + /// to an "anonymous" looking one. + /// </summary> + protected HashAlgorithm Hasher { get; private set; } + + /// <summary> + /// Gets the encoder to use for transforming the personal identifier into bytes for hashing. + /// </summary> + protected Encoding Encoder { get; private set; } + + /// <summary> + /// Gets or sets the new length of the salt. + /// </summary> + /// <value>The new length of the salt.</value> + protected int NewSaltLength { + get { + return this.newSaltLength; + } + + set { + Contract.Requires(value > 0); + ErrorUtilities.VerifyArgumentInRange(value > 0, "value"); + this.newSaltLength = value; + } + } + + #region IDirectedIdentityIdentifierProvider Members + + /// <summary> + /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of + /// an outgoing positive assertion. + /// </summary> + /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param> + /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param> + /// <returns> + /// 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. + /// </returns> + public Uri GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm) { + ErrorUtilities.VerifyArgumentNotNull(localIdentifier, "localIdentifier"); + ErrorUtilities.VerifyArgumentNotNull(relyingPartyRealm, "relyingPartyRealm"); + + byte[] salt = this.GetHashSaltForLocalIdentifier(localIdentifier); + string valueToHash = localIdentifier + "#"; + if (this.PairwiseUnique) { + valueToHash += relyingPartyRealm; + } + + 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; + } + + #endregion + + /// <summary> + /// Creates a new salt to assign to a user. + /// </summary> + /// <returns>A non-null buffer of length <see cref="NewSaltLength"/> filled with a random salt.</returns> + protected virtual byte[] CreateSalt() { + // We COULD use a crypto random function, but for a salt it seems overkill. + return MessagingUtilities.GetNonCryptoRandomData(this.NewSaltLength); + } + + /// <summary> + /// Creates a new PPID Identifier by appending a pseudonymous identifier suffix to + /// the <see cref="BaseIdentifier"/>. + /// </summary> + /// <param name="uriHash">The unique part of the Identifier to append to the common first part.</param> + /// <returns>The full PPID Identifier.</returns> + protected virtual Uri AppendIdentifiers(string uriHash) { + Contract.Requires(!String.IsNullOrEmpty(uriHash)); + ErrorUtilities.VerifyNonZeroLength(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); + } + } + + /// <summary> + /// Gets the salt to use for generating an anonymous identifier for a given OP local identifier. + /// </summary> + /// <param name="localIdentifier">The OP local identifier.</param> + /// <returns>The salt to use in the hash.</returns> + /// <remarks> + /// It is important that this method always return the same value for a given + /// <paramref name="localIdentifier"/>. + /// New salts can be generated for local identifiers without previously assigned salt + /// values by calling <see cref="CreateSalt"/> or by a custom method. + /// </remarks> + protected abstract byte[] GetHashSaltForLocalIdentifier(Identifier localIdentifier); + +#if CONTRACTS_FULL + /// <summary> + /// Verifies conditions that should be true for any valid state of this object. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] + [ContractInvariantMethod] + protected void ObjectInvariant() { + Contract.Invariant(this.Hasher != null); + Contract.Invariant(this.Encoder != null); + Contract.Invariant(this.BaseIdentifier != null); + Contract.Invariant(this.NewSaltLength > 0); + } +#endif + } +} |