using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; namespace BurnOutSharp.Builder { /// /// ASN.1 type indicators /// [Flags] public enum ASN1Type : byte { #region Modifiers V_ASN1_UNIVERSAL = 0x00, V_ASN1_PRIMITIVE_TAG = 0x1F, V_ASN1_CONSTRUCTED = 0x20, V_ASN1_APPLICATION = 0x40, V_ASN1_CONTEXT_SPECIFIC = 0x80, V_ASN1_PRIVATE = 0xC0, #endregion #region Types V_ASN1_EOC = 0x00, V_ASN1_BOOLEAN = 0x01, V_ASN1_INTEGER = 0x02, V_ASN1_BIT_STRING = 0x03, V_ASN1_OCTET_STRING = 0x04, V_ASN1_NULL = 0x05, V_ASN1_OBJECT = 0x06, V_ASN1_OBJECT_DESCRIPTOR = 0x07, V_ASN1_EXTERNAL = 0x08, V_ASN1_REAL = 0x09, V_ASN1_ENUMERATED = 0x0A, V_ASN1_UTF8STRING = 0x0C, V_ASN1_SEQUENCE = 0x10, V_ASN1_SET = 0x11, V_ASN1_NUMERICSTRING = 0x12, V_ASN1_PRINTABLESTRING = 0x13, V_ASN1_T61STRING = 0x14, V_ASN1_TELETEXSTRING = 0x14, V_ASN1_VIDEOTEXSTRING = 0x15, V_ASN1_IA5STRING = 0x16, V_ASN1_UTCTIME = 0x17, V_ASN1_GENERALIZEDTIME = 0x18, V_ASN1_GRAPHICSTRING = 0x19, V_ASN1_ISO64STRING = 0x1A, V_ASN1_VISIBLESTRING = 0x1A, V_ASN1_GENERALSTRING = 0x1B, V_ASN1_UNIVERSALSTRING = 0x1C, V_ASN1_BMPSTRING = 0x1E, #endregion } /// /// ASN.1 Parser /// public class AbstractSyntaxNotationOne { /// /// Parse a byte array into a DER-encoded ASN.1 structure /// /// Byte array representing the data /// Current pointer into the data /// public static List Parse(byte[] data, int pointer) { // Create the output list to return var topLevelValues = new List(); // Loop through the data and return all top-level values while (pointer < data.Length) { var topLevelValue = new ASN1TypeLengthValue(data, ref pointer); topLevelValues.Add(topLevelValue); } return topLevelValues; } } /// /// ASN.1 type/length/value class that all types are based on /// public class ASN1TypeLengthValue { /// /// The ASN.1 type /// public ASN1Type Type { get; private set; } /// /// Length of the value /// public ulong Length { get; private set; } /// /// Generic value associated with /// public object Value { get; private set; } /// /// Read from the source data array at an index /// /// Byte array representing data to read /// Index within the array to read at public ASN1TypeLengthValue(byte[] data, ref int index) { // Get the type and modifiers this.Type = (ASN1Type)data[index++]; // If we have an end indicator, we just return if (this.Type == ASN1Type.V_ASN1_EOC) return; // Get the length of the value this.Length = ReadLength(data, ref index); // Read the value if (this.Type.HasFlag(ASN1Type.V_ASN1_CONSTRUCTED)) { var valueList = new List(); int currentIndex = index; while (index < currentIndex + (int)this.Length) { valueList.Add(new ASN1TypeLengthValue(data, ref index)); } this.Value = valueList.ToArray(); } else { // TODO: Get more granular based on type this.Value = data.ReadBytes(ref index, (int)this.Length); } } /// /// Format the TLV as a string /// /// Padding level of the item when formatting /// String representing the TLV, if possible public string Format(int paddingLevel = 0) { // Create the left-padding string string padding = new string(' ', paddingLevel); // If we have an invalid item if (this.Type == 0) return $"{padding}UNKNOWN TYPE"; // Create the string builder StringBuilder formatBuilder = new StringBuilder(); // Append the type formatBuilder.Append($"{padding}Type: {this.Type}"); if (this.Type == ASN1Type.V_ASN1_EOC) return formatBuilder.ToString(); // Append the length formatBuilder.Append($", Length: {this.Length}"); if (this.Length == 0) return formatBuilder.ToString(); // If we have a constructed type if (this.Type.HasFlag(ASN1Type.V_ASN1_CONSTRUCTED)) { var valueAsObjectArray = this.Value as ASN1TypeLengthValue[]; if (valueAsObjectArray == null) { formatBuilder.Append(", Value: [INVALID DATA TYPE]"); return formatBuilder.ToString(); } formatBuilder.Append(", Value:\n"); for (int i = 0; i < valueAsObjectArray.Length; i++) { var child = valueAsObjectArray[i]; string childString = child.Format(paddingLevel + 1); formatBuilder.Append($"{childString}\n"); } return formatBuilder.ToString().TrimEnd('\n'); } // Get the value as a byte array byte[] valueAsByteArray = this.Value as byte[]; if (valueAsByteArray == null) { formatBuilder.Append(", Value: [INVALID DATA TYPE]"); return formatBuilder.ToString(); } // If we have a primitive type switch (this.Type) { /// case ASN1Type.V_ASN1_BOOLEAN: if (this.Length > 1 || valueAsByteArray.Length > 1) formatBuilder.Append($" [Expected length of 1]"); bool booleanValue = valueAsByteArray[0] == 0x00 ? false : true; formatBuilder.Append($", Value: {booleanValue}"); break; /// case ASN1Type.V_ASN1_INTEGER: Array.Reverse(valueAsByteArray); BigInteger integerValue = new BigInteger(valueAsByteArray); formatBuilder.Append($", Value: {integerValue}"); break; /// case ASN1Type.V_ASN1_BIT_STRING: // TODO: Read into a BitArray and print that out instead? int unusedBits = valueAsByteArray[0]; formatBuilder.Append($", Value with {unusedBits} unused bits: {BitConverter.ToString(valueAsByteArray.Skip(1).ToArray()).Replace('-', ' ')}"); break; /// case ASN1Type.V_ASN1_OCTET_STRING: formatBuilder.Append($", Value: {BitConverter.ToString(valueAsByteArray).Replace('-', ' ')}"); break; /// /// case ASN1Type.V_ASN1_OBJECT: // Derive array of values ulong[] objectNodes = ObjectIdentifier.ParseDERIntoArray(valueAsByteArray, this.Length); // Append the dot and modified OID-IRI notations string dotNotationString = ObjectIdentifier.ParseOIDToDotNotation(objectNodes); string oidIriString = ObjectIdentifier.ParseOIDToOIDIRINotation(objectNodes); formatBuilder.Append($", Value: {dotNotationString} ({oidIriString})"); break; /// case ASN1Type.V_ASN1_UTF8STRING: formatBuilder.Append($", Value: {Encoding.UTF8.GetString(valueAsByteArray)}"); break; /// case ASN1Type.V_ASN1_PRINTABLESTRING: formatBuilder.Append($", Value: {Encoding.ASCII.GetString(valueAsByteArray)}"); break; //case ASN1Type.V_ASN1_T61STRING: case ASN1Type.V_ASN1_TELETEXSTRING: formatBuilder.Append($", Value: {Encoding.ASCII.GetString(valueAsByteArray)}"); break; /// case ASN1Type.V_ASN1_IA5STRING: formatBuilder.Append($", Value: {Encoding.ASCII.GetString(valueAsByteArray)}"); break; case ASN1Type.V_ASN1_UTCTIME: string utctimeString = Encoding.ASCII.GetString(valueAsByteArray); if (DateTime.TryParse(utctimeString, out DateTime utctimeDateTime)) formatBuilder.Append($", Value: {utctimeDateTime}"); else formatBuilder.Append($", Value: {utctimeString}"); break; /// case ASN1Type.V_ASN1_BMPSTRING: formatBuilder.Append($", Value: {Encoding.Unicode.GetString(valueAsByteArray)}"); break; default: formatBuilder.Append($", Value (Unknown Format): {BitConverter.ToString(this.Value as byte[]).Replace('-', ' ')}"); break; } // Return the formatted string return formatBuilder.ToString(); } /// /// Reads the length field for a type /// /// Byte array representing data to read /// Index within the array to read at /// The length value read from the array private static ulong ReadLength(byte[] data, ref int index) { // If we have invalid data, throw an exception if (data == null || index < 0 && index >= data.Length) throw new ArgumentException(); // Read the first byte, assuming it's the length byte length = data[index++]; // If the bit 7 is not set, then use the value as it is if ((length & 0x80) == 0) return length; // Otherwise, use the value as the number of remaining bytes to read int bytesToRead = length & ~0x80; byte[] bytesRead = data.ReadBytes(ref index, bytesToRead); // TODO: Write extensions to read big-endian // Reverse the bytes to be in big-endian order Array.Reverse(bytesRead); switch (bytesRead.Length) { case 1: return bytesRead[0]; case 2: return BitConverter.ToUInt16(bytesRead, 0); case 3: Array.Resize(ref bytesRead, 4); goto case 4; case 4: return BitConverter.ToUInt32(bytesRead, 0); case 5: case 6: case 7: Array.Resize(ref bytesRead, 8); goto case 8; case 8: return BitConverter.ToUInt64(bytesRead, 0); default: throw new InvalidOperationException(); } } } }