Custom Protocol — End-to-End Encryption — Technical Document
sgcWebSockets · Technical Document

Custom Protocol — End-to-End Encryption

End-to-end encrypted custom subprotocol — peer-to-peer keys never reach the server, perfect for sensitive payloads.

Overview

End-to-End Encryption (E2EE) means that messages are encrypted on the sender device and can be decrypted only on recipient devices. The server routes packets but cannot read plaintext content.

At a glance

Component class
TsgcWSPClient_E2EE
Standards / spec
Transports
WebSocket, WebSocket Secure
Platforms
Windows, macOS, Linux, iOS, Android
Frameworks
VCL, FireMonkey, Lazarus / FPC, .NET
Edition
Enterprise

Features

Technical specification

Component classTsgcWSPClient_E2EE (unit sgcWebSocket_Protocols, ancestor TsgcWSProtocol_E2EE_Client in unit sgcWebSocket_Protocol_E2EE_Client)
FrameworksVCL, FireMonkey, Lazarus / FPC, .NET
PlatformsWindows, macOS, Linux, iOS, Android

Main properties

The principal published / public properties used to configure and drive the component. Consult the online help for the full list.

ClientWebSocket client component used as transport for the E2EE subprotocol.
BrokerIn-memory broker used for PubSub, RPC and QoS when the E2EE client participates in sgc message routing.
E2EE_OptionsClient-side end-to-end encryption options: local UserId, key/algorithm settings and acknowledgment flags.
GuidUnique identifier assigned to this protocol instance.
VersionRead-only E2EE subprotocol version string.

Main methods

The principal public methods exposed by the component.

GenerateIdentityKeyPair()Generates a long-term identity key pair in PEM form, to be assigned to E2EE_Options.Identity.
SendDirectMessage()Sends an encrypted direct message (text, stream or bytes) to a remote user.
SendGroupMessage()Sends an encrypted message (text, stream or bytes) to all online members of a group.
DeleteGroup()Deletes an existing encrypted group.
WriteData()Sends raw text or a stream through the underlying WebSocket connection.
CreateGroup()Creates a new encrypted group on the server.
JoinGroup()Joins an existing encrypted group to receive membership and key context.
LeaveGroup()Leaves a group the local user is currently a member of.

Public events

The component exposes the following published events; consult the online help for full event-handler signatures.

OnConnectFired when the underlying WebSocket connection is established.
OnDisconnectproperty OnDisconnect: TsgcWSDisconnectEvent; // TsgcWSDisconnectEvent = procedure(Connection: TsgcWSConnection; Code: Integer) of object __property TsgcWSDisconnectEvent OnDisconnect; // typedef void...
OnE2EEErrorFired when the remote peer or the E2EE layer reports a protocol error.
OnE2EEGroupCreatedproperty OnE2EEGroupCreated: TsgcWSE2EEOnGroupCreated; // TsgcWSE2EEOnGroupCreated = procedure(Sender: TObject; const aGroup: string) of object __property TsgcWSE2EEOnGroupCreated OnE2EEGroupCreated; ...
OnE2EEGroupDeletedproperty OnE2EEGroupDeleted: TsgcWSE2EEOnGroupDeleted; // TsgcWSE2EEOnGroupDeleted = procedure(Sender: TObject; const aGroup: string) of object __property TsgcWSE2EEOnGroupDeleted OnE2EEGroupDeleted; ...
OnE2EEGroupJoinFired when the local user joins a group; reports the current member list.
OnE2EEGroupLeaveproperty OnE2EEGroupLeave: TsgcWSE2EEOnGroupLeave; // TsgcWSE2EEOnGroupLeave = procedure(Sender: TObject; const aGroup: string) of object __property TsgcWSE2EEOnGroupLeave OnE2EEGroupLeave; // typedef...
OnE2EEGroupMemberJoinFired when another user joins a group the local user belongs to.
OnE2EEGroupMemberLeaveFired when another user leaves a group the local user belongs to.
OnE2EEGroupMessageAckFired when the server or a peer acknowledges a group message.
OnE2EEGroupMessageBinaryproperty OnE2EEGroupMessageBinary: TsgcWSE2EEOnGroupMessageBinary; // TsgcWSE2EEOnGroupMessageBinary = procedure(Sender: TObject; const aGroup, aFrom: string; const aBytes: TBytes) of object __propert...
OnE2EEGroupMessageTextproperty OnE2EEGroupMessageText: TsgcWSE2EEOnGroupMessageText; // TsgcWSE2EEOnGroupMessageText = procedure(Sender: TObject; const aGroup, aFrom, aText: string) of object __property TsgcWSE2EEOnGroupMe...
OnE2EEMessageAckproperty OnE2EEMessageAck: TsgcWSE2EEOnMessageAckEvent; // TsgcWSE2EEOnMessageAckEvent = procedure(Sender: TObject; const aId, aFrom, aTo, aState: string) of object __property TsgcWSE2EEOnMessageAckEv...
OnE2EEMessageBinaryFired when a decrypted direct binary message is received from another user.
OnE2EEMessageTextFired when a decrypted direct text message is received from another user.
OnE2EEUserCreatedproperty OnE2EEUserCreated: TsgcWSE2EEClientOnUserCreated; // TsgcWSE2EEClientOnUserCreated = procedure(Sender: TObject; const aUserId: string) of object __property TsgcWSE2EEClientOnUserCreated OnE2E...
OnE2EEUserDeletedproperty OnE2EEUserDeleted: TsgcWSE2EEClientOnUserDeleted; // TsgcWSE2EEClientOnUserDeleted = procedure(Sender: TObject; const aUserId: string) of object __property TsgcWSE2EEClientOnUserDeleted OnE2E...
OnErrorFired for transport-level errors on the underlying WebSocket connection.
OnExceptionFired when an unhandled exception is raised while processing E2EE traffic.

