117 lines
5.8 KiB
C#
117 lines
5.8 KiB
C#
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.ComponentModel;
|
|
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
using QSafe.Core;
|
|
|
|
namespace QSafe.Desktop;
|
|
|
|
public sealed class LocalStore
|
|
{
|
|
public string DirectoryPath { get; }
|
|
private string VaultPath => Path.Combine(DirectoryPath, "vault.qsafe");
|
|
private string BasePath => Path.Combine(DirectoryPath, "synced.qsafe");
|
|
private string ConfigPath => Path.Combine(DirectoryPath, "connection.enc");
|
|
public bool Exists => File.Exists(VaultPath);
|
|
public Connection Config { get; private set; } = new();
|
|
public bool Connected => Config.Token.Length > 0;
|
|
public LocalStore(string path)
|
|
{
|
|
DirectoryPath = path; Directory.CreateDirectory(path);
|
|
if (File.Exists(ConfigPath))
|
|
{
|
|
var plain = DeviceProtection.Unprotect(File.ReadAllBytes(ConfigPath));
|
|
try { Config = JsonSerializer.Deserialize<Connection>(plain, Vault.Json) ?? new(); }
|
|
finally { CryptographicOperations.ZeroMemory(plain); }
|
|
}
|
|
}
|
|
public static byte[] ReadBounded(string path, int limit = VaultCrypto.MaxBytes)
|
|
{
|
|
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
if (stream.Length > limit) throw new InvalidDataException("Файл слишком большой.");
|
|
using var output = new MemoryStream(); stream.CopyTo(output);
|
|
if (output.Length > limit) throw new InvalidDataException("Файл слишком большой.");
|
|
return output.ToArray();
|
|
}
|
|
public byte[] Read() => ReadBounded(VaultPath);
|
|
public byte[]? ReadBase() => File.Exists(BasePath) ? ReadBounded(BasePath) : null;
|
|
public static void AtomicWrite(string path, byte[] bytes)
|
|
{
|
|
var temp = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
|
|
try
|
|
{
|
|
using (var stream = new FileStream(temp, FileMode.CreateNew, FileAccess.Write, FileShare.None, 8192, FileOptions.WriteThrough)) { stream.Write(bytes); stream.Flush(true); }
|
|
File.Move(temp, path, true);
|
|
}
|
|
finally { if (File.Exists(temp)) File.Delete(temp); }
|
|
}
|
|
public void Save(VaultData data, VaultKey key) => SaveEnvelope(Vault.Encrypt(data, key));
|
|
public void SaveEnvelope(byte[] envelope)
|
|
{
|
|
VaultCrypto.Validate(envelope);
|
|
if (Exists)
|
|
{
|
|
var backup = Path.Combine(DirectoryPath, "backups"); Directory.CreateDirectory(backup);
|
|
AtomicWrite(Path.Combine(backup, DateTime.UtcNow.ToString("yyyyMMdd-HHmmss-fffffff") + ".qsafe"), Read());
|
|
foreach (var old in Directory.GetFiles(backup, "*.qsafe").OrderDescending().Skip(10)) File.Delete(old);
|
|
}
|
|
AtomicWrite(VaultPath, envelope);
|
|
}
|
|
public void SaveConfig()
|
|
{
|
|
var plain = JsonSerializer.SerializeToUtf8Bytes(Config, Vault.Json);
|
|
try { AtomicWrite(ConfigPath, DeviceProtection.Protect(plain)); } finally { CryptographicOperations.ZeroMemory(plain); }
|
|
}
|
|
public void Configure(Connection config)
|
|
{
|
|
config.Validate();
|
|
if (Config.Url != config.Url || Config.Token != config.Token)
|
|
{
|
|
config.Etag = ""; config.LastSync = null;
|
|
// Clear the ancestor before storing the new connection: a crash cannot use a wrong ancestor.
|
|
if (File.Exists(BasePath)) File.Delete(BasePath);
|
|
}
|
|
Config = config; SaveConfig();
|
|
}
|
|
public void MarkSynced(byte[] envelope)
|
|
{
|
|
AtomicWrite(BasePath, envelope); Config.Etag = VaultCrypto.Hash(envelope); Config.LastSync = DateTimeOffset.Now; SaveConfig();
|
|
}
|
|
public void ResetSync()
|
|
{
|
|
if (File.Exists(BasePath)) File.Delete(BasePath);
|
|
Config.Etag = ""; Config.LastSync = null; SaveConfig();
|
|
}
|
|
}
|
|
|
|
public static class DeviceProtection
|
|
{
|
|
[StructLayout(LayoutKind.Sequential)] private struct Blob { public int Length; public IntPtr Data; }
|
|
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
[return: MarshalAs(UnmanagedType.Bool)] private static extern bool CryptProtectData(ref Blob input, string description, IntPtr entropy, IntPtr reserved, IntPtr prompt, int flags, out Blob output);
|
|
[DllImport("crypt32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)] private static extern bool CryptUnprotectData(ref Blob input, IntPtr description, IntPtr entropy, IntPtr reserved, IntPtr prompt, int flags, out Blob output);
|
|
[DllImport("kernel32.dll")] private static extern IntPtr LocalFree(IntPtr memory);
|
|
public static byte[] Protect(byte[] bytes) => Transform(bytes, true);
|
|
public static byte[] Unprotect(byte[] bytes) => Transform(bytes, false);
|
|
private static byte[] Transform(byte[] bytes, bool encrypt)
|
|
{
|
|
var input = new Blob { Length = bytes.Length, Data = Marshal.AllocHGlobal(bytes.Length) };
|
|
Blob output = default;
|
|
try
|
|
{
|
|
Marshal.Copy(bytes, 0, input.Data, bytes.Length);
|
|
bool ok = encrypt ? CryptProtectData(ref input, "QSafe connection", IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, 1, out output) : CryptUnprotectData(ref input, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, 1, out output);
|
|
if (!ok) throw new Win32Exception(Marshal.GetLastWin32Error(), "Не удалось защитить настройки учётной записью Windows.");
|
|
var result = new byte[output.Length]; Marshal.Copy(output.Data, result, 0, result.Length); return result;
|
|
}
|
|
finally
|
|
{
|
|
for (int i = 0; i < input.Length; i++) Marshal.WriteByte(input.Data, i, 0);
|
|
Marshal.FreeHGlobal(input.Data);
|
|
if (output.Data != IntPtr.Zero) { for (int i = 0; i < output.Length; i++) Marshal.WriteByte(output.Data, i, 0); LocalFree(output.Data); }
|
|
}
|
|
}
|
|
}
|