Skip to content

Commit 6a3239a

Browse files
committed
Significantly reworked Base64 to string conversion which addresses #24 and clarifies the behavior of conversion rules and expectations. Added unit tests to cover AsnFormatter.TestInputString for Base64 encodings
1 parent 8ef4aa7 commit 6a3239a

3 files changed

Lines changed: 128 additions & 36 deletions

File tree

Asn1Parser/AsnFormatter.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,18 +100,17 @@ public static String BinaryToString(Asn1Reader asn, EncodingType encoding = Enco
100100
/// <see cref="BinaryToString(ReadOnlySpan{Byte}, EncodingType, EncodingFormat, Boolean)">BinaryToString</see>
101101
/// method.
102102
/// <para>
103-
/// If encoding parameter is set to <strong>Base64Header</strong>, the method will accept any properly formatted PEM header
103+
/// If <strong>encoding</strong> parameter is set to <strong>Base64Header</strong>, the method will accept any PEM header
104104
/// and footer.
105105
/// </para>
106106
/// </remarks>
107107
public static Byte[] StringToBinary(String input, EncodingType encoding = EncodingType.Base64) {
108108
Byte[]? rawData;
109109
if (PemHeader.ContainsEncoding(encoding)) {
110110
var pemHeader = PemHeader.GetHeader(encoding);
111-
rawData = StringToBinaryFormatter.FromBase64Header(input, pemHeader.GetHeader(), pemHeader.GetFooter());
111+
rawData = StringToBinaryFormatter.FromBase64Header(input, pemHeader.Header);
112112
} else {
113113
rawData = encoding switch {
114-
115114
EncodingType.Binary => StringToBinaryFormatter.FromBinary(input),
116115
EncodingType.Base64 => StringToBinaryFormatter.FromBase64(input),
117116
EncodingType.Base64Any => StringToBinaryFormatter.FromBase64Any(input),
@@ -139,15 +138,20 @@ public static Byte[] StringToBinary(String input, EncodingType encoding = Encodi
139138
/// <returns>
140139
/// Resolved input string format. If format cannot be determined, <string>Binary</string> type is returned.
141140
/// </returns>
141+
/// <remarks>
142+
/// Method returns <strong>Base64Header</strong> when input string contains
143+
/// <c>-----BEGIN ...-----</c> PEM header and <c>-----END ...-----</c> PEM footer which is not
144+
/// supported by <see cref="EncodingType"/> enumeration.
145+
/// </remarks>
142146
public static EncodingType TestInputString(String input) {
143147
Byte[]? rawBytes;
144148
foreach (PemHeader pemHeader in PemHeader.GetPemHeaders()) {
145-
rawBytes = StringToBinaryFormatter.FromBase64Header(input, pemHeader.GetHeader(), pemHeader.GetFooter());
149+
rawBytes = StringToBinaryFormatter.FromBase64Header(input, pemHeader.Header);
146150
if (rawBytes is not null) {
147151
return pemHeader.Encoding;
148152
}
149153
}
150-
rawBytes = StringToBinaryFormatter.FromBase64Header(input);
154+
rawBytes = StringToBinaryFormatter.FromBase64Header(input, String.Empty, true);
151155
if (rawBytes is not null) {
152156
return EncodingType.Base64Header;
153157
}

Asn1Parser/StringToBinaryFormatter.cs

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,92 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq;
43

54
namespace SysadminsLV.Asn1Parser;
65
static class StringToBinaryFormatter {
76
static readonly Char[] _delimiters = [' ', '-', ':', '\t', '\n', '\r'];
87

8+
/// <summary>
9+
/// Converts the specified string, which encodes binary data as base-64 digits, to
10+
/// an equivalent 8-bit unsigned integer array.
11+
/// </summary>
12+
/// <param name="input">The string to convert.</param>
13+
/// <returns>An array of 8-bit unsigned integers that is equivalent to <strong>input</strong>.</returns>
914
public static Byte[]? FromBase64(String input) {
1015
try {
1116
return Convert.FromBase64String(input.Trim());
1217
} catch {
1318
return null;
1419
}
1520
}
16-
// accept any header, not only certificate
17-
public static Byte[]? FromBase64Header(String input) {
18-
const String header = "-----BEGIN ";
19-
const String footer = "-----END ";
2021

21-
return FromBase64Header(input, header, footer, true);
22-
}
23-
public static Byte[]? FromBase64Header(String input, String header, String footer, Boolean skipHeaderValidation = false) {
24-
if (skipHeaderValidation && (!input.ToUpper().Contains(header) || !input.Contains(footer))) {
22+
/// <summary>
23+
/// Attempts to convert the specified string, which encodes binary data as base-64 digits with PEM header and footer,
24+
/// to an equivalent 8-bit unsigned integer array.
25+
/// </summary>
26+
/// <param name="input">The string to convert.</param>
27+
/// <param name="headerName">
28+
/// Specifies the header name. This parameter MUS NOT include 5-dash sequence and BEGIN/END constants.
29+
/// This parameter is ignored if <strong>skipHeaderValidation</strong> parameter is set to <c>true</c>.
30+
/// </param>
31+
/// <param name="skipHeaderValidation">
32+
/// Skips strict PEM header and footer check. By default, this method checks for exact PEM header and footer
33+
/// specified in <strong>header</strong> parameter, which includes only name, without fixed (5-dash sequences,
34+
/// BEGIN and END constants).
35+
/// <para>
36+
/// When this parameter is set to <c>true</c>, then <strong>headerName</strong> parameter is ignored and method will
37+
/// attempt to find any properly PEM formatted header and footer, even if PEM header and footer names don't match.
38+
/// For example, the following sequence would also be considered valid:
39+
/// <code>
40+
/// -----BEGIN CERTIFICATE-----
41+
/// MIIDBjCCAm8CAQAwcTERMA8GA1UEAxMIcXV1eC5jb20xDzANBgNVBAsTBkJyYWlu
42+
/// czEWMBQGA1UEChMNRGV2ZWxvcE1lbnRvcjERMA8GA1UEBxMIVG9ycmFuY2UxEzAR
43+
/// BgNVBAgTCkNhbGlmb3JuaWExCzAJBgNVBAYTAlVTMIGfMA0GCSqGSIb3DQEBAQUA
44+
/// &lt;...&gt;
45+
/// -----END X509 CRL-----
46+
/// </code>
47+
/// </para>
48+
/// </param>
49+
/// <returns>An array of 8-bit unsigned integers that is equivalent to input, or <c>null</c> if no valid PEM header was found.</returns>
50+
public static Byte[]? FromBase64Header(String input, String headerName, Boolean skipHeaderValidation = false) {
51+
String header, footer;
52+
if (skipHeaderValidation) {
53+
header = "-----BEGIN ";
54+
footer = "-----END ";
55+
} else {
56+
header = $"-----BEGIN {headerName.Trim()}-----";
57+
footer = $"-----END {headerName.Trim()}-----";
58+
}
59+
// search for header (uppercase) start position
60+
Int32 start = input.IndexOf(header, StringComparison.Ordinal);
61+
if (start < 0) {
62+
// if we don't find header, then it is not valid PEM header. End here.
2563
return null;
2664
}
27-
Int32 start = input.IndexOf(header, StringComparison.Ordinal) + 10;
28-
Int32 headerEndPos = input.IndexOf("-----", start, StringComparison.Ordinal) + 5;
29-
Int32 footerStartPos = input.IndexOf(footer, StringComparison.Ordinal);
65+
Int32 headerEndPos = skipHeaderValidation
66+
// 10 is the length of mandatory '-----BEGIN' header part. Seek after mandatory part and look where
67+
// terminating 5-dash sequence begins and append the length of 5-dash. This is the end of header and
68+
// where body begins.
69+
? input.IndexOf("-----", start + 10, StringComparison.Ordinal) + 5
70+
: start + header.Length;
71+
// search for footer starting with header end position. Empty
72+
Int32 footerStartPos = input.IndexOf(footer, headerEndPos, StringComparison.Ordinal);
73+
if (footerStartPos < 0) {
74+
// we found PEM header, but no PEM footer, or the order is wrong (footer defined before header)
75+
return null;
76+
}
77+
3078
try {
3179
return Convert.FromBase64String(input.Substring(headerEndPos, footerStartPos - headerEndPos));
3280
} catch {
3381
return null;
3482
}
3583
}
36-
public static Byte[]? FromBase64Request(String input) {
37-
String header;
38-
String footer;
39-
if (input.ToUpper().Contains(PemHeader.PEM_HEADER_REQ_NEW.GetHeader())) {
40-
header = PemHeader.PEM_HEADER_REQ_NEW.GetHeader();
41-
footer = PemHeader.PEM_HEADER_REQ_NEW.GetFooter();
42-
} else if (input.ToUpper().Contains(PemHeader.PEM_HEADER_REQ.GetHeader())) {
43-
header = PemHeader.PEM_HEADER_REQ.GetHeader();
44-
footer = PemHeader.PEM_HEADER_REQ.GetFooter();
45-
} else {
46-
return null;
47-
}
48-
49-
return FromBase64Header(input, header, footer, true);
50-
}
84+
/// <summary>
85+
/// Attempts to convert input string into a 8bit byte array. This method converts each character of input
86+
/// string to its 8bit numerical representation.
87+
/// </summary>
88+
/// <param name="input">String to convert.</param>
89+
/// <returns>Decoded byte array. This method returns <c>null</c> if input string contains non-8bit characters.</returns>
5190
public static Byte[]? FromBinary(String input) {
5291
Byte[] rawBytes = new Byte[input.Length];
5392
for (Int32 i = 0; i < input.Length; i++) {
@@ -321,11 +360,29 @@ static class StringToBinaryFormatter {
321360
return bytes.ToArray();
322361
}
323362

363+
/// <summary>
364+
/// Attempts to convert the specified string, which encodes binary data as base64 digits with
365+
/// or without PEM header and footer, to an equivalent 8-bit unsigned integer array.
366+
/// </summary>
367+
/// <param name="input">Base64 formatted string with optional PEM header and footer.</param>
368+
/// <returns>
369+
/// An array of 8-bit unsigned integers that is equivalent to input, or <c>null</c> if input string doesn't represent
370+
/// valid Base64 or PEM formatted string.
371+
/// </returns>
324372
public static Byte[]? FromBase64Any(String input) {
325-
return FromBase64Header(input) ?? FromBase64(input);
373+
if (input.Contains("-----BEGIN ")) {
374+
// if input string contains PEM begin sequence, then try only Base64 with header and footer
375+
// without strict validation.
376+
return FromBase64Header(input, String.Empty, true);
377+
}
378+
// if there is no PEM found, then try raw base64.
379+
return FromBase64(input);
326380
}
327381
public static Byte[] FromStringAny(String input) {
328-
return FromBase64Header(input) ?? FromBase64(input) ?? input.Select(Convert.ToByte).ToArray();
382+
if (input.Contains("-----BEGIN ")) {
383+
return FromBase64Header(input, String.Empty, true);
384+
}
385+
return FromBase64(input) ?? FromBinary(input);
329386
}
330387
public static Byte[]? FromHexAny(String input) {
331388
return FromHexAddr(input) ??

tests/Asn1Parser.Tests/Base64StringToBinaryTests.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,23 @@ public void TestBase64ToBinary() {
5050
[TestMethod]
5151
public void TestBase64HeaderToBinaryStrictValid() {
5252
foreach (EncodingType encoding in _b64Encodings) {
53-
Byte[] actual = AsnFormatter.StringToBinary(AsnFormatter.BinaryToString(_rawData, encoding), encoding);
53+
String input = AsnFormatter.BinaryToString(_rawData, encoding);
54+
Byte[] actual = AsnFormatter.StringToBinary(input, encoding);
55+
56+
EncodingType expected;
57+
switch (encoding) {
58+
case EncodingType.Base64Header:
59+
expected = EncodingType.PemCert;
60+
break;
61+
case EncodingType.Base64RequestHeader:
62+
expected = EncodingType.PemNewReq;
63+
break;
64+
default:
65+
expected = encoding;
66+
break;
67+
}
68+
EncodingType suggestedEncoding = AsnFormatter.TestInputString(input);
69+
Assert.AreEqual(expected, suggestedEncoding);
5470
validateBinary(actual);
5571
}
5672
}
@@ -70,5 +86,20 @@ void validateBinary(Byte[] actual) {
7086
Assert.IsTrue(_rawData.SequenceEqual(actual));
7187
}
7288

73-
89+
[TestMethod]
90+
public void TestMismatchHeaderAndFooter() {
91+
String pem = $"""
92+
-----BEGIN CERTIFICATE-----
93+
{_base64}
94+
-----END PKCS7-----
95+
""";
96+
EncodingType encoding = AsnFormatter.TestInputString(pem);
97+
Assert.AreEqual(EncodingType.Base64Header, encoding);
98+
}
99+
[TestMethod]
100+
public void TestInvalidBase64() {
101+
String base64 = "Xblue";
102+
EncodingType encoding = AsnFormatter.TestInputString(base64);
103+
Assert.AreEqual(EncodingType.Binary, encoding);
104+
}
74105
}

0 commit comments

Comments
 (0)