File: System\Security\Cryptography\PemEncoding.cs
Web Access
Project: src\src\libraries\System.Security.Cryptography\src\System.Security.Cryptography.csproj (System.Security.Cryptography)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Buffers.Text;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text.Unicode;
 
namespace System.Security.Cryptography
{
    /// <summary>
    /// Provides methods for reading and writing the IETF RFC 7468
    /// subset of PEM (Privacy-Enhanced Mail) textual encodings.
    /// This class cannot be inherited.
    /// </summary>
    public static class PemEncoding
    {
        private const int EncodedLineLength = 64;
 
        /// <summary>
        /// Finds the first PEM-encoded data.
        /// </summary>
        /// <param name="pemData">
        /// The text containing the PEM-encoded data.
        /// </param>
        /// <exception cref="ArgumentException">
        /// <paramref name="pemData"/> does not contain a well-formed PEM-encoded value.
        /// </exception>
        /// <returns>
        /// A value that specifies the location, label, and data location of
        /// the encoded data.
        /// </returns>
        /// <remarks>
        /// IETF RFC 7468 permits different decoding rules. This method
        /// always uses lax rules.
        /// </remarks>
        public static PemFields Find(ReadOnlySpan<char> pemData)
        {
            if (!TryFind(pemData, out PemFields fields))
            {
                throw new ArgumentException(SR.Argument_PemEncoding_NoPemFound, nameof(pemData));
            }
 
            return fields;
        }
 
        /// <summary>
        /// Finds the first PEM-encoded data.
        /// </summary>
        /// <param name="pemData">
        /// The text containing the PEM-encoded data.
        /// </param>
        /// <exception cref="ArgumentException">
        /// <paramref name="pemData"/> does not contain a well-formed PEM-encoded value.
        /// </exception>
        /// <returns>
        /// A value that specifies the location, label, and data location of
        /// the encoded data.
        /// </returns>
        /// <remarks>
        ///   <para>IETF RFC 7468 permits different decoding rules. This method always uses lax rules.</para>
        ///   <para>
        ///     This does not validate the UTF-8 data outside of encapsulation boundaries and is ignored. It is the caller's
        ///     responsibility to ensure the entire input is UTF-8 if required.
        ///   </para>
        /// </remarks>
        public static PemFields FindUtf8(ReadOnlySpan<byte> pemData)
        {
            if (!TryFindUtf8(pemData, out PemFields fields))
            {
                throw new ArgumentException(SR.Argument_PemEncoding_NoPemFound, nameof(pemData));
            }
 
            return fields;
        }
 
        /// <summary>
        /// Attempts to find the first PEM-encoded data.
        /// </summary>
        /// <param name="pemData">
        /// The text containing the PEM-encoded data.
        /// </param>
        /// <param name="fields">
        /// When this method returns, contains a value
        /// that specifies the location, label, and data location of the encoded data;
        /// or that specifies those locations as empty if no PEM-encoded data is found.
        /// This parameter is treated as uninitialized.
        /// </param>
        /// <returns>
        /// <c>true</c> if PEM-encoded data was found; otherwise <c>false</c>.
        /// </returns>
        /// <remarks>
        /// IETF RFC 7468 permits different decoding rules. This method
        /// always uses lax rules.
        /// </remarks>
        public static bool TryFind(ReadOnlySpan<char> pemData, out PemFields fields)
        {
            return TryFindCore<char, Utf16PemEncoder>(pemData, out fields);
        }
 
        /// <summary>
        ///   Attempts to find the first PEM-encoded data.
        /// </summary>
        /// <param name="pemData">
        ///   The text containing the PEM-encoded data.
        /// </param>
        /// <param name="fields">
        ///   When this method returns, contains a value
        ///   that specifies the location, label, and data location of the encoded data;
        ///   or that specifies those locations as empty if no PEM-encoded data is found.
        ///   This parameter is treated as uninitialized.
        /// </param>
        /// <returns>
        ///   <see langword="true" /> if PEM-encoded data was found; otherwise <see langword="false" />.
        /// </returns>
        /// <remarks>
        ///   <para>IETF RFC 7468 permits different decoding rules. This method always uses lax rules.</para>
        ///   <para>
        ///     This does not validate the UTF-8 data outside of encapsulation boundaries and is ignored. It is the caller's
        ///     responsibility to ensure the entire input is UTF-8 if required.
        ///   </para>
        /// </remarks>
        public static bool TryFindUtf8(ReadOnlySpan<byte> pemData, out PemFields fields)
        {
            return TryFindCore<byte, Utf8PemEncoder>(pemData, out fields);
        }
 