Quick Start

Drop the component on a form, configure the properties below and activate it. The snippet that follows shows the typical E2EE subprotocol client and server pair configuration.

About this scenario. Pair a TsgcWSPServer_E2EE with a TsgcWebSocketServer and a TsgcWSPClient_E2EE with a TsgcWebSocketClient. Give every client a distinct E2EE_Options.UserId, that is the address other peers send to. Key exchange happens client to client, the server only relays ciphertext, so a compromised server cannot read the traffic.

Delphi (VCL / FireMonkey)

// --- server side: relays ciphertext, never sees plaintext
oServer := TsgcWebSocketServer.Create(nil);
oServer.Port := 80;
oServerE2EE := TsgcWSPServer_E2EE.Create(nil);
oServerE2EE.Server := oServer;
oServer.Active := True;

// --- client side
oClient := TsgcWebSocketClient.Create(nil);
oClient.Host := '127.0.0.1';
oClient.Port := 80;
oClientE2EE := TsgcWSPClient_E2EE.Create(nil);
oClientE2EE.Client := oClient;
oClientE2EE.E2EE_Options.UserId := 'alice';
oClientE2EE.OnE2EEMessageText := OnE2EEMessageTextEvent;
oClientE2EE.OnE2EEError := OnE2EEErrorEvent;
oClient.Active := True;

// 1-to-1 encrypted message
oClientE2EE.SendDirectMessage('bob', 'hello bob');

C++ Builder

// --- server side
oServer = new TsgcWebSocketServer(this);
oServer->Port = 80;
oServerE2EE = new TsgcWSPServer_E2EE(this);
oServerE2EE->Server = oServer;
oServer->Active = true;

// --- client side
oClient = new TsgcWebSocketClient(this);
oClient->Host = "127.0.0.1";
oClient->Port = 80;
oClientE2EE = new TsgcWSPClient_E2EE(this);
oClientE2EE->Client = oClient;
oClientE2EE->E2EE_Options->UserId = "alice";
oClientE2EE->OnE2EEMessageText = OnE2EEMessageTextEvent;
oClientE2EE->OnE2EEError = OnE2EEErrorEvent;
oClient->Active = true;

oClientE2EE->SendDirectMessage("bob", "hello bob");

