| File: Secret.cs | Web Access |
| Project: src\aspnetcore\src\DataProtection\DataProtection\src\Microsoft.AspNetCore.DataProtection.csproj (Microsoft.AspNetCore.DataProtection) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.AspNetCore.Cryptography; using Microsoft.AspNetCore.Cryptography.Cng; using Microsoft.AspNetCore.Cryptography.SafeHandles; using Microsoft.AspNetCore.DataProtection.Managed; using Microsoft.AspNetCore.Shared; namespace Microsoft.AspNetCore.DataProtection; /// <summary> /// Represents a secret value stored in memory. /// </summary> public sealed unsafe class Secret : IDisposable, ISecret { // from wincrypt.h private const uint CRYPTPROTECTMEMORY_BLOCK_SIZE = 16; private readonly SecureLocalAllocHandle _localAllocHandle; private readonly uint _plaintextLength; /// <summary> /// Creates a new Secret from the provided input value, where the input value /// is specified as an array segment. /// </summary> public Secret(ArraySegment<byte> value) { value.Validate(); _localAllocHandle = Protect(value); _plaintextLength = (uint)value.Count; } /// <summary> /// Creates a new Secret from the provided input value, where the input value /// is specified as an array. /// </summary> public Secret(byte[] value) : this(new ArraySegment<byte>(value)) { ArgumentNullThrowHelper.ThrowIfNull(value); } /// <summary> /// Creates a new Secret from the provided input value, where the input value /// is specified as a pointer to unmanaged memory. /// </summary> public Secret(byte* secret, int secretLength) { if (secret == null) { throw new ArgumentNullException(nameof(secret)); } if (secretLength < 0) { throw Error.Common_ValueMustBeNonNegative(nameof(secretLength)); } _localAllocHandle = Protect(secret, (uint)secretLength); _plaintextLength = (uint)secretLength; } /// <summary> /// Creates a new Secret from another secret object. /// </summary> public Secret(ISecret secret) { ArgumentNullThrowHelper.ThrowIfNull(secret); var other = secret as Secret; if (other != null) { // Fast-track: simple deep copy scenario. this._localAllocHandle = other._localAllocHandle.Duplicate(); this._plaintextLength = other._plaintextLength; } else { // Copy the secret to a temporary managed buffer, then protect the buffer. // We pin the temp buffer and zero it out when we're finished to limit exposure of the secret. var tempPlaintextBuffer = new byte[secret.Length]; fixed (byte* pbTempPlaintextBuffer = tempPlaintextBuffer) { try { secret.WriteSecretIntoBuffer(new ArraySegment<byte>(tempPlaintextBuffer)); _localAllocHandle = Protect(pbTempPlaintextBuffer, (uint)tempPlaintextBuffer.Length); _plaintextLength = (uint)tempPlaintextBuffer.Length; } finally { UnsafeBufferUtil.SecureZeroMemory(pbTempPlaintextBuffer, tempPlaintextBuffer.Length); } } } } /// <summary> /// The length (in bytes) of the secret value. /// </summary> public int Length { get { return (int)_plaintextLength; // ctor guarantees the length fits into a signed int } } /// <summary> /// Wipes the secret from memory. /// </summary> public void Dispose() { _localAllocHandle.Dispose(); } private static SecureLocalAllocHandle Protect(ArraySegment<byte> plaintext) { fixed (byte* pbPlaintextArray = plaintext.Array) { return Protect(&pbPlaintextArray[plaintext.Offset], (uint)plaintext.Count); } } private static SecureLocalAllocHandle Protect(byte* pbPlaintext, uint cbPlaintext) { // If we're not running on a platform that supports CryptProtectMemory, // shove the plaintext directly into a LocalAlloc handle. Ideally we'd // mark this memory page as non-pageable, but this is fraught with peril. if (!OSVersionUtil.IsWindows()) { var handle = SecureLocalAllocHandle.Allocate((IntPtr)checked((int)cbPlaintext)); UnsafeBufferUtil.BlockCopy(from: pbPlaintext, to: handle, byteCount: cbPlaintext); return handle; } // We need to make sure we're a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE. var numTotalBytesToAllocate = cbPlaintext; var numBytesPaddingRequired = CRYPTPROTECTMEMORY_BLOCK_SIZE - (numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE); if (numBytesPaddingRequired == CRYPTPROTECTMEMORY_BLOCK_SIZE) { numBytesPaddingRequired = 0; // we're already a proper multiple of the block size } checked { numTotalBytesToAllocate += numBytesPaddingRequired; } CryptoUtil.Assert(numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0, "numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0"); // Allocate and copy plaintext data; padding is uninitialized / undefined. var encryptedMemoryHandle = SecureLocalAllocHandle.Allocate((IntPtr)numTotalBytesToAllocate); UnsafeBufferUtil.BlockCopy(from: pbPlaintext, to: encryptedMemoryHandle, byteCount: cbPlaintext); // Finally, CryptProtectMemory the whole mess. if (numTotalBytesToAllocate != 0) { MemoryProtection.CryptProtectMemory(encryptedMemoryHandle, byteCount: numTotalBytesToAllocate); } return encryptedMemoryHandle; } /// <summary> /// Returns a Secret made entirely of random bytes retrieved from /// a cryptographically secure RNG. /// </summary> public static Secret Random(int numBytes) { if (numBytes < 0) { throw Error.Common_ValueMustBeNonNegative(nameof(numBytes)); } if (numBytes == 0) { byte dummy; return new Secret(&dummy, 0); } else { // Don't use CNG if we're not on Windows. if (!OSVersionUtil.IsWindows()) { return new Secret(ManagedGenRandomImpl.Instance.GenRandom(numBytes)); } var bytes = new byte[numBytes]; fixed (byte* pbBytes = bytes) { try { BCryptUtil.GenRandom(pbBytes, (uint)numBytes); return new Secret(pbBytes, numBytes); } finally { UnsafeBufferUtil.SecureZeroMemory(pbBytes, numBytes); } } } } private void UnprotectInto(byte* pbBuffer) { // If we're not running on a platform that supports CryptProtectMemory, // the handle contains plaintext bytes. if (!OSVersionUtil.IsWindows()) { UnsafeBufferUtil.BlockCopy(from: _localAllocHandle, to: pbBuffer, byteCount: _plaintextLength); return; } if (_plaintextLength % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0) { // Case 1: Secret length is an exact multiple of the block size. Copy directly to the buffer and decrypt there. UnsafeBufferUtil.BlockCopy(from: _localAllocHandle, to: pbBuffer, byteCount: _plaintextLength); MemoryProtection.CryptUnprotectMemory(pbBuffer, _plaintextLength); } else { // Case 2: Secret length is not a multiple of the block size. We'll need to duplicate the data and // perform the decryption in the duplicate buffer, then copy the plaintext data over. using (var duplicateHandle = _localAllocHandle.Duplicate()) { MemoryProtection.CryptUnprotectMemory(duplicateHandle, checked((uint)duplicateHandle.Length)); UnsafeBufferUtil.BlockCopy(from: duplicateHandle, to: pbBuffer, byteCount: _plaintextLength); } } } /// <summary> /// Writes the secret value to the specified buffer. /// </summary> /// <remarks> /// The buffer size must exactly match the length of the secret value. /// </remarks> public void WriteSecretIntoBuffer(ArraySegment<byte> buffer) { // Parameter checking buffer.Validate(); if (buffer.Count != Length) { throw Error.Common_BufferIncorrectlySized(nameof(buffer), actualSize: buffer.Count, expectedSize: Length); } // only unprotect if the secret is zero-length, as CLR doesn't like pinning zero-length buffers if (Length != 0) { fixed (byte* pbBufferArray = buffer.Array) { UnprotectInto(&pbBufferArray[buffer.Offset]); } } } /// <summary> /// Writes the secret value to the specified buffer. /// </summary> /// <param name="buffer">The buffer into which to write the secret value.</param> /// <param name="bufferLength">The size (in bytes) of the provided buffer.</param> /// <remarks> /// The 'bufferLength' parameter must exactly match the length of the secret value. /// </remarks> public void WriteSecretIntoBuffer(byte* buffer, int bufferLength) { if (buffer == null) { throw new ArgumentNullException(nameof(buffer)); } if (bufferLength != Length) { throw Error.Common_BufferIncorrectlySized(nameof(bufferLength), actualSize: bufferLength, expectedSize: Length); } if (Length != 0) { UnprotectInto(buffer); } } }