        private static bool TryFindCore<TChar, T>(ReadOnlySpan<TChar> pemData, out PemFields fields)
            where T : IPemEncoder<TChar>
            where TChar : unmanaged, IEquatable<TChar>, INumber<TChar>
        {
            // Check for the minimum possible encoded length of a PEM structure
            // and exit early if there is no way the input could contain a well-formed
            // PEM.
            if (pemData.Length < T.PreEBPrefix.Length + T.Ending.Length * 2 + T.PostEBPrefix.Length)
            {
                fields = default;
                return false;
            }
 
            const int PostebStackBufferSize = 256;
            Span<TChar> postebStackBuffer = stackalloc TChar[PostebStackBufferSize];
            int areaOffset = 0;
            int preebIndex;
            while ((preebIndex = pemData.IndexOfByOffset(T.PreEBPrefix, areaOffset)) >= 0)
            {
                int labelStartIndex = preebIndex + T.PreEBPrefix.Length;
 
                // If there are any previous characters, the one prior to the PreEB
                // must be a white space character.
                if (preebIndex > 0 && !IsWhiteSpaceCharacter(pemData[preebIndex - 1], T.Whitespace))
                {
                    areaOffset = labelStartIndex;
                    continue;
                }
 
                int preebEndIndex = pemData.IndexOfByOffset(T.Ending, labelStartIndex);
 
                // There is no ending sequence, -----, in the remainder of
                // the document. Therefore, there can never be a complete PreEB
                // and we can exit.
                if (preebEndIndex < 0)
                {
                    fields = default;
                    return false;
                }
 
                Range labelRange = labelStartIndex..preebEndIndex;
                ReadOnlySpan<TChar> label = pemData[labelRange];
 
                // There could be a preeb that is valid after this one if it has an invalid
                // label, so move from there.
                if (!IsValidLabel(label))
                {
                    goto NextAfterLabel;
                }
 
                int contentStartIndex = preebEndIndex + T.Ending.Length;
                int postebLength = T.PostEBPrefix.Length + label.Length + T.Ending.Length;
 
                Span<TChar> postebBuffer = postebLength > PostebStackBufferSize
                    ? new TChar[postebLength]
                    : postebStackBuffer;
                ReadOnlySpan<TChar> posteb = WritePostEB(label, postebBuffer);
                int postebStartIndex = pemData.IndexOfByOffset(posteb, contentStartIndex);
 
                if (postebStartIndex < 0)
                {
                    goto NextAfterLabel;
                }
 
                int pemEndIndex = postebStartIndex + postebLength;
 
                // The PostEB must either end at the end of the string, or
                // have at least one white space character after it.
                if (pemEndIndex < pemData.Length - 1 &&
                    !IsWhiteSpaceCharacter(pemData[pemEndIndex], T.Whitespace))
                {
                    goto NextAfterLabel;
                }
 
                Range contentRange = contentStartIndex..postebStartIndex;
 
                if (!TryCountBase64<TChar, T>(pemData[contentRange], out int base64start, out int base64end, out int decodedSize))
                {
                    goto NextAfterLabel;
                }
 
                Range pemRange = preebIndex..pemEndIndex;
                Range base64range = (contentStartIndex + base64start)..(contentStartIndex + base64end);
                fields = new PemFields(labelRange, base64range, pemRange, decodedSize);
                return true;
 
            NextAfterLabel:
                if (preebEndIndex <= areaOffset)
                {
                    // We somehow ended up in a situation where we will advance
                    // backward or not at all, which means we'll probably end up here again,
                    // advancing backward, in a loop. To avoid getting stuck,
                    // detect this situation and return.
                    fields = default;
                    return false;
                }
                areaOffset = preebEndIndex;
            }
 
            fields = default;
            return false;
 
            static ReadOnlySpan<TChar> WritePostEB(ReadOnlySpan<TChar> label, Span<TChar> destination)
            {
                int size = T.PostEBPrefix.Length + label.Length + T.Ending.Length;
                Debug.Assert(destination.Length >= size);
                T.PostEBPrefix.CopyTo(destination);
                label.CopyTo(destination.Slice(T.PostEBPrefix.Length));
                T.Ending.CopyTo(destination.Slice(T.PostEBPrefix.Length + label.Length));
                return destination.Slice(0, size);
            }
        }
 
