From 84490846b658830ee46ba8adf97d814b71daf21c Mon Sep 17 00:00:00 2001 From: Levi B Date: Tue, 17 Mar 2015 10:49:15 -0700 Subject: [PATCH] Move time-limited data protector to Extensions project --- DataProtection.sln | 22 +++ .../BitHelpers.cs | 42 +++++ .../DataProtectionExtensions.cs | 108 +++++++++++ .../ITimeLimitedDataProtector.cs | 57 ++++++ ...oft.AspNet.DataProtection.Extensions.xproj | 17 ++ .../Properties/AssemblyInfo.cs | 7 + .../Properties/Resources.Designer.cs | 78 ++++++++ .../Resources.resx | 129 +++++++++++++ .../TimeLimitedDataProtector.cs | 115 +++++++++++ .../project.json | 18 ++ .../DataProtectionExtensions.cs | 7 +- .../DataProtectionExtensions.cs | 25 --- src/Microsoft.AspNet.DataProtection/Error.cs | 9 +- .../ITimeLimitedDataProtector.cs | 45 ----- .../Properties/Resources.Designer.cs | 16 -- .../Resources.resx | 3 - .../TimeLimitedDataProtector.cs | 102 ---------- .../DataProtectionExtensionsTests.cs | 102 ++++++++++ ...spNet.DataProtection.Extensions.Test.xproj | 17 ++ .../TimeLimitedDataProtectorTests.cs | 178 ++++++++++++++++++ .../project.json | 18 ++ .../DataProtectionExtensionsTests.cs | 40 ---- .../TimeLimitedDataProtectorTests.cs | 87 --------- 23 files changed, 913 insertions(+), 329 deletions(-) create mode 100644 src/Microsoft.AspNet.DataProtection.Extensions/BitHelpers.cs create mode 100644 src/Microsoft.AspNet.DataProtection.Extensions/DataProtectionExtensions.cs create mode 100644 src/Microsoft.AspNet.DataProtection.Extensions/ITimeLimitedDataProtector.cs create mode 100644 src/Microsoft.AspNet.DataProtection.Extensions/Microsoft.AspNet.DataProtection.Extensions.xproj create mode 100644 src/Microsoft.AspNet.DataProtection.Extensions/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.DataProtection.Extensions/Properties/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.DataProtection.Extensions/Resources.resx create mode 100644 src/Microsoft.AspNet.DataProtection.Extensions/TimeLimitedDataProtector.cs create mode 100644 src/Microsoft.AspNet.DataProtection.Extensions/project.json delete mode 100644 src/Microsoft.AspNet.DataProtection/DataProtectionExtensions.cs delete mode 100644 src/Microsoft.AspNet.DataProtection/ITimeLimitedDataProtector.cs delete mode 100644 src/Microsoft.AspNet.DataProtection/TimeLimitedDataProtector.cs create mode 100644 test/Microsoft.AspNet.DataProtection.Extensions.Test/DataProtectionExtensionsTests.cs create mode 100644 test/Microsoft.AspNet.DataProtection.Extensions.Test/Microsoft.AspNet.DataProtection.Extensions.Test.xproj create mode 100644 test/Microsoft.AspNet.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs create mode 100644 test/Microsoft.AspNet.DataProtection.Extensions.Test/project.json delete mode 100644 test/Microsoft.AspNet.DataProtection.Test/DataProtectionExtensionsTests.cs delete mode 100644 test/Microsoft.AspNet.DataProtection.Test/TimeLimitedDataProtectorTests.cs diff --git a/DataProtection.sln b/DataProtection.sln index c0e088b8b7..cf081e45be 100644 --- a/DataProtection.sln +++ b/DataProtection.sln @@ -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 diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/BitHelpers.cs b/src/Microsoft.AspNet.DataProtection.Extensions/BitHelpers.cs new file mode 100644 index 0000000000..145bb900fa --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection.Extensions/BitHelpers.cs @@ -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 + { + /// + /// Reads an unsigned 64-bit integer from + /// starting at offset . Data is read big-endian. + /// + 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]; + } + + /// + /// Writes an unsigned 64-bit integer to starting at + /// offset . Data is written big-endian. + /// + 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); + } + } +} diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/DataProtectionExtensions.cs b/src/Microsoft.AspNet.DataProtection.Extensions/DataProtectionExtensions.cs new file mode 100644 index 0000000000..250f86cf70 --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection.Extensions/DataProtectionExtensions.cs @@ -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 + { + /// + /// Cryptographically protects a piece of plaintext data, expiring the data after + /// the specified amount of time has elapsed. + /// + /// The protector to use. + /// The plaintext data to protect. + /// The amount of time after which the payload should no longer be unprotectable. + /// The protected form of the plaintext data. + public static byte[] Protect([NotNull] this ITimeLimitedDataProtector protector, [NotNull] byte[] plaintext, TimeSpan lifetime) + { + return protector.Protect(plaintext, DateTimeOffset.UtcNow + lifetime); + } + + /// + /// Cryptographically protects a piece of plaintext data, expiring the data at + /// the chosen time. + /// + /// The protector to use. + /// The plaintext data to protect. + /// The time when this payload should expire. + /// The protected form of the plaintext data. + public static string Protect([NotNull] this ITimeLimitedDataProtector protector, [NotNull] string plaintext, DateTimeOffset expiration) + { + var wrappingProtector = new TimeLimitedWrappingProtector(protector) { Expiration = expiration }; + return wrappingProtector.Protect(plaintext); + } + + /// + /// Cryptographically protects a piece of plaintext data, expiring the data after + /// the specified amount of time has elapsed. + /// + /// The protector to use. + /// The plaintext data to protect. + /// The amount of time after which the payload should no longer be unprotectable. + /// The protected form of the plaintext data. + public static string Protect([NotNull] this ITimeLimitedDataProtector protector, [NotNull] string plaintext, TimeSpan lifetime) + { + return Protect(protector, plaintext, DateTimeOffset.Now + lifetime); + } + + /// + /// Converts an into an + /// so that payloads can be protected with a finite lifetime. + /// + /// The to convert to a time-limited protector. + /// An . + public static ITimeLimitedDataProtector ToTimeLimitedDataProtector([NotNull] this IDataProtector protector) + { + return (protector as ITimeLimitedDataProtector) ?? new TimeLimitedDataProtector(protector); + } + + /// + /// Cryptographically unprotects a piece of protected data. + /// + /// The protector to use. + /// The protected data to unprotect. + /// An 'out' parameter which upon a successful unprotect + /// operation receives the expiration date of the payload. + /// The plaintext form of the protected data. + /// + /// Thrown if is invalid, malformed, or expired. + /// + 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); + } + } + } +} diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/ITimeLimitedDataProtector.cs b/src/Microsoft.AspNet.DataProtection.Extensions/ITimeLimitedDataProtector.cs new file mode 100644 index 0000000000..b3b7e8d150 --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection.Extensions/ITimeLimitedDataProtector.cs @@ -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 +{ + /// + /// An interface that can provide data protection services where payloads have + /// a finite lifetime. + /// + /// + /// 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). + /// + public interface ITimeLimitedDataProtector : IDataProtector + { + /// + /// Creates an given a purpose. + /// + /// + /// The purpose to be assigned to the newly-created . + /// + /// An tied to the provided purpose. + /// + /// The parameter must be unique for the intended use case; two + /// different instances created with two different + /// values will not be able to decipher each other's payloads. The parameter + /// value is not intended to be kept secret. + /// + new ITimeLimitedDataProtector CreateProtector([NotNull] string purpose); + + /// + /// Cryptographically protects a piece of plaintext data, expiring the data at + /// the chosen time. + /// + /// The plaintext data to protect. + /// The time when this payload should expire. + /// The protected form of the plaintext data. + byte[] Protect([NotNull] byte[] plaintext, DateTimeOffset expiration); + + /// + /// Cryptographically unprotects a piece of protected data. + /// + /// The protected data to unprotect. + /// An 'out' parameter which upon a successful unprotect + /// operation receives the expiration date of the payload. + /// The plaintext form of the protected data. + /// + /// Thrown if is invalid, malformed, or expired. + /// + byte[] Unprotect([NotNull] byte[] protectedData, out DateTimeOffset expiration); + } +} diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/Microsoft.AspNet.DataProtection.Extensions.xproj b/src/Microsoft.AspNet.DataProtection.Extensions/Microsoft.AspNet.DataProtection.Extensions.xproj new file mode 100644 index 0000000000..5497c05b2f --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection.Extensions/Microsoft.AspNet.DataProtection.Extensions.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + bf8681db-c28b-441f-bd92-0dcfe9537a9f + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.DataProtection.Extensions/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..da10ac701d --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection.Extensions/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.DataProtection.Extensions/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..76aaef653d --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection.Extensions/Properties/Resources.Designer.cs @@ -0,0 +1,78 @@ +// +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); + + /// + /// An error occurred during a cryptographic operation. + /// + internal static string CryptCommon_GenericError + { + get { return GetString("CryptCommon_GenericError"); } + } + + /// + /// An error occurred during a cryptographic operation. + /// + internal static string FormatCryptCommon_GenericError() + { + return GetString("CryptCommon_GenericError"); + } + + /// + /// The payload expired at {0}. + /// + internal static string TimeLimitedDataProtector_PayloadExpired + { + get { return GetString("TimeLimitedDataProtector_PayloadExpired"); } + } + + /// + /// The payload expired at {0}. + /// + internal static string FormatTimeLimitedDataProtector_PayloadExpired(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TimeLimitedDataProtector_PayloadExpired"), p0); + } + + /// + /// The payload is invalid. + /// + internal static string TimeLimitedDataProtector_PayloadInvalid + { + get { return GetString("TimeLimitedDataProtector_PayloadInvalid"); } + } + + /// + /// The payload is invalid. + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/Resources.resx b/src/Microsoft.AspNet.DataProtection.Extensions/Resources.resx new file mode 100644 index 0000000000..b53d26e321 --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection.Extensions/Resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An error occurred during a cryptographic operation. + + + The payload expired at {0}. + + + The payload is invalid. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/TimeLimitedDataProtector.cs b/src/Microsoft.AspNet.DataProtection.Extensions/TimeLimitedDataProtector.cs new file mode 100644 index 0000000000..3f5e5c3e9a --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection.Extensions/TimeLimitedDataProtector.cs @@ -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 +{ + /// + /// Wraps an existing and appends a purpose that allows + /// protecting data with a finite lifetime. + /// + 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); + } + } +} diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/project.json b/src/Microsoft.AspNet.DataProtection.Extensions/project.json new file mode 100644 index 0000000000..2fd42f5a21 --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection.Extensions/project.json @@ -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 + } +} diff --git a/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs index 4a7312e43c..bd41d6ba9a 100644 --- a/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs +++ b/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs @@ -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 /// The data protector to use for this operation. /// The protected data to unprotect. /// The plaintext form of the protected data. - /// - /// This method will throw CryptographicException if the input is invalid or malformed. - /// + /// + /// Thrown if is invalid or malformed. + /// public static string Unprotect([NotNull] this IDataProtector protector, [NotNull] string protectedData) { try diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionExtensions.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionExtensions.cs deleted file mode 100644 index f2709b584f..0000000000 --- a/src/Microsoft.AspNet.DataProtection/DataProtectionExtensions.cs +++ /dev/null @@ -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 -{ - /// - /// Helpful extension methods for data protection APIs. - /// - public static class DataProtectionExtensions - { - /// - /// Creates a time-limited data protector based on an existing protector. - /// - /// The existing protector from which to derive a time-limited protector. - /// A time-limited data protector. - public static ITimeLimitedDataProtector AsTimeLimitedDataProtector([NotNull] this IDataProtector protector) - { - return (protector as ITimeLimitedDataProtector) - ?? new TimeLimitedDataProtector(protector.CreateProtector(TimeLimitedDataProtector.PurposeString)); - } - } -} diff --git a/src/Microsoft.AspNet.DataProtection/Error.cs b/src/Microsoft.AspNet.DataProtection/Error.cs index 5d954946ee..034a61c51d 100644 --- a/src/Microsoft.AspNet.DataProtection/Error.cs +++ b/src/Microsoft.AspNet.DataProtection/Error.cs @@ -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); diff --git a/src/Microsoft.AspNet.DataProtection/ITimeLimitedDataProtector.cs b/src/Microsoft.AspNet.DataProtection/ITimeLimitedDataProtector.cs deleted file mode 100644 index 7e168a93bc..0000000000 --- a/src/Microsoft.AspNet.DataProtection/ITimeLimitedDataProtector.cs +++ /dev/null @@ -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 -{ - /// - /// An interface that can provide data protection services. - /// - public interface ITimeLimitedDataProtector : IDataProtector - { - /// - /// Creates an IDataProtector given a purpose. - /// - /// - /// The purpose to be assigned to the newly-created IDataProtector. - /// This parameter must be unique for the intended use case; two different IDataProtector - /// instances created with two different 'purpose' strings will not be able - /// to understand each other's payloads. The 'purpose' parameter is not intended to be - /// kept secret. - /// - /// An IDataProtector tied to the provided purpose. - new ITimeLimitedDataProtector CreateProtector(string purpose); - - /// - /// Cryptographically protects a piece of plaintext data and assigns an expiration date to the data. - /// - /// The plaintext data to protect. - /// The date after which the data can no longer be unprotected. - /// The protected form of the plaintext data. - byte[] Protect(byte[] plaintext, DateTimeOffset expiration); - - /// - /// Cryptographically unprotects a piece of protected data. - /// - /// The protected data to unprotect. - /// After unprotection, contains the expiration date of the protected data. - /// The plaintext form of the protected data. - /// - /// Implementations should throw CryptographicException if the protected data is invalid or malformed. - /// - byte[] Unprotect(byte[] protectedData, out DateTimeOffset expiration); - } -} diff --git a/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs index e9ac9e8f90..2d88c5206c 100644 --- a/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs @@ -170,22 +170,6 @@ namespace Microsoft.AspNet.DataProtection return GetString("ProtectionProvider_BadVersion"); } - /// - /// The payload expired at {0}. - /// - internal static string TimeLimitedDataProtector_PayloadExpired - { - get { return GetString("TimeLimitedDataProtector_PayloadExpired"); } - } - - /// - /// The payload expired at {0}. - /// - internal static string FormatTimeLimitedDataProtector_PayloadExpired(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("TimeLimitedDataProtector_PayloadExpired"), p0); - } - /// /// Value must be non-negative. /// diff --git a/src/Microsoft.AspNet.DataProtection/Resources.resx b/src/Microsoft.AspNet.DataProtection/Resources.resx index 5f368a39c0..80b564e98d 100644 --- a/src/Microsoft.AspNet.DataProtection/Resources.resx +++ b/src/Microsoft.AspNet.DataProtection/Resources.resx @@ -147,9 +147,6 @@ The provided payload cannot be decrypted because it was protected with a newer version of the protection provider. - - The payload expired at {0}. - Value must be non-negative. diff --git a/src/Microsoft.AspNet.DataProtection/TimeLimitedDataProtector.cs b/src/Microsoft.AspNet.DataProtection/TimeLimitedDataProtector.cs deleted file mode 100644 index a9033d4c25..0000000000 --- a/src/Microsoft.AspNet.DataProtection/TimeLimitedDataProtector.cs +++ /dev/null @@ -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); - } - } -} diff --git a/test/Microsoft.AspNet.DataProtection.Extensions.Test/DataProtectionExtensionsTests.cs b/test/Microsoft.AspNet.DataProtection.Extensions.Test/DataProtectionExtensionsTests.cs new file mode 100644 index 0000000000..d0ef0a770a --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Extensions.Test/DataProtectionExtensionsTests.cs @@ -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(); + 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(); + mockDataProtector.Setup(o => o.Protect(plaintextAsBytes, It.IsAny())) + .Returns((_, 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(); + mockDataProtector.Setup(o => o.Protect(new byte[] { 0x11, 0x22, 0x33 }, It.IsAny())) + .Returns((_, 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(); + 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; + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Extensions.Test/Microsoft.AspNet.DataProtection.Extensions.Test.xproj b/test/Microsoft.AspNet.DataProtection.Extensions.Test/Microsoft.AspNet.DataProtection.Extensions.Test.xproj new file mode 100644 index 0000000000..58119f15de --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Extensions.Test/Microsoft.AspNet.DataProtection.Extensions.Test.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 04aa8e60-a053-4d50-89fe-e76c3df45200 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + diff --git a/test/Microsoft.AspNet.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs b/test/Microsoft.AspNet.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs new file mode 100644 index 0000000000..fad95f09b9 --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs @@ -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(); + 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(); + 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(); + 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(); + 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(() => 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(); + 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(() => 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(); + 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(() => 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(() => ephemeralProtector.Unprotect(timeLimitedProtectedPayload)); + Assert.Throws(() => timeLimitedProtector.Unprotect(ephemeralProtectedPayload, out actualExpiration)); + } + + private static DateTime StringToDateTime(string input) + { + return DateTimeOffset.ParseExact(input, "u", CultureInfo.InvariantCulture).UtcDateTime; + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Extensions.Test/project.json b/test/Microsoft.AspNet.DataProtection.Extensions.Test/project.json new file mode 100644 index 0000000000..bcc0e2decf --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Extensions.Test/project.json @@ -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": { + + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/DataProtectionExtensionsTests.cs b/test/Microsoft.AspNet.DataProtection.Test/DataProtectionExtensionsTests.cs deleted file mode 100644 index 2b2c122265..0000000000 --- a/test/Microsoft.AspNet.DataProtection.Test/DataProtectionExtensionsTests.cs +++ /dev/null @@ -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().Object; - - // Act - var retVal = originalProtector.AsTimeLimitedDataProtector(); - - // Assert - Assert.Same(originalProtector, retVal); - } - - [Fact] - public void AsTimeLimitedProtector_ProtectorIsNotTimeLimited_CreatesNewProtector() - { - // Arrange - var innerProtector = new Mock().Object; - var outerProtectorMock = new Mock(); - outerProtectorMock.Setup(o => o.CreateProtector("Microsoft.AspNet.DataProtection.TimeLimitedDataProtector")).Returns(innerProtector); - - // Act - var timeLimitedProtector = (TimeLimitedDataProtector)outerProtectorMock.Object.AsTimeLimitedDataProtector(); - - // Assert - Assert.Same(innerProtector, timeLimitedProtector.InnerProtector); - } - } -} diff --git a/test/Microsoft.AspNet.DataProtection.Test/TimeLimitedDataProtectorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/TimeLimitedDataProtectorTests.cs deleted file mode 100644 index 354078de05..0000000000 --- a/test/Microsoft.AspNet.DataProtection.Test/TimeLimitedDataProtectorTests.cs +++ /dev/null @@ -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 innerProtectorMock = new Mock(); - innerProtectorMock.Setup(o => o.Protect(new byte[] { 0x08, 0xc1, 0x22, 0x02, 0x47, 0xe4, 0x40, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0x10, 0x11 }); - Mock outerProtectorMock = new Mock(); - outerProtectorMock.Setup(p => p.CreateProtector("new purpose")).Returns(innerProtectorMock.Object); - - // Act - var timeLimitedProtector = new TimeLimitedDataProtector(outerProtectorMock.Object); - var subProtector = timeLimitedProtector.CreateProtector("new purpose"); - var protectedPayload = subProtector.Protect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }, expiration); - - // Assert - Assert.Equal(new byte[] { 0x10, 0x11 }, protectedPayload); - } - - [Fact] - public void ExpiredData_Fails() - { - // Arrange - var timeLimitedProtector = CreateEphemeralTimeLimitedProtector(); - var expiration = DateTimeOffset.UtcNow.AddYears(-1); - - // Act & assert - var protectedData = timeLimitedProtector.Protect(new byte[] { 0x04, 0x08, 0x0c }, expiration); - Assert.Throws(() => - { - timeLimitedProtector.Unprotect(protectedData); - }); - } - - [Fact] - public void GoodData_RoundTrips() - { - // Arrange - var timeLimitedProtector = CreateEphemeralTimeLimitedProtector(); - var expectedExpiration = DateTimeOffset.UtcNow.AddYears(1); - - // Act - var protectedData = timeLimitedProtector.Protect(new byte[] { 0x04, 0x08, 0x0c }, expectedExpiration); - DateTimeOffset actualExpiration; - var unprotectedData = timeLimitedProtector.Unprotect(protectedData, out actualExpiration); - - // Assert - Assert.Equal(new byte[] { 0x04, 0x08, 0x0c }, unprotectedData); - Assert.Equal(expectedExpiration, actualExpiration); - } - - [Fact] - public void Protect_NoExpiration_UsesDateTimeOffsetMaxValue() - { - // Should pass DateTimeOffset.MaxValue (utc ticks = 0x2bca2875f4373fff) if no expiration date specified - - // Arrange - Mock innerProtectorMock = new Mock(); - innerProtectorMock.Setup(o => o.Protect(new byte[] { 0x2b, 0xca, 0x28, 0x75, 0xf4, 0x37, 0x3f, 0xff,0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0x10, 0x11 }); - - // Act - var timeLimitedProtector = new TimeLimitedDataProtector(innerProtectorMock.Object); - var protectedPayload = timeLimitedProtector.Protect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }); - - // Assert - Assert.Equal(new byte[] { 0x10, 0x11 }, protectedPayload); - } - - private static TimeLimitedDataProtector CreateEphemeralTimeLimitedProtector() - { - return new TimeLimitedDataProtector(new EphemeralDataProtectionProvider().CreateProtector("purpose")); - } - } -}