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
@@ -0,0 +1,73 @@
using System.Security.Cryptography;
using System.Text;
using Google.Protobuf;
using Wingnal.Protocol.Crypto;
using Wingnal.Protocol.Curve;
using Wingnal.Protocol.State;
using Wingnal.Service.Protos;
namespace Wingnal.Service.Crypto;
/// <summary>
/// Encrypts a linked device's display name to the account's identity key, matching Signal's
/// DeviceNameCipher: ECDH to an ephemeral key, HMAC-derived synthetic IV + cipher key, then
/// AES-256-CTR. The primary device decrypts this to label us in its linked-devices list.
/// </summary>
public static class DeviceNameCipher
{
public static byte[] EncryptDeviceName(string deviceName, IdentityKeyPair identityKeyPair)
{
byte[] plaintext = Encoding.UTF8.GetBytes(deviceName);
ECKeyPair ephemeral = Curve25519.GenerateKeyPair();
byte[] masterSecret = Curve25519.CalculateAgreement(identityKeyPair.PublicKey.PublicKey, ephemeral.PrivateKey);
byte[] syntheticIv = ComputeSyntheticIv(masterSecret, plaintext);
byte[] cipherKey = ComputeCipherKey(masterSecret, syntheticIv);
// Signal encrypts with a zero CTR IV; the syntheticIv only derives the key and is sent in the proto.
byte[] ciphertext = CryptoPrimitives.AesCtr(cipherKey, new byte[16], plaintext);
var message = new DeviceName
{
EphemeralPublic = ByteString.CopyFrom(Curve25519.EncodePoint(ephemeral.PublicKey)),
SyntheticIv = ByteString.CopyFrom(syntheticIv),
Ciphertext = ByteString.CopyFrom(ciphertext),
};
return message.ToByteArray();
}
/// <summary>Decrypts a serialized DeviceName proto with the identity key, mirroring Signal's
/// decrypt (re-derives and verifies the synthetic IV). Returns null if undecryptable/tampered.</summary>
public static string? DecryptDeviceName(byte[] serialized, IdentityKeyPair identityKeyPair)
{
DeviceName message = DeviceName.Parser.ParseFrom(serialized);
if (message.EphemeralPublic.IsEmpty || message.SyntheticIv.IsEmpty || message.Ciphertext.IsEmpty)
return null;
byte[] ephemeralPublic = Curve25519.DecodePoint(message.EphemeralPublic.Span);
byte[] masterSecret = Curve25519.CalculateAgreement(ephemeralPublic, identityKeyPair.PrivateKey);
byte[] syntheticIv = message.SyntheticIv.ToByteArray();
byte[] cipherKey = ComputeCipherKey(masterSecret, syntheticIv);
byte[] plaintext = CryptoPrimitives.AesCtr(cipherKey, new byte[16], message.Ciphertext.ToByteArray());
byte[] expectedIv = ComputeSyntheticIv(masterSecret, plaintext);
if (!CryptographicOperations.FixedTimeEquals(expectedIv, syntheticIv))
return null;
return Encoding.UTF8.GetString(plaintext);
}
private static byte[] ComputeSyntheticIv(byte[] masterSecret, byte[] plaintext)
{
byte[] keyMaterial = CryptoPrimitives.HmacSha256(masterSecret, "auth"u8);
byte[] mac = CryptoPrimitives.HmacSha256(keyMaterial, plaintext);
return mac[..16];
}
private static byte[] ComputeCipherKey(byte[] masterSecret, byte[] syntheticIv)
{
byte[] keyMaterial = CryptoPrimitives.HmacSha256(masterSecret, "cipher"u8);
return CryptoPrimitives.HmacSha256(keyMaterial, syntheticIv);
}
}
@@ -0,0 +1,19 @@
using Wingnal.Protocol.Crypto;
namespace Wingnal.Service.Crypto;
/// <summary>
/// Derives a recipient's "unidentified access key" from their profile key, used to send sealed-sender
/// (metadata-minimized) messages without our own auth credentials. Matches Signal-Android
/// <c>UnidentifiedAccess.deriveAccessKeyFrom</c>: the first 16 bytes of AES-256-GCM(profileKey, iv=0¹²,
/// plaintext=0¹⁶).
/// </summary>
public static class UnidentifiedAccess
{
public static byte[] DeriveAccessKey(byte[] profileKey)
{
if (profileKey.Length != 32) throw new ArgumentException("profile key must be 32 bytes", nameof(profileKey));
byte[] ctAndTag = CryptoPrimitives.AesGcmEncrypt(profileKey, new byte[12], new byte[16]);
return ctAndTag.AsSpan(0, 16).ToArray();
}
}