Add project files.

This commit is contained in:
micro
2026-06-20 09:31:59 -05:00
parent 9bb3f00d86
commit 4baa4ce8c0
346 changed files with 55751 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
using System.Security.Cryptography;
using System.Text;
using Wingnal.Protocol.State;
namespace Wingnal.Protocol.Identity;
/// <summary>
/// Computes Signal's numeric "safety number" (a.k.a. fingerprint) for two identity keys, so a user can
/// verify out-of-band that they have the right keys for a contact — and detect a man-in-the-middle.
/// Byte-exact with libsignal v0.96.1 (rust/protocol/src/fingerprint.rs): per party, iterate
/// <c>SHA-512(prevHash ‖ key)</c> 5200× starting from <c>SHA-512(0x0000 ‖ key ‖ stableId ‖ key)</c>,
/// take 30 bytes as six 5-byte big-endian chunks mod 100000 (→ 30 digits), then concatenate the two
/// parties' halves in sorted order (so both sides see the same 60-digit number).
///
/// The modern (ACI-based) safety number uses version 2 and each party's 16-byte ACI UUID as the stable
/// identifier — matching what the official Signal app shows for the same contact.
/// </summary>
public static class SafetyNumber
{
public const int DefaultIterations = 5200;
/// <summary>The 60-digit safety number for the two parties (order-independent).</summary>
public static string Generate(byte[] localStableId, IdentityKey localKey,
byte[] remoteStableId, IdentityKey remoteKey, int iterations = DefaultIterations)
{
string local = Encode(GetFingerprint(iterations, localStableId, localKey));
string remote = Encode(GetFingerprint(iterations, remoteStableId, remoteKey));
// Sorted concatenation makes both participants compute the identical string.
return string.CompareOrdinal(local, remote) <= 0 ? local + remote : remote + local;
}
/// <summary>Convenience for the ACI-based safety number: pass each party's ACI UUID.</summary>
public static string GenerateForAci(Guid localAci, IdentityKey localKey, Guid remoteAci, IdentityKey remoteKey) =>
Generate(UuidBytes(localAci), localKey, UuidBytes(remoteAci), remoteKey);
/// <summary>Groups the 60 digits into the usual 12 blocks of 5 for display.</summary>
public static string FormatForDisplay(string digits)
{
var sb = new StringBuilder(digits.Length + digits.Length / 5);
for (int i = 0; i < digits.Length; i += 5)
{
if (i > 0) sb.Append(i % 25 == 0 ? '\n' : ' ');
sb.Append(digits.AsSpan(i, Math.Min(5, digits.Length - i)));
}
return sb.ToString();
}
private static byte[] GetFingerprint(int iterations, byte[] stableId, IdentityKey key)
{
if (iterations <= 1) throw new ArgumentOutOfRangeException(nameof(iterations));
byte[] keyBytes = key.Serialize(); // 33-byte DjbECPublicKey
// Iteration 0: SHA-512( 0x0000 ‖ key ‖ stableId ‖ key ).
byte[] buf = SHA512.HashData(Concat(new byte[] { 0, 0 }, keyBytes, stableId, keyBytes));
for (int i = 1; i < iterations; i++)
buf = SHA512.HashData(Concat(buf, keyBytes));
return buf; // 64 bytes
}
private static string Encode(byte[] fingerprint)
{
var sb = new StringBuilder(30);
for (int chunk = 0; chunk < 6; chunk++)
{
ulong x = 0;
for (int i = 0; i < 5; i++)
x = (x << 8) | fingerprint[chunk * 5 + i];
sb.Append((x % 100000).ToString("D5"));
}
return sb.ToString();
}
/// <summary>A UUID's 16 bytes in RFC 4122 / big-endian order (the ACI service-id bytes).</summary>
public static byte[] UuidBytes(Guid id) => id.ToByteArray(bigEndian: true);
private static byte[] Concat(params byte[][] parts)
{
var result = new byte[parts.Sum(p => p.Length)];
int o = 0;
foreach (byte[] p in parts) { Buffer.BlockCopy(p, 0, result, o, p.Length); o += p.Length; }
return result;
}
}