Add time-limiting data protection capabilities.
This commit is contained in:
parent
a0138735a8
commit
132802435b
|
|
@ -11,6 +11,17 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class DataProtectionExtensions
|
public static class DataProtectionExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a time-limited data protector based on an existing protector.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="protector">The existing protector from which to derive a time-limited protector.</param>
|
||||||
|
/// <returns>A time-limited data protector.</returns>
|
||||||
|
public static ITimeLimitedDataProtector AsTimeLimitedDataProtector([NotNull] this IDataProtector protector)
|
||||||
|
{
|
||||||
|
return (protector as ITimeLimitedDataProtector)
|
||||||
|
?? new TimeLimitedDataProtector(protector.CreateProtector(TimeLimitedDataProtector.PurposeString));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cryptographically protects a piece of plaintext data.
|
/// Cryptographically protects a piece of plaintext data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -70,5 +70,11 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
return new CryptographicException(Resources.Common_DecryptionFailed, inner);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An interface that can provide data protection services.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITimeLimitedDataProtector : IDataProtector
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an IDataProtector given a purpose.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="purposes">
|
||||||
|
/// 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.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>An IDataProtector tied to the provided purpose.</returns>
|
||||||
|
new ITimeLimitedDataProtector CreateProtector(string purpose);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cryptographically protects a piece of plaintext data and assigns an expiration date to the data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unprotectedData">The plaintext data to protect.</param>
|
||||||
|
/// <param name="expiration">The date after which the data can no longer be unprotected.</param>
|
||||||
|
/// <returns>The protected form of the plaintext data.</returns>
|
||||||
|
byte[] Protect(byte[] unprotectedData, DateTimeOffset expiration);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cryptographically unprotects a piece of protected data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="protectedData">The protected data to unprotect.</param>
|
||||||
|
/// <param name="expiration">After unprotection, contains the expiration date of the protected data.</param>
|
||||||
|
/// <returns>The plaintext form of the protected data.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Implementations should throw CryptographicException if the protected data is invalid or malformed.
|
||||||
|
/// </remarks>
|
||||||
|
byte[] Unprotect(byte[] protectedData, out DateTimeOffset expiration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -202,6 +202,22 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
return GetString("Common_PayloadProducedByNewerVersion");
|
return GetString("Common_PayloadProducedByNewerVersion");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The payload expired at {0}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string TimeLimitedDataProtector_PayloadExpired
|
||||||
|
{
|
||||||
|
get { return GetString("TimeLimitedDataProtector_PayloadExpired"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The payload expired at {0}.
|
||||||
|
/// </summary>
|
||||||
|
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)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -153,4 +153,7 @@
|
||||||
<data name="Common_PayloadProducedByNewerVersion" xml:space="preserve">
|
<data name="Common_PayloadProducedByNewerVersion" xml:space="preserve">
|
||||||
<value>The protected payload cannot be decrypted because it was protected with a newer version of the protection provider.</value>
|
<value>The protected payload cannot be decrypted because it was protected with a newer version of the protection provider.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TimeLimitedDataProtector_PayloadExpired" xml:space="preserve">
|
||||||
|
<value>The payload expired at {0}.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,34 @@ namespace Microsoft.AspNet.Security.DataProtection.Test
|
||||||
{
|
{
|
||||||
public class DataProtectionExtensionsTests
|
public class DataProtectionExtensionsTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AsTimeLimitedProtector_ProtectorIsAlreadyTimeLimited_ReturnsThis()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var originalProtector = new Mock<ITimeLimitedDataProtector>().Object;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var retVal = originalProtector.AsTimeLimitedDataProtector();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(originalProtector, retVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AsTimeLimitedProtector_ProtectorIsNotTimeLimited_CreatesNewProtector()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var innerProtector = new Mock<IDataProtector>().Object;
|
||||||
|
var outerProtectorMock = new Mock<IDataProtector>();
|
||||||
|
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]
|
[Fact]
|
||||||
public void Protect_InvalidUtf_Failure()
|
public void Protect_InvalidUtf_Failure()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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<IDataProtector> innerProtectorMock = new Mock<IDataProtector>();
|
||||||
|
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<IDataProtector> outerProtectorMock = new Mock<IDataProtector>();
|
||||||
|
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<CryptographicException>(() =>
|
||||||
|
{
|
||||||
|
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<IDataProtector> innerProtectorMock = new Mock<IDataProtector>();
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue