Files
QSafe/windows/QSafe.Desktop/LocalStore.cs
T

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