From b9d8027cf6acd10010c481d27423de01a4926cb4 Mon Sep 17 00:00:00 2001 From: Cyd Date: Tue, 30 Jun 2026 08:15:17 -0500 Subject: [PATCH] Extract shared contract, drop BinaryFormatter wire, modernize to net8/x64 Make the Console<->Launcher system source-built and modern now that the console is under our control and the WinXP-era pods are gone. Contract extraction (Contract/Tesla.Contract.csproj): - One multi-targeted (net48;net8.0-windows) source project for the RPC contract, replacing the vendored TeslaConsoleLaunchLib.dll and the hand-synced Tesla.Net replica in Launcher/LaunchModels_Shared.cs. Emits assembly TeslaConsoleLaunchLib. SecureConfig extraction (SecureConfig/Tesla.SecureConfig.csproj): - net48 source of the first-boot provisioning protocol (UDP beacons, OFB crypto, RSA key exchange), replacing the vendored TeslaSecureConfiguration.dll. Remove BinaryFormatter from the wire (RCE sink + the reason net6 was pinned): - Console<->Launcher RPC is now length-prefixed System.Text.Json frames (Contract/PodRpcProtocol.cs) over the unchanged OFB transport; dispatch by method name. Deleted the SerializationBinder / MethodInfoProxy machinery. - Console-local BinaryFormatter (Site config, mission replays) intentionally retained: local net48 file I/O, not the network surface. Runtime modernization: - Launcher Service + Agent: net6 -> net8, win-x86 -> win-x64 (all pods are 64-bit Win10). Kept the SHA1-default PBKDF2 (Console key-derivation compat) with SYSLIB0041 suppressed and documented. Tests: differential suite now 73 green. Added SecureConfigCompatTests (OFB ciphertext byte-identical to the vendored DLL) and PodRpcProtocolTests (JSON round-trip of every request/response shape); removed the now-obsolete BinaryFormatter byte-identity guard. Build hygiene: per-project obj dirs (Launcher/Directory.Build.props) fix a NuGet restore collision between the two Launcher projects sharing one folder. NOT runtime-verified against a live pod. Co-Authored-By: Claude Opus 4.8 --- Console/README.md | 47 +- Console/TeslaConsole.csproj | 16 +- .../PodRpcProtocolTests.cs | 156 +++ .../SecureConfigCompatTests.cs | 147 +++ .../TeslaConsole.DiffTests.csproj | 7 + Contract/Client/MockPodManagerConnection.cs | 177 +++ Contract/Client/PodManagerConnection.cs | 276 +++++ Contract/PodRpcProtocol.cs | 120 ++ Contract/Tesla.Contract.csproj | 47 + Contract/WireContract.cs | 176 +++ Launcher/Directory.Build.props | 23 + Launcher/LaunchModels_Shared.cs | 105 +- Launcher/SecureConfig.cs | 9 +- Launcher/TeslaLauncherAgent.csproj | 9 +- Launcher/TeslaLauncherService.cs | 359 ++---- Launcher/TeslaLauncherService.csproj | 25 +- SecureConfig/SecureConfig.cs | 1079 +++++++++++++++++ SecureConfig/Tesla.SecureConfig.csproj | 28 + TeslaSuite.sln | 28 +- 19 files changed, 2441 insertions(+), 393 deletions(-) create mode 100644 Console/tests/TeslaConsole.DiffTests/PodRpcProtocolTests.cs create mode 100644 Console/tests/TeslaConsole.DiffTests/SecureConfigCompatTests.cs create mode 100644 Contract/Client/MockPodManagerConnection.cs create mode 100644 Contract/Client/PodManagerConnection.cs create mode 100644 Contract/PodRpcProtocol.cs create mode 100644 Contract/Tesla.Contract.csproj create mode 100644 Contract/WireContract.cs create mode 100644 Launcher/Directory.Build.props create mode 100644 SecureConfig/SecureConfig.cs create mode 100644 SecureConfig/Tesla.SecureConfig.csproj diff --git a/Console/README.md b/Console/README.md index b670fe3..dd3bdb9 100644 --- a/Console/README.md +++ b/Console/README.md @@ -20,20 +20,49 @@ it is otherwise a faithful decompilation, not a rewrite. ## Dependencies -The console references five assemblies, vendored as binaries under `lib/` -(copied from `assets/Tesla Console/`): +The console references these assemblies. Most are vendored as binaries under +`lib/` (copied from `assets/Tesla Console/`): | Assembly | Origin | |----------|--------| | `WeifenLuo.WinFormsUI.Docking.dll` | Third-party docking UI (open source, see `assets/Tesla Console/WeifenLuo.txt`) | -| `TeslaConsoleLaunchLib.dll` | Wire types / launch protocol (proprietary) | -| `TeslaSecureConfiguration.dll` | First-boot secure config protocol (proprietary) | -| `Munga Net.dll` | Networking helpers (proprietary) | -| `BitmapLibrary.dll` | Plasma-display bitmap rendering (proprietary) | +| `TeslaConsoleLaunchLib.dll` | Wire types / launch protocol — **now built from source** (see below) | +| `TeslaSecureConfiguration.dll` | First-boot secure config protocol — **now built from source** (see below) | +| `Munga Net.dll` | Managed C# client for the Red Planet game's Munga protocol (TCP 1501); manual binary serialization, no BinaryFormatter (proprietary, vendored) | +| `BitmapLibrary.dll` | Plasma-display bitmap rendering (proprietary, vendored) | -These are still referenced as compiled binaries. They are also .NET 2.0 managed -assemblies and could be decompiled to source later if full-source builds are -needed. +> The Red Planet game itself is C++ and lives in its own repo (`c:\vwe\rp411` / +> `gitea.mysticmachines.com/VWE/RP411.git`). That source defines the Munga protocol +> but is **not** a drop-in for the managed `Munga Net.dll` above, so it is not +> vendored here. + +Two of these are no longer vendored binaries — they are built from source and +shared across the suite: + +- `TeslaConsoleLaunchLib` ← `../Contract/Tesla.Contract.csproj`, a multi-targeted + `net48;net8.0-windows` project: the single source of truth for the Console↔Launcher + RPC contract (wire types, the `PodManagerConnection` client, and the framed-JSON + `PodRpc` protocol), shared with the Launcher Service. The assembly keeps the + `TeslaConsoleLaunchLib` name so the original-exe baseline still resolves in the + differential tests; the wire no longer embeds assembly names (see RPC note below). +- `TeslaSecureConfiguration` ← `../SecureConfig/Tesla.SecureConfig.csproj` (net48), + the first-boot provisioning protocol (UDP beacons, OFB crypto, RSA key exchange). + +The original `TeslaSecureConfiguration.dll` is retained under `lib/` as the baseline +for the byte-identical crypto guard (`SecureConfigCompatTests`). The remaining +vendored assemblies (`Munga Net`, `BitmapLibrary`) are .NET 2.0 managed and could be +decompiled to source the same way if full-source builds are needed. + +### Console ↔ Launcher RPC (no BinaryFormatter) + +The pod-management channel (TCP 53290) runs **length-prefixed System.Text.Json** +frames over the existing OFB-encrypted stream — see `Contract/PodRpcProtocol.cs`, +shared verbatim by both ends. This replaced the original `BinaryFormatter` + +serialized-`MethodBase` scheme (a remote-code-execution sink and the reason the +Launcher was pinned to EOL .NET 6); dispatch is now by method-name string. The +Launcher Service/Agent target **net8**. Note the Console still uses `BinaryFormatter` +for *local* disk persistence (`Site` config, mission results) — that is local file +I/O on net48, not the network surface, and is intentionally left alone. ## Layout diff --git a/Console/TeslaConsole.csproj b/Console/TeslaConsole.csproj index 1ea7af3..9529057 100644 --- a/Console/TeslaConsole.csproj +++ b/Console/TeslaConsole.csproj @@ -53,17 +53,21 @@ lib\WeifenLuo.WinFormsUI.Docking.dll - - lib\TeslaSecureConfiguration.dll - lib\Munga Net.dll - - lib\TeslaConsoleLaunchLib.dll - lib\BitmapLibrary.dll + + + + + + + diff --git a/Console/tests/TeslaConsole.DiffTests/PodRpcProtocolTests.cs b/Console/tests/TeslaConsole.DiffTests/PodRpcProtocolTests.cs new file mode 100644 index 0000000..a324b34 --- /dev/null +++ b/Console/tests/TeslaConsole.DiffTests/PodRpcProtocolTests.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using Tesla.Net; +using Xunit; + +namespace TeslaConsole.DiffTests +{ + /// + /// Exercises the framed JSON RPC protocol (PodRpc) that replaced the OFB + + /// BinaryFormatter wire. Both the Console client (PodManagerConnection) and the + /// Launcher Service share this exact code, so an in-process round-trip + /// (encode request → decode → encode response → decode) validates the wire + /// contract both ends depend on. The live console↔pod path still needs a pod; + /// this covers the serialization, which is the part that can silently break. + /// + public class PodRpcProtocolTests + { + private static readonly Guid Key = new Guid("7D241B1F-AB6D-4e08-9C20-12294E743D94"); + + [Fact] + public void Request_RoundTrips_Method_And_Args() + { + var ms = new MemoryStream(); + PodRpc.WriteRequest(ms, "KillApp", new object[] { Key, 4242 }); + ms.Position = 0; + + var req = PodRpc.ReadRequest(ms); + Assert.Equal("KillApp", req.Method); + Assert.Equal(2, req.Args.Count); + Assert.Equal(Key, req.Args[0].GetGuid()); + Assert.Equal(4242, req.Args[1].GetInt32()); + } + + [Fact] + public void Request_With_No_Args_RoundTrips() + { + var ms = new MemoryStream(); + PodRpc.WriteRequest(ms, "FullUpdate", null); + ms.Position = 0; + + var req = PodRpc.ReadRequest(ms); + Assert.Equal("FullUpdate", req.Method); + Assert.Empty(req.Args); + } + + [Fact] + public void Two_Frames_Read_Back_To_Back() + { + var ms = new MemoryStream(); + PodRpc.WriteRequest(ms, "Ping", new object[] { new DateTime(2026, 6, 30, 12, 0, 0, DateTimeKind.Utc) }); + PodRpc.WriteRequest(ms, "KillAllApps", null); + ms.Position = 0; + + Assert.Equal("Ping", PodRpc.ReadRequest(ms).Method); + Assert.Equal("KillAllApps", PodRpc.ReadRequest(ms).Method); + } + + [Fact] + public void Response_Error_RoundTrips() + { + var ms = new MemoryStream(); + PodRpc.WriteResponse(ms, null, "boom on the pod"); + ms.Position = 0; + + var resp = PodRpc.ReadResponse(ms); + Assert.Equal("boom on the pod", resp.Error); + } + + [Fact] + public void Response_Primitive_Results_RoundTrip() + { + Assert.Equal(99, RoundTripResult(99)); + Assert.Equal(Key, RoundTripResult(Key)); + Assert.Equal(0.5f, RoundTripResult(0.5f)); + Assert.Equal(new DateTime(2026, 6, 30), RoundTripResult(new DateTime(2026, 6, 30))); + } + + [Fact] + public void Response_LaunchData_Array_RoundTrips_With_Fields() + { + var apps = new[] + { + new LaunchData + { + LaunchPair = new LaunchPair { LaunchKey = Key, DisplayName = "Red Planet" }, + WorkingDirectory = @"C:\Games\RedPlanet", + ExeFile = "RedPlanet.exe", + Arguments = "-res 1024 768", + AutoRestart = true, + }, + }; + + var got = RoundTripResult(apps); + Assert.Single(got); + Assert.Equal(Key, got[0].LaunchPair.LaunchKey); + Assert.Equal("Red Planet", got[0].LaunchPair.DisplayName); + Assert.Equal(@"C:\Games\RedPlanet", got[0].WorkingDirectory); + Assert.Equal("RedPlanet.exe", got[0].ExeFile); + Assert.Equal("-res 1024 768", got[0].Arguments); + Assert.True(got[0].AutoRestart); + } + + [Fact] + public void Response_FullUpdateData_RoundTrips() + { + var full = new FullUpdateData + { + InstalledApps = new[] + { + new LaunchData { LaunchPair = new LaunchPair { LaunchKey = Key, DisplayName = "RP" } }, + }, + LaunchedApps = new[] { new LaunchedAppData { LaunchKey = Key, ProcessId = 1234 } }, + VolumeLevel = 0.75f, + }; + + var got = RoundTripResult(full); + Assert.Equal(0.75f, got.VolumeLevel); + Assert.Equal("RP", got.InstalledApps[0].LaunchPair.DisplayName); + Assert.Equal(1234, got.LaunchedApps[0].ProcessId); + Assert.Equal(Key, got.LaunchedApps[0].LaunchKey); + } + + [Fact] + public void Response_OutOfBandProgress_RoundTrips() + { + var p = new OutOfBandProgress { PercentComplete = 73, Status = "Extracting...", IsCompleted = false }; + var got = RoundTripResult(p); + Assert.Equal(73, got.PercentComplete); + Assert.Equal("Extracting...", got.Status); + Assert.False(got.IsCompleted); + } + + [Fact] + public void Oversized_Frame_Length_Is_Rejected() + { + var ms = new MemoryStream(); + // Craft a frame header claiming more than MaxFrameBytes. + ms.Write(BitConverter.GetBytes(PodRpc.MaxFrameBytes + 1), 0, 4); + ms.Position = 0; + Assert.Throws(() => PodRpc.ReadFrame(ms)); + } + + // Serialize a result the way the Launcher does, read it the way the Console does. + private static T RoundTripResult(object result) + { + var ms = new MemoryStream(); + PodRpc.WriteResponse(ms, result, null); + ms.Position = 0; + var resp = PodRpc.ReadResponse(ms); + Assert.Null(resp.Error); + return resp.Result.Deserialize(PodRpc.JsonOptions); + } + } +} diff --git a/Console/tests/TeslaConsole.DiffTests/SecureConfigCompatTests.cs b/Console/tests/TeslaConsole.DiffTests/SecureConfigCompatTests.cs new file mode 100644 index 0000000..54fab53 --- /dev/null +++ b/Console/tests/TeslaConsole.DiffTests/SecureConfigCompatTests.cs @@ -0,0 +1,147 @@ +using System; +using System.IO; +using System.Net; +using System.Reflection; +using System.Security.Cryptography; +using Xunit; + +namespace TeslaConsole.DiffTests +{ + /// + /// Proves the source-built secure-config library (SecureConfig/Tesla.SecureConfig, + /// emitting assembly TeslaSecureConfiguration) is behaviourally identical to the + /// original vendored TeslaSecureConfiguration.dll for the parts that define the + /// over-the-wire provisioning format: the OFB keystream cipher and the + /// BasicConfigResponse byte layout. Byte-identity against the original binary + /// transitively guarantees compatibility with the pod-side counterpart (the + /// Launcher's hand-written OFBDuplexStream), which was built against that binary. + /// + /// The original is loaded out-of-band with Assembly.LoadFile (it shares identity + /// with the source build); construction logic is identical reflection over both. + /// + public class SecureConfigCompatTests + { + private static readonly Assembly SourceCfg = typeof(Tesla.BasicConfigResponse).Assembly; + private static readonly Assembly VendoredCfg = LoadVendored(); + + private static Assembly LoadVendored() + { + string path = Path.Combine(AssemblyPaths.RepoRoot, "lib", "TeslaSecureConfiguration.dll"); + Assert.True(File.Exists(path), "Vendored baseline not found: " + path); + return Assembly.LoadFile(path); + } + + // Fixed, deterministic inputs so both assemblies see identical material. + private static readonly byte[] Key = BuildBytes(32, i => (byte)(i * 7 + 1)); + private static readonly byte[] Iv = BuildBytes(16, i => (byte)(i * 13 + 5)); + private static readonly byte[] Plaintext = + System.Text.Encoding.UTF8.GetBytes("The quick brown pod jumps over the lazy console. 0123456789!"); + + [Fact] + public void Baseline_And_Source_Are_Distinct_Assemblies_With_Same_Identity() + { + Assert.NotEqual(SourceCfg.Location, VendoredCfg.Location); + Assert.Equal(VendoredCfg.GetName().Name, SourceCfg.GetName().Name); + Assert.Equal(VendoredCfg.GetName().Version, SourceCfg.GetName().Version); + } + + [Fact] + public void OFBCryptoStream_Produces_Identical_Ciphertext() + { + byte[] fromVendored = Encrypt(VendoredCfg); + byte[] fromSource = Encrypt(SourceCfg); + Assert.Equal(fromVendored, fromSource); + // Sanity: it actually encrypted (output differs from plaintext). + Assert.NotEqual(Plaintext, fromSource); + } + + [Fact] + public void OFBCryptoStream_RoundTrips() + { + byte[] cipher = Encrypt(SourceCfg); + byte[] plain = Decrypt(SourceCfg, cipher); + Assert.Equal(Plaintext, plain); + } + + [Fact] + public void BasicConfigResponse_ToBytes_Is_Identical() + { + Assert.Equal(ConfigToBytes(VendoredCfg), ConfigToBytes(SourceCfg)); + } + + [Fact] + public void BasicConfigResponse_RoundTrips_Across_Implementations() + { + // Bytes produced by the vendored impl must parse correctly in the source impl. + byte[] bytes = ConfigToBytes(VendoredCfg); + object parsed = Activator.CreateInstance( + SourceCfg.GetType("Tesla.BasicConfigResponse", true), bytes); + Assert.Equal("192.168.1.50", Prop(parsed, "Address").ToString()); + Assert.Equal("255.255.255.0", Prop(parsed, "Mask").ToString()); + Assert.Equal("192.168.1.1", Prop(parsed, "Gateway").ToString()); + Assert.Equal("8.8.8.8", Prop(parsed, "Dns").ToString()); + Assert.Equal("POD-01", Prop(parsed, "HostName")); + } + + // ── helpers ────────────────────────────────────────────────────────── + + private static byte[] Encrypt(Assembly asm) + { + using var aes = Rijndael.Create(); + aes.Mode = CipherMode.ECB; + aes.Key = Key; + var ms = new MemoryStream(); + var ofb = NewOfb(asm, ms, aes.CreateEncryptor(), CryptoStreamMode.Write, Iv); + ofb.Write(Plaintext, 0, Plaintext.Length); + ofb.Flush(); + return ms.ToArray(); + } + + private static byte[] Decrypt(Assembly asm, byte[] cipher) + { + using var aes = Rijndael.Create(); + aes.Mode = CipherMode.ECB; + aes.Key = Key; + var ms = new MemoryStream(cipher); + var ofb = NewOfb(asm, ms, aes.CreateEncryptor(), CryptoStreamMode.Read, Iv); + var outBuf = new byte[Plaintext.Length]; + int off = 0; + while (off < outBuf.Length) + { + int n = ofb.Read(outBuf, off, outBuf.Length - off); + if (n == 0) break; + off += n; + } + return outBuf; + } + + private static Stream NewOfb(Assembly asm, Stream inner, ICryptoTransform t, CryptoStreamMode mode, byte[] iv) + // OFBCryptoStream keeps the IV array by reference and reuses it as its + // ping-pong keystream scratch buffer, so it MUST get its own copy — the + // real callers always pass a freshly generated IV. + => (Stream)Activator.CreateInstance( + asm.GetType("System.Security.Cryptography.OFBCryptoStream", true), + inner, t, mode, (byte[])iv.Clone()); + + private static byte[] ConfigToBytes(Assembly asm) + { + Type t = asm.GetType("Tesla.BasicConfigResponse", true); + object cfg = Activator.CreateInstance(t, + IPAddress.Parse("192.168.1.50"), + IPAddress.Parse("255.255.255.0"), + IPAddress.Parse("192.168.1.1"), + IPAddress.Parse("8.8.8.8"), + "POD-01"); + return (byte[])t.GetMethod("ToBytes").Invoke(cfg, null); + } + + private static object Prop(object o, string name) => o.GetType().GetProperty(name).GetValue(o, null); + + private static byte[] BuildBytes(int n, Func f) + { + var b = new byte[n]; + for (int i = 0; i < n; i++) b[i] = f(i); + return b; + } + } +} diff --git a/Console/tests/TeslaConsole.DiffTests/TeslaConsole.DiffTests.csproj b/Console/tests/TeslaConsole.DiffTests/TeslaConsole.DiffTests.csproj index 0fca65f..052a2fb 100644 --- a/Console/tests/TeslaConsole.DiffTests/TeslaConsole.DiffTests.csproj +++ b/Console/tests/TeslaConsole.DiffTests/TeslaConsole.DiffTests.csproj @@ -41,6 +41,13 @@ false false + + + + diff --git a/Contract/Client/MockPodManagerConnection.cs b/Contract/Client/MockPodManagerConnection.cs new file mode 100644 index 0000000..64f9109 --- /dev/null +++ b/Contract/Client/MockPodManagerConnection.cs @@ -0,0 +1,177 @@ +// ============================================================================= +// Tesla.Contract — Mock RPC client (net48 only) +// ============================================================================= +// Reconstructed verbatim from TeslaConsoleLaunchLib.dll. An offline test/dev +// stand-in for PodManagerConnection. Not referenced by the shipping Console UI, +// but retained for parity with the original assembly surface. +// ============================================================================= + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; + +namespace Tesla.Net +{ + public class MockPodManagerConnection : IPodManagerConnection, ILauncherService + { + public static readonly Guid RPAppGuid = new Guid("7D241B1F-AB6D-4e08-9C20-12294E743D94"); + + public static readonly Guid RPMRAppGuid = new Guid("8F71D5C2-38E4-413c-8E22-88CAD08774D2"); + + public static readonly Guid RPLCAppGuid = new Guid("57A0B3C2-D5CF-46d6-ABED-A8F4A26AB086"); + + public static readonly Guid MW4AppGuid = new Guid("CC8500ED-A653-45a7-BEF8-C332D30371A6"); + + public static readonly Guid MW4LCAppGuid = new Guid("8EE93A6C-F16A-49be-B867-37FAE9087FFF"); + + private bool mIsOpen; + + private List mLaunchedApps = new List(); + + public bool IsOpen => mIsOpen; + + public float VolumeLevel + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public void Open(IPEndPoint remoteEndPoint, byte[] secureKey) + { + Thread.Sleep(5000); + mIsOpen = true; + } + + public void Close() + { + Thread.Sleep(5000); + mIsOpen = false; + } + + public Guid InstallProduct(string filename) + { + throw new NotImplementedException(); + } + + public void ClearStore() + { + throw new NotImplementedException(); + } + + public DateTime Ping(DateTime now) + { + throw new NotImplementedException(); + } + + public Guid InitiateInstallProduct() + { + throw new NotImplementedException(); + } + + public OutOfBandProgress GetOutOfBandProgress(Guid invokeCallId) + { + throw new NotImplementedException(); + } + + public int LaunchApp(Guid launchKey) + { + throw new NotImplementedException(); + } + + public LaunchPair[] GetLaunchableApps() + { + return new LaunchPair[2] + { + new LaunchPair + { + DisplayName = "BattleTech Firestorm", + LaunchKey = MW4AppGuid + }, + new LaunchPair + { + DisplayName = "Red Planet", + LaunchKey = RPAppGuid + } + }; + } + + public void KillApp(Guid launchKey, int processId) + { + throw new NotImplementedException(); + } + + public void KillAllOfType(Guid launchKey) + { + throw new NotImplementedException(); + } + + public void KillAllApps() + { + throw new NotImplementedException(); + } + + public void Shutdown(bool restart) + { + throw new NotImplementedException(); + } + + public LaunchedAppData[] GetLaunchedApps() + { + return mLaunchedApps.ToArray(); + } + + public LaunchData[] GetInstalledApps() + { + return new LaunchData[2] + { + new LaunchData + { + LaunchPair = new LaunchPair + { + DisplayName = "BattleTech Firestorm", + LaunchKey = MW4AppGuid + } + }, + new LaunchData + { + LaunchPair = new LaunchPair + { + DisplayName = "Red Planet", + LaunchKey = RPAppGuid + } + } + }; + } + + public void RemoveApp(Guid index) + { + throw new NotImplementedException(); + } + + public void InstallApp(LaunchData data) + { + throw new NotImplementedException(); + } + + public void UninstallApp(Guid launchKey) + { + throw new NotImplementedException(); + } + + public FullUpdateData FullUpdate() + { + FullUpdateData result = default(FullUpdateData); + result.InstalledApps = GetInstalledApps(); + result.LaunchedApps = GetLaunchedApps(); + result.VolumeLevel = VolumeLevel; + return result; + } + } +} diff --git a/Contract/Client/PodManagerConnection.cs b/Contract/Client/PodManagerConnection.cs new file mode 100644 index 0000000..8933dd0 --- /dev/null +++ b/Contract/Client/PodManagerConnection.cs @@ -0,0 +1,276 @@ +// ============================================================================= +// Tesla.Contract — Console-side RPC client (net48 only) +// ============================================================================= +// Opens an OFB-encrypted TCP connection to the pod (port 53290) and dispatches +// ILauncherService calls as framed JSON RpcRequest / RpcResponse pairs (see +// PodRpcProtocol.cs). Replaces the original serialized-MethodBase + BinaryFormatter +// scheme; the OFB transport (PodConfigurationServer.NegotiateCryptoStreams) is +// unchanged. +// +// Depends on Tesla.PodConfigurationServer (Tesla.SecureConfig) for the crypto +// handshake, so it is compiled for net48 only. The Launcher is the server end. +// ============================================================================= + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Text.Json; +using System.Threading; + +namespace Tesla.Net +{ + public class PodManagerConnection : IPodManagerConnection, ILauncherService, IDisposable + { + private TcpClient mOpenClient; + + private object mCommSyncRoot = new object(); + + protected Stream mUnderlyingOutStream; + + private BufferedStream mReader; + + private BufferedStream mWriter; + + private IPEndPoint mEndPoint; + + private byte[] mKey; + + public bool IsOpen + { + get + { + if (mOpenClient == null || mOpenClient.Client == null || mWriter == null || mReader == null) + { + return false; + } + return mOpenClient.Connected; + } + } + + protected object SyncRoot => mCommSyncRoot; + + public float VolumeLevel + { + get => Invoke("get_VolumeLevel"); + set => InvokeVoid("set_VolumeLevel", value); + } + + public void Open(IPEndPoint remoteEndPoint, byte[] secureKey) + { + mEndPoint = remoteEndPoint; + mKey = secureKey; + mOpenClient = new TcpClient(); + mOpenClient.SendTimeout = 10000; + mOpenClient.ReceiveTimeout = 10000; + mOpenClient.Connect(remoteEndPoint); + if (!PodConfigurationServer.NegotiateCryptoStreams(mOpenClient.GetStream(), secureKey, out var outStream, out var inStream)) + { + mOpenClient.Close(); + throw new IOException("Error connecting to pod."); + } + mUnderlyingOutStream = outStream; + mWriter = new BufferedStream(outStream); + mReader = new BufferedStream(inStream); + } + + public void Close() + { + try + { + mWriter.Close(); + mReader.Close(); + mOpenClient.Client.Close(); + mOpenClient = null; + } + catch + { + } + } + + public Guid InstallProduct(string filepath) + { + if (!File.Exists(filepath)) + { + return Guid.Empty; + } + FileStream outOfBandData = File.OpenRead(filepath); + Guid result = Guid.Empty; + try + { + result = OutOfBandInvocation.Invoke(mEndPoint, mKey, "InitiateInstallProduct", new object[0], outOfBandData); + return result; + } + catch (IOException) + { + return result; + } + } + + // ── RPC core ───────────────────────────────────────────────────────── + // Writes a framed JSON request and (optionally) reads the framed response, + // deserializing its Result into the caller's expected type. + + protected object InvokeCore(string method, bool waitForResponse, Type resultType, object[] parameters) + { + try + { + lock (mCommSyncRoot) + { + PodRpc.WriteRequest(mWriter, method, parameters); + if (!waitForResponse) + { + return null; + } + RpcResponse response = PodRpc.ReadResponse(mReader); + if (response.Error != null) + { + throw new Exception("Server function threw an exception: " + response.Error); + } + if (resultType == null + || response.Result.ValueKind == JsonValueKind.Null + || response.Result.ValueKind == JsonValueKind.Undefined) + { + return resultType != null && resultType.IsValueType + ? Activator.CreateInstance(resultType) + : null; + } + return response.Result.Deserialize(resultType, PodRpc.JsonOptions); + } + } + catch (IOException innerException) + { + throw new TeslaConsole.ConnectionLostException("Connection Lost", innerException); + } + } + + protected T Invoke(string method, params object[] parameters) + => (T)InvokeCore(method, waitForResponse: true, typeof(T), parameters); + + protected void InvokeVoid(string method, params object[] parameters) + => InvokeCore(method, waitForResponse: true, null, parameters); + + protected void InvokeOneWay(string method, params object[] parameters) + => InvokeCore(method, waitForResponse: false, null, parameters); + + // ── ILauncherService ───────────────────────────────────────────────── + + public void ClearStore() => InvokeOneWay("ClearStore"); + + public DateTime Ping(DateTime now) => Invoke("Ping", now); + + public int LaunchApp(Guid launchKey) => Invoke("LaunchApp", launchKey); + + public LaunchPair[] GetLaunchableApps() => Invoke("GetLaunchableApps"); + + public Guid InitiateInstallProduct() => Invoke("InitiateInstallProduct"); + + public OutOfBandProgress GetOutOfBandProgress(Guid installId) + => Invoke("GetOutOfBandProgress", installId); + + public void KillApp(Guid launchKey, int processId) + => InvokeVoid("KillApp", launchKey, processId); + + public void KillAllOfType(Guid launchKey) => InvokeVoid("KillAllOfType", launchKey); + + public void KillAllApps() => InvokeVoid("KillAllApps"); + + public void Shutdown(bool doRestart) => InvokeVoid("Shutdown", doRestart); + + public LaunchedAppData[] GetLaunchedApps() => Invoke("GetLaunchedApps"); + + public LaunchData[] GetInstalledApps() => Invoke("GetInstalledApps"); + + public void RemoveApp(Guid index) => InvokeVoid("RemoveApp", index); + + public void InstallApp(LaunchData data) => InvokeVoid("InstallApp", data); + + public void UninstallApp(Guid launchKey) => InvokeVoid("UninstallApp", launchKey); + + public FullUpdateData FullUpdate() => Invoke("FullUpdate"); + + public void Dispose() + { + Close(); + } + } + + public class OutOfBandInvocation : PodManagerConnection + { + private Stream mOutOfBandData; + + public static Guid Invoke(IPEndPoint target, byte[] key, string methodName, object[] parameters, Stream outOfBandData) + { + return Invoke(target, key, methodName, parameters, outOfBandData, null); + } + + public static Guid Invoke(IPEndPoint target, byte[] key, MethodBase method, object[] parameters, Stream outofBandData) + { + return Invoke(target, key, method.Name, parameters, outofBandData, null); + } + + public static Guid Invoke(IPEndPoint target, byte[] key, MethodBase method, object[] parameters, Stream outOfBandData, OutOfBandProgressChanged progressCallback) + { + return Invoke(target, key, method.Name, parameters, outOfBandData, progressCallback); + } + + public static Guid Invoke(IPEndPoint target, byte[] key, string methodName, object[] parameters, Stream outOfBandData, OutOfBandProgressChanged progressCallback) + { + OutOfBandInvocation outOfBandInvocation = new OutOfBandInvocation(target, key); + // progressCallback is vestigial: install progress is polled via + // GetOutOfBandProgress, never pushed. Kept on the signature for surface parity. + _ = progressCallback; + if (outOfBandData != null) + { + outOfBandInvocation.mOutOfBandData = outOfBandData; + } + Guid result = outOfBandInvocation.Invoke(methodName, parameters ?? new object[0]); + ThreadPool.QueueUserWorkItem(AppendOutOfBandData, outOfBandInvocation); + return result; + } + + protected static void AppendOutOfBandData(object state) + { + OutOfBandInvocation outOfBandInvocation = (OutOfBandInvocation)state; + using (outOfBandInvocation) + { + try + { + outOfBandInvocation.WriteOutOfBandData(); + } + catch (IOException) + { + } + } + } + + protected OutOfBandInvocation(IPEndPoint target, byte[] key) + { + Open(target, key); + } + + protected void WriteOutOfBandData() + { + lock (base.SyncRoot) + { + int num = 8192; + byte[] buffer = new byte[num]; + long num2 = mOutOfBandData.Length; + mUnderlyingOutStream.Write(BitConverter.GetBytes(num2), 0, 8); + while (num2 > num) + { + mOutOfBandData.Read(buffer, 0, num); + mUnderlyingOutStream.Write(buffer, 0, num); + num2 -= num; + } + if (num2 > 0) + { + mOutOfBandData.Read(buffer, 0, (int)num2); + mUnderlyingOutStream.Write(buffer, 0, (int)num2); + } + mUnderlyingOutStream.Flush(); + } + } + } +} diff --git a/Contract/PodRpcProtocol.cs b/Contract/PodRpcProtocol.cs new file mode 100644 index 0000000..60a028e --- /dev/null +++ b/Contract/PodRpcProtocol.cs @@ -0,0 +1,120 @@ +// ============================================================================= +// Tesla.Contract — Pod RPC protocol (Console <-> Launcher Service) +// ============================================================================= +// The framed, JSON-based replacement for the old OFB-encrypted BinaryFormatter +// RPC. Runs ON TOP of the same OFB-encrypted stream (NegotiateCryptoStreams is +// unchanged); only the per-message serialization changed. +// +// Wire format, after the OFB/CONF handshake: +// request : [4-byte little-endian length][UTF-8 JSON RpcRequest] +// response: [4-byte little-endian length][UTF-8 JSON RpcResponse] +// The InitiateInstallProduct out-of-band file transfer (8-byte length + raw +// bytes) is unchanged and still follows its response frame on the same stream. +// +// Dispatch is by method NAME (RpcRequest.Method) — the old serialized-MethodBase +// + SerializationBinder + MethodInfoProxy machinery is gone. Both ends share this +// one file, so the request/response shape cannot drift. +// ============================================================================= + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace Tesla.Net +{ + /// One RPC call: a method name plus its arguments as JSON elements. + public sealed class RpcRequest + { + public string Method { get; set; } + public List Args { get; set; } + } + + /// One RPC result: the return value as JSON, or an error message. + public sealed class RpcResponse + { + public JsonElement Result { get; set; } // JsonValueKind.Null for void / null + public string Error { get; set; } // null on success + } + + /// Framing + (de)serialization shared by the Console client and the + /// Launcher Service. Synchronous to match the existing stream usage. + public static class PodRpc + { + /// Upper bound on a single JSON frame (the bulk install payload is + /// streamed out-of-band, not framed), guarding against hostile lengths. + public const int MaxFrameBytes = 16 * 1024 * 1024; + + public static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + // The Tesla.Net wire types (LaunchData, LaunchPair, ...) expose public + // FIELDS, which System.Text.Json ignores unless this is set. + IncludeFields = true, + }; + + // ── Framing ────────────────────────────────────────────────────────── + + public static void WriteFrame(Stream stream, byte[] payload) + { + if (payload.Length > MaxFrameBytes) + throw new IOException($"RPC frame too large ({payload.Length} bytes)."); + var len = BitConverter.GetBytes(payload.Length); // little-endian on x86/x64 + stream.Write(len, 0, 4); + stream.Write(payload, 0, payload.Length); + stream.Flush(); + } + + public static byte[] ReadFrame(Stream stream) + { + var lenBuf = ReadExact(stream, 4); + int len = BitConverter.ToInt32(lenBuf, 0); + if (len < 0 || len > MaxFrameBytes) + throw new IOException($"RPC frame length out of range ({len})."); + return ReadExact(stream, len); + } + + private static byte[] ReadExact(Stream stream, int count) + { + var buf = new byte[count]; + int off = 0; + while (off < count) + { + int n = stream.Read(buf, off, count - off); + if (n == 0) throw new EndOfStreamException("Connection closed mid-frame."); + off += n; + } + return buf; + } + + // ── Request ────────────────────────────────────────────────────────── + + public static void WriteRequest(Stream stream, string method, object[] args) + { + var req = new RpcRequest { Method = method, Args = new List() }; + if (args != null) + foreach (var a in args) + req.Args.Add(JsonSerializer.SerializeToElement(a, JsonOptions)); + WriteFrame(stream, JsonSerializer.SerializeToUtf8Bytes(req, JsonOptions)); + } + + public static RpcRequest ReadRequest(Stream stream) + => JsonSerializer.Deserialize(ReadFrame(stream), JsonOptions); + + // ── Response ───────────────────────────────────────────────────────── + + public static void WriteResponse(Stream stream, object result, string error) + { + var resp = new RpcResponse + { + // Always a valid element (JSON null when there is no result): a + // default(JsonElement) is ValueKind.Undefined and is not serializable. + Result = JsonSerializer.SerializeToElement(error == null ? result : null, JsonOptions), + Error = error, + }; + WriteFrame(stream, JsonSerializer.SerializeToUtf8Bytes(resp, JsonOptions)); + } + + public static RpcResponse ReadResponse(Stream stream) + => JsonSerializer.Deserialize(ReadFrame(stream), JsonOptions); + } +} diff --git a/Contract/Tesla.Contract.csproj b/Contract/Tesla.Contract.csproj new file mode 100644 index 0000000..d6fe5b4 --- /dev/null +++ b/Contract/Tesla.Contract.csproj @@ -0,0 +1,47 @@ + + + + + net48;net8.0-windows + + + TeslaConsoleLaunchLib + Tesla.Net + 1.0.0.0 + 1.0.0.0 + 1.0.0.0 + + disable + disable + latest + true + + $(NoWarn);SYSLIB0011 + + + + + + + + + + + + + + + + + + + diff --git a/Contract/WireContract.cs b/Contract/WireContract.cs new file mode 100644 index 0000000..7dcc34b --- /dev/null +++ b/Contract/WireContract.cs @@ -0,0 +1,176 @@ +// ============================================================================= +// Tesla.Contract — Shared RPC Wire Contract (assembly: TeslaConsoleLaunchLib) +// ============================================================================= +// Single source of truth for the Console <-> Launcher protocol. Previously this +// lived in two hand-synced places: the vendored binary +// Console/lib/TeslaConsoleLaunchLib.dll (consumed by the Console) and a manual +// replica in Launcher/LaunchModels_Shared.cs (consumed by the Launcher Service). +// +// The wire types below are reconstructed verbatim from the original +// TeslaConsoleLaunchLib.dll. Field NAMES, TYPES and ORDER are load-bearing: +// they are serialized with BinaryFormatter and must match byte-for-byte or the +// protocol breaks silently at runtime. The byte-identical guarantee is asserted +// by WireContractCompatTests in the differential suite. +// +// This file is compiled for BOTH net48 (Console) and net6.0-windows (Launcher). +// The TCP/OFB/BinaryFormatter client (PodManagerConnection) is net48-only and +// lives under Client/ — it depends on TeslaSecureConfiguration.dll. +// ============================================================================= + +using System; +using System.Net; +using System.Reflection; + +namespace Tesla.Net +{ + /// App identifier + display name pair. + [Serializable] + public struct LaunchPair + { + public Guid LaunchKey; + + public string DisplayName; + } + + /// Describes one launchable simulation application. + [Serializable] + public struct LaunchData + { + public LaunchPair LaunchPair; + + public string WorkingDirectory; + + public string ExeFile; + + public string Arguments; + + public bool AutoRestart; + } + + /// Tracks a currently running simulation process. + [Serializable] + public struct LaunchedAppData + { + public int ProcessId; + + public Guid LaunchKey; + } + + /// Progress of an out-of-band product installation. + [Serializable] + public struct OutOfBandProgress + { + public int PercentComplete; + + public string Status; + + public bool IsCompleted; + } + + /// Complete pod state snapshot for the FullUpdate RPC. + [Serializable] + public struct FullUpdateData + { + public LaunchData[] InstalledApps; + + public LaunchedAppData[] LaunchedApps; + + public float VolumeLevel; + } + + /// RPC command from the Console. Function is a MethodBase of ILauncherService. + [Serializable] + public struct InvokeCommand + { + public MethodBase Function; + + public object[] Parameters; + } + + /// RPC result returned to the Console. + [Serializable] + public struct InvokeResult + { + public object Result; + + public Exception Exception; + + public TimeSpan CallDuration; + } + + public delegate void OutOfBandProgressChanged(Guid outOfBandCallId, OutOfBandProgress progress); + + /// Server-side RPC surface. Method signatures are resolved by name from a + /// serialized MethodBase, so they must match the original exactly. + public interface ILauncherService + { + float VolumeLevel { get; set; } + + void ClearStore(); + + DateTime Ping(DateTime now); + + Guid InitiateInstallProduct(); + + OutOfBandProgress GetOutOfBandProgress(Guid invokeCallId); + + int LaunchApp(Guid launchKey); + + LaunchPair[] GetLaunchableApps(); + + void KillApp(Guid launchKey, int processId); + + void KillAllOfType(Guid launchKey); + + void KillAllApps(); + + void Shutdown(bool restart); + + LaunchedAppData[] GetLaunchedApps(); + + LaunchData[] GetInstalledApps(); + + void RemoveApp(Guid index); + + void InstallApp(LaunchData data); + + void UninstallApp(Guid launchKey); + + FullUpdateData FullUpdate(); + } + + /// Console-side connection: ILauncherService plus connection management + /// and the out-of-band InstallProduct file transfer. + public interface IPodManagerConnection : ILauncherService + { + bool IsOpen { get; } + + void Open(IPEndPoint remoteEndPoint, byte[] secureKey); + + void Close(); + + Guid InstallProduct(string filename); + } +} + +namespace TeslaConsole +{ + /// Thrown by the Console-side connection when the pod link drops. + [Serializable] + public class ConnectionLostException : Exception + { + public ConnectionLostException() + { + } + + public ConnectionLostException(string message) + : base(message) + { + } + + public ConnectionLostException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/Launcher/Directory.Build.props b/Launcher/Directory.Build.props new file mode 100644 index 0000000..6c87852 --- /dev/null +++ b/Launcher/Directory.Build.props @@ -0,0 +1,23 @@ + + + + + obj\$(MSBuildProjectName)\ + + + + + $(DefaultItemExcludes);$(MSBuildProjectDirectory)\obj\**;$(MSBuildProjectDirectory)\bin\** + + + diff --git a/Launcher/LaunchModels_Shared.cs b/Launcher/LaunchModels_Shared.cs index e3372d4..8f8bdbf 100644 --- a/Launcher/LaunchModels_Shared.cs +++ b/Launcher/LaunchModels_Shared.cs @@ -1,105 +1,16 @@ // ============================================================================= -// TeslaLauncher — Shared Models +// TeslaLauncher — Shared IPC Models // ============================================================================= -// Wire types (Tesla.Net): BinaryFormatter-compatible replicas of the original -// structs from TeslaConsoleLaunchLib.dll. Field names, types, and order must -// match exactly for serialization compatibility. +// IPC types (Tesla.Launcher.Shared): JSON messages between the Windows Service +// and the user-session Agent over a Named Pipe. These never touch the Console +// TCP connection. // -// IPC types (Tesla.Launcher.Shared): JSON messages between Service and Agent -// over Named Pipe. These never touch the Console TCP connection. +// The BinaryFormatter wire types (Tesla.Net: LaunchData, InvokeCommand, ...) +// formerly replicated here by hand now live in the shared Tesla.Contract project +// (assembly TeslaConsoleLaunchLib), referenced by the Service. This file is also +// compiled into the Agent, which uses only the IPC types below. // ============================================================================= -using System; -using System.Reflection; -using System.Runtime.Serialization; - -namespace Tesla.Net -{ - /// RPC command from TeslaConsole. Function is a MethodBase of ILauncherService. - [Serializable] - public struct InvokeCommand - { - public MethodBase Function; - public object[] Parameters; - } - - /// RPC result returned to TeslaConsole. - [Serializable] - public struct InvokeResult - { - public object Result; - public Exception Exception; - public TimeSpan CallDuration; - } - - /// App identifier + display name pair. - [Serializable] - public struct LaunchPair - { - public Guid LaunchKey; - public string DisplayName; - } - - /// Describes one launchable simulation application. - [Serializable] - public struct LaunchData - { - public LaunchPair LaunchPair; - public string WorkingDirectory; - public string ExeFile; - public string Arguments; - public bool AutoRestart; - } - - /// Tracks a currently running simulation process. - [Serializable] - public struct LaunchedAppData - { - public int ProcessId; - public Guid LaunchKey; - } - - /// Complete pod state snapshot for FullUpdate RPC. - [Serializable] - public struct FullUpdateData - { - public LaunchData[] InstalledApps; - public LaunchedAppData[] LaunchedApps; - public float VolumeLevel; - } - - /// Progress of an out-of-band product installation. - [Serializable] - public struct OutOfBandProgress - { - public int PercentComplete; - public string Status; - public bool IsCompleted; - } - - /// Interface for BinaryFormatter MethodBase resolution. Signatures must match the original exactly. - public interface ILauncherService - { - float VolumeLevel { get; set; } - void ClearStore(); - DateTime Ping(DateTime now); - Guid InitiateInstallProduct(); - OutOfBandProgress GetOutOfBandProgress(Guid invokeCallId); - int LaunchApp(Guid launchKey); - LaunchPair[] GetLaunchableApps(); - void KillApp(Guid launchKey, int processId); - void KillAllOfType(Guid launchKey); - void KillAllApps(); - void Shutdown(bool restart); - LaunchedAppData[] GetLaunchedApps(); - LaunchData[] GetInstalledApps(); - void RemoveApp(Guid index); - void InstallApp(LaunchData data); - void UninstallApp(Guid launchKey); - FullUpdateData FullUpdate(); - } -} - namespace Tesla.Launcher.Shared { /// Command forwarded from the Windows Service to the Userspace Agent. diff --git a/Launcher/SecureConfig.cs b/Launcher/SecureConfig.cs index b5d1718..6cc9ca7 100644 --- a/Launcher/SecureConfig.cs +++ b/Launcher/SecureConfig.cs @@ -196,13 +196,20 @@ namespace TeslaSecureConfig // ---- Crypto helpers ---- internal static class CryptoHelper { - /// Derive AES key from passphrase using PBKDF2 with the hard-coded salt + /// Derive AES key from passphrase using PBKDF2 with the hard-coded salt. + /// COMPATIBILITY: this MUST use the SHA1-default Rfc2898DeriveBytes(string, + /// byte[], int) overload. The Console derives the same session key with the + /// identical SHA1-default PBKDF2 (Tesla.PodConfigurationServer.GenerateKeyFromPassphrase), + /// so switching to a SHA256 overload — as SYSLIB0041 suggests on net8 — would + /// silently break the secure-config key handshake. Do not "modernize" this. +#pragma warning disable SYSLIB0041 // SHA1-default PBKDF2 is required for Console wire compatibility public static byte[] DeriveKey(string passphrase) { var pbkdf2 = new Rfc2898DeriveBytes(passphrase, Proto.PassphraseSalt, Proto.Pbkdf2Iterations); return pbkdf2.GetBytes(Proto.AesKeyBytes); } +#pragma warning restore SYSLIB0041 /// Encrypt data with AES-256 (CBC mode) public static byte[] Encrypt(byte[] data, byte[] key) diff --git a/Launcher/TeslaLauncherAgent.csproj b/Launcher/TeslaLauncherAgent.csproj index 59a88b8..1e399ce 100644 --- a/Launcher/TeslaLauncherAgent.csproj +++ b/Launcher/TeslaLauncherAgent.csproj @@ -1,19 +1,18 @@ - net6.0-windows + net8.0-windows WinExe true disable disable - true 0.1.0 app.ico TeslaLauncherAgent Tesla.Launcher.Agent - x86 - win-x86 + x64 + win-x64 true true Tesla.Launcher.Agent.AgentApplication @@ -27,7 +26,7 @@ - + diff --git a/Launcher/TeslaLauncherService.cs b/Launcher/TeslaLauncherService.cs index f3d9d20..2a6dba7 100644 --- a/Launcher/TeslaLauncherService.cs +++ b/Launcher/TeslaLauncherService.cs @@ -3,7 +3,7 @@ // ============================================================================= // Replaces TeslaLauncherService.exe (Elsewhen Studios LLC, .NET Framework 2.0). // Runs in Session 0, auto-starts at boot before user login. -// Listens on TCP 53290 for OFB-encrypted BinaryFormatter RPC from TeslaConsole. +// Listens on TCP 53290 for OFB-encrypted framed-JSON RPC from TeslaConsole. // Forwards commands to TeslaLauncherAgent (user session) via Named Pipe. // // On first boot (DHCP detected), runs SecureConfig to receive network @@ -17,9 +17,6 @@ using System.IO.Compression; using System.IO.Pipes; using System.Net; using System.Net.Sockets; -using System.Reflection; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -32,8 +29,6 @@ using TeslaSecureConfig; using Tesla.Launcher.Shared; using Tesla.Net; -#pragma warning disable SYSLIB0011 // BinaryFormatter is required for Console wire compatibility - namespace Tesla.Launcher.Service { public class TeslaLauncherService : BackgroundService @@ -92,11 +87,6 @@ namespace Tesla.Launcher.Service _logger.LogInformation( "Tesla Launcher Service starting on port {Port}", CONSOLE_PORT); - // Enable BinaryFormatter at runtime. Must also be set in .csproj. - // Both switches are required on .NET 6+. - AppContext.SetSwitch( - "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization", true); - // ── Boot diagnostic — always written so every reboot is traceable ── // Logs adapter state to podconf.log before IsMachineConfigured() decides, // visible even if SecureConfig does not run. @@ -170,8 +160,8 @@ namespace Tesla.Launcher.Service "Console connected from {EP}", client.Client.RemoteEndPoint); // Handle each Console connection on its own thread. - // BinaryFormatter.Deserialize is synchronous, so we - // offload to a thread-pool thread via Task.Run. + // Frame reads are synchronous, so we offload to a + // thread-pool thread via Task.Run. var task = Task.Run( () => HandleConsoleClient(client, stoppingToken), stoppingToken); @@ -247,88 +237,70 @@ namespace Tesla.Launcher.Service } Stream rpcStream = ofb; - - var formatter = CreateFormatter(); + var diagLog = Path.Combine(AppContext.BaseDirectory, "podconf.log"); _logger.LogInformation("Console session started."); try { while (!ct.IsCancellationRequested && client.Connected) { - InvokeCommand command; + RpcRequest request; try { - MethodInfoProxy.LastResolvedName = null; - var obj = formatter.Deserialize(rpcStream); - command = (InvokeCommand)obj; + request = PodRpc.ReadRequest(rpcStream); } - catch (IOException) + catch (EndOfStreamException) { _logger.LogInformation("Console disconnected (EOF)."); break; } - catch (SerializationException ex) - when (ex.Message.Contains("End of Stream")) + catch (IOException ex) { - _logger.LogInformation("Console disconnected (end of stream)."); - break; - } - catch (SerializationException ex) - when (ex.InnerException is IOException) - { - // Socket timeout or reset — Console went idle or disconnected - _logger.LogInformation("Console disconnected (I/O: {Msg}).", - ex.InnerException.Message); + _logger.LogInformation("Console disconnected (I/O: {Msg}).", ex.Message); break; } catch (Exception ex) { - _logger.LogWarning("Deserialization error ({Type}): {Msg}", + _logger.LogWarning("Request read error ({Type}): {Msg}", ex.GetType().Name, ex.Message); - File.AppendAllText( - Path.Combine(AppContext.BaseDirectory, "podconf.log"), - $"[{DateTime.Now:HH:mm:ss}] DESER ERROR: {ex}{Environment.NewLine}"); + File.AppendAllText(diagLog, + $"[{DateTime.Now:HH:mm:ss}] REQ ERROR: {ex}{Environment.NewLine}"); break; } - // Fall back to ThreadStatic name if IObjectReference fixup - // didn't update the boxed struct field. - var funcName = command.Function?.Name - ?? MethodInfoProxy.LastResolvedName ?? "???"; - _logger.LogInformation( - "Command: {Func}({Args})", funcName, - command.Parameters != null - ? string.Join(", ", command.Parameters) : ""); + if (request == null) break; + + var funcName = request.Method ?? "???"; + IReadOnlyList args = request.Args ?? new List(); + _logger.LogInformation("Command: {Func}({ArgCount} args)", funcName, args.Count); var start = DateTime.UtcNow; - var result = new InvokeResult(); - var diagLog = Path.Combine(AppContext.BaseDirectory, "podconf.log"); + object resultObj = null; + string error = null; try { - result.Result = await DispatchCommandAsync( - funcName, command.Parameters, ct); + resultObj = await DispatchCommandAsync(funcName, args, ct); } catch (Exception ex) { _logger.LogError(ex, "Error executing {Func}", funcName); - result.Exception = new Exception(ex.Message); + error = ex.Message; File.AppendAllText(diagLog, $"[{DateTime.Now:HH:mm:ss}] CMD {funcName} ERROR: {ex.Message}{Environment.NewLine}"); } - result.CallDuration = DateTime.UtcNow - start; - if (result.Exception == null) + var dur = DateTime.UtcNow - start; + if (error == null) { File.AppendAllText(diagLog, - $"[{DateTime.Now:HH:mm:ss}] CMD {funcName} OK ({result.CallDuration.TotalMilliseconds:F0}ms)" + - $"{(result.Result != null ? $" → {result.Result}" : "")}{Environment.NewLine}"); + $"[{DateTime.Now:HH:mm:ss}] CMD {funcName} OK ({dur.TotalMilliseconds:F0}ms)" + + $"{(resultObj != null ? $" → {resultObj}" : "")}{Environment.NewLine}"); } try { - formatter.Serialize(rpcStream, result); - rpcStream.Flush(); + PodRpc.WriteResponse(rpcStream, resultObj, error); } catch (IOException) { @@ -337,19 +309,10 @@ namespace Tesla.Launcher.Service } catch (Exception serEx) { - // Result type not BinaryFormatter-serializable — send error instead + // Result type not JSON-serializable — send error instead. _logger.LogWarning("Serialize failed for {Func}: {Msg}", funcName, serEx.Message); - var fallback = new InvokeResult - { - Exception = new Exception($"Pod error: {serEx.Message}"), - CallDuration = result.CallDuration - }; - try - { - formatter.Serialize(rpcStream, fallback); - rpcStream.Flush(); - } + try { PodRpc.WriteResponse(rpcStream, null, $"Pod error: {serEx.Message}"); } catch { break; } } @@ -359,8 +322,8 @@ namespace Tesla.Launcher.Service // 8-byte Int64 file size + raw file bytes // Then closes the connection (FIN). if (funcName == "InitiateInstallProduct" - && result.Exception == null - && result.Result is Guid installGuid) + && error == null + && resultObj is Guid installGuid) { ReceiveInstallFile(rpcStream, installGuid, diagLog); break; // connection done after file transfer @@ -370,7 +333,6 @@ namespace Tesla.Launcher.Service catch (Exception ex) { _logger.LogError(ex, "Unhandled error in Console handler"); - var diagLog = Path.Combine(AppContext.BaseDirectory, "podconf.log"); File.AppendAllText(diagLog, $"[{DateTime.Now:HH:mm:ss}] HANDLER ERROR: {ex.GetType().Name}: {ex.Message}{Environment.NewLine}" + $"[{DateTime.Now:HH:mm:ss}] Inner: {ex.InnerException?.Message}{Environment.NewLine}"); @@ -380,19 +342,24 @@ namespace Tesla.Launcher.Service } // ── Command Dispatch ───────────────────────────────────────────── + // Arguments arrive as JSON elements (RpcRequest.Args); each case extracts + // the typed values it needs. The returned object is serialized straight + // into the RpcResponse. private async Task DispatchCommandAsync( - string functionName, object[] parameters, CancellationToken ct) + string functionName, IReadOnlyList args, CancellationToken ct) { switch (functionName) { case "Ping": // Echo back the DateTime parameter (Console measures round-trip) - return parameters?.Length > 0 ? parameters[0] : DateTime.Now; + return args.Count > 0 && args[0].ValueKind != JsonValueKind.Null + ? args[0].GetDateTime() + : DateTime.Now; case "GetLaunchableApps": { - var json = await ForwardToAgentJsonAsync(functionName, parameters, ct); + var json = await ForwardToAgentJsonAsync(functionName, args, ct); var wire = FlatArrayToWire(json); var pairs = new LaunchPair[wire.Length]; for (int i = 0; i < wire.Length; i++) @@ -402,19 +369,19 @@ namespace Tesla.Launcher.Service case "GetInstalledApps": { - var json = await ForwardToAgentJsonAsync(functionName, parameters, ct); + var json = await ForwardToAgentJsonAsync(functionName, args, ct); return FlatArrayToWire(json); } case "GetLaunchedApps": { - var json = await ForwardToAgentJsonAsync(functionName, parameters, ct); + var json = await ForwardToAgentJsonAsync(functionName, args, ct); return FlatLaunchedArrayToWire(json); } case "FullUpdate": { - var json = await ForwardToAgentJsonAsync(functionName, parameters, ct); + var json = await ForwardToAgentJsonAsync(functionName, args, ct); if (json == null) return new FullUpdateData(); var flat = JsonSerializer.Deserialize(json); if (flat == null) return new FullUpdateData(); @@ -436,54 +403,34 @@ namespace Tesla.Launcher.Service // Handled directly in the Service — progress is tracked // during file receive on the InitiateInstallProduct connection. var diagPath = Path.Combine(AppContext.BaseDirectory, "podconf.log"); - if (parameters?.Length > 0) + Guid progressId = Guid.Empty; + bool guidMatch = false; + if (args.Count > 0 && args[0].ValueKind == JsonValueKind.String + && Guid.TryParse(args[0].GetString(), out var pg)) { - var p0 = parameters[0]; - var p0Type = p0?.GetType().FullName ?? "null"; - Guid progressId; - bool guidMatch = false; + progressId = pg; + guidMatch = true; + } - if (p0 is Guid g) + if (guidMatch) + { + lock (_installLock) { - progressId = g; - guidMatch = true; - } - else if (p0 is string s && Guid.TryParse(s, out var gs)) - { - progressId = gs; - guidMatch = true; - } - else - { - progressId = Guid.Empty; - } - - if (guidMatch) - { - lock (_installLock) + if (_installProgress.TryGetValue(progressId, out var prog)) { - if (_installProgress.TryGetValue(progressId, out var prog)) - { - // Log first poll and when status changes - _logger.LogDebug( - "GetOutOfBandProgress({Id}): {Pct}% {Status} done={Done}", - progressId, prog.PercentComplete, prog.Status, prog.IsCompleted); - return prog; - } + _logger.LogDebug( + "GetOutOfBandProgress({Id}): {Pct}% {Status} done={Done}", + progressId, prog.PercentComplete, prog.Status, prog.IsCompleted); + return prog; } - File.AppendAllText(diagPath, - $"[{DateTime.Now:HH:mm:ss}] OOBP: Guid {progressId} NOT FOUND in _installProgress (type={p0Type}, keys={string.Join(",", GetProgressKeys())}){Environment.NewLine}"); - } - else - { - File.AppendAllText(diagPath, - $"[{DateTime.Now:HH:mm:ss}] OOBP: param[0] type={p0Type} value={p0} — NOT a Guid{Environment.NewLine}"); } + File.AppendAllText(diagPath, + $"[{DateTime.Now:HH:mm:ss}] OOBP: Guid {progressId} NOT FOUND in _installProgress (keys={string.Join(",", GetProgressKeys())}){Environment.NewLine}"); } else { File.AppendAllText(diagPath, - $"[{DateTime.Now:HH:mm:ss}] OOBP: NO parameters{Environment.NewLine}"); + $"[{DateTime.Now:HH:mm:ss}] OOBP: arg[0] not a Guid (kind={(args.Count > 0 ? args[0].ValueKind.ToString() : "none")}){Environment.NewLine}"); } return new OutOfBandProgress { @@ -495,7 +442,7 @@ namespace Tesla.Launcher.Service case "LaunchApp": { - var json = await ForwardToAgentJsonAsync(functionName, parameters, ct); + var json = await ForwardToAgentJsonAsync(functionName, args, ct); if (json == null) return 0; var doc = JsonDocument.Parse(json); if (doc.RootElement.TryGetProperty("ProcessId", out var pid)) @@ -505,7 +452,7 @@ namespace Tesla.Launcher.Service case "get_VolumeLevel": { - var json = await ForwardToAgentJsonAsync(functionName, parameters, ct); + var json = await ForwardToAgentJsonAsync(functionName, args, ct); return json != null ? JsonSerializer.Deserialize(json) : 1.0f; } @@ -524,6 +471,26 @@ namespace Tesla.Launcher.Service return callId; } + case "InstallApp": + { + // Flatten the wire LaunchData into the Agent's flat JSON format. + var ld = args.Count > 0 + ? args[0].Deserialize(PodRpc.JsonOptions) + : default; + var flat = new + { + LaunchKey = ld.LaunchPair.LaunchKey.ToString(), + DisplayName = ld.LaunchPair.DisplayName, + WorkingDirectory = ld.WorkingDirectory, + ExeFile = ld.ExeFile, + Arguments = ld.Arguments, + AutoRestart = ld.AutoRestart + }; + var payloadJson = JsonSerializer.Serialize(new object[] { flat }); + await ForwardToAgentCoreAsync(functionName, launchKey: null, payloadJson, ct); + return null; + } + case "KillApp": case "KillAllOfType": case "KillAllApps": @@ -531,33 +498,18 @@ namespace Tesla.Launcher.Service case "ClearStore": case "RemoveApp": case "UninstallApp": - case "InstallApp": { - // Flatten nested LaunchPair for Agent's flat JSON format - if (parameters?.Length > 0 && parameters[0] is LaunchData ld) - { - var flat = new - { - LaunchKey = ld.LaunchPair.LaunchKey.ToString(), - DisplayName = ld.LaunchPair.DisplayName, - WorkingDirectory = ld.WorkingDirectory, - ExeFile = ld.ExeFile, - Arguments = ld.Arguments, - AutoRestart = ld.AutoRestart - }; - parameters = new object[] { flat }; - } - await ForwardToAgentJsonAsync(functionName, parameters, ct); + await ForwardToAgentJsonAsync(functionName, args, ct); return null; } case "set_VolumeLevel": - await ForwardToAgentJsonAsync(functionName, parameters, ct); + await ForwardToAgentJsonAsync(functionName, args, ct); return null; default: _logger.LogWarning("Unknown command: {Func}", functionName); - await ForwardToAgentJsonAsync(functionName, parameters, ct); + await ForwardToAgentJsonAsync(functionName, args, ct); return null; } } @@ -882,30 +834,31 @@ namespace Tesla.Launcher.Service } } - // ── BinaryFormatter Setup ──────────────────────────────────────── - - private static BinaryFormatter CreateFormatter() - { - return new BinaryFormatter - { - Binder = new TeslaSerializationBinder() - }; - } - // ── Named Pipe IPC ─────────────────────────────────────────────── - private async Task ForwardToAgentJsonAsync( - string functionName, object[] parameters, CancellationToken ct) + + private Task ForwardToAgentJsonAsync( + string functionName, IReadOnlyList args, CancellationToken ct) { string launchKey = null; - if (parameters?.Length > 0 && parameters[0] is Guid guid) + if (args != null && args.Count > 0 && args[0].ValueKind == JsonValueKind.String + && Guid.TryParse(args[0].GetString(), out var guid)) launchKey = guid.ToString(); + string payloadJson = (args != null && args.Count > 0) + ? JsonSerializer.Serialize(args) + : null; + + return ForwardToAgentCoreAsync(functionName, launchKey, payloadJson, ct); + } + + private async Task ForwardToAgentCoreAsync( + string functionName, string launchKey, string payloadJson, CancellationToken ct) + { var msg = new IpcMessage { Command = functionName.ToUpperInvariant(), LaunchKey = launchKey, - PayloadJson = parameters?.Length > 0 - ? JsonSerializer.Serialize(parameters) : null + PayloadJson = payloadJson }; using var pipe = new NamedPipeClientStream( @@ -962,124 +915,12 @@ namespace Tesla.Launcher.Service } } - // ── Serialization Binder ───────────────────────────────────────────── - // BindToType: resolves Console-side type names to local Tesla.Net types. - // BindToName: writes "TeslaConsoleLaunchLib" as assembly for outgoing types. - - internal sealed class TeslaSerializationBinder : SerializationBinder - { - private static readonly Assembly TeslaAssembly = - typeof(ILauncherService).Assembly; - - public override Type BindToType(string assemblyName, string typeName) - { - // Intercept MemberInfoSerializationHolder — handles cross-runtime - // MethodBase deserialization (.NET Framework 2.0 → .NET 6). - if (typeName == "System.Reflection.MemberInfoSerializationHolder") - return typeof(MethodInfoProxy); - - // Try to resolve Tesla.Net types from our assembly - var type = TeslaAssembly.GetType(typeName); - if (type != null) return type; - - // Return null → default resolution (triggers AssemblyResolve if needed) - return null; - } - - public override void BindToName( - Type serializedType, out string assemblyName, out string typeName) - { - if (serializedType.Namespace == "Tesla.Net") - { - // Console expects types from TeslaConsoleLaunchLib - assemblyName = "TeslaConsoleLaunchLib, Version=4.11.4.0, Culture=neutral, PublicKeyToken=null"; - typeName = serializedType.FullName; - } - else - { - assemblyName = null; - typeName = null; - } - } - } - - // ── MethodInfo Proxy ───────────────────────────────────────────────── - // Replaces MemberInfoSerializationHolder during deserialization. - // Resolves serialized MethodBase references to ILauncherService methods. - - [Serializable] - internal sealed class MethodInfoProxy : ISerializable, IObjectReference - { - /// - /// Thread-static capture of the last deserialized method name. - /// IObjectReference fixup may fail to update fields inside boxed - /// value types (InvokeCommand is a struct), leaving Function null. - /// The handler reads this field as a fallback for the method name. - /// - [ThreadStatic] - internal static string LastResolvedName; - - private readonly string _memberName; - - private MethodInfoProxy(SerializationInfo info, StreamingContext ctx) - { - _memberName = info.GetString("Name"); - LastResolvedName = _memberName; - } - - public object GetRealObject(StreamingContext context) - { - // Look up the method on our ILauncherService interface. - // This handles both regular methods (Ping, LaunchApp, etc.) - // and property accessors (get_VolumeLevel, set_VolumeLevel). - var type = typeof(ILauncherService); - var method = type.GetMethod(_memberName, - BindingFlags.Public | BindingFlags.Instance); - if (method != null) return method; - - // Property accessors: get_X / set_X - if (_memberName.StartsWith("get_") || _memberName.StartsWith("set_")) - { - var propName = _memberName.Substring(4); - var prop = type.GetProperty(propName, - BindingFlags.Public | BindingFlags.Instance); - if (prop != null) - { - var accessor = _memberName.StartsWith("get_") - ? prop.GetMethod : prop.SetMethod; - if (accessor != null) return accessor; - } - } - - throw new SerializationException( - $"Cannot resolve method '{_memberName}' on ILauncherService"); - } - - public void GetObjectData(SerializationInfo info, StreamingContext context) - { - throw new NotSupportedException("MethodInfoProxy is read-only."); - } - } - // ── Entry Point ────────────────────────────────────────────────────── public class Program { public static void Main(string[] args) { - AppContext.SetSwitch( - "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization", true); - - // Redirect TeslaConsoleLaunchLib assembly references to our assembly - AppDomain.CurrentDomain.AssemblyResolve += (sender, resolveArgs) => - { - var name = new AssemblyName(resolveArgs.Name); - if (string.Equals(name.Name, "TeslaConsoleLaunchLib", - StringComparison.OrdinalIgnoreCase)) - return typeof(ILauncherService).Assembly; - return null; - }; - Host.CreateDefaultBuilder(args) .UseWindowsService(options => { diff --git a/Launcher/TeslaLauncherService.csproj b/Launcher/TeslaLauncherService.csproj index 36d5819..f47cb15 100644 --- a/Launcher/TeslaLauncherService.csproj +++ b/Launcher/TeslaLauncherService.csproj @@ -1,18 +1,17 @@ - net6.0-windows + net8.0-windows disable disable true - true 0.1.0 app.ico TeslaLauncherService Tesla.Launcher.Service - x86 - win-x86 + x64 + win-x64 true true true @@ -24,11 +23,21 @@ - - - + + + - + + + + + + diff --git a/SecureConfig/SecureConfig.cs b/SecureConfig/SecureConfig.cs new file mode 100644 index 0000000..131604d --- /dev/null +++ b/SecureConfig/SecureConfig.cs @@ -0,0 +1,1079 @@ +// ============================================================================= +// Tesla.SecureConfig — First-boot secure provisioning (assembly: TeslaSecureConfiguration) +// ============================================================================= +// Reconstructed verbatim (ilspycmd) from the original proprietary +// TeslaSecureConfiguration.dll. Provides the Console-side pod provisioning +// protocol: UDP RQST/RPLY beacons, PBKDF2-from-passphrase key derivation, the +// OFB crypto-stream handshake (NegotiateCryptoStreams), and RSA session-key +// exchange. Previously consumed by the Console (and the Tesla.Contract client) +// as a vendored binary; now built from source as the single definition. +// +// The wire/crypto behaviour is load-bearing: it must stay byte-compatible with +// the pod-side counterpart (the Launcher's hand-written OFBDuplexStream / +// CryptoHelper in Launcher/SecureConfig.cs). Byte-identity against the original +// binary is asserted by SecureConfigCompatTests in the differential suite. +// +// NOTE: this is a faithful decompilation — local names and some control flow +// differ from the lost original source (e.g. the dead GetInterface(byte[]) +// comparison loop is preserved as-is for behavioural fidelity). +// ============================================================================= + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Timers; +using Microsoft.Win32; + +namespace Tesla +{ + public class PodConfigurationClient : IDisposable + { + private static readonly Random sRandom = new Random(); + + private static readonly char[] sPassphraseAlphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ".ToCharArray(); + + private Process mPasscodeDisplay; + + private string mPassphrase; + + private string mRequestId; + + private static TextWriter mLogger; + + public string Passphrase => mPassphrase; + + public string RequestId => mRequestId; + + public static TextWriter Logger + { + get + { + return mLogger; + } + set + { + mLogger = value; + } + } + + public PodConfigurationClient(uint passpharaseLength, uint requestIdLength) + { + mPassphrase = GenerateRandomString(passpharaseLength); + mRequestId = GenerateRandomString(requestIdLength); + mPasscodeDisplay = new Process(); + mPasscodeDisplay.StartInfo = new ProcessStartInfo("TeslaPasscodeDisplay.exe", $"{mRequestId} {mPassphrase}"); + mPasscodeDisplay.StartInfo.CreateNoWindow = false; + mPasscodeDisplay.StartInfo.UseShellExecute = true; + mPasscodeDisplay.StartInfo.WorkingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + mPasscodeDisplay.Start(); + } + + private string GenerateRandomString(uint length) + { + char[] array = new char[length]; + for (int i = 0; i < array.Length; i++) + { + array[i] = sPassphraseAlphabet[sRandom.Next(sPassphraseAlphabet.Length)]; + } + return new string(array); + } + + public byte[] WaitForEncryptionKey(ushort localPort, ushort remotePort, IPAddress identifyingAddress) + { + mLogger.WriteLine("Waiting for configuration. Request ID: {0}\tPassphrase: {1}", mRequestId, mPassphrase); + mLogger.WriteLine("Configuring temporary ip address."); + mLogger.Flush(); + string adapterName; + byte[] @interface = GetInterface(identifyingAddress, out adapterName); + try + { + ConfigureTempIp(adapterName); + } + catch (Exception innerException) + { + throw new Exception("Could not initalize temporary IP address.", innerException); + } + mLogger.WriteLine("Temp IP configured."); + mLogger.Flush(); + byte[] array = PodConfigurationServer.GenerateKeyFromPassphrase(mPassphrase); + mLogger.WriteLine("Setting up broadcast."); + mLogger.Flush(); + byte[] bytes = Encoding.ASCII.GetBytes(mRequestId); + byte[] array2 = new byte[@interface.Length + bytes.Length]; + @interface.CopyTo(array2, 0); + bytes.CopyTo(array2, @interface.Length); + UdpBeacon udpBeacon = new UdpBeacon(PodConfigurationServer.sMessageRequestID, array2, 10000.0, remotePort, null); + udpBeacon.Start(); + mLogger.WriteLine("Broadcasting... waiting for packets."); + mLogger.Flush(); + BasicConfigResponse basicConfigResponse = ReceiveUdpResponse(localPort, array); + mLogger.WriteLine("Packets received."); + mLogger.Flush(); + udpBeacon.Stop(); + mLogger.WriteLine("Attempting to configure final IP."); + mLogger.Flush(); + try + { + SetNetworkProperties(adapterName, basicConfigResponse.Address, basicConfigResponse.Mask, basicConfigResponse.Gateway, basicConfigResponse.Dns, basicConfigResponse.HostName); + } + catch (Exception innerException2) + { + throw new Exception("Could not initalize final IP address.", innerException2); + } + mLogger.WriteLine("Waiting for console to connect."); + mLogger.Flush(); + TcpListener tcpListener = new TcpListener(IPAddress.Any, localPort); + tcpListener.Start(); + while (true) + { + TcpClient tcpClient = null; + try + { + mLogger.WriteLine("Console connection received. Negotiating..."); + mLogger.Flush(); + tcpClient = tcpListener.AcceptTcpClient(); + if (!PodConfigurationServer.NegotiateCryptoStreams(tcpClient.GetStream(), array, out var outStream, out var inStream)) + { + continue; + } + BinaryWriter binaryWriter = new BinaryWriter(outStream); + BinaryReader binaryReader = new BinaryReader(inStream); + mLogger.WriteLine("Secure connection to console negotiated."); + mLogger.Flush(); + RSACryptoServiceProvider rSACryptoServiceProvider = new RSACryptoServiceProvider(); + int dwKeySize = rSACryptoServiceProvider.KeySize; + KeySizes[] legalKeySizes = rSACryptoServiceProvider.LegalKeySizes; + foreach (KeySizes keySizes in legalKeySizes) + { + if (keySizes.MaxSize > rSACryptoServiceProvider.KeySize) + { + if (keySizes.MaxSize > 2048) + { + dwKeySize = 2048; + break; + } + rSACryptoServiceProvider.KeySize = keySizes.MaxSize; + } + } + mLogger.WriteLine("Sending console final key."); + mLogger.Flush(); + rSACryptoServiceProvider = new RSACryptoServiceProvider(dwKeySize); + binaryWriter.Write(rSACryptoServiceProvider.ToXmlString(includePrivateParameters: false)); + mLogger.WriteLine("Receiving console key."); + mLogger.Flush(); + byte[] rgb = binaryReader.ReadBytes(binaryReader.ReadInt32()); + byte[] result = rSACryptoServiceProvider.Decrypt(rgb, fOAEP: false); + mLogger.WriteLine("Console key received."); + mLogger.Flush(); + return result; + } + finally + { + try + { + tcpClient.Close(); + } + catch + { + } + try + { + tcpListener.Stop(); + } + catch + { + } + } + } + } + + private static byte[] GetInterface(IPAddress identifyingAddress, out string adapterName) + { + NetworkInterface[] allNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); + foreach (NetworkInterface networkInterface in allNetworkInterfaces) + { + foreach (UnicastIPAddressInformation unicastAddress in networkInterface.GetIPProperties().UnicastAddresses) + { + if (unicastAddress.Address == identifyingAddress) + { + adapterName = networkInterface.Name; + return networkInterface.GetPhysicalAddress().GetAddressBytes(); + } + } + } + NetworkInterface[] allNetworkInterfaces2 = NetworkInterface.GetAllNetworkInterfaces(); + foreach (NetworkInterface networkInterface2 in allNetworkInterfaces2) + { + if (networkInterface2.OperationalStatus == OperationalStatus.Up && networkInterface2.NetworkInterfaceType != NetworkInterfaceType.Loopback) + { + adapterName = networkInterface2.Name; + return networkInterface2.GetPhysicalAddress().GetAddressBytes(); + } + } + throw new Exception("Could not find a valid network adapter."); + } + + private static string GetInterface(byte[] identifyingMacAddress) + { + NetworkInterface[] allNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); + int num = 0; + if (num < allNetworkInterfaces.Length) + { + NetworkInterface networkInterface = allNetworkInterfaces[num]; + byte[] addressBytes = networkInterface.GetPhysicalAddress().GetAddressBytes(); + if (addressBytes.Length == identifyingMacAddress.Length) + { + for (int i = 0; i < addressBytes.Length; i++) + { + _ = addressBytes[i]; + _ = identifyingMacAddress[i]; + } + } + return networkInterface.Name; + } + NetworkInterface[] allNetworkInterfaces2 = NetworkInterface.GetAllNetworkInterfaces(); + foreach (NetworkInterface networkInterface2 in allNetworkInterfaces2) + { + if (networkInterface2.OperationalStatus == OperationalStatus.Up && networkInterface2.NetworkInterfaceType != NetworkInterfaceType.Loopback) + { + return networkInterface2.Name; + } + } + throw new Exception("Could not find a valid network adapter."); + } + + public static void SetNetworkProperties(byte[] macAddress, IPAddress address, IPAddress mask, IPAddress gateway, IPAddress dns, string hostName) + { + SetNetworkProperties(GetInterface(macAddress), address, mask, gateway, dns, hostName); + } + + public static void SetNetworkProperties(string adapterName, IPAddress address, IPAddress mask, IPAddress gateway, IPAddress dns, string hostName) + { + if (adapterName == null) + { + throw new ArgumentNullException("adapterName"); + } + if (adapterName.Length == 0) + { + throw new ArgumentOutOfRangeException("adapterName", "Adapter name can not be empty."); + } + if (address.AddressFamily != AddressFamily.InterNetwork) + { + throw new ArgumentOutOfRangeException("address", "Address must be an IPv4 address."); + } + if (mask.AddressFamily != AddressFamily.InterNetwork) + { + throw new ArgumentOutOfRangeException("mask", "Mask must be an IPv4 address."); + } + if (gateway.AddressFamily != AddressFamily.InterNetwork) + { + throw new ArgumentOutOfRangeException("gateway", "Default gateway must be an IPv4 address."); + } + if (dns.AddressFamily != AddressFamily.InterNetwork) + { + throw new ArgumentOutOfRangeException("dns", "DNS server address must be an IPv4 address."); + } + mLogger.WriteLine(string.Format("Setting data for adapter \"{0}\": IP {1}, Mask {2}, Gateway {3}, DNS {4}, Host {5}.", adapterName, address.ToString(), mask.ToString(), gateway.Equals(IPAddress.Any) ? "none" : gateway.ToString(), dns.Equals(IPAddress.Any) ? "none" : dns.ToString(), string.IsNullOrEmpty(hostName) ? "no change" : hostName)); + mLogger.Flush(); + using (Process process = new Process()) + { + process.StartInfo.FileName = "netsh"; + process.StartInfo.Arguments = string.Format("interface ip set address \"{0}\" static {1} {2} {3}", adapterName, address, mask, gateway.Equals(IPAddress.Any) ? "none" : (gateway.ToString() + " 1")); + process.Start(); + process.WaitForExit(); + if (process.ExitCode != 0) + { + throw new Exception("Could not initalize IP address."); + } + } + mLogger.WriteLine("IP Set"); + mLogger.Flush(); + using (Process process2 = new Process()) + { + process2.StartInfo.FileName = "netsh"; + process2.StartInfo.Arguments = string.Format("interface ip set dns \"{0}\" static {1}", adapterName, dns.Equals(IPAddress.Any) ? "none" : dns.ToString()); + process2.Start(); + process2.WaitForExit(); + if (process2.ExitCode != 0) + { + throw new Exception("Could not set DNS server."); + } + } + mLogger.WriteLine("DNS set."); + mLogger.Flush(); + if (string.IsNullOrEmpty(hostName)) + { + return; + } + using (RegistryKey registryKey = Registry.LocalMachine.OpenSubKey("SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ComputerName", writable: true)) + { + try + { + registryKey.SetValue("ComputerName", hostName, RegistryValueKind.String); + } + catch (Exception innerException) + { + throw new Exception("Could not set host name.", innerException); + } + finally + { + registryKey.Close(); + } + } + using (RegistryKey registryKey2 = Registry.LocalMachine.OpenSubKey("SYSTEM\\CurrentControlSet\\services\\Tcpip\\Parameters", writable: true)) + { + try + { + registryKey2.SetValue("NV Hostname", hostName, RegistryValueKind.String); + } + catch (Exception innerException2) + { + throw new Exception("Could not set host name.", innerException2); + } + finally + { + registryKey2.Close(); + } + } + mLogger.WriteLine("Host set."); + mLogger.Flush(); + } + + private static void ConfigureTempIp(string adapterName) + { + byte[] array = new byte[4]; + sRandom.NextBytes(array); + array[0] = 172; + array[1] = (byte)((array[1] & 0xFu) | 0x10u); + IPAddress address = new IPAddress(array); + IPAddress mask = IPAddress.Parse("255.240.0.0"); + SetNetworkProperties(adapterName, address, mask, IPAddress.Any, IPAddress.Any, null); + } + + private static BasicConfigResponse ReceiveUdpResponse(ushort localPort, byte[] weakAesKey) + { + UdpBeaconListener udpBeaconListener = new UdpBeaconListener(PodConfigurationServer.sMessageReplyID, localPort, weakAesKey); + BasicConfigResponse basicConfigResponse; + do + { + basicConfigResponse = new BasicConfigResponse(udpBeaconListener.Receive()); + } + while (basicConfigResponse == null); + return basicConfigResponse; + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && mPasscodeDisplay != null && mPasscodeDisplay.Responding) + { + mPasscodeDisplay.Kill(); + } + } + } + public delegate void ConfigurationRequestReceivedDelegate(byte[] macAddress, string reqeustId); + public class PodConfigurationServer + { + internal static readonly byte[] sMessageRequestID = Encoding.UTF8.GetBytes("RQST"); + + internal static readonly byte[] sMessageReplyID = Encoding.UTF8.GetBytes("RPLY"); + + private static readonly byte[] sMessageConfID = Encoding.UTF8.GetBytes("CONF"); + + private static readonly byte[] mPassphraseSalt = new byte[32] + { + 23, 171, 81, 217, 236, 209, 212, 116, 169, 9, + 74, 52, 39, 251, 31, 242, 222, 196, 249, 241, + 166, 216, 158, 218, 21, 17, 71, 101, 50, 231, + 231, 239 + }; + + private readonly ushort mLocalPort; + + private readonly ushort mRemotePort; + + private readonly ConfigurationRequestReceivedDelegate mRequestDelegate; + + private UdpBeaconListener mBeaconListener; + + private List mActiveConfigPods = new List(); + + internal static byte[] GenerateKeyFromPassphrase(string passphrase) + { + return new Rfc2898DeriveBytes(passphrase, mPassphraseSalt, 1000).GetBytes(32); + } + + public static bool NegotiateCryptoStreams(NetworkStream clientStream, byte[] key, out Stream outStream, out Stream inStream) + { + outStream = null; + inStream = null; + Rijndael rijndael = Rijndael.Create(); + rijndael.Mode = CipherMode.ECB; + rijndael.Key = key; + rijndael.GenerateIV(); + clientStream.Write(rijndael.IV, 0, rijndael.IV.Length); + inStream = new OFBCryptoStream(clientStream, rijndael.CreateEncryptor(), CryptoStreamMode.Read, rijndael.IV); + byte[] array = new byte[rijndael.IV.Length]; + for (int i = 0; i < array.Length; i += clientStream.Read(array, i, array.Length - i)) + { + } + rijndael.IV = array; + outStream = new OFBCryptoStream(clientStream, rijndael.CreateEncryptor(), CryptoStreamMode.Write, array); + outStream.Write(sMessageConfID, 0, sMessageConfID.Length); + byte[] array2 = new byte[sMessageConfID.Length]; + for (int j = 0; j < array2.Length; j += inStream.Read(array2, j, array2.Length - j)) + { + } + for (int k = 0; k < array2.Length; k++) + { + if (array2[k] != sMessageConfID[k]) + { + return false; + } + } + return true; + } + + public PodConfigurationServer(ushort localPort, ushort remotePort, ConfigurationRequestReceivedDelegate requestDelegate) + { + if (requestDelegate == null) + { + throw new ArgumentNullException("requestDelegate"); + } + mLocalPort = localPort; + mRemotePort = remotePort; + mRequestDelegate = requestDelegate; + mBeaconListener = new UdpBeaconListener(sMessageRequestID, mLocalPort, null); + mBeaconListener.BeginReceive(BeaconReceived, null); + } + + private void BeaconReceived(IAsyncResult ar) + { + byte[] array; + try + { + array = mBeaconListener.EndReceive(ar); + } + catch + { + mBeaconListener = new UdpBeaconListener(sMessageRequestID, mLocalPort, null); + mBeaconListener.BeginReceive(BeaconReceived, null); + return; + } + mBeaconListener.BeginReceive(BeaconReceived, null); + byte[] array2 = new byte[6]; + byte[] array3 = new byte[array.Length - array2.Length]; + Buffer.BlockCopy(array, 0, array2, 0, array2.Length); + Buffer.BlockCopy(array, array2.Length, array3, 0, array3.Length); + byte[] array4 = new byte[8]; + Array.Copy(array2, array4, array2.Length); + long item = BitConverter.ToInt64(array4, 0); + lock (mActiveConfigPods) + { + if (mActiveConfigPods.Contains(item)) + { + return; + } + mActiveConfigPods.Add(item); + } + try + { + mRequestDelegate(array2, Encoding.ASCII.GetString(array3)); + } + finally + { + lock (mActiveConfigPods) + { + mActiveConfigPods.Remove(item); + } + } + } + + public byte[] SendEncryptionKey(string passphrase, IPAddress ipAddress, IPAddress mask, IPAddress gateway, IPAddress dns, string hostName, TimeSpan timeout) + { + DateTime dateTime = DateTime.Now + timeout; + byte[] array = GenerateKeyFromPassphrase(passphrase); + UdpBeacon udpBeacon = new UdpBeacon(sMessageReplyID, new BasicConfigResponse(ipAddress, mask, gateway, dns, hostName).ToBytes(), 10000.0, mRemotePort, array); + udpBeacon.Start(); + try + { + do + { + TcpClient tcpClient = null; + try + { + tcpClient = new TcpClient(AddressFamily.InterNetwork); + tcpClient.Connect(new IPEndPoint(ipAddress, mRemotePort)); + if (NegotiateCryptoStreams(tcpClient.GetStream(), array, out var outStream, out var inStream)) + { + BinaryWriter binaryWriter = new BinaryWriter(outStream); + BinaryReader binaryReader = new BinaryReader(inStream); + RSACryptoServiceProvider rSACryptoServiceProvider = new RSACryptoServiceProvider(); + rSACryptoServiceProvider.FromXmlString(binaryReader.ReadString()); + Rijndael rijndael = Rijndael.Create(); + rijndael.KeySize = 256; + rijndael.GenerateKey(); + byte[] key = rijndael.Key; + byte[] array2 = rSACryptoServiceProvider.Encrypt(key, fOAEP: false); + binaryWriter.Write(array2.Length); + binaryWriter.Write(array2); + return key; + } + } + catch (SocketException) + { + } + finally + { + try + { + tcpClient.Close(); + } + catch + { + } + } + } + while (DateTime.Now < dateTime); + throw new TimeoutException(); + } + finally + { + udpBeacon.Stop(); + } + } + } + public class UdpBeaconListener + { + private class InternalAsyncResult : IAsyncResult + { + public AsyncCallback UserCallback; + + public IAsyncResult UdpAsyncResult; + + public Exception ThrownException; + + public byte[] ReceivedMessage; + + private object mState; + + public EventWaitHandle WaitHandle = new EventWaitHandle(initialState: false, EventResetMode.ManualReset); + + private bool mIsCompleted; + + public object AsyncState => mState; + + public WaitHandle AsyncWaitHandle => WaitHandle; + + public bool CompletedSynchronously => false; + + public bool IsCompleted + { + get + { + return mIsCompleted; + } + set + { + mIsCompleted = value; + } + } + + public InternalAsyncResult(AsyncCallback userCallback, object userState) + { + UserCallback = userCallback; + mState = userState; + } + } + + private readonly byte[] mHeader; + + private readonly Rijndael mAes; + + private readonly SHA1 mSha1; + + private UdpClient mClient; + + public UdpBeaconListener(byte[] header, ushort port, byte[] encryptionKey) + { + mHeader = (byte[])header.Clone(); + mClient = new UdpClient(port); + mClient.EnableBroadcast = true; + if (encryptionKey != null) + { + mAes = UdpBeacon.CreateRijndael(encryptionKey); + mSha1 = SHA1.Create(); + } + } + + public IAsyncResult BeginReceive(AsyncCallback requestCallback, object state) + { + InternalAsyncResult internalAsyncResult = new InternalAsyncResult(requestCallback, state); + internalAsyncResult.UdpAsyncResult = mClient.BeginReceive(InternalEndReceive, internalAsyncResult); + return internalAsyncResult; + } + + public byte[] EndReceive(IAsyncResult ar) + { + InternalAsyncResult internalAsyncResult = (InternalAsyncResult)ar; + internalAsyncResult.AsyncWaitHandle.WaitOne(); + if (internalAsyncResult.ThrownException != null) + { + throw internalAsyncResult.ThrownException; + } + return internalAsyncResult.ReceivedMessage; + } + + public byte[] Receive() + { + return EndReceive(BeginReceive(null, null)); + } + + private void InternalEndReceive(IAsyncResult udpAr) + { + InternalAsyncResult internalAsyncResult = (InternalAsyncResult)udpAr.AsyncState; + ReceiveAndTransform(internalAsyncResult); + if (internalAsyncResult.ThrownException != null || internalAsyncResult.ReceivedMessage != null) + { + internalAsyncResult.IsCompleted = true; + internalAsyncResult.WaitHandle.Set(); + if (internalAsyncResult.UserCallback != null) + { + internalAsyncResult.UserCallback(internalAsyncResult); + } + } + else + { + internalAsyncResult.UdpAsyncResult = mClient.BeginReceive(InternalEndReceive, internalAsyncResult); + } + } + + private void ReceiveAndTransform(InternalAsyncResult ar) + { + IPEndPoint remoteEP = new IPEndPoint(0L, 0); + byte[] array; + try + { + array = mClient.EndReceive(ar.UdpAsyncResult, ref remoteEP); + } + catch (Exception thrownException) + { + Exception ex = (ar.ThrownException = thrownException); + return; + } + finally + { + ar.UdpAsyncResult = null; + } + try + { + if (array.Length < mHeader.Length) + { + return; + } + for (int i = 0; i < mHeader.Length; i++) + { + if (array[i] != mHeader[i]) + { + return; + } + } + if (mAes == null) + { + ar.ReceivedMessage = new byte[array.Length - mHeader.Length]; + Buffer.BlockCopy(array, mHeader.Length, ar.ReceivedMessage, 0, ar.ReceivedMessage.Length); + return; + } + byte[] array2 = new byte[mAes.IV.Length]; + if (array.Length < mHeader.Length + array2.Length) + { + return; + } + Buffer.BlockCopy(array, mHeader.Length, array2, 0, array2.Length); + ICryptoTransform transform = mAes.CreateDecryptor(mAes.Key, array2); + MemoryStream memoryStream = new MemoryStream(); + CryptoStream cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write); + cryptoStream.Write(array, mHeader.Length + array2.Length, array.Length - (mHeader.Length + array2.Length)); + cryptoStream.FlushFinalBlock(); + byte[] array3 = memoryStream.ToArray(); + int num = array3.Length - mSha1.HashSize / 8; + if (num <= 0) + { + return; + } + byte[] array4 = mSha1.ComputeHash(array3, 0, num); + for (int j = 0; j < array4.Length; j++) + { + if (array3[j + num] != array4[j]) + { + return; + } + } + ar.ReceivedMessage = new byte[num]; + Buffer.BlockCopy(array3, 0, ar.ReceivedMessage, 0, num); + } + catch + { + } + } + } + public class UdpBeacon + { + private readonly byte[] mHeader; + + private readonly byte[] mMessage; + + private readonly double mMilliseconds; + + private readonly IPEndPoint mBroadcast; + + private readonly object mSynchronizer; + + private readonly Rijndael mAes; + + private readonly SHA1 mSha1; + + private UdpClient mClient; + + private System.Timers.Timer mTimer; + + internal static Rijndael CreateRijndael(byte[] encryptionKey) + { + Rijndael rijndael = Rijndael.Create(); + rijndael.Key = (byte[])encryptionKey.Clone(); + return rijndael; + } + + public UdpBeacon(byte[] header, byte[] message, double milliseconds, ushort port, byte[] encryptionKey) + { + mHeader = (byte[])header.Clone(); + mMessage = (byte[])message.Clone(); + if (encryptionKey != null) + { + mAes = CreateRijndael(encryptionKey); + mSha1 = SHA1.Create(); + } + mMilliseconds = milliseconds; + mBroadcast = new IPEndPoint(IPAddress.Broadcast, port); + mSynchronizer = new object(); + } + + public void Start() + { + lock (mSynchronizer) + { + Initalize(); + BroadcastBeacon(allowReset: false); + mTimer.Start(); + } + } + + public void Stop() + { + lock (mSynchronizer) + { + if (mTimer != null) + { + mTimer.Stop(); + } + mTimer = null; + if (mClient != null) + { + mClient.Close(); + } + mClient = null; + } + } + + private void Initalize() + { + try + { + Stop(); + } + catch + { + } + mClient = new UdpClient(); + mClient.EnableBroadcast = true; + mTimer = new System.Timers.Timer(mMilliseconds); + mTimer.Elapsed += mTimer_Elapsed; + mTimer.AutoReset = true; + } + + private void mTimer_Elapsed(object sender, ElapsedEventArgs e) + { + lock (mSynchronizer) + { + BroadcastBeacon(allowReset: true); + } + } + + private void BroadcastBeacon(bool allowReset) + { + try + { + List list = new List(mHeader); + if (mAes == null) + { + list.AddRange(mMessage); + } + else + { + mAes.GenerateIV(); + list.AddRange(mAes.IV); + byte[] array = mSha1.ComputeHash(mMessage); + byte[] array2 = new byte[mMessage.Length + array.Length]; + mMessage.CopyTo(array2, 0); + array.CopyTo(array2, mMessage.Length); + MemoryStream memoryStream = new MemoryStream(); + CryptoStream cryptoStream = new CryptoStream(memoryStream, mAes.CreateEncryptor(), CryptoStreamMode.Write); + cryptoStream.Write(array2, 0, array2.Length); + cryptoStream.FlushFinalBlock(); + list.AddRange(memoryStream.ToArray()); + } + byte[] array3 = list.ToArray(); + mClient.Send(array3, array3.Length, mBroadcast); + } + catch + { + if (allowReset) + { + Stop(); + Initalize(); + Start(); + } + } + } + } +} +namespace System.Security.Cryptography +{ + public class OFBCryptoStream : Stream + { + private Stream mStream; + + private ICryptoTransform mTransform; + + private CryptoStreamMode mStreamMode; + + private byte[] mOutputBuffer; + + private byte[] mOutputBufferAlternate; + + private int mOutputBufferPosition; + + public override bool CanRead => mStreamMode == CryptoStreamMode.Read; + + public override bool CanSeek => false; + + public override bool CanWrite => mStreamMode == CryptoStreamMode.Write; + + public override long Length + { + get + { + throw new NotSupportedException(); + } + } + + public override long Position + { + get + { + throw new NotSupportedException(); + } + set + { + throw new NotSupportedException(); + } + } + + public OFBCryptoStream(Stream stream, ICryptoTransform transform, CryptoStreamMode mode, byte[] IV) + { + if (transform.InputBlockSize != transform.OutputBlockSize) + { + throw new ArgumentException("transform"); + } + if (!transform.CanTransformMultipleBlocks) + { + throw new ArgumentException("transform"); + } + if (mode != 0 && mode != CryptoStreamMode.Write) + { + throw new ArgumentOutOfRangeException("mode"); + } + if ((mode == CryptoStreamMode.Read && !stream.CanRead) || (mode != CryptoStreamMode.Write && !stream.CanWrite)) + { + throw new ArgumentException("mode"); + } + mStream = stream; + mTransform = transform; + mStreamMode = mode; + mOutputBuffer = IV; + mOutputBufferAlternate = new byte[mTransform.OutputBlockSize]; + NextBuffer(); + } + + private void NextBuffer() + { + mTransform.TransformBlock(mOutputBuffer, 0, mOutputBuffer.Length, mOutputBufferAlternate, 0); + byte[] array = mOutputBuffer; + mOutputBuffer = mOutputBufferAlternate; + mOutputBufferAlternate = array; + mOutputBufferPosition = 0; + } + + public override void Flush() + { + mStream.Flush(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!CanRead) + { + throw new NotSupportedException(); + } + byte[] array = new byte[count]; + int num = mStream.Read(array, 0, count); + int num2 = 0; + while (num2 < num) + { + if (mOutputBufferPosition >= mOutputBuffer.Length) + { + NextBuffer(); + } + while (mOutputBufferPosition < mOutputBuffer.Length && num2 < num) + { + buffer[offset++] = (byte)(mOutputBuffer[mOutputBufferPosition++] ^ array[num2++]); + } + } + return num; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (!CanWrite) + { + throw new NotSupportedException(); + } + byte[] array = new byte[count]; + int num = 0; + while (num < count) + { + if (mOutputBufferPosition >= mOutputBuffer.Length) + { + NextBuffer(); + } + while (mOutputBufferPosition < mOutputBuffer.Length && num < count) + { + array[num++] = (byte)(mOutputBuffer[mOutputBufferPosition++] ^ buffer[offset++]); + } + } + mStream.Write(array, 0, count); + } + } +} +namespace Tesla +{ + public class BasicConfigResponse + { + private IPAddress mAddress; + + private IPAddress mMask; + + private IPAddress mGateway; + + private IPAddress mDns; + + private string mHostName; + + public IPAddress Address => mAddress; + + public IPAddress Mask => mMask; + + public IPAddress Gateway => mGateway; + + public IPAddress Dns => mDns; + + public string HostName => mHostName; + + public BasicConfigResponse(IPAddress address, IPAddress mask, IPAddress gateway, IPAddress dns, string hostName) + { + if (address.AddressFamily != AddressFamily.InterNetwork) + { + throw new ArgumentOutOfRangeException("address", "Address must be an IPv4 address."); + } + if (mask.AddressFamily != AddressFamily.InterNetwork) + { + throw new ArgumentOutOfRangeException("mask", "Address mask must be an IPv4 address."); + } + if (gateway.AddressFamily != AddressFamily.InterNetwork) + { + throw new ArgumentOutOfRangeException("gateway", "Default gateway must be an IPv4 address."); + } + if (dns.AddressFamily != AddressFamily.InterNetwork) + { + throw new ArgumentOutOfRangeException("dns", "DNS server must be an IPv4 address."); + } + if (hostName == null) + { + throw new ArgumentNullException("hostName"); + } + mAddress = address; + mMask = mask; + mGateway = gateway; + mDns = dns; + mHostName = hostName; + } + + public byte[] ToBytes() + { + List list = new List(8); + list.AddRange(mAddress.GetAddressBytes()); + list.AddRange(mMask.GetAddressBytes()); + list.AddRange(mGateway.GetAddressBytes()); + list.AddRange(mDns.GetAddressBytes()); + list.AddRange(Encoding.UTF8.GetBytes(mHostName)); + return list.ToArray(); + } + + public BasicConfigResponse(byte[] data) + { + byte[] array = new byte[4]; + Buffer.BlockCopy(data, 0, array, 0, array.Length); + mAddress = new IPAddress(array); + Buffer.BlockCopy(data, 4, array, 0, array.Length); + mMask = new IPAddress(array); + Buffer.BlockCopy(data, 8, array, 0, array.Length); + mGateway = new IPAddress(array); + Buffer.BlockCopy(data, 12, array, 0, array.Length); + mDns = new IPAddress(array); + mHostName = Encoding.UTF8.GetString(data, 16, data.Length - 16); + } + } +} diff --git a/SecureConfig/Tesla.SecureConfig.csproj b/SecureConfig/Tesla.SecureConfig.csproj new file mode 100644 index 0000000..143f862 --- /dev/null +++ b/SecureConfig/Tesla.SecureConfig.csproj @@ -0,0 +1,28 @@ + + + + + net48 + + + TeslaSecureConfiguration + Tesla + 1.0.0.0 + 1.0.0.0 + 1.0.0.0 + + disable + disable + latest + + + + + + + diff --git a/TeslaSuite.sln b/TeslaSuite.sln index 6e6b8fc..43359de 100644 --- a/TeslaSuite.sln +++ b/TeslaSuite.sln @@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherService", "Lau EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherAgent", "Launcher\TeslaLauncherAgent.csproj", "{916DCDA5-3379-4383-96F0-AC7B26FAF64E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesla.Contract", "Contract\Tesla.Contract.csproj", "{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesla.SecureConfig", "SecureConfig\Tesla.SecureConfig.csproj", "{070A6093-6C46-4A5B-A119-47ED195530E1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,14 +36,22 @@ Global {467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Release|Any CPU.Build.0 = Release|Any CPU - {910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.ActiveCfg = Debug|x86 - {910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.Build.0 = Debug|x86 - {910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.ActiveCfg = Release|x86 - {910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.Build.0 = Release|x86 - {916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.ActiveCfg = Debug|x86 - {916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.Build.0 = Debug|x86 - {916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.ActiveCfg = Release|x86 - {916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.Build.0 = Release|x86 + {910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.ActiveCfg = Debug|x64 + {910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.Build.0 = Debug|x64 + {910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.ActiveCfg = Release|x64 + {910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.Build.0 = Release|x64 + {916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.ActiveCfg = Debug|x64 + {916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.Build.0 = Debug|x64 + {916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.ActiveCfg = Release|x64 + {916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.Build.0 = Release|x64 + {0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Release|Any CPU.Build.0 = Release|Any CPU + {070A6093-6C46-4A5B-A119-47ED195530E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {070A6093-6C46-4A5B-A119-47ED195530E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {070A6093-6C46-4A5B-A119-47ED195530E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {070A6093-6C46-4A5B-A119-47ED195530E1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {27C769F3-F07F-456E-ACB7-F4A4A21AB6D1} = {91E92ADD-9F67-41E4-B43C-CCB7B2D95F15}