Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions DSharpPlus.Voice/E2EE/DaveStateHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using System;
using System.Threading.Tasks;

using DSharpPlus.Voice.Transport;
using DSharpPlus.Voice.Transport.Models.VoicePayloads;

namespace DSharpPlus.Voice.E2EE;

/// <summary>
/// Keeps track of the DAVE state for a single media connection
/// </summary>
public class DaveStateHandler : IDisposable
{
/// <summary>
/// Dave protocol we are using
/// </summary>
public ushort ProtocolVersion { get; private set; }
/// <summary>
/// Current epoch for our dave session
/// </summary>
public uint CurrentEpoch { get; private set; }

private uint? pendingTransitionId;
private uint? pendingEpochId;
private bool pendingDowngrade;
private readonly MlsSession mlsSession;
private readonly ITransportService voiceNegotiationTransportService;
private readonly Action<bool> setE2eeActive;

/// <summary>
/// Internal constructor as this type should be constructed with its corresponding factory
/// </summary>
internal DaveStateHandler(MlsSession mlsSession, ITransportService voiceNegotiationTransportService, Action<bool> setE2eeActive)
{
this.mlsSession = mlsSession;
this.voiceNegotiationTransportService = voiceNegotiationTransportService;
this.setE2eeActive = setE2eeActive;
}

/// <summary>
/// Sets the version of DAVE we are using
/// </summary>
/// <param name="version">DAVE version</param>
public void SetNegotiatedDaveVersion(ushort version) => this.ProtocolVersion = version;

/// <summary>
/// add an external send to the MLS group's extensions
/// </summary>
/// <param name="payload"></param>
public void OnExternalSender(ReadOnlySpan<byte> payload) => this.mlsSession.SetExternalSender(payload);

/// <summary>
/// Lets us know that we are moving to a new epoch. The payload includes the new epochs upcoming protocol.
/// If the epoch is 1 then we know it is actually a new MLS group we are creating with the given protocol
/// </summary>
/// <param name="payload"></param>
/// <returns></returns>
public async Task OnPrepareEpochAsync(DiscordGatewayMessage<DavePrepareEpochData> payload)
{
if (payload.Data.ProtocolVersion != this.ProtocolVersion || this.CurrentEpoch == 0)
{
this.mlsSession.ReinitializeE2EESession(payload.Data.ProtocolVersion);
this.ProtocolVersion = payload.Data.ProtocolVersion;
}

if (payload.Data.Epoch == 1)
{
await SendMlsKeyPackageAsync();
}
}

/// <summary>
/// Sends our KeyPackage over to the voice negotiation client
/// </summary>
/// <returns></returns>
public async Task SendMlsKeyPackageAsync()
{
CommunityToolkit.HighPerformance.Buffers.ArrayPoolBufferWriter<byte> writer = new();
this.mlsSession.WriteKeyPackage(writer);
await SendDaveBinaryAsync(this.voiceNegotiationTransportService, (int)VoiceGatewayOpcode.MlsKeyPackage, writer.WrittenSpan);
}

/// <summary>
/// We have been requested to downgrade to a lower DAVE version
/// </summary>
/// <param name="payload"></param>
/// <returns></returns>
public async Task OnPrepareTransitionAsync(DiscordGatewayMessage<DavePrepareTransitionData> payload)
{
uint transitionId = payload.Data.TransitionId;
this.pendingTransitionId = transitionId;
this.pendingEpochId = null;
this.pendingDowngrade = true;

await this.voiceNegotiationTransportService.SendAsync<DiscordGatewayMessage<DavePrepareTransitionData>>(
new()
{
OpCode = (int)VoiceGatewayOpcode.TransitionReady,
Data = new() { TransitionId = payload.Data.TransitionId }
}); // Transition Ready
}

/// <summary>
/// Execute a pending transition. If the transitionId is 0 we are (re)initializing
/// </summary>
/// <param name="transitionId"></param>
public void OnExecuteTransition(uint transitionId)
{
if (this.pendingTransitionId != transitionId && transitionId != 0)
{
Console.WriteLine("The pending transaction and the one asked to execute didnt match.");
}

if (this.pendingDowngrade)
{
this.setE2eeActive(false);
this.ProtocolVersion = 0;
this.CurrentEpoch = 0;
}
else
{
this.setE2eeActive(true);
if (this.pendingEpochId.HasValue)
{
this.CurrentEpoch = this.pendingEpochId.Value;
}
}

this.pendingTransitionId = null;
this.pendingEpochId = null;
this.pendingDowngrade = false;
}

/// <summary>
/// Processes proposals that we want to append and/or revoke for our MLS group
/// </summary>
/// <param name="payload"></param>
/// <param name="roster"></param>
/// <returns></returns>
public async Task OnProposalsAsync(byte[] payload, ulong[] roster)
{
byte[] commitBytes = this.mlsSession.ProcessProposals(payload, roster);
if (commitBytes is { Length: > 0 })
{
await SendDaveBinaryAsync(this.voiceNegotiationTransportService, (int)VoiceGatewayOpcode.MlsCommitWelcome, commitBytes); // MLS Commit/Welcome
}
}

/// <summary>
/// Proccesses a commit made to the MLS group
/// </summary>
/// <param name="payload"></param>
public void OnAnnounceCommitTransition(ReadOnlySpan<byte> payload) => this.mlsSession.ProcessCommit(payload.ToArray());

/// <summary>
/// Processes the MLS welcome message
/// </summary>
/// <param name="payload">Welcome Message</param>
/// <param name="roster">Roster of users in the media channel</param>
public void OnWelcome(ReadOnlySpan<byte> payload, ReadOnlySpan<ulong> roster) => this.mlsSession.ProcessWelcome(payload.ToArray(), roster.ToArray());

/// <summary>
/// Sends a DAVE binary payload to the transport service
/// </summary>
/// <param name="client">Transport service</param>
/// <param name="opcode">OpCode to send</param>
/// <param name="payload">Data frame to send</param>
/// <returns></returns>
private static Task SendDaveBinaryAsync(ITransportService client, byte opcode, ReadOnlySpan<byte> payload)
{
byte[] buf = new byte[1 + payload.Length];
buf[0] = opcode;
payload.CopyTo(buf.AsSpan(1));
return client.SendAsync((ReadOnlyMemory<byte>)buf, null);
}

/// <summary>
/// Cleanup
/// </summary>
public void Dispose()
{
this.mlsSession.Dispose();
this.voiceNegotiationTransportService.Dispose();
}
}
66 changes: 42 additions & 24 deletions DSharpPlus.Voice/E2EE/MlsSession.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;

