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(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); } } } }