Add time-limiting data protection capabilities.

This commit is contained in:
Levi B 2014-10-15 12:22:01 -07:00
parent a0138735a8
commit 132802435b
8 changed files with 297 additions and 0 deletions

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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()
{

View File

@ -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"));
}
}
}