diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PemEncoding.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PemEncoding.cs index a87acb371078b9..9d419688fc94f4 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PemEncoding.cs +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PemEncoding.cs @@ -40,8 +40,7 @@ internal static bool TryFind(ReadOnlySpan pemData, out PemFields fields) return false; } - const int PostebStackBufferSize = 256; - Span postebStackBuffer = stackalloc char[PostebStackBufferSize]; + ReadOnlySpan hyphen = PostEBPrefix.AsSpan(0, 1); int areaOffset = 0; int preebIndex; while ((preebIndex = pemData.IndexOfByOffset(PreEBPrefix, areaOffset)) >= 0) @@ -78,19 +77,57 @@ internal static bool TryFind(ReadOnlySpan pemData, out PemFields fields) } int contentStartIndex = preebEndIndex + Ending.Length; - int postebLength = PostEBPrefix.Length + label.Length + Ending.Length; - Span postebBuffer = postebLength > PostebStackBufferSize - ? new char[postebLength] - : postebStackBuffer; - ReadOnlySpan posteb = WritePostEB(label, postebBuffer); - int postebStartIndex = pemData.IndexOfByOffset(posteb, contentStartIndex); + // Find the next hyphen. If it's not the start of a PostEB, then it's not our + // PostEB, so we're not a PreEB, so resume searching there. + // + // There's no chance that the PreEB suffix is the start of a different PreEB, + // because IsValidLabel would have returned false if the label ended with whitespace, + // and it's a requirement that the PreEB prefix be preceded by whitespace (or be + // at index 0). + int nextHyphen = pemData.IndexOfByOffset(hyphen, contentStartIndex); + + // No hyphen at all? No PostEB, so we're done. + if (nextHyphen < 0) + { + fields = default; + return false; + } - if (postebStartIndex < 0) + if (!pemData.Slice(nextHyphen).StartsWith(PostEBPrefix)) { + preebEndIndex = nextHyphen; goto NextAfterLabel; } + int postebStartIndex = nextHyphen; + int postebLabelStartIndex = postebStartIndex + PostEBPrefix.Length; + int postebEndIndex = pemData.IndexOfByOffset(Ending, postebLabelStartIndex); + + if (postebEndIndex < 0) + { + fields = default; + return false; + } + + ReadOnlySpan postLabel = pemData[postebLabelStartIndex..postebEndIndex]; + + // If the PostEB label doesn't match, the document looks (at best), like + // + // -----BEGIN SOMELABEL----- + // base64 + // -----END OTHERLABEL----- + // + // We know none of the purported base64 has any other hyphens, so no other PreEB + // could be there. Resume searching at postebEndIndex in case it says + // "-----END SOMELABEL\n-----BEGIN " + if (!postLabel.SequenceEqual(label)) + { + preebEndIndex = postebEndIndex; + goto NextAfterLabel; + } + + int postebLength = PostEBPrefix.Length + label.Length + Ending.Length; int pemEndIndex = postebStartIndex + postebLength; // The PostEB must either end at the end of the string, or @@ -98,6 +135,9 @@ internal static bool TryFind(ReadOnlySpan pemData, out PemFields fields) if (pemEndIndex < pemData.Length - 1 && !IsWhiteSpaceCharacter(pemData[pemEndIndex])) { + // No matches can occur before the alleged post-EB suffix, + // so jump ahead. + preebEndIndex = postebEndIndex; goto NextAfterLabel; } @@ -105,6 +145,9 @@ internal static bool TryFind(ReadOnlySpan pemData, out PemFields fields) if (!TryCountBase64(pemData[contentRange], out int base64start, out int base64end, out int decodedSize)) { + // No matches can occur before the alleged post-EB suffix, + // so jump ahead. + preebEndIndex = postebEndIndex; goto NextAfterLabel; } @@ -128,16 +171,6 @@ internal static bool TryFind(ReadOnlySpan pemData, out PemFields fields) fields = default; return false; - - static ReadOnlySpan WritePostEB(ReadOnlySpan label, Span destination) - { - int size = PostEBPrefix.Length + label.Length + Ending.Length; - Debug.Assert(destination.Length >= size); - PostEBPrefix.AsSpan().CopyTo(destination); - label.CopyTo(destination.Slice(PostEBPrefix.Length)); - Ending.AsSpan().CopyTo(destination.Slice(PostEBPrefix.Length + label.Length)); - return destination.Slice(0, size); - } } private static int IndexOfByOffset(this ReadOnlySpan str, ReadOnlySpan value, int startPosition) diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/PemEncodingFindTests.cs b/src/libraries/Microsoft.Bcl.Cryptography/tests/PemEncodingFindTests.cs index 7c224e6b4f78de..501f7319fa066f 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/PemEncodingFindTests.cs +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/PemEncodingFindTests.cs @@ -429,6 +429,69 @@ public void Find_Success_DecodeSize(string base64, int expectedSize) Assert.Equal(base64, content[fields.Base64Data]); } + [Fact] + public void Find_ManyBegins_OneEnd() + { + const int ContentLength = 4 * 1024 * 1024; + const string GoodPrefix = "-----BEGIN Y-----\n"; + const string MinPayload = "base64AAAA==\n"; + + int div4 = 0; + int mod4 = 0; + int goodStart = 0; + + // Build a string that looks like + // -----BEGIN X----- + // -----BEGIN X----- + // ... + // -----BEGIN X----- + // -----BEGIN Y----- + // [mod4 whitespace][content] + // -----END Y----- + StringBuilder builder = new StringBuilder(ContentLength); + const string Suffix = "-----END Y-----\n"; + const string BadPrefix = "-----BEGIN X-----\n"; + + int tailLength = Suffix.Length + MinPayload.Length; + int reserved = GoodPrefix.Length + tailLength; + int badPrefixCount = (ContentLength - reserved) / BadPrefix.Length; + + for (int i = 0; i < badPrefixCount; i++) + { + builder.Append(BadPrefix); + } + + goodStart = builder.Length; + builder.Append(GoodPrefix); + + int remain = builder.Capacity - builder.Length - tailLength; + div4 = Math.DivRem(remain, 4, out mod4); + + for (int i = 0; i < mod4; i++) + { + builder.Append('\n'); + } + + for (int i = 0; i < div4; i++) + { + builder.Append("AAAA"); + } + + builder.Append(MinPayload); + builder.Append(Suffix); + + string content = builder.ToString(); + + int expectedBase64Start = goodStart + GoodPrefix.Length + mod4; + int expectedBase64Len = 4 * div4 + MinPayload.Length - 1; + + AssertPemFound( + content, + expectedLocation: goodStart..(ContentLength - 1), + expectedBase64: expectedBase64Start..(expectedBase64Start + expectedBase64Len), + expectedLabel: (goodStart + 11)..(goodStart + 12)); + } + private PemFields AssertPemFound( ReadOnlySpan input, Range expectedLocation, diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/PemEncoding.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/PemEncoding.cs index f9bb02b7e91038..c161b076f1205c 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/PemEncoding.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/PemEncoding.cs @@ -140,8 +140,7 @@ private static unsafe bool TryFindCore(ReadOnlySpan pemData, ou return false; } - const int PostebStackBufferSize = 256; - Span postebStackBuffer = stackalloc TChar[PostebStackBufferSize]; + ReadOnlySpan hyphen = T.PostEBPrefix[0..1]; int areaOffset = 0; int preebIndex; while ((preebIndex = pemData.IndexOfByOffset(T.PreEBPrefix, areaOffset)) >= 0) @@ -178,19 +177,57 @@ private static unsafe bool TryFindCore(ReadOnlySpan pemData, ou } int contentStartIndex = preebEndIndex + T.Ending.Length; - int postebLength = T.PostEBPrefix.Length + label.Length + T.Ending.Length; - Span postebBuffer = postebLength > PostebStackBufferSize - ? new TChar[postebLength] - : postebStackBuffer; - ReadOnlySpan posteb = WritePostEB(label, postebBuffer); - int postebStartIndex = pemData.IndexOfByOffset(posteb, contentStartIndex); + // Find the next hyphen. If it's not the start of a PostEB, then it's not our + // PostEB, so we're not a PreEB, so resume searching there. + // + // There's no chance that the PreEB suffix is the start of a different PreEB, + // because IsValidLabel would have returned false if the label ended with whitespace, + // and it's a requirement that the PreEB prefix be preceded by whitespace (or be + // at index 0). + int nextHyphen = pemData.IndexOfByOffset(hyphen, contentStartIndex); + + // No hyphen at all? No PostEB, so we're done. + if (nextHyphen < 0) + { + fields = default; + return false; + } - if (postebStartIndex < 0) + if (!pemData.Slice(nextHyphen).StartsWith(T.PostEBPrefix)) { + preebEndIndex = nextHyphen; goto NextAfterLabel; } + int postebStartIndex = nextHyphen; + int postebLabelStartIndex = postebStartIndex + T.PostEBPrefix.Length; + int postebEndIndex = pemData.IndexOfByOffset(T.Ending, postebLabelStartIndex); + + if (postebEndIndex < 0) + { + fields = default; + return false; + } + + ReadOnlySpan postLabel = pemData[postebLabelStartIndex..postebEndIndex]; + + // If the PostEB label doesn't match, the document looks (at best), like + // + // -----BEGIN SOMELABEL----- + // base64 + // -----END OTHERLABEL----- + // + // We know none of the purported base64 has any other hyphens, so no other PreEB + // could be there. Resume searching at postebEndIndex in case it says + // "-----END SOMELABEL\n-----BEGIN " + if (!postLabel.SequenceEqual(label)) + { + preebEndIndex = postebEndIndex; + goto NextAfterLabel; + } + + int postebLength = T.PostEBPrefix.Length + label.Length + T.Ending.Length; int pemEndIndex = postebStartIndex + postebLength; // The PostEB must either end at the end of the string, or @@ -198,6 +235,9 @@ private static unsafe bool TryFindCore(ReadOnlySpan pemData, ou if (pemEndIndex < pemData.Length - 1 && !IsWhiteSpaceCharacter(pemData[pemEndIndex], T.Whitespace)) { + // No matches can occur before the alleged post-EB suffix, + // so jump ahead. + preebEndIndex = postebEndIndex; goto NextAfterLabel; } @@ -205,6 +245,9 @@ private static unsafe bool TryFindCore(ReadOnlySpan pemData, ou if (!TryCountBase64(pemData[contentRange], out int base64start, out int base64end, out int decodedSize)) { + // No matches can occur before the alleged post-EB suffix, + // so jump ahead. + preebEndIndex = postebEndIndex; goto NextAfterLabel; } @@ -228,16 +271,6 @@ private static unsafe bool TryFindCore(ReadOnlySpan pemData, ou fields = default; return false; - - static ReadOnlySpan WritePostEB(ReadOnlySpan label, Span 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(this ReadOnlySpan str, ReadOnlySpan value, int startPosition) diff --git a/src/libraries/System.Security.Cryptography/tests/PemEncodingFindTests.cs b/src/libraries/System.Security.Cryptography/tests/PemEncodingFindTests.cs index 7b6028251b4afa..e9ed7949b8a75d 100644 --- a/src/libraries/System.Security.Cryptography/tests/PemEncodingFindTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/PemEncodingFindTests.cs @@ -424,6 +424,74 @@ public void Find_Success_DecodeSize(string base64, int expectedSize) AssertExtensions.SequenceEqual(Create(base64), content[fields.Base64Data]); } + [Fact] + public void Find_ManyBegins_OneEnd() + { + const int ContentLength = 4 * 1024 * 1024; + const string GoodPrefix = "-----BEGIN Y-----\n"; + const string MinPayload = "base64AAAA==\n"; + + int div4 = 0; + int mod4 = 0; + int goodStart = 0; + + // Build a string that looks like + // -----BEGIN X----- + // -----BEGIN X----- + // ... + // -----BEGIN X----- + // -----BEGIN Y----- + // [mod4 whitespace][content] + // -----END Y----- + string content = string.Create( + ContentLength, + 0, + (span, _) => + { + string suffix = "-----END Y-----\n"; + ReadOnlySpan badPrefix = "-----BEGIN X-----\n"; + ReadOnlySpan goodPrefix = GoodPrefix; + ReadOnlySpan minPayload = MinPayload; + + int tailLength = suffix.Length + minPayload.Length; + int reserved = goodPrefix.Length + tailLength; + int badPrefixCount = (span.Length - reserved) / badPrefix.Length; + + for (int i = 0; i < badPrefixCount; i++) + { + badPrefix.CopyTo(span); + span = span.Slice(badPrefix.Length); + } + + goodStart = ContentLength - span.Length; + goodPrefix.CopyTo(span); + span = span.Slice(goodPrefix.Length); + + int remain = span.Length - tailLength; + (div4, mod4) = int.DivRem(remain, 4); + + span.Slice(0, mod4).Fill('\n'); + span = span.Slice(mod4); + + span.Slice(0, 4 * div4).Fill('A'); + span = span.Slice(4 * div4); + + minPayload.CopyTo(span); + span = span.Slice(minPayload.Length); + + suffix.CopyTo(span); + }); + + int expectedBase64Start = goodStart + GoodPrefix.Length + mod4; + int expectedBase64Len = 4 * div4 + MinPayload.Length - 1; + + AssertPemFound( + content, + expectedLocation: goodStart .. (ContentLength - 1), + expectedBase64: expectedBase64Start .. (expectedBase64Start + expectedBase64Len), + expectedLabel: (goodStart + 11) .. (goodStart + 12)); + } + private PemFields AssertPemFound( string input, Range expectedLocation,