| File: TimeLimitedDataProtector.cs | Web Access |
| Project: Microsoft.AspNetCore.DataProtection.Extensions.csproj (Microsoft.AspNetCore.DataProtection.Extensions) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; #if NET using System.Buffers; using System.Buffers.Binary; #endif using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using System.Threading; using Microsoft.AspNetCore.DataProtection.Extensions; using Microsoft.AspNetCore.Shared; namespace Microsoft.AspNetCore.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.AspNetCore.DataProtection.TimeLimitedDataProtector.v1"; private const int ExpirationTimeHeaderSize = 8; // size of the expiration time header in bytes (64-bit UTC tick count) private readonly IDataProtector _innerProtector; private IDataProtector? _innerProtectorWithTimeLimitedPurpose; // created on-demand public TimeLimitedDataProtector(IDataProtector innerProtector) { _innerProtector = innerProtector; } public ITimeLimitedDataProtector CreateProtector(string purpose) { ArgumentNullThrowHelper.ThrowIfNull(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(byte[] plaintext, DateTimeOffset expiration) { ArgumentNullThrowHelper.ThrowIfNull(plaintext); var innerProtector = GetInnerProtectorWithTimeLimitedPurpose(); #if NET if (innerProtector is ISpanDataProtector spanProtector) { return ProtectWithSpanProtector(spanProtector, plaintext, expiration); } #endif // We prepend the expiration time (as a 64-bit UTC tick count) to the unprotected data. byte[] plaintextWithHeader = new byte[checked(ExpirationTimeHeaderSize + plaintext.Length)]; BitHelpers.WriteUInt64(plaintextWithHeader, 0, (ulong)expiration.UtcTicks); Buffer.BlockCopy(plaintext, 0, plaintextWithHeader, ExpirationTimeHeaderSize, plaintext.Length); return innerProtector.Protect(plaintextWithHeader); } public byte[] Unprotect(byte[] protectedData, out DateTimeOffset expiration) { ArgumentNullThrowHelper.ThrowIfNull(protectedData); return UnprotectCore(protectedData, DateTimeOffset.UtcNow, out expiration); } internal byte[] UnprotectCore(byte[] protectedData, DateTimeOffset now, out DateTimeOffset expiration) { ArgumentNullThrowHelper.ThrowIfNull(protectedData); try { var innerProtector = GetInnerProtectorWithTimeLimitedPurpose(); #if NET if (innerProtector is ISpanDataProtector spanProtector) { return UnprotectWithSpanProtector(spanProtector, protectedData, now, out expiration); } #endif byte[] plaintextWithHeader = innerProtector.Unprotect(protectedData); if (plaintextWithHeader.Length < ExpirationTimeHeaderSize) { // 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 - ExpirationTimeHeaderSize]; Buffer.BlockCopy(plaintextWithHeader, ExpirationTimeHeaderSize, retVal, 0, retVal.Length); expiration = embeddedExpiration; return retVal; } catch (Exception ex) when (ex.RequiresHomogenization()) { // Homogenize all failures to CryptographicException throw new CryptographicException(Resources.CryptCommon_GenericError, ex); } } #if NET // The span-based paths produce and consume exactly the same payload as the byte[]-based paths below, but avoid // allocating the intermediate { expiration || plaintext } array. Buffers start on the stack and only rent from the // pool when the payload doesn't fit. Rented buffers are cleared when returned to the pool, and the stack buffers // that may hold plaintext are cleared before returning. Nothing pooled escapes this call. private static byte[] ProtectWithSpanProtector(ISpanDataProtector protector, byte[] plaintext, DateTimeOffset expiration) { var plaintextWithHeaderLength = checked(ExpirationTimeHeaderSize + plaintext.Length); Span<byte> plaintextWithHeaderInitialBuffer = stackalloc byte[256]; using var plaintextWithHeader = new RefPooledArrayBufferWriter<byte>(plaintextWithHeaderInitialBuffer); var destination = new RefPooledArrayBufferWriter<byte>(stackalloc byte[256]); try { // We prepend the expiration time (as a 64-bit UTC tick count) to the unprotected data. var span = plaintextWithHeader.GetSpan(plaintextWithHeaderLength); BinaryPrimitives.WriteUInt64BigEndian(span, (ulong)expiration.UtcTicks); plaintext.CopyTo(span.Slice(ExpirationTimeHeaderSize)); plaintextWithHeader.Advance(plaintextWithHeaderLength); protector.Protect(plaintextWithHeader.WrittenSpan, ref destination); return destination.WrittenSpan.ToArray(); } finally { // 'destination' is passed by ref, so it can't be a 'using' variable (CS1657). destination.Dispose(); plaintextWithHeaderInitialBuffer.Clear(); } } private static byte[] UnprotectWithSpanProtector(ISpanDataProtector protector, byte[] protectedData, DateTimeOffset now, out DateTimeOffset expiration) { Span<byte> initialBuffer = stackalloc byte[256]; var destination = new RefPooledArrayBufferWriter<byte>(initialBuffer); try { protector.Unprotect(protectedData, ref destination); var plaintextWithHeader = destination.WrittenSpan; if (plaintextWithHeader.Length < ExpirationTimeHeaderSize) { // header isn't present throw new CryptographicException(Resources.TimeLimitedDataProtector_PayloadInvalid); } // Read expiration time back out of the payload ulong utcTicksExpiration = BinaryPrimitives.ReadUInt64BigEndian(plaintextWithHeader); 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 - return payload expiration = embeddedExpiration; return plaintextWithHeader.Slice(ExpirationTimeHeaderSize).ToArray(); } finally { // 'destination' is passed by ref, so it can't be a 'using' variable (CS1657). destination.Dispose(); initialBuffer.Clear(); } } #endif /* * EXPLICIT INTERFACE IMPLEMENTATIONS */ IDataProtector IDataProtectionProvider.CreateProtector(string purpose) { ArgumentNullThrowHelper.ThrowIfNull(purpose); return CreateProtector(purpose); } byte[] IDataProtector.Protect(byte[] plaintext) { ArgumentNullThrowHelper.ThrowIfNull(plaintext); // MaxValue essentially means 'no expiration' return Protect(plaintext, DateTimeOffset.MaxValue); } byte[] IDataProtector.Unprotect(byte[] protectedData) { ArgumentNullThrowHelper.ThrowIfNull(protectedData); return Unprotect(protectedData, out _); } }