using System; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using static System.BitConverter; namespace UFiber.Configurator { public class NVRAM { private const int NvRamCrcOffset = 0x3FC; private const int NvRamCrcLength = 4; private const uint NvRamOffset = 0x580; private const uint NvRamLength = 0x400; private static readonly Regex _snMatcher = new Regex(@"^(([a-fA-F0-9]){2}-){7}([a-fA-F0-9]){2}$"); private static readonly Regex _pwMatcher = new Regex(@"^(([a-fA-F0-9]){2}-){9}([a-fA-F0-9]){2}$"); public uint Checksum { get; private set; } public byte[] AfeId { get; } public byte[] VoiceBoardId { get; } public uint NandPartSizeKb { get; } public uint NandPartOfsKb { get; } public uint SyslogSize { get; } public byte[] WLanParams { get; } public byte[] WpsDevPin { get; } public string GponPassword { get; private set; } public byte[] GponVendorId { get; private set; } public byte[] GponVendorSerialNumber { get; private set; } public uint OldCheckSum { get; } public byte[] BaseMacAddr { get; private set; } public uint Version { get; } public uint NumMacAddr { get; } public uint PsiSize { get; } public uint MainThread { get; } public byte[] BoardId { get; } public byte[] BootLine { get; } private readonly byte[] _data; private readonly int _offset; private readonly int _length; public NVRAM(ReadOnlySpan data, uint offset = NvRamOffset, uint length = NvRamLength) { _data = new byte[data.Length]; data.CopyTo(_data.AsSpan()); _offset = (int)offset; _length = (int)length; Version = GetUint(0); BootLine = _data[OffsetOf(0x4)..OffsetOf(0x104)]; BoardId = _data[OffsetOf(0x104)..OffsetOf(0x114)]; MainThread = GetUint(0x114); PsiSize = GetUint(0x118); NumMacAddr = GetUint(0x11C); BaseMacAddr = _data[OffsetOf(0x120)..OffsetOf(0x126)]; OldCheckSum = GetUint(0x128); GponVendorId = _data[OffsetOf(0x12C)..OffsetOf(0x130)]; GponVendorSerialNumber = _data[OffsetOf(0x130)..OffsetOf(0x139)]; GponPassword = Encoding.UTF8.GetString(_data[OffsetOf(0x139)..OffsetOf(0x144)]); WpsDevPin = _data[OffsetOf(0x144)..OffsetOf(0x14C)]; WLanParams = _data[OffsetOf(0x14C)..OffsetOf(0x24C)]; SyslogSize = GetUint(0x24C); NandPartOfsKb = GetUint(0x250); NandPartSizeKb = GetUint(0x264); VoiceBoardId = _data[OffsetOf(0x278)..OffsetOf(0x288)]; AfeId = _data[OffsetOf(0x288)..OffsetOf(0x290)]; Checksum = GetUint(NvRamCrcOffset); var crcIndex = _offset + NvRamCrcOffset; _data[crcIndex++] = 0; _data[crcIndex++] = 0; _data[crcIndex++] = 0; _data[crcIndex] = 0; var crc = CRC32.GenerateCrc(_data.AsSpan(_offset, _length)); if (crc != Checksum) { throw new InvalidOperationException("Invalid data, CRC doesn't match"); } } public void SetBaseMacAddress(string mac) { var data = AsBytes(mac); SetBaseMacAddress(data); } public void SetBaseMacAddress(ReadOnlySpan mac) { const int MacRelativeOffset = 0x120; const int MacLength = 6; if (mac.Length != MacLength) { throw new ArgumentOutOfRangeException(nameof(mac)); } BaseMacAddr = SetBytes(mac, MacRelativeOffset, MacLength, false); } public void SetGponId(string id) { SetGponId(Encoding.UTF8.GetBytes(id)); } public void SetGponId(ReadOnlySpan id) { const int VendorIdRelativeOffset = 0x12C; const int VendorIdLength = 4; if (id.Length != VendorIdLength) { throw new ArgumentOutOfRangeException(nameof(id)); } GponVendorId = SetBytes(id, VendorIdRelativeOffset, VendorIdLength); } public void SetGponSerialNumber(string serialNumber) { if (_snMatcher.IsMatch(serialNumber)) { this.SetGponId(AsBytes(serialNumber.Replace("-", "")[0..8])); SetGponSerialNumber(Encoding.UTF8.GetBytes(serialNumber.Replace("-", "")[8..])); } else { if (serialNumber.Contains("-")) throw new InvalidOperationException($"Invalid serial number: {serialNumber}."); SetGponSerialNumber(Encoding.UTF8.GetBytes(serialNumber)); } } public void SetGponSerialNumber(ReadOnlySpan serialNumber) { const int VendorSnRelativeOffset = 0x130; const int VendorSnLength = 8; if (serialNumber.Length != VendorSnLength) { throw new ArgumentOutOfRangeException(nameof(serialNumber)); } GponVendorSerialNumber = SetBytes(serialNumber, VendorSnRelativeOffset, VendorSnLength); } public void SetGponPassword(string password) { const int PasswordOffset = 0x139; const int MaxPasswordLength = 10; if (password == null) { throw new ArgumentNullException(nameof(password)); } if (password.Length > MaxPasswordLength) { throw new ArgumentOutOfRangeException(nameof(password)); } byte[] bits = default!; if (_pwMatcher.IsMatch(password)) { password = password.Replace("-", "").PadLeft(MaxPasswordLength, '0'); bits = AsBytes(password); } else { password = password.PadLeft(MaxPasswordLength, '0'); bits = Encoding.UTF8.GetBytes(password); } SetBytes(bits.AsSpan(), PasswordOffset, password.Length); GponPassword = password; } public byte[] CompletePatch() { var bits = _data.AsSpan(_offset, _length); Checksum = CRC32.GenerateCrc(bits); var bitsIndex = NvRamCrcOffset; bits[bitsIndex++] = (byte)(Checksum >> 24); bits[bitsIndex++] = (byte)((Checksum >> 16) & 0xFF); bits[bitsIndex++] = (byte)((Checksum >> 8) & 0xFF); bits[bitsIndex] = (byte)(Checksum & 0xFF); return _data; } public override string ToString() { var sb = new StringBuilder(); sb.AppendLine("--- NVRAM Information --"); sb.AppendLine($"- mtdblock3 hash: {ComputeSha256Hash(this._data)}"); sb.AppendLine($"- NVRAM Version: {this.Version}"); sb.AppendLine($"- Boot parameters: {UnsafeAsciiBytesToString(this.BootLine)}"); sb.AppendLine($"- Board Id: {UnsafeAsciiBytesToString(this.BoardId)}"); sb.AppendLine($"- PSI size: {this.PsiSize}"); sb.AppendLine($"- Total MAC addresses: {this.NumMacAddr}"); sb.AppendLine($"- GPON MAC address: { BitConverter.ToString(this.BaseMacAddr).Replace("-", ":")}"); sb.AppendLine($"- GPON Vendor Id: {Encoding.UTF8.GetString(this.GponVendorId)}"); sb.AppendLine($"- GPON Serial Number: {UnsafeAsciiBytesToString(this.GponVendorSerialNumber)}"); sb.AppendLine($"- GPON SLID (password): {this.GponPassword.TrimEnd((char)0)}"); sb.AppendLine($"- Checksum: {this.Checksum}"); return sb.ToString(); } public static string UnsafeAsciiBytesToString(byte[] buffer) { unsafe { fixed (byte* pAscii = buffer) { return new String((sbyte*)pAscii); } } } private int OffsetOf(int value) => _offset + value; private int OffsetOf(int value1, int value2) => _offset + value1 + value2; private byte[] SetBytes(ReadOnlySpan source, int relativeOffset, int length, bool zeroTerminate = true) { var dataIndex = _offset + relativeOffset; var dataEnd = dataIndex + length; var sourceIndex = 0; while (dataIndex < dataEnd) { _data[dataIndex++] = source[sourceIndex++]; } if (zeroTerminate) { _data[dataIndex] = 0; } return _data[OffsetOf(relativeOffset)..OffsetOf(relativeOffset, length)]; } private static byte[] AsBytes(string data) { if (data == null) { throw new ArgumentNullException(nameof(data)); } if (data.Length % 2 != 0) { throw new ArgumentException("Data must be an even length.", nameof(data)); } data = data.ToLower(); var len = data.Length / 2; var bits = new byte[len]; var bitsIndex = 0; var dataIndex = 0; while (dataIndex < data.Length) { var highNibble = FromHex(data[dataIndex++]); var lowNibble = FromHex(data[dataIndex++]); bits[bitsIndex++] = (byte)(highNibble << 4 | lowNibble); } return bits; static int FromHex(char c) => c switch { >= 'a' and <= 'f' => 10 + (c - 'a'), >= '0' and <= '9' => c - '0', _ => throw new ArgumentOutOfRangeException(nameof(c)) }; } private static string ComputeSha256Hash(byte[] rawData) { using (SHA256 sha256Hash = SHA256.Create()) { byte[] bytes = sha256Hash.ComputeHash(rawData); StringBuilder builder = new StringBuilder(); for (int i = 0; i < bytes.Length; i++) { builder.Append(bytes[i].ToString("x2")); } return builder.ToString(); } } private uint GetUint(int offset) { var bits = _data.AsSpan(OffsetOf(offset), 4); return (uint)(bits[0] << 24 | bits[1] << 16 | bits[2] << 8 | bits[3]); } } }