using CommunityToolkit.HighPerformance.Buffers;
Expand All @@ -9,8 +8,10 @@

namespace DSharpPlus.Voice.E2EE;

/// <inheritdoc/>
public sealed class MlsSession : IE2EESession
/// <summary>
/// Provides the E2EE implementation for DSharpPlus.Voice, based on DAVE 1.1.4.
/// </summary>
public sealed class MlsSession : IDisposable
{
private readonly KoanaInterop koana;
private readonly Lock mlsLock;
Expand All @@ -34,7 +35,9 @@ public MlsSession(ushort protocolVersion, ulong channelId, ulong userId, uint ss
this.ssrc = ssrc;
}

/// <inheritdoc/>
/// <summary>
/// Reinitializes the E2EE session with a different DAVE protocol version.
/// </summary>
public void ReinitializeE2EESession(ushort protocolVersion)
{
lock (this.mlsLock)
Expand All @@ -44,28 +47,31 @@ public void ReinitializeE2EESession(ushort protocolVersion)
}
}

/// <inheritdoc/>
public void SetExternalSender(byte[] payload)
/// <summary>
/// Sets the voice gateway as an external sender capable of adding members to the E2EE group.
/// </summary>
public void SetExternalSender(ReadOnlySpan<byte> payload)
{
lock (this.mlsLock)
{
this.koana.SetExternalSender(payload);
}
}

