Initial project snapshot

This commit is contained in:
2026-04-28 22:29:50 +03:00
commit 8ba0561f4f
365 changed files with 91832 additions and 0 deletions
@@ -0,0 +1,129 @@
using System.Runtime.InteropServices;
using System.Text.Json;
using RemoteAccessPlatform.Windows.Contracts;
using RemoteAccessPlatform.Windows.Models;
namespace RemoteAccessPlatform.Windows.Settings;
public sealed class DpapiTokenStore : ITokenStore
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
public async Task<StoredAuthState?> LoadAsync(CancellationToken cancellationToken)
{
if (!File.Exists(StoragePathResolver.TokenFilePath))
{
return null;
}
byte[] protectedPayload = await File.ReadAllBytesAsync(StoragePathResolver.TokenFilePath, cancellationToken);
byte[] payload = NativeDpapi.Unprotect(protectedPayload);
return JsonSerializer.Deserialize<StoredAuthState>(payload, SerializerOptions);
}
public async Task SaveAsync(StoredAuthState state, CancellationToken cancellationToken)
{
StoragePathResolver.EnsureRoot();
byte[] payload = JsonSerializer.SerializeToUtf8Bytes(state, SerializerOptions);
byte[] protectedPayload = NativeDpapi.Protect(payload);
await File.WriteAllBytesAsync(StoragePathResolver.TokenFilePath, protectedPayload, cancellationToken);
}
public Task ClearAsync(CancellationToken cancellationToken)
{
if (File.Exists(StoragePathResolver.TokenFilePath))
{
File.Delete(StoragePathResolver.TokenFilePath);
}
return Task.CompletedTask;
}
private static class NativeDpapi
{
private const int CryptProtectUiForbidden = 0x1;
public static byte[] Protect(byte[] data)
{
return Execute(data, true);
}
public static byte[] Unprotect(byte[] data)
{
return Execute(data, false);
}
private static byte[] Execute(byte[] data, bool protect)
{
DATA_BLOB input = default;
DATA_BLOB output = default;
IntPtr inputPointer = IntPtr.Zero;
try
{
inputPointer = Marshal.AllocHGlobal(data.Length);
Marshal.Copy(data, 0, inputPointer, data.Length);
input.cbData = data.Length;
input.pbData = inputPointer;
bool success = protect
? CryptProtectData(ref input, null, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, CryptProtectUiForbidden, ref output)
: CryptUnprotectData(ref input, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, CryptProtectUiForbidden, ref output);
if (!success)
{
throw new InvalidOperationException($"DPAPI operation failed with Win32 error {Marshal.GetLastWin32Error()}.");
}
byte[] result = new byte[output.cbData];
Marshal.Copy(output.pbData, result, 0, output.cbData);
return result;
}
finally
{
if (inputPointer != IntPtr.Zero)
{
Marshal.FreeHGlobal(inputPointer);
}
if (output.pbData != IntPtr.Zero)
{
LocalFree(output.pbData);
}
}
}
[StructLayout(LayoutKind.Sequential)]
private struct DATA_BLOB
{
public int cbData;
public IntPtr pbData;
}
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CryptProtectData(
ref DATA_BLOB pDataIn,
string? szDataDescr,
IntPtr pOptionalEntropy,
IntPtr pvReserved,
IntPtr pPromptStruct,
int dwFlags,
ref DATA_BLOB pDataOut);
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CryptUnprotectData(
ref DATA_BLOB pDataIn,
IntPtr ppszDataDescr,
IntPtr pOptionalEntropy,
IntPtr pvReserved,
IntPtr pPromptStruct,
int dwFlags,
ref DATA_BLOB pDataOut);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LocalFree(IntPtr hMem);
}
}
@@ -0,0 +1,32 @@
using System.Text.Json;
using RemoteAccessPlatform.Windows.Contracts;
using RemoteAccessPlatform.Windows.Models;
namespace RemoteAccessPlatform.Windows.Settings;
public sealed class JsonLocalSettingsStore : ILocalSettingsStore
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
public async Task<LocalClientSettings> LoadAsync(CancellationToken cancellationToken)
{
if (!File.Exists(StoragePathResolver.SettingsFilePath))
{
return new LocalClientSettings();
}
await using FileStream stream = File.OpenRead(StoragePathResolver.SettingsFilePath);
LocalClientSettings? settings = await JsonSerializer.DeserializeAsync<LocalClientSettings>(stream, SerializerOptions, cancellationToken);
return settings ?? new LocalClientSettings();
}
public async Task SaveAsync(LocalClientSettings settings, CancellationToken cancellationToken)
{
StoragePathResolver.EnsureRoot();
await using FileStream stream = File.Create(StoragePathResolver.SettingsFilePath);
await JsonSerializer.SerializeAsync(stream, settings, SerializerOptions, cancellationToken);
}
}
@@ -0,0 +1,24 @@
using RemoteAccessPlatform.Windows.Contracts;
namespace RemoteAccessPlatform.Windows.Settings;
public sealed class LocalDeviceIdentityProvider(ILocalSettingsStore settingsStore) : IDeviceIdentityProvider
{
public async Task<string> GetOrCreateFingerprintAsync(CancellationToken cancellationToken)
{
var settings = await settingsStore.LoadAsync(cancellationToken);
if (!string.IsNullOrWhiteSpace(settings.DeviceFingerprint))
{
return settings.DeviceFingerprint;
}
settings.DeviceFingerprint = Guid.NewGuid().ToString("N");
await settingsStore.SaveAsync(settings, cancellationToken);
return settings.DeviceFingerprint;
}
public Task<string> GetDeviceLabelAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Environment.MachineName);
}
}
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\RemoteAccessPlatform.Windows.Contracts\RemoteAccessPlatform.Windows.Contracts.csproj" />
<ProjectReference Include="..\RemoteAccessPlatform.Windows.Models\RemoteAccessPlatform.Windows.Models.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -0,0 +1,18 @@
namespace RemoteAccessPlatform.Windows.Settings;
internal static class StoragePathResolver
{
private static readonly string Root = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"RemoteAccessPlatform",
"WindowsClient");
public static string RootDirectory => Root;
public static string TokenFilePath => Path.Combine(Root, "auth-state.dat");
public static string SettingsFilePath => Path.Combine(Root, "client-settings.json");
public static void EnsureRoot()
{
Directory.CreateDirectory(Root);
}
}