73 lines
2.7 KiB
C#
73 lines
2.7 KiB
C#
using System.Text;
|
|
|
|
namespace VoiceCat.Crypto;
|
|
|
|
public enum TofuStatus { FirstConnect, Matched, Mismatch }
|
|
|
|
public sealed class TofuStore
|
|
{
|
|
private readonly string path;
|
|
private readonly Dictionary<string, string> pins = new(StringComparer.Ordinal);
|
|
|
|
public TofuStore(string path)
|
|
{
|
|
this.path = Path.GetFullPath(path);
|
|
if (!File.Exists(this.path)) return;
|
|
foreach (string line in File.ReadLines(this.path))
|
|
{
|
|
if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#')) continue;
|
|
string[] parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length != 2) throw new InvalidDataException("Malformed TOFU pin entry.");
|
|
pins[parts[0]] = NormalizeFingerprint(parts[1]);
|
|
}
|
|
}
|
|
|
|
public TofuStatus Check(string host, ushort port, string fingerprint)
|
|
{
|
|
string key = Endpoint(host, port);
|
|
string normalized = NormalizeFingerprint(fingerprint);
|
|
return !pins.TryGetValue(key, out var pin) ? TofuStatus.FirstConnect :
|
|
pin == normalized ? TofuStatus.Matched : TofuStatus.Mismatch;
|
|
}
|
|
|
|
public void Pin(string host, ushort port, string fingerprint)
|
|
{
|
|
string key = Endpoint(host, port);
|
|
string value = NormalizeFingerprint(fingerprint);
|
|
var updated = new Dictionary<string, string>(pins, StringComparer.Ordinal) { [key] = value };
|
|
Save(updated);
|
|
pins[key] = value;
|
|
}
|
|
|
|
public void Remove(string host, ushort port)
|
|
{
|
|
string key = Endpoint(host, port);
|
|
var updated = new Dictionary<string, string>(pins, StringComparer.Ordinal);
|
|
updated.Remove(key);
|
|
Save(updated);
|
|
pins.Remove(key);
|
|
}
|
|
|
|
private void Save(Dictionary<string, string> updated)
|
|
{
|
|
string contents = string.Concat(updated.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key} {pair.Value}\n"));
|
|
PrivateFiles.Write(path, Encoding.UTF8.GetBytes(contents));
|
|
}
|
|
|
|
private static string Endpoint(string host, ushort port)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(host);
|
|
if (host.Any(char.IsWhiteSpace)) throw new ArgumentException("Host cannot contain whitespace.", nameof(host));
|
|
ArgumentOutOfRangeException.ThrowIfZero(port);
|
|
return $"{host}:{port}";
|
|
}
|
|
|
|
private static string NormalizeFingerprint(string fingerprint)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(fingerprint);
|
|
if (fingerprint.Length != 64 || !fingerprint.All(Uri.IsHexDigit))
|
|
throw new InvalidDataException("TLS certificate fingerprints must contain 64 hexadecimal characters.");
|
|
return fingerprint.ToLowerInvariant();
|
|
}
|
|
}
|