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>
|
||||
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>
|
||||
/// Cryptographically protects a piece of plaintext data.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -153,4 +153,7 @@
|
|||
<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>
|
||||
</data>
|
||||
<data name="TimeLimitedDataProtector_PayloadExpired" xml:space="preserve">
|
||||
<value>The payload expired at {0}.</value>
|
||||
</data>
|
||||
</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
|
||||
{
|
||||
[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]
|
||||
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