diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs
index cf7c9fa7cd..f46852c797 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs
+++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs
@@ -11,6 +11,17 @@ namespace Microsoft.AspNet.Security.DataProtection
///
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));
+ }
+
///
/// Cryptographically protects a piece of plaintext data.
///
diff --git a/src/Microsoft.AspNet.Security.DataProtection/Error.cs b/src/Microsoft.AspNet.Security.DataProtection/Error.cs
index aa75abce2c..8571a4074c 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/Error.cs
+++ b/src/Microsoft.AspNet.Security.DataProtection/Error.cs
@@ -70,5 +70,11 @@ namespace Microsoft.AspNet.Security.DataProtection
return new CryptographicException(Resources.Common_DecryptionFailed, inner);
}
+ 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);
+ }
}
}
diff --git a/src/Microsoft.AspNet.Security.DataProtection/ITimeLimitedDataProtector.cs b/src/Microsoft.AspNet.Security.DataProtection/ITimeLimitedDataProtector.cs
new file mode 100644
index 0000000000..f4770e410d
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.DataProtection/ITimeLimitedDataProtector.cs
@@ -0,0 +1,45 @@
+// 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.Security.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[] unprotectedData, 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.Security.DataProtection/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs
index 8d35437c5a..f1d2151ed5 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs
@@ -202,6 +202,22 @@ namespace Microsoft.AspNet.Security.DataProtection
return GetString("Common_PayloadProducedByNewerVersion");
}
+ ///
+ /// 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);
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Security.DataProtection/Resources.resx b/src/Microsoft.AspNet.Security.DataProtection/Resources.resx
index b03285c38d..8029969b2a 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/Resources.resx
+++ b/src/Microsoft.AspNet.Security.DataProtection/Resources.resx
@@ -153,4 +153,7 @@
The protected payload cannot be decrypted because it was protected with a newer version of the protection provider.
+
+ The payload expired at {0}.
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Security.DataProtection/TimeLimitedDataProtector.cs b/src/Microsoft.AspNet.Security.DataProtection/TimeLimitedDataProtector.cs
new file mode 100644
index 0000000000..2bcf57dadd
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.DataProtection/TimeLimitedDataProtector.cs
@@ -0,0 +1,101 @@
+// 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;
+
+namespace Microsoft.AspNet.Security.DataProtection
+{
+ internal sealed class TimeLimitedDataProtector : ITimeLimitedDataProtector
+ {
+ internal const string PurposeString = "Microsoft.AspNet.Security.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[] unprotectedData)
+ {
+ return Protect(unprotectedData, DateTimeOffset.MaxValue);
+ }
+
+ public byte[] Protect([NotNull] byte[] unprotectedData, 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[] unprotectedDataWithHeader = new byte[checked(8 + unprotectedData.Length)];
+ unprotectedDataWithHeader[0] = (byte)(utcTicksExpiration >> 56);
+ unprotectedDataWithHeader[1] = (byte)(utcTicksExpiration >> 48);
+ unprotectedDataWithHeader[2] = (byte)(utcTicksExpiration >> 40);
+ unprotectedDataWithHeader[3] = (byte)(utcTicksExpiration >> 32);
+ unprotectedDataWithHeader[4] = (byte)(utcTicksExpiration >> 24);
+ unprotectedDataWithHeader[5] = (byte)(utcTicksExpiration >> 16);
+ unprotectedDataWithHeader[6] = (byte)(utcTicksExpiration >> 8);
+ unprotectedDataWithHeader[7] = (byte)(utcTicksExpiration);
+ Buffer.BlockCopy(unprotectedData, 0, unprotectedDataWithHeader, 8, unprotectedData.Length);
+
+ return InnerProtector.Protect(unprotectedDataWithHeader);
+ }
+
+ public byte[] Unprotect([NotNull] byte[] protectedData)
+ {
+ DateTimeOffset unused;
+ return Unprotect(protectedData, out unused);
+ }
+
+ public byte[] Unprotect([NotNull] byte[] protectedData, out DateTimeOffset expiration)
+ {
+ try
+ {
+ byte[] unprotectedDataWithHeader = InnerProtector.Unprotect(protectedData);
+ CryptoUtil.Assert(unprotectedDataWithHeader.Length >= 8, "No header present.");
+
+ // Read expiration time back out of the payload
+ ulong utcTicksExpiration = (((ulong)unprotectedDataWithHeader[0]) << 56)
+ | (((ulong)unprotectedDataWithHeader[1]) << 48)
+ | (((ulong)unprotectedDataWithHeader[2]) << 40)
+ | (((ulong)unprotectedDataWithHeader[3]) << 32)
+ | (((ulong)unprotectedDataWithHeader[4]) << 24)
+ | (((ulong)unprotectedDataWithHeader[5]) << 16)
+ | (((ulong)unprotectedDataWithHeader[6]) << 8)
+ | (ulong)unprotectedDataWithHeader[7];
+
+ // Are we expired?
+ DateTime utcNow = DateTime.UtcNow;
+ if ((ulong)utcNow.Ticks > utcTicksExpiration)
+ {
+ throw Error.TimeLimitedDataProtector_PayloadExpired(utcTicksExpiration);
+ }
+
+ byte[] retVal = new byte[unprotectedDataWithHeader.Length - 8];
+ Buffer.BlockCopy(unprotectedDataWithHeader, 8, retVal, 0, retVal.Length);
+
+ expiration = new DateTimeOffset((long)utcTicksExpiration, TimeSpan.Zero);
+ return retVal;
+ }
+ catch (Exception ex) if (!(ex is CryptographicException))
+ {
+ // 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.Security.DataProtection.Test/DataProtectionExtensionsTests.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/DataProtectionExtensionsTests.cs
index 53fc15656c..6993f4ab9b 100644
--- a/test/Microsoft.AspNet.Security.DataProtection.Test/DataProtectionExtensionsTests.cs
+++ b/test/Microsoft.AspNet.Security.DataProtection.Test/DataProtectionExtensionsTests.cs
@@ -11,6 +11,34 @@ namespace Microsoft.AspNet.Security.DataProtection.Test
{
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.Security.DataProtection.TimeLimitedDataProtector")).Returns(innerProtector);
+
+ // Act
+ var timeLimitedProtector = (TimeLimitedDataProtector)outerProtectorMock.Object.AsTimeLimitedDataProtector();
+
+ // Assert
+ Assert.Same(innerProtector, timeLimitedProtector.InnerProtector);
+ }
+
[Fact]
public void Protect_InvalidUtf_Failure()
{
diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/TimeLimitedDataProtectorTests.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/TimeLimitedDataProtectorTests.cs
new file mode 100644
index 0000000000..671eb65aec
--- /dev/null
+++ b/test/Microsoft.AspNet.Security.DataProtection.Test/TimeLimitedDataProtectorTests.cs
@@ -0,0 +1,87 @@
+// 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.Security.DataProtection.Test
+{
+ 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"));
+ }
+ }
+}