using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; using BinaryObjectScanner.Utilities; namespace BinaryObjectScanner.ASN1 { /// /// ASN.1 type/length/value class that all types are based on /// public class TypeLengthValue { /// /// 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 TypeLengthValue(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 TypeLengthValue(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 TypeLengthValue[]; 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(); } } } }