.NET (C#)

// --- server side
oServer = new TsgcWebSocketServer();
oServer.Port = 80;
oServerE2EE = new TsgcWSPServer_E2EE();
oServerE2EE.Server = oServer;
oServer.Active = true;

// --- client side
oClient = new TsgcWebSocketClient();
oClient.Host = "127.0.0.1";
oClient.Port = 80;
oClientE2EE = new TsgcWSPClient_E2EE();
oClientE2EE.Client = oClient;
oClientE2EE.E2EE_Options.UserId = "alice";
oClientE2EE.OnE2EEMessageText += OnE2EEMessageTextEvent;
oClientE2EE.OnE2EEError += OnE2EEErrorEvent;
oClient.Active = true;

oClientE2EE.SendDirectMessage("bob", "hello bob");

Common scenarios

Each scenario shows the configuration and method calls needed to drive the component through a specific real-world flow. Every identifier below is taken from the component declaration shipped with the library.

1 · Direct messages, text and binary

SendDirectMessage addresses one peer by its UserId and returns the id of the message it queued. Incoming plaintext surfaces on OnE2EEMessageText, incoming binary on OnE2EEMessageBinary. Note that the E2EE handlers take a Sender of type TObject, not a connection, because the message has already been decrypted by the time it reaches you. Delivery receipts arrive on OnE2EEMessageAck when E2EE_Options.Ack is enabled.

Delphi (VCL / FireMonkey)
oClientE2EE.E2EE_Options.Ack.RcvDirectMessage := True;
oClientE2EE.OnE2EEMessageAck := OnE2EEMessageAckEvent;

vId := oClientE2EE.SendDirectMessage('bob', 'hello bob');
oClientE2EE.SendDirectMessage('bob', oStream);

procedure TForm1.OnE2EEMessageTextEvent(Sender: TObject;
  const aFrom, aText: string);
begin
  DoLog(aFrom + ': ' + aText);
end;

procedure TForm1.OnE2EEMessageAckEvent(Sender: TObject;
  const aId, aFrom, aTo, aState: string);
begin
  DoLog(aId + ' -> ' + aTo + ' ' + aState);
end;
C++ Builder
oClientE2EE->E2EE_Options->Ack->RcvDirectMessage = true;
oClientE2EE->OnE2EEMessageAck = OnE2EEMessageAckEvent;

String vId = oClientE2EE->SendDirectMessage("bob", "hello bob");

void __fastcall TForm1::OnE2EEMessageTextEvent(TObject *Sender,
  const String aFrom, const String aText)
{
  DoLog(aFrom + ": " + aText);
}

void __fastcall TForm1::OnE2EEMessageAckEvent(TObject *Sender,
  const String aId, const String aFrom, const String aTo,
  const String aState)
{
  DoLog(aId + " -> " + aTo + " " + aState);
}
.NET (C#)
oClientE2EE.E2EE_Options.Ack.RcvDirectMessage = true;
oClientE2EE.OnE2EEMessageAck += OnE2EEMessageAckEvent;

oClientE2EE.SendDirectMessage("bob", "hello bob");
oClientE2EE.SendDirectMessage_Bytes("bob", vBytes);

void OnE2EEMessageTextEvent(object Sender, string From, string Text)
{
  DoLog(From + ": " + Text);
}

2 · Encrypted group chat

CreateGroup provisions a group and its sender key, JoinGroup adds this client to an existing one, SendGroupMessage fans a message out to every member, and LeaveGroup or DeleteGroup tears it down. Membership changes surface on OnE2EEGroupMemberJoin and OnE2EEGroupMemberLeave, group traffic on OnE2EEGroupMessageText. Each of these methods takes an optional timeout in milliseconds, 10000 by default.

Delphi (VCL / FireMonkey)
oClientE2EE.OnE2EEGroupMessageText := OnE2EEGroupMessageTextEvent;
oClientE2EE.OnE2EEGroupMemberJoin := OnE2EEGroupMemberJoinEvent;

oClientE2EE.CreateGroup('team-42');
oClientE2EE.JoinGroup('team-42');
oClientE2EE.SendGroupMessage('team-42', 'standup at 10');
oClientE2EE.LeaveGroup('team-42');
oClientE2EE.DeleteGroup('team-42');

procedure TForm1.OnE2EEGroupMessageTextEvent(Sender: TObject;
  const aGroup, aFrom, aText: string);
begin
  DoLog(aGroup + ' / ' + aFrom + ': ' + aText);
end;

procedure TForm1.OnE2EEGroupMemberJoinEvent(Sender: TObject;
  const aGroup, aUserId: string);
begin
  DoLog(aUserId + ' joined ' + aGroup);
end;
C++ Builder
oClientE2EE->OnE2EEGroupMessageText = OnE2EEGroupMessageTextEvent;
oClientE2EE->OnE2EEGroupMemberJoin = OnE2EEGroupMemberJoinEvent;

oClientE2EE->CreateGroup("team-42");
oClientE2EE->JoinGroup("team-42");
oClientE2EE->SendGroupMessage("team-42", "standup at 10");
oClientE2EE->LeaveGroup("team-42");

void __fastcall TForm1::OnE2EEGroupMessageTextEvent(TObject *Sender,
  const String aGroup, const String aFrom, const String aText)
{
  DoLog(aGroup + " / " + aFrom + ": " + aText);
}
.NET (C#)
oClientE2EE.OnE2EEGroupMessageText += OnE2EEGroupMessageTextEvent;
oClientE2EE.OnE2EEGroupMemberJoin += OnE2EEGroupMemberJoinEvent;

oClientE2EE.CreateGroup("team-42");
oClientE2EE.JoinGroup("team-42");
oClientE2EE.SendGroupMessage("team-42", "standup at 10");
oClientE2EE.LeaveGroup("team-42");

void OnE2EEGroupMessageTextEvent(object Sender, string Group,
  string From, string Text)
{
  DoLog(Group + " / " + From + ": " + Text);
}

3 · Long-term identity keys and fingerprint pinning

Enable E2EE_Options.Identity and give the component a long-term key pair, then every peer key is signed and can be verified. OnE2EEVerifyPeerIdentity fires the first time a peer key is seen, carrying the peer public key and its fingerprint, and you accept or refuse it through the Accept var parameter. OnE2EEKeyChange fires when a known peer presents a different key, the classic warning sign of an interception attempt.

Delphi (VCL / FireMonkey)
oClientE2EE.E2EE_Options.Identity.Enabled := True;
oClientE2EE.E2EE_Options.Identity.PrivateKey := vPrivateKeyPEM;
oClientE2EE.E2EE_Options.Identity.PublicKey := vPublicKeyPEM;
oClientE2EE.OnE2EEVerifyPeerIdentity := OnVerifyPeerIdentityEvent;
oClientE2EE.OnE2EEKeyChange := OnKeyChangeEvent;

procedure TForm1.OnVerifyPeerIdentityEvent(Sender: TObject;
  const aUserId, aIdentityPublicKey, aFingerprint: string;
  var aAccept: Boolean);
begin
  aAccept := aFingerprint = KnownFingerprint(aUserId);
end;

procedure TForm1.OnKeyChangeEvent(Sender: TObject;
  const aUserId, aOldFingerprint, aNewFingerprint: string);
begin
  DoLog('identity key changed for ' + aUserId);
end;
C++ Builder
oClientE2EE->E2EE_Options->Identity->Enabled = true;
oClientE2EE->E2EE_Options->Identity->PrivateKey = vPrivateKeyPEM;
oClientE2EE->E2EE_Options->Identity->PublicKey = vPublicKeyPEM;
oClientE2EE->OnE2EEVerifyPeerIdentity = OnVerifyPeerIdentityEvent;
oClientE2EE->OnE2EEKeyChange = OnKeyChangeEvent;

void __fastcall TForm1::OnVerifyPeerIdentityEvent(TObject *Sender,
  const String aUserId, const String aIdentityPublicKey,
  const String aFingerprint, bool &aAccept)
{
  aAccept = (aFingerprint == KnownFingerprint(aUserId));
}
.NET (C#)
oClientE2EE.GenerateIdentityKeyPair(out string vPrivateKeyPEM,
  out string vPublicKeyPEM);

oClientE2EE.E2EE_Options.Identity.Enabled = true;
oClientE2EE.E2EE_Options.Identity.PrivateKey = vPrivateKeyPEM;
oClientE2EE.E2EE_Options.Identity.PublicKey = vPublicKeyPEM;
oClientE2EE.OnE2EEVerifyPeerIdentity += OnVerifyPeerIdentityEvent;
oClientE2EE.OnE2EEKeyChange += OnKeyChangeEvent;

Sources used to build this document

Every external claim links back to a primary source. The online-help references decode the canonical deep-link the company maintains for this component.

Document scope. This document covers the publicly-documented surface of the Custom Protocol — End-to-End Encryption component shipped with sgcWebSockets. For full property, method and event reference consult the online help linked above.