Initial project snapshot
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+24
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user