        private static int IndexOfByOffset<TChar>(this ReadOnlySpan<TChar> str, ReadOnlySpan<TChar> value, int startPosition)
            where TChar : IEquatable<TChar>
        {
            Debug.Assert(startPosition <= str.Length);
            int index = str.Slice(startPosition).IndexOf(value);
            return index == -1 ? -1 : index + startPosition;
        }
 
        private static bool IsValidLabel<TChar>(ReadOnlySpan<TChar> data)
            where TChar : IEquatable<TChar>, INumber<TChar>
        {
            static bool IsLabelChar(TChar c)
            {
                return (c - TChar.CreateTruncating(0x21u)) <= TChar.CreateTruncating(0x5du) &&
                        c != TChar.CreateTruncating('-');
            }
 
            // Empty labels are permitted per RFC 7468.
            if (data.IsEmpty)
                return true;
 
            // The first character must be a labelchar, so initialize to false
            bool previousIsLabelChar = false;
 
            for (int index = 0; index < data.Length; index++)
            {
                TChar c = data[index];
 
                if (IsLabelChar(c))
                {
                    previousIsLabelChar = true;
                    continue;
                }
 
                bool isSpaceOrHyphen = c == TChar.CreateTruncating(' ') || c == TChar.CreateTruncating('-');
 
                // IETF RFC 7468 states that every character in a label must
                // be a labelchar, and each labelchar may have zero or one
                // preceding space or hyphen, except the first labelchar.
                // If this character is not a space or hyphen, then this characer
                // is invalid.
                // If it is a space or hyphen, and the previous character was
                // also not a labelchar (another hyphen or space), then we have
                // two consecutive spaces or hyphens which is invalid.
                if (!isSpaceOrHyphen || !previousIsLabelChar)
                {
                    return false;
                }
 
                previousIsLabelChar = false;
            }
 
            // The last character must also be a labelchar. It cannot be a
            // hyphen or space since these are only allowed to precede
            // a labelchar.
            return previousIsLabelChar;
        }
 
        private static bool TryCountBase64<TChar, T>(
            ReadOnlySpan<TChar> str,
            out int base64Start,
            out int base64End,
            out int base64DecodedSize) where TChar : IEquatable<TChar> where T : IPemEncoder<TChar>
        {
            // Trim starting and ending allowed white space characters
            int start = 0;
            int end = str.Length - 1;
            for (; start < str.Length && IsWhiteSpaceCharacter(str[start], T.Whitespace); start++);
            for (; end > start && IsWhiteSpaceCharacter(str[end], T.Whitespace); end--);
 
            // Validate that the remaining characters are valid base-64 encoded data.
            if (T.IsValidBase64(str.Slice(start, end + 1 - start), out base64DecodedSize))
            {
                base64Start = start;
                base64End = end + 1;
                return true;
            }
 
            base64Start = 0;
            base64End = 0;
            return false;
        }
 
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static bool IsWhiteSpaceCharacter<TChar>(TChar ch, ReadOnlySpan<TChar> whitespace)
            where TChar : IEquatable<TChar>
        {
            return whitespace.Contains(ch);
        }
 
