diff --git a/DataProtection.sln b/DataProtection.sln
index c0e088b8b7..cf081e45be 100644
--- a/DataProtection.sln
+++ b/DataProtection.sln
@@ -29,6 +29,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtec
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtection.SystemWeb", "src\Microsoft.AspNet.DataProtection.SystemWeb\Microsoft.AspNet.DataProtection.SystemWeb.xproj", "{E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtection.Extensions.Test", "test\Microsoft.AspNet.DataProtection.Extensions.Test\Microsoft.AspNet.DataProtection.Extensions.Test.xproj", "{04AA8E60-A053-4D50-89FE-E76C3DF45200}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtection.Extensions", "src\Microsoft.AspNet.DataProtection.Extensions\Microsoft.AspNet.DataProtection.Extensions.xproj", "{BF8681DB-C28B-441F-BD92-0DCFE9537A9F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -121,6 +125,22 @@ Global
{E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}.Release|Any CPU.Build.0 = Release|Any CPU
{E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}.Release|x86.ActiveCfg = Release|Any CPU
{E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}.Release|x86.Build.0 = Release|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Debug|x86.Build.0 = Debug|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Release|Any CPU.Build.0 = Release|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Release|x86.ActiveCfg = Release|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Release|x86.Build.0 = Release|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Debug|x86.Build.0 = Debug|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|x86.ActiveCfg = Release|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -137,5 +157,7 @@ Global
{4F14BA2A-4F04-4676-8586-EC380977EE2E} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
{3277BB22-033F-4010-8131-A515B910CAAD} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
{E3552DEB-4173-43AE-BF69-3C10DFF3BAB6} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
EndGlobalSection
EndGlobal
diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/BitHelpers.cs b/src/Microsoft.AspNet.DataProtection.Extensions/BitHelpers.cs
new file mode 100644
index 0000000000..145bb900fa
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Extensions/BitHelpers.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ internal static class BitHelpers
+ {
+ ///
+ /// Reads an unsigned 64-bit integer from
+ /// starting at offset . Data is read big-endian.
+ ///
+ public static ulong ReadUInt64(byte[] buffer, int offset)
+ {
+ return (((ulong)buffer[offset + 0]) << 56)
+ | (((ulong)buffer[offset + 1]) << 48)
+ | (((ulong)buffer[offset + 2]) << 40)
+ | (((ulong)buffer[offset + 3]) << 32)
+ | (((ulong)buffer[offset + 4]) << 24)
+ | (((ulong)buffer[offset + 5]) << 16)
+ | (((ulong)buffer[offset + 6]) << 8)
+ | (ulong)buffer[offset + 7];
+ }
+
+ ///
+ /// Writes an unsigned 64-bit integer to starting at
+ /// offset . Data is written big-endian.
+ ///
+ public static void WriteUInt64(byte[] buffer, int offset, ulong value)
+ {
+ buffer[offset + 0] = (byte)(value >> 56);
+ buffer[offset + 1] = (byte)(value >> 48);
+ buffer[offset + 2] = (byte)(value >> 40);
+ buffer[offset + 3] = (byte)(value >> 32);
+ buffer[offset + 4] = (byte)(value >> 24);
+ buffer[offset + 5] = (byte)(value >> 16);
+ buffer[offset + 6] = (byte)(value >> 8);
+ buffer[offset + 7] = (byte)(value);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/DataProtectionExtensions.cs b/src/Microsoft.AspNet.DataProtection.Extensions/DataProtectionExtensions.cs
new file mode 100644
index 0000000000..250f86cf70
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Extensions/DataProtectionExtensions.cs
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ public static class DataProtectionExtensions
+ {
+ ///
+ /// Cryptographically protects a piece of plaintext data, expiring the data after
+ /// the specified amount of time has elapsed.
+ ///
+ /// The protector to use.
+ /// The plaintext data to protect.
+ /// The amount of time after which the payload should no longer be unprotectable.
+ /// The protected form of the plaintext data.
+ public static byte[] Protect([NotNull] this ITimeLimitedDataProtector protector, [NotNull] byte[] plaintext, TimeSpan lifetime)
+ {
+ return protector.Protect(plaintext, DateTimeOffset.UtcNow + lifetime);
+ }
+
+ ///
+ /// Cryptographically protects a piece of plaintext data, expiring the data at
+ /// the chosen time.
+ ///
+ /// The protector to use.
+ /// The plaintext data to protect.
+ /// The time when this payload should expire.
+ /// The protected form of the plaintext data.
+ public static string Protect([NotNull] this ITimeLimitedDataProtector protector, [NotNull] string plaintext, DateTimeOffset expiration)
+ {
+ var wrappingProtector = new TimeLimitedWrappingProtector(protector) { Expiration = expiration };
+ return wrappingProtector.Protect(plaintext);
+ }
+
+ ///
+ /// Cryptographically protects a piece of plaintext data, expiring the data after
+ /// the specified amount of time has elapsed.
+ ///
+ /// The protector to use.
+ /// The plaintext data to protect.
+ /// The amount of time after which the payload should no longer be unprotectable.
+ /// The protected form of the plaintext data.
+ public static string Protect([NotNull] this ITimeLimitedDataProtector protector, [NotNull] string plaintext, TimeSpan lifetime)
+ {
+ return Protect(protector, plaintext, DateTimeOffset.Now + lifetime);
+ }
+
+ ///
+ /// Converts an into an
+ /// so that payloads can be protected with a finite lifetime.
+ ///
+ /// The to convert to a time-limited protector.
+ /// An .
+ public static ITimeLimitedDataProtector ToTimeLimitedDataProtector([NotNull] this IDataProtector protector)
+ {
+ return (protector as ITimeLimitedDataProtector) ?? new TimeLimitedDataProtector(protector);
+ }
+
+ ///
+ /// Cryptographically unprotects a piece of protected data.
+ ///
+ /// The protector to use.
+ /// The protected data to unprotect.
+ /// An 'out' parameter which upon a successful unprotect
+ /// operation receives the expiration date of the payload.
+ /// The plaintext form of the protected data.
+ ///
+ /// Thrown if is invalid, malformed, or expired.
+ ///
+ public static string Unprotect([NotNull] this ITimeLimitedDataProtector protector, [NotNull] string protectedData, out DateTimeOffset expiration)
+ {
+ var wrappingProtector = new TimeLimitedWrappingProtector(protector);
+ string retVal = wrappingProtector.Unprotect(protectedData);
+ expiration = wrappingProtector.Expiration;
+ return retVal;
+ }
+
+ private sealed class TimeLimitedWrappingProtector : IDataProtector
+ {
+ public DateTimeOffset Expiration;
+ private readonly ITimeLimitedDataProtector _innerProtector;
+
+ public TimeLimitedWrappingProtector(ITimeLimitedDataProtector innerProtector)
+ {
+ _innerProtector = innerProtector;
+ }
+
+ public IDataProtector CreateProtector(string purpose)
+ {
+ throw new NotImplementedException();
+ }
+
+ public byte[] Protect(byte[] plaintext)
+ {
+ return _innerProtector.Protect(plaintext, Expiration);
+ }
+
+ public byte[] Unprotect(byte[] protectedData)
+ {
+ return _innerProtector.Unprotect(protectedData, out Expiration);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/ITimeLimitedDataProtector.cs b/src/Microsoft.AspNet.DataProtection.Extensions/ITimeLimitedDataProtector.cs
new file mode 100644
index 0000000000..b3b7e8d150
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Extensions/ITimeLimitedDataProtector.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// An interface that can provide data protection services where payloads have
+ /// a finite lifetime.
+ ///
+ ///
+ /// It is intended that payload lifetimes be somewhat short. Payloads protected
+ /// via this mechanism are not intended for long-term persistence (e.g., longer
+ /// than a few weeks).
+ ///
+ public interface ITimeLimitedDataProtector : IDataProtector
+ {
+ ///
+ /// Creates an given a purpose.
+ ///
+ ///
+ /// The purpose to be assigned to the newly-created .
+ ///
+ /// An tied to the provided purpose.
+ ///
+ /// The parameter must be unique for the intended use case; two
+ /// different instances created with two different
+ /// values will not be able to decipher each other's payloads. The parameter
+ /// value is not intended to be kept secret.
+ ///
+ new ITimeLimitedDataProtector CreateProtector([NotNull] string purpose);
+
+ ///
+ /// Cryptographically protects a piece of plaintext data, expiring the data at
+ /// the chosen time.
+ ///
+ /// The plaintext data to protect.
+ /// The time when this payload should expire.
+ /// The protected form of the plaintext data.
+ byte[] Protect([NotNull] byte[] plaintext, DateTimeOffset expiration);
+
+ ///
+ /// Cryptographically unprotects a piece of protected data.
+ ///
+ /// The protected data to unprotect.
+ /// An 'out' parameter which upon a successful unprotect
+ /// operation receives the expiration date of the payload.
+ /// The plaintext form of the protected data.
+ ///
+ /// Thrown if is invalid, malformed, or expired.
+ ///
+ byte[] Unprotect([NotNull] byte[] protectedData, out DateTimeOffset expiration);
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/Microsoft.AspNet.DataProtection.Extensions.xproj b/src/Microsoft.AspNet.DataProtection.Extensions/Microsoft.AspNet.DataProtection.Extensions.xproj
new file mode 100644
index 0000000000..5497c05b2f
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Extensions/Microsoft.AspNet.DataProtection.Extensions.xproj
@@ -0,0 +1,17 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ bf8681db-c28b-441f-bd92-0dcfe9537a9f
+ ..\..\artifacts\obj\$(MSBuildProjectName)
+ ..\..\artifacts\bin\$(MSBuildProjectName)\
+
+
+ 2.0
+
+
+
diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.DataProtection.Extensions/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..da10ac701d
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Extensions/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNet.DataProtection.Extensions.Test")]
diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.DataProtection.Extensions/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..76aaef653d
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Extensions/Properties/Resources.Designer.cs
@@ -0,0 +1,78 @@
+//
+namespace Microsoft.AspNet.DataProtection.Extensions
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNet.DataProtection.Extensions.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// An error occurred during a cryptographic operation.
+ ///
+ internal static string CryptCommon_GenericError
+ {
+ get { return GetString("CryptCommon_GenericError"); }
+ }
+
+ ///
+ /// An error occurred during a cryptographic operation.
+ ///
+ internal static string FormatCryptCommon_GenericError()
+ {
+ return GetString("CryptCommon_GenericError");
+ }
+
+ ///
+ /// The payload expired at {0}.
+ ///
+ internal static string TimeLimitedDataProtector_PayloadExpired
+ {
+ get { return GetString("TimeLimitedDataProtector_PayloadExpired"); }
+ }
+
+ ///
+ /// The payload expired at {0}.
+ ///
+ internal static string FormatTimeLimitedDataProtector_PayloadExpired(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TimeLimitedDataProtector_PayloadExpired"), p0);
+ }
+
+ ///
+ /// The payload is invalid.
+ ///
+ internal static string TimeLimitedDataProtector_PayloadInvalid
+ {
+ get { return GetString("TimeLimitedDataProtector_PayloadInvalid"); }
+ }
+
+ ///
+ /// The payload is invalid.
+ ///
+ internal static string FormatTimeLimitedDataProtector_PayloadInvalid()
+ {
+ return GetString("TimeLimitedDataProtector_PayloadInvalid");
+ }
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/Resources.resx b/src/Microsoft.AspNet.DataProtection.Extensions/Resources.resx
new file mode 100644
index 0000000000..b53d26e321
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Extensions/Resources.resx
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ An error occurred during a cryptographic operation.
+
+
+ The payload expired at {0}.
+
+
+ The payload is invalid.
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/TimeLimitedDataProtector.cs b/src/Microsoft.AspNet.DataProtection.Extensions/TimeLimitedDataProtector.cs
new file mode 100644
index 0000000000..3f5e5c3e9a
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Extensions/TimeLimitedDataProtector.cs
@@ -0,0 +1,115 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using System.Threading;
+using Microsoft.AspNet.DataProtection.Extensions;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// Wraps an existing and appends a purpose that allows
+ /// protecting data with a finite lifetime.
+ ///
+ internal sealed class TimeLimitedDataProtector : ITimeLimitedDataProtector
+ {
+ private const string MyPurposeString = "Microsoft.AspNet.DataProtection.TimeLimitedDataProtector.v1";
+
+ private readonly IDataProtector _innerProtector;
+ private IDataProtector _innerProtectorWithTimeLimitedPurpose; // created on-demand
+
+ public TimeLimitedDataProtector(IDataProtector innerProtector)
+ {
+ _innerProtector = innerProtector;
+ }
+
+ public ITimeLimitedDataProtector CreateProtector([NotNull] string purpose)
+ {
+ return new TimeLimitedDataProtector(_innerProtector.CreateProtector(purpose));
+ }
+
+ private IDataProtector GetInnerProtectorWithTimeLimitedPurpose()
+ {
+ // thread-safe lazy init pattern with multi-execution and single publication
+ var retVal = Volatile.Read(ref _innerProtectorWithTimeLimitedPurpose);
+ if (retVal == null)
+ {
+ var newValue = _innerProtector.CreateProtector(MyPurposeString); // we always append our purpose to the end of the chain
+ retVal = Interlocked.CompareExchange(ref _innerProtectorWithTimeLimitedPurpose, newValue, null) ?? newValue;
+ }
+ return retVal;
+ }
+
+ public byte[] Protect([NotNull] byte[] plaintext, DateTimeOffset expiration)
+ {
+ // We prepend the expiration time (as a 64-bit UTC tick count) to the unprotected data.
+ byte[] plaintextWithHeader = new byte[checked(8 + plaintext.Length)];
+ BitHelpers.WriteUInt64(plaintextWithHeader, 0, (ulong)expiration.UtcTicks);
+ Buffer.BlockCopy(plaintext, 0, plaintextWithHeader, 8, plaintext.Length);
+
+ return GetInnerProtectorWithTimeLimitedPurpose().Protect(plaintextWithHeader);
+ }
+
+ public byte[] Unprotect([NotNull] byte[] protectedData, out DateTimeOffset expiration)
+ {
+ return UnprotectCore(protectedData, DateTimeOffset.UtcNow, out expiration);
+ }
+
+ internal byte[] UnprotectCore([NotNull] byte[] protectedData, DateTimeOffset now, out DateTimeOffset expiration)
+ {
+ try
+ {
+ byte[] plaintextWithHeader = GetInnerProtectorWithTimeLimitedPurpose().Unprotect(protectedData);
+ if (plaintextWithHeader.Length < 8)
+ {
+ // header isn't present
+ throw new CryptographicException(Resources.TimeLimitedDataProtector_PayloadInvalid);
+ }
+
+ // Read expiration time back out of the payload
+ ulong utcTicksExpiration = BitHelpers.ReadUInt64(plaintextWithHeader, 0);
+ DateTimeOffset embeddedExpiration = new DateTimeOffset(checked((long)utcTicksExpiration), TimeSpan.Zero /* UTC */);
+
+ // Are we expired?
+ if (now > embeddedExpiration)
+ {
+ throw new CryptographicException(Resources.FormatTimeLimitedDataProtector_PayloadExpired(embeddedExpiration));
+ }
+
+ // Not expired - split and return payload
+ byte[] retVal = new byte[plaintextWithHeader.Length - 8];
+ Buffer.BlockCopy(plaintextWithHeader, 8, retVal, 0, retVal.Length);
+ expiration = new DateTimeOffset((long)utcTicksExpiration, TimeSpan.Zero);
+ return retVal;
+ }
+ catch (Exception ex) when (ex.RequiresHomogenization())
+ {
+ // Homogenize all failures to CryptographicException
+ throw new CryptographicException(Resources.CryptCommon_GenericError, ex);
+ }
+ }
+
+ /*
+ * EXPLICIT INTERFACE IMPLEMENTATIONS
+ */
+
+ IDataProtector IDataProtectionProvider.CreateProtector(string purpose)
+ {
+ return CreateProtector(purpose);
+ }
+
+ byte[] IDataProtector.Protect(byte[] plaintext)
+ {
+ // MaxValue essentially means 'no expiration'
+ return Protect(plaintext, DateTimeOffset.MaxValue);
+ }
+
+ byte[] IDataProtector.Unprotect(byte[] protectedData)
+ {
+ DateTimeOffset expiration; // unused
+ return Unprotect(protectedData, out expiration);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/project.json b/src/Microsoft.AspNet.DataProtection.Extensions/project.json
new file mode 100644
index 0000000000..2fd42f5a21
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Extensions/project.json
@@ -0,0 +1,18 @@
+{
+ "version": "1.0.0-*",
+ "description": "Additional APIs for ASP.NET 5 data protection.",
+ "dependencies": {
+ "Microsoft.AspNet.DataProtection": "1.0.0-*",
+ "Microsoft.AspNet.DataProtection.Shared": { "type": "build", "version": "" },
+ "Microsoft.Framework.DependencyInjection": "1.0.0-*",
+ "Microsoft.Framework.NotNullAttribute.Internal": { "type": "build", "version": "1.0.0-*" }
+ },
+ "frameworks": {
+ "net451": { },
+ "dnx451": { },
+ "dnxcore50": { }
+ },
+ "compilationOptions": {
+ "warningsAsErrors": true
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs
index 4a7312e43c..bd41d6ba9a 100644
--- a/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
+using System.Security.Cryptography;
using Microsoft.AspNet.DataProtection.Interfaces;
using Microsoft.Framework.Internal;
@@ -201,9 +202,9 @@ namespace Microsoft.AspNet.DataProtection
/// The data protector to use for this operation.
/// The protected data to unprotect.
/// The plaintext form of the protected data.
- ///
- /// This method will throw CryptographicException if the input is invalid or malformed.
- ///
+ ///
+ /// Thrown if is invalid or malformed.
+ ///
public static string Unprotect([NotNull] this IDataProtector protector, [NotNull] string protectedData)
{
try
diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionExtensions.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionExtensions.cs
deleted file mode 100644
index f2709b584f..0000000000
--- a/src/Microsoft.AspNet.DataProtection/DataProtectionExtensions.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using Microsoft.Framework.Internal;
-
-namespace Microsoft.AspNet.DataProtection
-{
- ///
- /// Helpful extension methods for data protection APIs.
- ///
- public static class DataProtectionExtensions
- {
- ///
- /// Creates a time-limited data protector based on an existing protector.
- ///
- /// The existing protector from which to derive a time-limited protector.
- /// A time-limited data protector.
- public static ITimeLimitedDataProtector AsTimeLimitedDataProtector([NotNull] this IDataProtector protector)
- {
- return (protector as ITimeLimitedDataProtector)
- ?? new TimeLimitedDataProtector(protector.CreateProtector(TimeLimitedDataProtector.PurposeString));
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/Error.cs b/src/Microsoft.AspNet.DataProtection/Error.cs
index 5d954946ee..034a61c51d 100644
--- a/src/Microsoft.AspNet.DataProtection/Error.cs
+++ b/src/Microsoft.AspNet.DataProtection/Error.cs
@@ -85,14 +85,7 @@ namespace Microsoft.AspNet.DataProtection
{
return new CryptographicException(Resources.ProtectionProvider_BadVersion);
}
-
- public static CryptographicException TimeLimitedDataProtector_PayloadExpired(ulong utcTicksExpiration)
- {
- DateTimeOffset expiration = new DateTimeOffset((long)utcTicksExpiration, TimeSpan.Zero).ToLocalTime();
- string message = String.Format(CultureInfo.CurrentCulture, Resources.TimeLimitedDataProtector_PayloadExpired, expiration);
- return new CryptographicException(message);
- }
-
+
public static InvalidOperationException XmlKeyManager_DuplicateKey(Guid keyId)
{
string message = String.Format(CultureInfo.CurrentCulture, Resources.XmlKeyManager_DuplicateKey, keyId);
diff --git a/src/Microsoft.AspNet.DataProtection/ITimeLimitedDataProtector.cs b/src/Microsoft.AspNet.DataProtection/ITimeLimitedDataProtector.cs
deleted file mode 100644
index 7e168a93bc..0000000000
--- a/src/Microsoft.AspNet.DataProtection/ITimeLimitedDataProtector.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-
-namespace Microsoft.AspNet.DataProtection
-{
- ///
- /// An interface that can provide data protection services.
- ///
- public interface ITimeLimitedDataProtector : IDataProtector
- {
- ///
- /// Creates an IDataProtector given a purpose.
- ///
- ///
- /// The purpose to be assigned to the newly-created IDataProtector.
- /// This parameter must be unique for the intended use case; two different IDataProtector
- /// instances created with two different 'purpose' strings will not be able
- /// to understand each other's payloads. The 'purpose' parameter is not intended to be
- /// kept secret.
- ///
- /// An IDataProtector tied to the provided purpose.
- new ITimeLimitedDataProtector CreateProtector(string purpose);
-
- ///
- /// Cryptographically protects a piece of plaintext data and assigns an expiration date to the data.
- ///
- /// The plaintext data to protect.
- /// The date after which the data can no longer be unprotected.
- /// The protected form of the plaintext data.
- byte[] Protect(byte[] plaintext, DateTimeOffset expiration);
-
- ///
- /// Cryptographically unprotects a piece of protected data.
- ///
- /// The protected data to unprotect.
- /// After unprotection, contains the expiration date of the protected data.
- /// The plaintext form of the protected data.
- ///
- /// Implementations should throw CryptographicException if the protected data is invalid or malformed.
- ///
- byte[] Unprotect(byte[] protectedData, out DateTimeOffset expiration);
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs
index e9ac9e8f90..2d88c5206c 100644
--- a/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs
@@ -170,22 +170,6 @@ namespace Microsoft.AspNet.DataProtection
return GetString("ProtectionProvider_BadVersion");
}
- ///
- /// The payload expired at {0}.
- ///
- internal static string TimeLimitedDataProtector_PayloadExpired
- {
- get { return GetString("TimeLimitedDataProtector_PayloadExpired"); }
- }
-
- ///
- /// The payload expired at {0}.
- ///
- internal static string FormatTimeLimitedDataProtector_PayloadExpired(object p0)
- {
- return string.Format(CultureInfo.CurrentCulture, GetString("TimeLimitedDataProtector_PayloadExpired"), p0);
- }
-
///
/// Value must be non-negative.
///
diff --git a/src/Microsoft.AspNet.DataProtection/Resources.resx b/src/Microsoft.AspNet.DataProtection/Resources.resx
index 5f368a39c0..80b564e98d 100644
--- a/src/Microsoft.AspNet.DataProtection/Resources.resx
+++ b/src/Microsoft.AspNet.DataProtection/Resources.resx
@@ -147,9 +147,6 @@
The provided payload cannot be decrypted because it was protected with a newer version of the protection provider.
-
- The payload expired at {0}.
-
Value must be non-negative.
diff --git a/src/Microsoft.AspNet.DataProtection/TimeLimitedDataProtector.cs b/src/Microsoft.AspNet.DataProtection/TimeLimitedDataProtector.cs
deleted file mode 100644
index a9033d4c25..0000000000
--- a/src/Microsoft.AspNet.DataProtection/TimeLimitedDataProtector.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using Microsoft.AspNet.Cryptography;
-using Microsoft.Framework.Internal;
-
-namespace Microsoft.AspNet.DataProtection
-{
- internal sealed class TimeLimitedDataProtector : ITimeLimitedDataProtector
- {
- internal const string PurposeString = "Microsoft.AspNet.DataProtection.TimeLimitedDataProtector";
-
- public TimeLimitedDataProtector(IDataProtector innerProtector)
- {
- InnerProtector = innerProtector;
- }
-
- internal IDataProtector InnerProtector
- {
- get;
- private set;
- }
-
- public ITimeLimitedDataProtector CreateProtector([NotNull] string purpose)
- {
- return new TimeLimitedDataProtector(InnerProtector.CreateProtector(purpose));
- }
-
- public byte[] Protect([NotNull] byte[] plaintext)
- {
- return Protect(plaintext, DateTimeOffset.MaxValue);
- }
-
- public byte[] Protect([NotNull] byte[] plaintext, DateTimeOffset expiration)
- {
- // We prepend the expiration time (as a big-endian 64-bit UTC tick count) to the unprotected data.
- ulong utcTicksExpiration = (ulong)expiration.UtcTicks;
-
- byte[] plaintextWithHeader = new byte[checked(8 + plaintext.Length)];
- plaintextWithHeader[0] = (byte)(utcTicksExpiration >> 56);
- plaintextWithHeader[1] = (byte)(utcTicksExpiration >> 48);
- plaintextWithHeader[2] = (byte)(utcTicksExpiration >> 40);
- plaintextWithHeader[3] = (byte)(utcTicksExpiration >> 32);
- plaintextWithHeader[4] = (byte)(utcTicksExpiration >> 24);
- plaintextWithHeader[5] = (byte)(utcTicksExpiration >> 16);
- plaintextWithHeader[6] = (byte)(utcTicksExpiration >> 8);
- plaintextWithHeader[7] = (byte)(utcTicksExpiration);
- Buffer.BlockCopy(plaintext, 0, plaintextWithHeader, 8, plaintext.Length);
-
- return InnerProtector.Protect(plaintextWithHeader);
- }
-
- public byte[] Unprotect([NotNull] byte[] protectedData)
- {
- DateTimeOffset unused;
- return Unprotect(protectedData, out unused);
- }
-
- public byte[] Unprotect([NotNull] byte[] protectedData, out DateTimeOffset expiration)
- {
- try
- {
- byte[] plaintextWithHeader = InnerProtector.Unprotect(protectedData);
- CryptoUtil.Assert(plaintextWithHeader.Length >= 8, "No header present.");
-
- // Read expiration time back out of the payload
- ulong utcTicksExpiration = (((ulong)plaintextWithHeader[0]) << 56)
- | (((ulong)plaintextWithHeader[1]) << 48)
- | (((ulong)plaintextWithHeader[2]) << 40)
- | (((ulong)plaintextWithHeader[3]) << 32)
- | (((ulong)plaintextWithHeader[4]) << 24)
- | (((ulong)plaintextWithHeader[5]) << 16)
- | (((ulong)plaintextWithHeader[6]) << 8)
- | (ulong)plaintextWithHeader[7];
-
- // Are we expired?
- DateTime utcNow = DateTime.UtcNow;
- if ((ulong)utcNow.Ticks > utcTicksExpiration)
- {
- throw Error.TimeLimitedDataProtector_PayloadExpired(utcTicksExpiration);
- }
-
- byte[] retVal = new byte[plaintextWithHeader.Length - 8];
- Buffer.BlockCopy(plaintextWithHeader, 8, retVal, 0, retVal.Length);
-
- expiration = new DateTimeOffset((long)utcTicksExpiration, TimeSpan.Zero);
- return retVal;
- }
- catch (Exception ex) when (ex.RequiresHomogenization())
- {
- // Homogenize all failures to CryptographicException
- throw Error.CryptCommon_GenericError(ex);
- }
- }
-
- IDataProtector IDataProtectionProvider.CreateProtector([NotNull] string purpose)
- {
- return CreateProtector(purpose);
- }
- }
-}
diff --git a/test/Microsoft.AspNet.DataProtection.Extensions.Test/DataProtectionExtensionsTests.cs b/test/Microsoft.AspNet.DataProtection.Extensions.Test/DataProtectionExtensionsTests.cs
new file mode 100644
index 0000000000..d0ef0a770a
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Extensions.Test/DataProtectionExtensionsTests.cs
@@ -0,0 +1,102 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Globalization;
+using System.Text;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ public class DataProtectionExtensionsTests
+ {
+ private const string SampleEncodedString = "AQI"; // = WebEncoders.Base64UrlEncode({ 0x01, 0x02 })
+
+ [Fact]
+ public void Protect_PayloadAsString_WithExplicitExpiration()
+ {
+ // Arrange
+ var plaintextAsBytes = Encoding.UTF8.GetBytes("this is plaintext");
+ var expiration = StringToDateTime("2015-01-01 00:00:00Z");
+ var mockDataProtector = new Mock();
+ mockDataProtector.Setup(o => o.Protect(plaintextAsBytes, expiration)).Returns(new byte[] { 0x01, 0x02 });
+
+ // Act
+ string protectedPayload = mockDataProtector.Object.Protect("this is plaintext", expiration);
+
+ // Assert
+ Assert.Equal(SampleEncodedString, protectedPayload);
+ }
+
+ [Fact]
+ public void Protect_PayloadAsString_WithLifetimeAsTimeSpan()
+ {
+ // Arrange
+ var plaintextAsBytes = Encoding.UTF8.GetBytes("this is plaintext");
+ DateTimeOffset actualExpiration = default(DateTimeOffset);
+ var mockDataProtector = new Mock();
+ mockDataProtector.Setup(o => o.Protect(plaintextAsBytes, It.IsAny()))
+ .Returns((_, exp) =>
+ {
+ actualExpiration = exp;
+ return new byte[] { 0x01, 0x02 };
+ });
+
+ // Act
+ DateTimeOffset lowerBound = DateTimeOffset.UtcNow.AddHours(48);
+ string protectedPayload = mockDataProtector.Object.Protect("this is plaintext", TimeSpan.FromHours(48));
+ DateTimeOffset upperBound = DateTimeOffset.UtcNow.AddHours(48);
+
+ // Assert
+ Assert.Equal(SampleEncodedString, protectedPayload);
+ Assert.InRange(actualExpiration, lowerBound, upperBound);
+ }
+
+ [Fact]
+ public void Protect_PayloadAsBytes_WithLifetimeAsTimeSpan()
+ {
+ // Arrange
+ DateTimeOffset actualExpiration = default(DateTimeOffset);
+ var mockDataProtector = new Mock();
+ mockDataProtector.Setup(o => o.Protect(new byte[] { 0x11, 0x22, 0x33 }, It.IsAny()))
+ .Returns((_, exp) =>
+ {
+ actualExpiration = exp;
+ return new byte[] { 0x01, 0x02 };
+ });
+
+ // Act
+ DateTimeOffset lowerBound = DateTimeOffset.UtcNow.AddHours(48);
+ byte[] protectedPayload = mockDataProtector.Object.Protect(new byte[] { 0x11, 0x22, 0x33 }, TimeSpan.FromHours(48));
+ DateTimeOffset upperBound = DateTimeOffset.UtcNow.AddHours(48);
+
+ // Assert
+ Assert.Equal(new byte[] { 0x01, 0x02 }, protectedPayload);
+ Assert.InRange(actualExpiration, lowerBound, upperBound);
+ }
+
+ [Fact]
+ public void Unprotect_PayloadAsString()
+ {
+ // Arrange
+ var futureDate = DateTimeOffset.UtcNow.AddYears(1);
+ var controlExpiration = futureDate;
+ var mockDataProtector = new Mock();
+ mockDataProtector.Setup(o => o.Unprotect(new byte[] { 0x01, 0x02 }, out controlExpiration)).Returns(Encoding.UTF8.GetBytes("this is plaintext"));
+
+ // Act
+ DateTimeOffset testExpiration;
+ string unprotectedPayload = mockDataProtector.Object.Unprotect(SampleEncodedString, out testExpiration);
+
+ // Assert
+ Assert.Equal("this is plaintext", unprotectedPayload);
+ Assert.Equal(futureDate, testExpiration);
+ }
+
+ private static DateTime StringToDateTime(string input)
+ {
+ return DateTimeOffset.ParseExact(input, "u", CultureInfo.InvariantCulture).UtcDateTime;
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Extensions.Test/Microsoft.AspNet.DataProtection.Extensions.Test.xproj b/test/Microsoft.AspNet.DataProtection.Extensions.Test/Microsoft.AspNet.DataProtection.Extensions.Test.xproj
new file mode 100644
index 0000000000..58119f15de
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Extensions.Test/Microsoft.AspNet.DataProtection.Extensions.Test.xproj
@@ -0,0 +1,17 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 04aa8e60-a053-4d50-89fe-e76c3df45200
+ ..\..\artifacts\obj\$(MSBuildProjectName)
+ ..\..\artifacts\bin\$(MSBuildProjectName)\
+
+
+ 2.0
+
+
+
diff --git a/test/Microsoft.AspNet.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs b/test/Microsoft.AspNet.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs
new file mode 100644
index 0000000000..fad95f09b9
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs
@@ -0,0 +1,178 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Globalization;
+using System.Security.Cryptography;
+using Microsoft.AspNet.DataProtection.Extensions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ public class TimeLimitedDataProtectorTests
+ {
+ private const string TimeLimitedPurposeString = "Microsoft.AspNet.DataProtection.TimeLimitedDataProtector.v1";
+
+ [Fact]
+ public void Protect_LifetimeSpecified()
+ {
+ // Arrange
+ // 0x08c1220247e44000 is the representation of midnight 2000-01-01 UTC.
+ DateTimeOffset expiration = StringToDateTime("2000-01-01 00:00:00Z");
+ var mockInnerProtector = new Mock();
+ mockInnerProtector.Setup(o => o.CreateProtector("new purpose").CreateProtector(TimeLimitedPurposeString).Protect(
+ new byte[] {
+ 0x08, 0xc1, 0x22, 0x02, 0x47, 0xe4, 0x40, 0x00, /* header */
+ 0x01, 0x02, 0x03, 0x04, 0x05 /* payload */
+ })).Returns(new byte[] { 0x10, 0x11 });
+
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act
+ var subProtector = timeLimitedProtector.CreateProtector("new purpose");
+ var protectedPayload = subProtector.Protect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }, expiration);
+
+ // Assert
+ Assert.Equal(new byte[] { 0x10, 0x11 }, protectedPayload);
+ }
+
+ [Fact]
+ public void Protect_LifetimeNotSpecified_UsesInfiniteLifetime()
+ {
+ // Arrange
+ // 0x2bca2875f4373fff is the representation of DateTimeOffset.MaxValue.
+ DateTimeOffset expiration = StringToDateTime("2000-01-01 00:00:00Z");
+ var mockInnerProtector = new Mock();
+ mockInnerProtector.Setup(o => o.CreateProtector("new purpose").CreateProtector(TimeLimitedPurposeString).Protect(
+ new byte[] {
+ 0x2b, 0xca, 0x28, 0x75, 0xf4, 0x37, 0x3f, 0xff, /* header */
+ 0x01, 0x02, 0x03, 0x04, 0x05 /* payload */
+ })).Returns(new byte[] { 0x10, 0x11 });
+
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act
+ var subProtector = timeLimitedProtector.CreateProtector("new purpose");
+ var protectedPayload = subProtector.Protect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 });
+
+ // Assert
+ Assert.Equal(new byte[] { 0x10, 0x11 }, protectedPayload);
+ }
+
+ [Fact]
+ public void Unprotect_WithinPayloadValidityPeriod_Success()
+ {
+ // Arrange
+ // 0x08c1220247e44000 is the representation of midnight 2000-01-01 UTC.
+ DateTimeOffset expectedExpiration = StringToDateTime("2000-01-01 00:00:00Z");
+ DateTimeOffset now = StringToDateTime("1999-01-01 00:00:00Z");
+ var mockInnerProtector = new Mock();
+ mockInnerProtector.Setup(o => o.CreateProtector(TimeLimitedPurposeString).Unprotect(new byte[] { 0x10, 0x11 })).Returns(
+ new byte[] {
+ 0x08, 0xc1, 0x22, 0x02, 0x47, 0xe4, 0x40, 0x00, /* header */
+ 0x01, 0x02, 0x03, 0x04, 0x05 /* payload */
+ });
+
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act
+ DateTimeOffset actualExpiration;
+ var retVal = timeLimitedProtector.UnprotectCore(new byte[] { 0x10, 0x11 }, now, out actualExpiration);
+
+ // Assert
+ Assert.Equal(expectedExpiration, actualExpiration);
+ Assert.Equal(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }, retVal);
+ }
+
+ [Fact]
+ public void Unprotect_PayloadHasExpired_Fails()
+ {
+ // Arrange
+ // 0x08c1220247e44000 is the representation of midnight 2000-01-01 UTC.
+ DateTimeOffset expectedExpiration = StringToDateTime("2000-01-01 00:00:00Z");
+ DateTimeOffset now = StringToDateTime("2001-01-01 00:00:00Z");
+ var mockInnerProtector = new Mock();
+ mockInnerProtector.Setup(o => o.CreateProtector(TimeLimitedPurposeString).Unprotect(new byte[] { 0x10, 0x11 })).Returns(
+ new byte[] {
+ 0x08, 0xc1, 0x22, 0x02, 0x47, 0xe4, 0x40, 0x00, /* header */
+ 0x01, 0x02, 0x03, 0x04, 0x05 /* payload */
+ });
+
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act & assert
+ DateTimeOffset unused;
+ var ex = Assert.Throws(() => timeLimitedProtector.UnprotectCore(new byte[] { 0x10, 0x11 }, now, out unused));
+
+ // Assert
+ Assert.Equal(Resources.FormatTimeLimitedDataProtector_PayloadExpired(expectedExpiration), ex.Message);
+ }
+
+ [Fact]
+ public void Unprotect_ProtectedDataMalformed_Fails()
+ {
+ // Arrange
+ // 0x08c1220247e44000 is the representation of midnight 2000-01-01 UTC.
+ var mockInnerProtector = new Mock();
+ mockInnerProtector.Setup(o => o.CreateProtector(TimeLimitedPurposeString).Unprotect(new byte[] { 0x10, 0x11 })).Returns(
+ new byte[] {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 /* header too short */
+ });
+
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act & assert
+ DateTimeOffset unused;
+ var ex = Assert.Throws(() => timeLimitedProtector.Unprotect(new byte[] { 0x10, 0x11 }, out unused));
+
+ // Assert
+ Assert.Equal(Resources.TimeLimitedDataProtector_PayloadInvalid, ex.Message);
+ }
+
+ [Fact]
+ public void Unprotect_UnprotectOperationFails_HomogenizesExceptionToCryptographicException()
+ {
+ // Arrange
+ // 0x08c1220247e44000 is the representation of midnight 2000-01-01 UTC.
+ var mockInnerProtector = new Mock();
+ mockInnerProtector.Setup(o => o.CreateProtector(TimeLimitedPurposeString).Unprotect(new byte[] { 0x10, 0x11 })).Throws(new Exception("How exceptional!"));
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act & assert
+ DateTimeOffset unused;
+ var ex = Assert.Throws(() => timeLimitedProtector.Unprotect(new byte[] { 0x10, 0x11 }, out unused));
+
+ // Assert
+ Assert.Equal(Resources.CryptCommon_GenericError, ex.Message);
+ Assert.Equal("How exceptional!", ex.InnerException.Message);
+ }
+
+ [Fact]
+ public void RoundTrip_ProtectedData()
+ {
+ // Arrange
+ var ephemeralProtector = new EphemeralDataProtectionProvider().CreateProtector("my purpose");
+ var timeLimitedProtector = new TimeLimitedDataProtector(ephemeralProtector);
+ var expectedExpiration = StringToDateTime("2020-01-01 00:00:00Z");
+
+ // Act
+ byte[] ephemeralProtectedPayload = ephemeralProtector.Protect(new byte[] { 0x01, 0x02, 0x03, 0x04 });
+ byte[] timeLimitedProtectedPayload = timeLimitedProtector.Protect(new byte[] { 0x11, 0x22, 0x33, 0x44 }, expectedExpiration);
+
+ // Assert
+ DateTimeOffset actualExpiration;
+ Assert.Equal(new byte[] { 0x11, 0x22, 0x33, 0x44 }, timeLimitedProtector.UnprotectCore(timeLimitedProtectedPayload, StringToDateTime("2010-01-01 00:00:00Z"), out actualExpiration));
+ Assert.Equal(expectedExpiration, actualExpiration);
+
+ // the two providers shouldn't be able to talk to one another (due to the purpose chaining)
+ Assert.Throws(() => ephemeralProtector.Unprotect(timeLimitedProtectedPayload));
+ Assert.Throws(() => timeLimitedProtector.Unprotect(ephemeralProtectedPayload, out actualExpiration));
+ }
+
+ private static DateTime StringToDateTime(string input)
+ {
+ return DateTimeOffset.ParseExact(input, "u", CultureInfo.InvariantCulture).UtcDateTime;
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Extensions.Test/project.json b/test/Microsoft.AspNet.DataProtection.Extensions.Test/project.json
new file mode 100644
index 0000000000..bcc0e2decf
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Extensions.Test/project.json
@@ -0,0 +1,18 @@
+{
+ "dependencies": {
+ "Microsoft.AspNet.DataProtection.Interfaces": "1.0.0-*",
+ "Microsoft.AspNet.DataProtection.Extensions": "1.0.0-*",
+ "Microsoft.AspNet.Testing": "1.0.0-*",
+ "Moq": "4.2.1312.1622",
+ "xunit.runner.aspnet": "2.0.0-aspnet-*"
+ },
+ "frameworks": {
+ "dnx451": { }
+ },
+ "commands": {
+ "test": "xunit.runner.aspnet"
+ },
+ "compilationOptions": {
+
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/DataProtectionExtensionsTests.cs b/test/Microsoft.AspNet.DataProtection.Test/DataProtectionExtensionsTests.cs
deleted file mode 100644
index 2b2c122265..0000000000
--- a/test/Microsoft.AspNet.DataProtection.Test/DataProtectionExtensionsTests.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using Moq;
-using Xunit;
-
-namespace Microsoft.AspNet.DataProtection
-{
- public class DataProtectionExtensionsTests
- {
- [Fact]
- public void AsTimeLimitedProtector_ProtectorIsAlreadyTimeLimited_ReturnsThis()
- {
- // Arrange
- var originalProtector = new Mock().Object;
-
- // Act
- var retVal = originalProtector.AsTimeLimitedDataProtector();
-
- // Assert
- Assert.Same(originalProtector, retVal);
- }
-
- [Fact]
- public void AsTimeLimitedProtector_ProtectorIsNotTimeLimited_CreatesNewProtector()
- {
- // Arrange
- var innerProtector = new Mock().Object;
- var outerProtectorMock = new Mock();
- outerProtectorMock.Setup(o => o.CreateProtector("Microsoft.AspNet.DataProtection.TimeLimitedDataProtector")).Returns(innerProtector);
-
- // Act
- var timeLimitedProtector = (TimeLimitedDataProtector)outerProtectorMock.Object.AsTimeLimitedDataProtector();
-
- // Assert
- Assert.Same(innerProtector, timeLimitedProtector.InnerProtector);
- }
- }
-}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/TimeLimitedDataProtectorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/TimeLimitedDataProtectorTests.cs
deleted file mode 100644
index 354078de05..0000000000
--- a/test/Microsoft.AspNet.DataProtection.Test/TimeLimitedDataProtectorTests.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Security.Cryptography;
-using Moq;
-using Xunit;
-
-namespace Microsoft.AspNet.DataProtection
-{
- public class TimeLimitedDataProtectorTests
- {
- [Fact]
- public void CreateProtector_And_Protect()
- {
- // Arrange
- // 0x08c1220247e44000 is the representation of midnight 2000-01-01 UTC.
- DateTimeOffset expiration = new DateTimeOffset(new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc));
- Mock innerProtectorMock = new Mock();
- innerProtectorMock.Setup(o => o.Protect(new byte[] { 0x08, 0xc1, 0x22, 0x02, 0x47, 0xe4, 0x40, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0x10, 0x11 });
- Mock outerProtectorMock = new Mock();
- outerProtectorMock.Setup(p => p.CreateProtector("new purpose")).Returns(innerProtectorMock.Object);
-
- // Act
- var timeLimitedProtector = new TimeLimitedDataProtector(outerProtectorMock.Object);
- var subProtector = timeLimitedProtector.CreateProtector("new purpose");
- var protectedPayload = subProtector.Protect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }, expiration);
-
- // Assert
- Assert.Equal(new byte[] { 0x10, 0x11 }, protectedPayload);
- }
-
- [Fact]
- public void ExpiredData_Fails()
- {
- // Arrange
- var timeLimitedProtector = CreateEphemeralTimeLimitedProtector();
- var expiration = DateTimeOffset.UtcNow.AddYears(-1);
-
- // Act & assert
- var protectedData = timeLimitedProtector.Protect(new byte[] { 0x04, 0x08, 0x0c }, expiration);
- Assert.Throws(() =>
- {
- timeLimitedProtector.Unprotect(protectedData);
- });
- }
-
- [Fact]
- public void GoodData_RoundTrips()
- {
- // Arrange
- var timeLimitedProtector = CreateEphemeralTimeLimitedProtector();
- var expectedExpiration = DateTimeOffset.UtcNow.AddYears(1);
-
- // Act
- var protectedData = timeLimitedProtector.Protect(new byte[] { 0x04, 0x08, 0x0c }, expectedExpiration);
- DateTimeOffset actualExpiration;
- var unprotectedData = timeLimitedProtector.Unprotect(protectedData, out actualExpiration);
-
- // Assert
- Assert.Equal(new byte[] { 0x04, 0x08, 0x0c }, unprotectedData);
- Assert.Equal(expectedExpiration, actualExpiration);
- }
-
- [Fact]
- public void Protect_NoExpiration_UsesDateTimeOffsetMaxValue()
- {
- // Should pass DateTimeOffset.MaxValue (utc ticks = 0x2bca2875f4373fff) if no expiration date specified
-
- // Arrange
- Mock innerProtectorMock = new Mock();
- innerProtectorMock.Setup(o => o.Protect(new byte[] { 0x2b, 0xca, 0x28, 0x75, 0xf4, 0x37, 0x3f, 0xff,0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0x10, 0x11 });
-
- // Act
- var timeLimitedProtector = new TimeLimitedDataProtector(innerProtectorMock.Object);
- var protectedPayload = timeLimitedProtector.Protect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 });
-
- // Assert
- Assert.Equal(new byte[] { 0x10, 0x11 }, protectedPayload);
- }
-
- private static TimeLimitedDataProtector CreateEphemeralTimeLimitedProtector()
- {
- return new TimeLimitedDataProtector(new EphemeralDataProtectionProvider().CreateProtector("purpose"));
- }
- }
-}