Move time-limited data protector to Extensions project

This commit is contained in:
Levi B 2015-03-17 10:49:15 -07:00
parent 94233e76ff
commit 84490846b6
23 changed files with 913 additions and 329 deletions

View File

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

View File

@ -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
{
/// <summary>
/// Reads an unsigned 64-bit integer from <paramref name="buffer"/>
/// starting at offset <paramref name="offset"/>. Data is read big-endian.
/// </summary>
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];
}
/// <summary>
/// Writes an unsigned 64-bit integer to <paramref name="buffer"/> starting at
/// offset <paramref name="offset"/>. Data is written big-endian.
/// </summary>
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);
}
}
}

View File

@ -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
{
/// <summary>
/// Cryptographically protects a piece of plaintext data, expiring the data after
/// the specified amount of time has elapsed.
/// </summary>
/// <param name="protector">The protector to use.</param>
/// <param name="plaintext">The plaintext data to protect.</param>
/// <param name="lifetime">The amount of time after which the payload should no longer be unprotectable.</param>
/// <returns>The protected form of the plaintext data.</returns>
public static byte[] Protect([NotNull] this ITimeLimitedDataProtector protector, [NotNull] byte[] plaintext, TimeSpan lifetime)
{
return protector.Protect(plaintext, DateTimeOffset.UtcNow + lifetime);
}
/// <summary>
/// Cryptographically protects a piece of plaintext data, expiring the data at
/// the chosen time.
/// </summary>
/// <param name="protector">The protector to use.</param>
/// <param name="plaintext">The plaintext data to protect.</param>
/// <param name="expiration">The time when this payload should expire.</param>
/// <returns>The protected form of the plaintext data.</returns>
public static string Protect([NotNull] this ITimeLimitedDataProtector protector, [NotNull] string plaintext, DateTimeOffset expiration)
{
var wrappingProtector = new TimeLimitedWrappingProtector(protector) { Expiration = expiration };
return wrappingProtector.Protect(plaintext);
}
/// <summary>
/// Cryptographically protects a piece of plaintext data, expiring the data after
/// the specified amount of time has elapsed.
/// </summary>
/// <param name="protector">The protector to use.</param>
/// <param name="plaintext">The plaintext data to protect.</param>
/// <param name="lifetime">The amount of time after which the payload should no longer be unprotectable.</param>
/// <returns>The protected form of the plaintext data.</returns>
public static string Protect([NotNull] this ITimeLimitedDataProtector protector, [NotNull] string plaintext, TimeSpan lifetime)
{
return Protect(protector, plaintext, DateTimeOffset.Now + lifetime);
}
/// <summary>
/// Converts an <see cref="IDataProtector"/> into an <see cref="ITimeLimitedDataProtector"/>
/// so that payloads can be protected with a finite lifetime.
/// </summary>
/// <param name="protector">The <see cref="IDataProtector"/> to convert to a time-limited protector.</param>
/// <returns>An <see cref="ITimeLimitedDataProtector"/>.</returns>
public static ITimeLimitedDataProtector ToTimeLimitedDataProtector([NotNull] this IDataProtector protector)
{
return (protector as ITimeLimitedDataProtector) ?? new TimeLimitedDataProtector(protector);
}
/// <summary>
/// Cryptographically unprotects a piece of protected data.
/// </summary>
/// <param name="protector">The protector to use.</param>
/// <param name="protectedData">The protected data to unprotect.</param>
/// <param name="expiration">An 'out' parameter which upon a successful unprotect
/// operation receives the expiration date of the payload.</param>
/// <returns>The plaintext form of the protected data.</returns>
/// <exception cref="CryptographicException">
/// Thrown if <paramref name="protectedData"/> is invalid, malformed, or expired.
/// </exception>
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);
}
}
}
}

View File