        /// <summary>
        /// Determines the length of a PEM-encoded value, in characters,
        /// given the length of a label and binary data.
        /// </summary>
        /// <param name="labelLength">
        /// The length of the label, in characters.
        /// </param>
        /// <param name="dataLength">
        /// The length of the data, in bytes.
        /// </param>
        /// <returns>
        /// The number of characters in the encoded PEM.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        ///   <paramref name="labelLength"/> is a negative value.
        ///   <para>
        ///       -or-
        ///   </para>
        ///   <paramref name="dataLength"/> is a negative value.
        ///   <para>
        ///       -or-
        ///   </para>
        ///   <paramref name="labelLength"/> exceeds the maximum possible label length.
        ///   <para>
        ///       -or-
        ///   </para>
        ///   <paramref name="dataLength"/> exceeds the maximum possible encoded data length.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The length of the PEM-encoded value is larger than <see cref="int.MaxValue"/>.
        /// </exception>
        public static int GetEncodedSize(int labelLength, int dataLength)
        {
            // The largest possible label is MaxLabelSize - when included in the posteb
            // and preeb lines new lines, assuming the base64 content is empty.
            //     -----BEGIN {char * MaxLabelSize}-----\n
            //     -----END {char * MaxLabelSize}-----
            const int MaxLabelSize = 1_073_741_808;
 
            // The largest possible binary value to fit in a padded base64 string
            // is 1,610,612,733 bytes. RFC 7468 states:
            //   Generators MUST wrap the base64-encoded lines so that each line
            //   consists of exactly 64 characters except for the final line
            // We need to account for new line characters, every 64 characters.
            // This works out to 1,585,834,053 maximum bytes in data when wrapping
            // is accounted for assuming an empty label.
            const int MaxDataLength = 1_585_834_053;
 
            ArgumentOutOfRangeException.ThrowIfNegative(labelLength);
            ArgumentOutOfRangeException.ThrowIfNegative(dataLength);
 
            if (labelLength > MaxLabelSize)
                throw new ArgumentOutOfRangeException(nameof(labelLength), SR.Argument_PemEncoding_EncodedSizeTooLarge);
            if (dataLength > MaxDataLength)
                throw new ArgumentOutOfRangeException(nameof(dataLength), SR.Argument_PemEncoding_EncodedSizeTooLarge);
 
            Debug.Assert(Utf16PemEncoder.PostEBPrefix.Length == Utf8PemEncoder.PostEBPrefix.Length);
            Debug.Assert(Utf16PemEncoder.PreEBPrefix.Length == Utf8PemEncoder.PreEBPrefix.Length);
            Debug.Assert(Utf16PemEncoder.Ending.Length == Utf8PemEncoder.Ending.Length);
 
            // Which PemEncoder that is used for determining the length of things we use does not matter since the
            // reported value is in characters.
            int preebLength = Utf16PemEncoder.PreEBPrefix.Length + labelLength + Utf16PemEncoder.Ending.Length;
            int postebLength = Utf16PemEncoder.PostEBPrefix.Length + labelLength + Utf16PemEncoder.Ending.Length;
            int totalEncapLength = preebLength + postebLength + 1; //Add one for newline after preeb
 
            // dataLength is already known to not overflow here
            int encodedDataLength = ((dataLength + 2) / 3) << 2;
            int lineCount = Math.DivRem(encodedDataLength, EncodedLineLength, out int remainder);
 
            if (remainder > 0)
            {
                lineCount++;
            }
 
            int encodedDataLengthWithBreaks = encodedDataLength + lineCount;
 
            if (int.MaxValue - encodedDataLengthWithBreaks < totalEncapLength)
            {
                throw new ArgumentException(SR.Argument_PemEncoding_EncodedSizeTooLarge);
            }
 
            return encodedDataLengthWithBreaks + totalEncapLength;
        }
 
        /// <summary>
        /// Tries to write the provided data and label as PEM-encoded data into
        /// a provided buffer.
        /// </summary>
        /// <param name="label">
        /// The label to write.
        /// </param>
        /// <param name="data">
        /// The data to write.
        /// </param>
        /// <param name="destination">
        /// The buffer to receive the PEM-encoded text.
        /// </param>
        /// <param name="charsWritten">
        /// When this method returns, this parameter contains the number of characters
        /// written to <paramref name="destination"/>. This parameter is treated
        /// as uninitialized.
        /// </param>
        /// <returns>
        /// <c>true</c> if <paramref name="destination"/> is large enough to contain
        /// the PEM-encoded text, otherwise <c>false</c>.
        /// </returns>
        /// <remarks>
        /// This method always wraps the base-64 encoded text to 64 characters, per the
        /// recommended wrapping of IETF RFC 7468. Unix-style line endings are used for line breaks.
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">
        ///   <paramref name="label"/> exceeds the maximum possible label length.
        ///   <para>
        ///       -or-
        ///   </para>
        ///   <paramref name="data"/> exceeds the maximum possible encoded data length.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The resulting PEM-encoded text is larger than <see cref="int.MaxValue"/>.
        ///   <para>
        ///       - or -
        ///   </para>
        /// <paramref name="label"/> contains invalid characters.
        /// </exception>
        public static bool TryWrite(ReadOnlySpan<char> label, ReadOnlySpan<byte> data, Span<char> destination, out int charsWritten)
        {
            if (!IsValidLabel(label))
                throw new ArgumentException(SR.Argument_PemEncoding_InvalidLabel, nameof(label));
 
            int encodedSize = GetEncodedSize(label.Length, data.Length);
 
            if (destination.Length < encodedSize)
            {
                charsWritten = 0;
                return false;
            }
 
            charsWritten = WriteCore<char, Utf16PemEncoder>(label, data, destination);
            Debug.Assert(encodedSize == charsWritten);
            return true;
        }
 