/// <inheritdoc/>
public byte[] ProcessProposals(byte[] payload)
/// <summary>
/// Processes proposals and retuns a message with the E2EE client's response in turn.
/// </summary>
public byte[] ProcessProposals(byte[] payload, ulong[] roster)
{
List<ulong> roster = this.koana.GetUserList();

lock (this.mlsLock)
{
Span<ulong> rosterSpan = CollectionsMarshal.AsSpan(roster);
return this.koana.ProcessProposals(payload, rosterSpan);
return this.koana.ProcessProposals(payload, roster);
}
}

/// <inheritdoc/>
/// <summary>
/// Processes an otherwise unspecified commit.
/// </summary>
public void ProcessCommit(byte[] payload)
{
lock (this.mlsLock)
Expand All @@ -77,23 +83,30 @@ public void ProcessCommit(byte[] payload)
}
}

/// <inheritdoc/>
public void ProcessWelcome(byte[] payload)
/// <summary>
/// Welcomes a new user to the E2EE group.
/// </summary>
public void ProcessWelcome(byte[] payload, ulong[] roster)
{
List<ulong> roster = this.koana.GetUserList();

lock (this.mlsLock)
{
Span<ulong> rosterSpan = CollectionsMarshal.AsSpan(roster);
this.koana.ProcessWelcome(payload, rosterSpan);
this.koana.ProcessWelcome(payload, roster);
}
}

/// <inheritdoc/>
/// <summary>
/// Writes the client's key package to the writer.
/// </summary>
public void WriteKeyPackage(ArrayPoolBufferWriter<byte> writer)
=> this.koana.GetMarshalledKeyPackage(writer);

/// <inheritdoc/>
/// <summary>
/// Decrypts the provided frame.
/// </summary>
/// <param name="userId">The snowflake identifier of the user who sent this payload.</param>
/// <param name="encryptedFrame">The E2EE-encrypted frame data.</param>
/// <param name="decryptedFrame">A buffer for the decrypted frame data, equal in length to the encrypted data.</param>
/// <returns>The amount of bytes written to <paramref name="decryptedFrame"/>.</returns>
public int DecryptFrame(ulong userId, ReadOnlySpan<byte> encryptedFrame, Span<byte> decryptedFrame)
{
lock (this.mlsLock)
Expand All @@ -102,7 +115,12 @@ public int DecryptFrame(ulong userId, ReadOnlySpan<byte> encryptedFrame, Span<by
}
}

/// <inheritdoc/>
/// <summary>
/// Encrypts the provided frame.
/// </summary>
/// <param name="unencryptedFrame">The unencrypted frame data.</param>
/// <param name="encryptedFrame">A buffer for the E2EE-encrypted frame data.</param>
/// <returns>The amount of bytes written to <paramref name="encryptedFrame"/>.</returns>
public int EncryptFrame(ReadOnlySpan<byte> unencryptedFrame, Span<byte> encryptedFrame)
{
lock (this.mlsLock)
Expand All @@ -112,6 +130,6 @@ public int EncryptFrame(ReadOnlySpan<byte> unencryptedFrame, Span<byte> encrypte
}

/// <inheritdoc/>
public void Dispose()
public void Dispose()
=> this.koana.Dispose();
}
19 changes: 19 additions & 0 deletions DSharpPlus.Voice/E2EE/UserInVoice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <summary>
/// Represents the known data of a user in our voice call
/// </summary>
public class UserInVoice
{
/// <summary>
/// The users snowflake id
/// </summary>
public ulong UserId { get; set; }
/// <summary>
/// If the user is currently speaking or not
/// </summary>
public bool IsSpeaking { get; set; }
/// <summary>
/// The users ssrc, this can be null as we only know
/// what this value is after their first time speaking
/// </summary>
public int? Ssrc { get; set; }
}
Loading
Loading