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,53 @@
using System.Security.Cryptography;
using Google.Protobuf;
using Wingnal.Protocol.Crypto;
using Wingnal.Protocol.Curve;
using Wingnal.Service.Protos;
namespace Wingnal.Service.Provisioning;
/// <summary>
/// Decrypts the <see cref="ProvisionEnvelope"/> a primary device sends during secondary-device
/// linking. This client advertises an ephemeral Curve25519 public key in the linking QR code; the
/// phone performs ECDH against it, derives keys via HKDF, and AES-256-CBC+HMAC encrypts a
/// <see cref="ProvisionMessage"/> carrying our new identity key and account identifiers.
/// </summary>
public sealed class ProvisioningCipher
{
private static readonly byte[] Info = "TextSecure Provisioning Message"u8.ToArray();
private readonly ECKeyPair _ephemeralKeyPair;
public ProvisioningCipher() : this(Curve25519.GenerateKeyPair()) { }
public ProvisioningCipher(ECKeyPair ephemeralKeyPair) => _ephemeralKeyPair = ephemeralKeyPair;
/// <summary>The 33-byte DjbECPublicKey advertised to the primary device in the QR code.</summary>
public byte[] PublicKey => Curve25519.EncodePoint(_ephemeralKeyPair.PublicKey);
public ProvisionMessage Decrypt(ProvisionEnvelope envelope)
{
byte[] theirPublicKey = Curve25519.DecodePoint(envelope.PublicKey.Span);
byte[] body = envelope.Body.ToByteArray();
if (body.Length < 1 + 16 + 32 || body[0] != 0x01)
throw new InvalidOperationException("malformed provision envelope body");
byte[] sharedSecret = Curve25519.CalculateAgreement(theirPublicKey, _ephemeralKeyPair.PrivateKey);
byte[] keys = CryptoPrimitives.Hkdf(sharedSecret, salt: null, info: Info, outputLength: 64);
byte[] cipherKey = keys[..32];
byte[] macKey = keys[32..];
int macOffset = body.Length - 32;
byte[] mac = body[macOffset..];
byte[] computed = CryptoPrimitives.HmacSha256(macKey, body.AsSpan(0, macOffset));
if (!CryptographicOperations.FixedTimeEquals(mac, computed))
throw new InvalidOperationException("provision envelope MAC verification failed");
byte[] iv = body[1..17];
byte[] ciphertext = body[17..macOffset];
byte[] plaintext = CryptoPrimitives.AesCbcDecrypt(cipherKey, iv, ciphertext);
return ProvisionMessage.Parser.ParseFrom(plaintext);
}
}
@@ -0,0 +1,60 @@
using Google.Protobuf;
using Wingnal.Service.Net;
using Wingnal.Service.Protos;
namespace Wingnal.Service.Provisioning;
/// <summary>
/// Drives the secondary-device linking handshake over the provisioning WebSocket: receives the
/// provisioning address, surfaces a QR URI for the primary phone to scan, then awaits and decrypts
/// the <see cref="ProvisionMessage"/> the phone sends back.
/// </summary>
public sealed class ProvisioningManager
{
private readonly ProvisioningCipher _cipher = new();
/// <summary>
/// Connects, invokes <paramref name="onQrReady"/> with the QR URI once the provisioning address
/// arrives, then returns the decrypted provision message after the phone responds.
/// </summary>
/// <summary>libsignal/Signal-Desktop link+sync capability token advertised in the QR so the primary
/// offers a message-history transfer archive (see docs/SYNC.md).</summary>
public const string LinkAndSyncCapability = "backup5";
public async Task<ProvisionMessage> LinkAsync(Func<string, Task> onQrReady, CancellationToken ct,
IReadOnlyList<string>? capabilities = null)
{
using var socket = new SignalWebSocket();
var uri = new Uri(SignalServiceConfig.WebSocketUrl + SignalServiceConfig.ProvisioningWebSocketPath);
await socket.ConnectAsync(uri, headers: null, ct).ConfigureAwait(false);
// Frame 1: server assigns this client a provisioning address.
WebSocketRequestMessage addressRequest = await socket.ReadRequestAsync(ct).ConfigureAwait(false)
?? throw new InvalidOperationException("provisioning socket closed before address");
var address = ProvisioningUuid.Parser.ParseFrom(addressRequest.Body);
await socket.SendResponseAsync(addressRequest.Id, 200, "OK", ct).ConfigureAwait(false);
await onQrReady(BuildQrUri(address.Uuid, _cipher.PublicKey, capabilities)).ConfigureAwait(false);
// Frame 2: the phone has scanned the QR and sent the encrypted provision message.
WebSocketRequestMessage envelopeRequest = await socket.ReadRequestAsync(ct).ConfigureAwait(false)
?? throw new InvalidOperationException("provisioning socket closed before envelope");
var envelope = ProvisionEnvelope.Parser.ParseFrom(envelopeRequest.Body);
await socket.SendResponseAsync(envelopeRequest.Id, 200, "OK", ct).ConfigureAwait(false);
return _cipher.Decrypt(envelope);
}
/// <summary>Builds the <c>sgnl://linkdevice</c> URI encoded into the linking QR code. Optional
/// <paramref name="capabilities"/> are added as a comma-separated <c>capabilities</c> param (e.g.
/// <see cref="LinkAndSyncCapability"/> to request message-history transfer).</summary>
public static string BuildQrUri(string provisioningUuid, byte[] ephemeralPublicKey,
IReadOnlyList<string>? capabilities = null)
{
string pubKey = Convert.ToBase64String(ephemeralPublicKey);
string uri = $"sgnl://linkdevice?uuid={Uri.EscapeDataString(provisioningUuid)}&pub_key={Uri.EscapeDataString(pubKey)}";
if (capabilities is { Count: > 0 })
uri += $"&capabilities={Uri.EscapeDataString(string.Join(',', capabilities))}";
return uri;
}
}