        /// <summary>
        /// Tries to write the provided data and label as PEM-encoded data into
        /// a provided buffer.
        /// </summary>
        /// <param name="utf8Label">
        /// The label to write.
        /// </param>
        /// <param name="data">
        /// The data to write.
        /// </param>
        /// <param name="destination">
        /// The buffer to receive the PEM-encoded text.
        /// </param>
        /// <param name="bytesWritten">
        /// When this method returns, this parameter contains the number of UTF-8 encoded bytes
        /// written to <paramref name="destination"/>.
        /// </param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="destination"/> is large enough to contain
        /// the PEM-encoded text, otherwise <see langword="false"/>.
        /// </returns>
        /// <remarks>
        /// This method always wraps the base-64 encoded text to 64 characters, per the
        /// recommended wrapping of IETF RFC 7468. Unix-style line endings are used for line breaks.
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">
        ///   <paramref name="utf8Label"/> exceeds the maximum possible label length.
        ///   <para>
        ///       -or-
        ///   </para>
        ///   <paramref name="data"/> exceeds the maximum possible encoded data length.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <para>
        ///   The resulting PEM-encoded text is larger than <see cref="int.MaxValue"/>.
        /// </para>
        /// <para>- or -</para>
        /// <para>
        ///   <paramref name="utf8Label"/> contains invalid characters or is malformed UTF-8.
        /// </para>
        /// </exception>
        public static bool TryWriteUtf8(
            ReadOnlySpan<byte> utf8Label,
            ReadOnlySpan<byte> data,
            Span<byte> destination,
            out int bytesWritten)
        {
            if (!Utf8.IsValid(utf8Label) || !IsValidLabel(utf8Label))
                throw new ArgumentException(SR.Argument_PemEncoding_InvalidLabel, nameof(utf8Label));
 
            int encodedSize = GetEncodedSize(utf8Label.Length, data.Length);
 
            if (destination.Length < encodedSize)
            {
                bytesWritten = 0;
                return false;
            }
 
            bytesWritten = WriteCore<byte, Utf8PemEncoder>(utf8Label, data, destination);
            Debug.Assert(encodedSize == bytesWritten);
            return true;
        }
 
        /// <summary>
        /// Creates an encoded PEM with the given label and data.
        /// </summary>
        /// <param name="utf8Label">
        /// The label to encode.
        /// </param>
        /// <param name="data">
        /// The data to encode.
        /// </param>
        /// <returns>
        ///   An array containing the bytes representing the UTF-8 encoding of the PEM.
        /// </returns>
        /// <remarks>
        /// This method always wraps the base-64 encoded text to 64 characters, per the
        /// recommended wrapping of RFC-7468. Unix-style line endings are used for line breaks.
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">
        ///   <para>
        ///     <paramref name="utf8Label"/> exceeds the maximum possible label length.
        ///   </para>
        ///   <para>
        ///       -or-
        ///   </para>
        ///   <para>
        ///     <paramref name="data"/> exceeds the maximum possible encoded data length.
        ///   </para>
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <para>
        ///   The resulting PEM-encoded text is larger than <see cref="int.MaxValue"/>.
        /// </para>
        /// <para> -or- </para>
        /// <para>
        ///   <paramref name="utf8Label"/> contains invalid characters or is malformed UTF-8.
        /// </para>
        /// </exception>
        public static byte[] WriteUtf8(ReadOnlySpan<byte> utf8Label, ReadOnlySpan<byte> data)
        {
            if (!Utf8.IsValid(utf8Label) || !IsValidLabel(utf8Label))
                throw new ArgumentException(SR.Argument_PemEncoding_InvalidLabel, nameof(utf8Label));
 
            int encodedSize = GetEncodedSize(utf8Label.Length, data.Length);
            byte[] buffer = new byte[encodedSize];
 
            int byteWritten = WriteCore<byte, Utf8PemEncoder>(utf8Label, data, buffer);
            Debug.Assert(byteWritten == encodedSize);
            return buffer;
        }
 
