Files
windows/Network/IdentityStore.cs
T
ros d5e827112a Add MusicBridge protocol documentation and implement system volume control
- Created PROTOCOL.md to outline the MusicBridge protocol v1, detailing setup, TLS, pairing, state management, and command structure.
- Implemented SystemVolume class for managing system audio levels, including methods for setting, reading, changing, and muting volume.
- Added unit tests for the MusicBridge agent, covering pairing, identity storage, command validation, and media state management.
- Included tests for real-time media reading from Windows and ensured proper handling of artwork caching and retrieval.
2026-09-09 01:05:52 +03:00

91 lines
4.1 KiB
C#

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
namespace MusicBridge.Agent.Network;
internal sealed record TrustedDevice(string Id, string Name, string TokenHash, DateTimeOffset CreatedAt);
internal sealed class IdentityStore : IDisposable
{
private sealed record Identity(int Version, byte[] Certificate, TrustedDevice[] Devices);
private readonly FileStream owner;
private readonly string path;
private Identity identity = null!;
public X509Certificate2 Certificate { get; private set; } = null!;
public IReadOnlyList<TrustedDevice> Devices => identity.Devices;
public static string DefaultDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MusicBridge");
public IdentityStore(string directory)
{
Directory.CreateDirectory(directory);
path = Path.Combine(directory, "identity.dat");
// Prevent two agents from racing the certificate or overwriting trusted devices.
owner = new FileStream(Path.Combine(directory, "identity.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
try
{
if (File.Exists(path))
{
if (new FileInfo(path).Length > 1024 * 1024) throw new InvalidDataException("Хранилище слишком большое.");
var clear = UserProtection.Unprotect(File.ReadAllBytes(path));
try { identity = JsonSerializer.Deserialize<Identity>(clear) ?? throw new InvalidDataException("Пустое хранилище."); }
finally { CryptographicOperations.ZeroMemory(clear); }
if (identity.Version != 1 || identity.Certificate is null || identity.Devices is null || identity.Devices.Length > 8
|| identity.Devices.Any(d => d is null || !Guid.TryParseExact(d.Id, "N", out _) || string.IsNullOrWhiteSpace(d.Name)
|| d.Name.Length > 64 || d.TokenHash is null || d.TokenHash.Length != 64 || !d.TokenHash.All(Uri.IsHexDigit))
|| identity.Devices.Select(d => d.Id).Distinct().Count() != identity.Devices.Length)
throw new InvalidDataException("Неверный формат хранилища.");
Certificate = X509CertificateLoader.LoadPkcs12(identity.Certificate, null);
if (!Certificate.HasPrivateKey || Certificate.NotAfter.ToUniversalTime() <= DateTime.UtcNow)
throw new InvalidDataException("Сертификат недоступен или истёк. Хранилище сохранено без изменений.");
}
else
{
Certificate = AgentCertificate.Create();
identity = new(1, Certificate.Export(X509ContentType.Pfx), []);
Save(identity);
}
}
catch
{
Certificate?.Dispose();
if (identity?.Certificate is { } bytes) CryptographicOperations.ZeroMemory(bytes);
owner.Dispose();
throw;
}
}
public void SaveDevices(TrustedDevice[] devices)
{
var updated = identity with { Devices = devices };
Save(updated); // Commit to memory only after the protected file is safely replaced.
identity = updated;
}
private void Save(Identity value)
{
var clear = JsonSerializer.SerializeToUtf8Bytes(value);
byte[] encrypted;
try { encrypted = UserProtection.Protect(clear); }
finally { CryptographicOperations.ZeroMemory(clear); }
var temporary = path + ".tmp";
try
{
using (var file = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None))
{
file.Write(encrypted);
file.Flush(flushToDisk: true);
}
File.Move(temporary, path, overwrite: true);
}
finally { if (File.Exists(temporary)) File.Delete(temporary); }
}
public void Dispose()
{
Certificate.Dispose();
CryptographicOperations.ZeroMemory(identity.Certificate);
owner.Dispose();
}
}