// Copyright (c) .NET Foundation. 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.AspNet.Cryptography.Cng; using Microsoft.AspNet.Cryptography.SafeHandles; using Microsoft.AspNet.DataProtection.Managed; namespace Microsoft.AspNet.DataProtection { /// /// Represents a secret value stored in memory. /// public unsafe sealed class Secret : IDisposable, ISecret { // from wincrypt.h private const uint CRYPTPROTECTMEMORY_BLOCK_SIZE = 16; private readonly SecureLocalAllocHandle _localAllocHandle; private readonly uint _plaintextLength; /// /// Creates a new Secret from the provided input value, where the input value /// is specified as an array segment. /// public Secret(ArraySegment value) { value.Validate(); _localAllocHandle = Protect(value); _plaintextLength = (uint)value.Count; } /// /// Creates a new Secret from the provided input value, where the input value /// is specified as an array. /// public Secret(byte[] value) : this(new ArraySegment(value)) { if (value == null) { throw new ArgumentNullException(nameof(value)); } } /// /// Creates a new Secret from the provided input value, where the input value /// is specified as a pointer to unmanaged memory. /// 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; } /// /// Creates a new Secret from another secret object. /// public Secret(ISecret secret) { if (secret == null) { throw new ArgumentNullException(nameof(secret)); } Secret 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. byte[] tempPlaintextBuffer = new byte[secret.Length]; fixed (byte* pbTempPlaintextBuffer = tempPlaintextBuffer) { try { secret.WriteSecretIntoBuffer(new ArraySegment(tempPlaintextBuffer)); _localAllocHandle = Protect(pbTempPlaintextBuffer, (uint)tempPlaintextBuffer.Length); _plaintextLength = (uint)tempPlaintextBuffer.Length; } finally { UnsafeBufferUtil.SecureZeroMemory(pbTempPlaintextBuffer, tempPlaintextBuffer.Length); } } } } /// /// The length (in bytes) of the secret value. /// public int Length { get { return (int)_plaintextLength; // ctor guarantees the length fits into a signed int } } /// /// Wipes the secret from memory. /// public void Dispose() { _localAllocHandle.Dispose(); } private static SecureLocalAllocHandle Protect(ArraySegment 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()) { SecureLocalAllocHandle 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. uint numTotalBytesToAllocate = cbPlaintext; uint 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. SecureLocalAllocHandle 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; } /// /// Returns a Secret comprised entirely of random bytes retrieved from /// a cryptographically secure RNG. /// 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)); } byte[] 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); } } } /// /// Writes the secret value to the specified buffer. /// /// /// The buffer size must exactly match the length of the secret value. /// public void WriteSecretIntoBuffer(ArraySegment 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]); } } } /// /// Writes the secret value to the specified buffer. /// /// The buffer into which to write the secret value. /// The size (in bytes) of the provided buffer. /// /// The 'bufferLength' parameter must exactly match the length of the secret value. /// 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); } } } }