        private static int WriteCore<TChar, T>(ReadOnlySpan<TChar> label, ReadOnlySpan<byte> data, Span<TChar> destination)
            where T : IPemEncoder<TChar>
            where TChar : IEquatable<TChar>
        {
            static int Write(ReadOnlySpan<TChar> str, Span<TChar> dest, int offset)
            {
                str.CopyTo(dest.Slice(offset));
                return str.Length;
            }
 
            const int BytesPerLine = 48;
 
            int charsWritten = 0;
            charsWritten += Write(T.PreEBPrefix, destination, charsWritten);
            charsWritten += Write(label, destination, charsWritten);
            charsWritten += Write(T.Ending, destination, charsWritten);
            charsWritten += Write(T.NewLine, destination, charsWritten);
 
            ReadOnlySpan<byte> remainingData = data;
            while (remainingData.Length >= BytesPerLine)
            {
                charsWritten += T.WriteBase64(remainingData.Slice(0, BytesPerLine), destination, charsWritten);
                charsWritten += Write(T.NewLine, destination, charsWritten);
                remainingData = remainingData.Slice(BytesPerLine);
            }
 
            Debug.Assert(remainingData.Length < BytesPerLine);
 
            if (remainingData.Length > 0)
            {
                charsWritten += T.WriteBase64(remainingData, destination, charsWritten);
                charsWritten += Write(T.NewLine, destination, charsWritten);
            }
 
            charsWritten += Write(T.PostEBPrefix, destination, charsWritten);
            charsWritten += Write(label, destination, charsWritten);
            charsWritten += Write(T.Ending, destination, charsWritten);
 
            return charsWritten;
        }
 
        /// <summary>
        /// Creates an encoded PEM with the given label and data.
        /// </summary>
        /// <param name="label">
        /// The label to encode.
        /// </param>
        /// <param name="data">
        /// The data to encode.
        /// </param>
        /// <returns>
        /// A character array of the encoded PEM.
        /// </returns>
        /// <remarks>
        /// This method always wraps the base-64 encoded text to 64 characters, per the
        /// recommended wrapping of RFC-7468. Unix-style line endings are used for line breaks.
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">
        ///   <paramref name="label"/> exceeds the maximum possible label length.
        ///   <para>
        ///       -or-
        ///   </para>
        ///   <paramref name="data"/> exceeds the maximum possible encoded data length.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The resulting PEM-encoded text is larger than <see cref="int.MaxValue"/>.
        ///   <para>
        ///       - or -
        ///   </para>
        /// <paramref name="label"/> contains invalid characters.
        /// </exception>
        public static char[] Write(ReadOnlySpan<char> label, ReadOnlySpan<byte> data)
        {
            if (!IsValidLabel(label))
                throw new ArgumentException(SR.Argument_PemEncoding_InvalidLabel, nameof(label));
 
            int encodedSize = GetEncodedSize(label.Length, data.Length);
            char[] buffer = new char[encodedSize];
 
            int charsWritten = WriteCore<char, Utf16PemEncoder>(label, data, buffer);
            Debug.Assert(charsWritten == encodedSize);
            return buffer;
        }
 
