using System.Security.Cryptography; using Org.BouncyCastle.Crypto.Parameters; namespace VoiceCat.Crypto; public sealed class ServerIdentity : IDisposable { private readonly byte[] seed; private readonly byte[] publicKey; private bool disposed; private ServerIdentity(byte[] seed) { this.seed = seed; publicKey = new Ed25519PrivateKeyParameters(seed, 0).GeneratePublicKey().GetEncoded(); } public byte[] PublicKey => (byte[])publicKey.Clone(); public string Fingerprint => Convert.ToHexString(SHA256.HashData(publicKey)); public static ServerIdentity Generate() => new(RandomNumberGenerator.GetBytes(32)); public static ServerIdentity Load(string path) { byte[] data = File.ReadAllBytes(path); try { if (data.Length != 96) throw new InvalidDataException("Server identity must contain 96 bytes."); var identity = new ServerIdentity(data.AsSpan(32, 32).ToArray()); if (!CryptographicOperations.FixedTimeEquals(identity.publicKey, data.AsSpan(0, 32)) || !CryptographicOperations.FixedTimeEquals(identity.publicKey, data.AsSpan(64, 32))) { identity.Dispose(); throw new InvalidDataException("Server identity public key does not match its seed."); } return identity; } finally { CryptographicOperations.ZeroMemory(data); } } public void Save(string path) { ObjectDisposedException.ThrowIf(disposed, this); byte[] data = new byte[96]; publicKey.CopyTo(data, 0); seed.CopyTo(data, 32); publicKey.CopyTo(data, 64); try { PrivateFiles.Write(path, data); } finally { CryptographicOperations.ZeroMemory(data); } } public void Dispose() { if (disposed) return; disposed = true; CryptographicOperations.ZeroMemory(seed); } }