@ -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
{
/// <summary>
/// An interface that can provide data protection services where payloads have
/// a finite lifetime.
/// </summary>
/// <remarks>
/// 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).
/// </remarks>
public interface ITimeLimitedDataProtector : IDataProtector
{
/// <summary>
/// Creates an <see cref="ITimeLimitedDataProtector"/> given a purpose.
/// </summary>
/// <param name="purposes">
/// The purpose to be assigned to the newly-created <see cref="ITimeLimitedDataProtector"/>.
/// </param>
/// <returns>An <see cref="ITimeLimitedDataProtector"/> tied to the provided purpose.</returns>
/// <remarks>
/// The <paramref name="purpose"/> parameter must be unique for the intended use case; two
/// different <see cref="ITimeLimitedDataProtector"/> instances created with two different <paramref name="purpose"/>
/// values will not be able to decipher each other's payloads. The <paramref name="purpose"/> parameter
/// value is not intended to be kept secret.
/// </remarks>
new ITimeLimitedDataProtector CreateProtector([NotNull] string purpose);
/// <summary>
/// Cryptographically protects a piece of plaintext data, expiring the data at
/// the chosen time.
/// </summary>
/// <param name="plaintext">The plaintext data to protect.</param>
/// <param name="expiration">The time when this payload should expire.</param>
/// <returns>The protected form of the plaintext data.</returns>
byte[] Protect([NotNull] byte[] plaintext, DateTimeOffset expiration);
/// <summary>
/// Cryptographically unprotects a piece of protected data.
/// </summary>
/// <param name="protectedData">The protected data to unprotect.</param>
/// <param name="expiration">An 'out' parameter which upon a successful unprotect
/// operation receives the expiration date of the payload.</param>
/// <returns>The plaintext form of the protected data.</returns>
/// <exception cref="CryptographicException">
/// Thrown if <paramref name="protectedData"/> is invalid, malformed, or expired.
/// </exception>
byte[] Unprotect([NotNull] byte[] protectedData, out DateTimeOffset expiration);
}
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>bf8681db-c28b-441f-bd92-0dcfe9537a9f</ProjectGuid>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -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")]

View File

@ -0,0 +1,78 @@
// <auto-generated />
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);
/// <summary>
/// An error occurred during a cryptographic operation.
/// </summary>
internal static string CryptCommon_GenericError
{
get { return GetString("CryptCommon_GenericError"); }
}
/// <summary>
/// An error occurred during a cryptographic operation.
/// </summary>
internal static string FormatCryptCommon_GenericError()
{
return GetString("CryptCommon_GenericError");
}
/// <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);
}
/// <summary>
/// The payload is invalid.
/// </summary>
internal static string TimeLimitedDataProtector_PayloadInvalid
{
get { return GetString("TimeLimitedDataProtector_PayloadInvalid"); }
}
/// <summary>
/// The payload is invalid.
/// </summary>
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;
}
}
}

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CryptCommon_GenericError" xml:space="preserve">
<value>An error occurred during a cryptographic operation.</value>
</data>
<data name="TimeLimitedDataProtector_PayloadExpired" xml:space="preserve">
<value>The payload expired at {0}.</value>
</data>
<data name="TimeLimitedDataProtector_PayloadInvalid" xml:space="preserve">
<value>The payload is invalid.</value>
</data>
</root>

View File

@ -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
{
/// <summary>
/// Wraps an existing <see cref="IDataProtector"/> and appends a purpose that allows
/// protecting data with a finite lifetime.
/// </summary>
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);
}
}
}

View File

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

View File

@ -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
/// <param name="protector">The data protector to use for this operation.</param>
/// <param name="protectedData">The protected data to unprotect.</param>
/// <returns>The plaintext form of the protected data.</returns>
/// <remarks>
/// This method will throw CryptographicException if the input is invalid or malformed.
/// </remarks>
/// <exception cref="CryptographicException">
/// Thrown if <paramref name="protectedData"/> is invalid or malformed.
/// </exception>
public static string Unprotect([NotNull] this IDataProtector protector, [NotNull] string protectedData)
{
try

View File

@ -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
{
/// <summary>
/// Helpful extension methods for data protection APIs.
/// </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));
}
}
}

View File

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

View File

@ -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
{
/// <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="plaintext">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[] plaintext, 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

@ -170,22 +170,6 @@ namespace Microsoft.AspNet.DataProtection
return GetString("ProtectionProvider_BadVersion");
}
/// <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);
}
/// <summary>
/// Value must be non-negative.
/// </summary>

View File

@ -147,9 +147,6 @@
<data name="ProtectionProvider_BadVersion" xml:space="preserve">
<value>The provided 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>
<data name="Common_ValueMustBeNonNegative" xml:space="preserve">
<value>Value must be non-negative.</value>
</data>

View File

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

View File

@ -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<ITimeLimitedDataProtector>();
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<ITimeLimitedDataProtector>();
mockDataProtector.Setup(o => o.Protect(plaintextAsBytes, It.IsAny<DateTimeOffset>()))
.Returns<byte[], DateTimeOffset>((_, 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<ITimeLimitedDataProtector>();
mockDataProtector.Setup(o => o.Protect(new byte[] { 0x11, 0x22, 0x33 }, It.IsAny<DateTimeOffset>()))
.Returns<byte[], DateTimeOffset>((_, 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<ITimeLimitedDataProtector>();
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;
}
}
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>04aa8e60-a053-4d50-89fe-e76c3df45200</ProjectGuid>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -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<IDataProtector>();
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<IDataProtector>();
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<IDataProtector>();
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<IDataProtector>();
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<CryptographicException>(() => 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<IDataProtector>();
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<CryptographicException>(() => 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<IDataProtector>();
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<CryptographicException>(() => 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<CryptographicException>(() => ephemeralProtector.Unprotect(timeLimitedProtectedPayload));
Assert.Throws<CryptographicException>(() => timeLimitedProtector.Unprotect(ephemeralProtectedPayload, out actualExpiration));
}
private static DateTime StringToDateTime(string input)
{
return DateTimeOffset.ParseExact(input, "u", CultureInfo.InvariantCulture).UtcDateTime;
}
}
}

View File

@ -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": {
}
}

View File

@ -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<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.DataProtection.TimeLimitedDataProtector")).Returns(innerProtector);
// Act
var timeLimitedProtector = (TimeLimitedDataProtector)outerProtectorMock.Object.AsTimeLimitedDataProtector();
// Assert
Assert.Same(innerProtector, timeLimitedProtector.InnerProtector);
}
}
}

View File

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