        /// <summary>
        ///   Creates an encoded PEM with the given label and data.
        /// </summary>
        /// <param name="label">
        ///   The label to encode.
        /// </param>
        /// <param name="data">
        ///   The data to encode.
        /// </param>
        /// <returns>
        ///   A string of the encoded PEM.
        /// </returns>
        /// <remarks>
        ///   This method always wraps the base-64 encoded text to 64 characters, per the
        ///   recommended wrapping of RFC-7468. Unix-style line endings are used for line breaks.
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">
        ///   <paramref name="label"/> exceeds the maximum possible label length.
        ///
        ///   -or-
        ///
        ///   <paramref name="data"/> exceeds the maximum possible encoded data length.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///   The resulting PEM-encoded text is larger than <see cref="int.MaxValue"/>.
        ///
        ///   - or -
        ///
        ///   <paramref name="label"/> contains invalid characters.
        /// </exception>
        public static unsafe string WriteString(ReadOnlySpan<char> label, ReadOnlySpan<byte> data)
        {
            if (!IsValidLabel(label))
                throw new ArgumentException(SR.Argument_PemEncoding_InvalidLabel, nameof(label));
 
            int encodedSize = GetEncodedSize(label.Length, data.Length);
 
            return string.Create(
                encodedSize,
                (LabelPointer: (IntPtr)(&label), DataPointer: (IntPtr)(&data)),
                static (destination, state) =>
                {
                    ReadOnlySpan<char> label = *(ReadOnlySpan<char>*)state.LabelPointer;
                    ReadOnlySpan<byte> data = *(ReadOnlySpan<byte>*)state.DataPointer;
 
                    int charsWritten = WriteCore<char, Utf16PemEncoder>(label, data, destination);
 
                    if (charsWritten != destination.Length)
                    {
                        Debug.Fail("WriteCore wrote the wrong amount of data");
                        throw new CryptographicException();
                    }
                });
        }
 
        private interface IPemEncoder<TChar> where TChar : IEquatable<TChar>
        {
            static abstract ReadOnlySpan<TChar> PreEBPrefix { get; }
            static abstract ReadOnlySpan<TChar> PostEBPrefix { get; }
            static abstract ReadOnlySpan<TChar> Ending { get; }
            static abstract ReadOnlySpan<TChar> Whitespace { get; }
            static abstract ReadOnlySpan<TChar> NewLine { get; }
            static abstract bool IsValidBase64(ReadOnlySpan<TChar> base64Text, out int decodedLength);
            static abstract int WriteBase64(ReadOnlySpan<byte> bytes, Span<TChar> destination, int offset);
        }
 
        private sealed class Utf16PemEncoder : IPemEncoder<char>
        {
            public static ReadOnlySpan<char> PreEBPrefix => "-----BEGIN ";
            public static ReadOnlySpan<char> PostEBPrefix => "-----END ";
            public static ReadOnlySpan<char> Ending => "-----";
            public static ReadOnlySpan<char> Whitespace => " \t\n\r";
            public static ReadOnlySpan<char> NewLine => "\n";
 
            public static bool IsValidBase64(ReadOnlySpan<char> base64Text, out int decodedLength)
            {
                return Base64.IsValid(base64Text, out decodedLength);
            }
 
            public static int WriteBase64(ReadOnlySpan<byte> bytes, Span<char> destination, int offset)
            {
                bool success = Convert.TryToBase64Chars(bytes, destination.Slice(offset), out int base64Written);
 
                if (!success)
                {
                    Debug.Fail("Convert.TryToBase64Chars failed with a pre-sized buffer");
                    throw new ArgumentException(null, nameof(destination));
                }
 
                return base64Written;
            }
        }
 
        private sealed class Utf8PemEncoder : IPemEncoder<byte>
        {
            public static ReadOnlySpan<byte> PreEBPrefix => "-----BEGIN "u8;
            public static ReadOnlySpan<byte> PostEBPrefix => "-----END "u8;
            public static ReadOnlySpan<byte> Ending => "-----"u8;
            public static ReadOnlySpan<byte> Whitespace => " \t\n\r"u8;
            public static ReadOnlySpan<byte> NewLine => "\n"u8;
 
            public static bool IsValidBase64(ReadOnlySpan<byte> base64Text, out int decodedLength)
            {
                return Base64.IsValid(base64Text, out decodedLength);
            }
 
            public static int WriteBase64(ReadOnlySpan<byte> bytes, Span<byte> destination, int offset)
            {
                OperationStatus status = Base64.EncodeToUtf8(
                    bytes,
                    destination.Slice(offset),
                    out int bytesConsumed,
                    out int bytesWritten);
 
                if (status != OperationStatus.Done)
                {
                    Debug.Fail("Base64.EncodeToUtf8 failed with a pre-sized buffer");
                    throw new ArgumentException(null, nameof(destination));
                }
 
                Debug.Assert(bytesConsumed == bytes.Length);
                return bytesWritten;
            }
        }
    }
}