Ende-zu-Ende-Verschlüsselung
Die Kommunikation zwischen STP.Documents.OnPremise Mobile DESK und dem On-premise-Agent findet verschlüsselt statt. Da Daten und Dokumente aus dem sicheren On-premise-System durch die Cloud übertragen werden, müssen diese so verschlüsselt werden, dass nur die jeweiligen Empfänger sie wieder entschlüsseln können. Dieser Verschlüsselungsmechanismus wird auch als Ende-zu-Ende-Verschlüsselung bezeichnet, da nur die jeweiligen Endpunkte der Kommunikation den Klartext kennen order wiederherstellen können. Andere Kommunikationsteilnehmer, wie beispielsweise Smartphones anderer Benutzer oder Agents anderer Kanzleien, können die Kommunikationsinhalte nicht entschlüsseln. Nicht mal STP kann sehen, was zwischen App und Agent übertragen wird.
Dabei werden die folgenden Algorithmen verwendet:
- ECDH/ECDSA on curve P-256 für den Schlüsselaustausch
- AES-GCM 256bit für die symmetrische Verschlüsselung
Dabei wird sichergestellt:
- Bevor das Gerät eine verschlüsselte Verbindung aufbauen kann, muss es sich im Namen des Benutzers mit OpenID Connect (OIDC) authentifizieren.
- Geheime Schlüssel (Private Keys) bleiben jeweils in den Endpunkten, also in der App und im Agenten.
- Nur die öffentlichen Schlüssel (Public Keys) der beteiligten Endpunkte sind den jeweils anderen Endpunkten bekannt.
- Aus den geheimen Schlüsseln der Endpunkte werden kurzlebige verschlüsselte Verbindungen abgeleitet.
- Selbst wenn ein geheimer Schlüssel einmal bekannt werden würde, kann aufgezeichnete Kommunikation nicht entschlüsselt werden (Perfect Forward Secrecy).
- Nach 15 Minutes oder 1000 Anfragen wird eine neue verschlüsselte Verbindung etabliert und Daten aus der alten können nicht mehr wiederhergestellt werden.
- Es werden separate Schlüssel für den Versand und den Empfang von Daten verwendet.
- Verschlüsselte Anfragen werden nur einmal akzeptiert und können nicht wiederholt werden (Replay Attack Protection).
- Jede Dokumentversion hat seinen eigenen eindeutigen geheimen Schlüssel.
- Die Ende-zu-Ende-verschlüsselte Verbindung wird zwischen App zu Cloud und Cloud zu Agent zusätzlich noch in eine TLS 1.2+ Verbindung eingebettet.
- Die verwendeten kryptografischen Algorithmen werden von allen modernen Browsern unterstützt, auf dem iPhone sogar mit Hardware-Beschleunigung.
Verbindungsaufbau
Wenn sich die App also mit dem Agent verbinden will, um Daten und Dokumente abzurufen, muss sie zunächst eine sichere Verbindung herstellen. Das funktioniert folgendermaßen:
//Authentifizierung
var tokenProvider = new ResourceOwnerPasswordCredentials( authority: new Authority(new Uri("https://...stp-cloud.de/identity/")), ...);
var keys = new KeyStore("local-keys");
keys.Prepare();
//Vorbereitung der Ende-zu-Ende-Verschlüsselung
using var device = new Device(tokenProvider);
await device.LoadAgentInfo();
Info($"Agent {device.Agent.Fingerprint}");
var deviceName = "Example Application";
if (!keys.Contains(deviceName)) //falls das Gerät neu ist, muss es zunächst registriert werden
{
Info($"Registering '{deviceName}' new device...");
keys.Add(deviceName, await device.RegisterAs(deviceName));
keys.Postpare();
}
//Aufbau der Ende-zu-Ende-verschlüsselten Verbindung
using (var session = await device.Login(keys[deviceName]))
{
Info($"Session {session.SessionId} established");
//Anfragen können nun verschlüsselt & gesendet und Antworten empfangen & entschlüsselt werden
...
}
Der erste Kontakt der App mit dem Agenten findet über die LoadAgentInfo-Methode (/api/sessions/agentinfo?api-version=1.0) statt. Hier wird der Public Key und einige Eigenschaften des Agenten ermittelt, um sicherzustellen, dass die App sich im nächsten Schritt mit dem richtigen Agenten verbindet.
Falls noch kein Gerät registriert ist, kann dies über die RegisterAs-Methode (/api/sessions/register?api-version=1.0) vorgenommen werden. Dabei wird über die WebCrypto API des Browsers ein neues zufälliges Schlüsselpaar erstellt, welches aus einem geheimen und einem öffentlichen Schlüssel besteht. Der geheime Schlüssel wird mit dem öffentlichen Schlüssel des Agenten verbunden wird und verbleibt auf dem Gerät. Der dazugehörige öffentliche Schlüssel wird an den Agenten gesendet. Anschließend kennen sowohl App als auch Agent jeweils die öffentlichen Schlüssel der anderen Seite.
var key = WebCrypto.CreateAsymmetricKey();
var myFingerprint = WebCrypto.Fingerprint( key.GetParams( exportPrivate: false));
var privateKey = WebCrypto.EncodeKeyToBase64( key.GetParams( exportPrivate: true), this.Agent.PublicKey.GetParams( exportPrivate: false));
var (myPrivateKeyParams, agentKeyParams) = WebCrypto.DecodeKeyFromBase64( privateKey);
var myPublicKey = new JsonWebKey(myPrivateKeyParams);
var response = await Http.Post(
url: Url($"/api/sessions/register?api-version=1.0"),
body: new
{
Device = deviceName,
DeviceFingerprint = myFingerprint,
PublicKey = myPublicKey,
});
Bei der Registrierung muss sichergestellt werden, dass der in der App bekannte öffentliche Schlüssel des Agenten auch tatsächlich dem öffentlichen Schlüssel des Agenten in der On-premises-Umgebung entspricht. Dieses Prinzip ist als Trust On First Use (TOFU) bekannt. Dies muss manuell durch den Anwender erfolgen, indem die Fingerprints der öffentlichen Schlüssel verglichen werden. Wird dies nicht sichergestellt, könnte es sein, dass die App sich bei einem vorgetäuschten Agenten verbindet.
Sobald App und Agent die öffentlichen Schlüssel der jeweils anderen Seite kennen, kann der Schlüsselaustausch stattfinden. Dazu wird die Login-Methode aufgerufen (/api/sessions/start?api-version=1.0). Zunächst wird geprüft, ob der Agent immer noch über den öffentlichen Schlüssel verfügt, der bei der Geräteregistrierung verwendet wurde. Anschließend werden aus dem geheimen Schlüssel der App kryptografische Verbindungsparameter abgeleitet, die an den Agenten geschickt werden. Der Agent leitet aus seinem geheimen Schlüssel ebenfalls kryptografische Verbindungsparameter ab, die als Antwort zurück zur App kommen. Aus den kryptografischen Verbindungsparametern können jetzt App und Agent mittels Elliptic-curve Diffie-Hellman on curve P-256 (ECDH-P256) geheimes Schlüsselmaterial für die Ende-zu-Ende-verschlüsselte Verbindung ableiten.
var (myPrivateKeyParams, agentKeyParams) = WebCrypto.DecodeKeyFromBase64( privateKey);
this.myPrivateKey = new JsonWebKey(myPrivateKeyParams);
var agentKey = new JsonWebKey(agentKeyParams);
var myPublicJwk = new JsonWebKey( this.myPrivateKey.GetParams( exportPrivate: false));
if (this.Agent.PublicKey.X != agentKey.X || this.Agent.PublicKey.Y != agentKey.Y)
{
throw new Exception("AGENT_PUBLIC_KEY_CHANGED");
}
this.MyFingerprint = WebCrypto.Fingerprint( myPublicJwk.GetParams( exportPrivate: false));
var payload = WebCrypto.PrepareSession( this.myPrivateKey.GetParams( exportPrivate: true), out var a, out var b);
var response = await Http.Post(
url: Url($"/api/sessions/start?api-version=1.0"),
body: new ByteArrayContent(payload),
customHeaders: new[]
{
("Device", this.MyFingerprint)
});
var content = await response.Content.ReadAsByteArrayAsync();
var sessionId = string.Join(",", response.Headers.GetValues("x-session-id"));
var (encryptionKey, decryptionKey) = WebCrypto.SessionKeyExchange(a, b, this.Agent.PublicKey.GetParams( exportPrivate: false), content);
return new Session(this, sessionId, encryptionKey, decryptionKey);
Verbindung
App und Agent können nun durch die etablierte Ende-zu-Ende-verschlüsselte Verbindung sicher kommunizieren (/api/requests?api-version=2.0). Eine Anfrage der App wird also mit dem Verbindungsschlüssel verschlüsselt. Die Antwort des Agenten wird dann wieder entschlüsselt.
object paylod = requestDataFromAgent;
var request = new
{
PayloadType = paylod.GetType().Name,
Payload = paylod,
};
var requestEncoded = Encoding.UTF8.GetBytes( Device.Serialize(request));
var encryptedRequest = WebCrypto.EncryptSymmetric( requestEncoded, this.encryptionKey);
var response = await Http.Post(
url: device.Url($"/api/requests?api-version=2.0"),
body: new ByteArrayContent(encryptedRequest),
customHeaders: new[]
{
("x-session-id", this.SessionId)
});
var content = await response.Content.ReadAsByteArrayAsync();
var dataFromAgent = WebCrypto.DecryptSymmetric(content, this.decryptionKey);
Während die App nur eine Verbindung mit dem Agent halten brauch, muss der Agent mehrere parallele Verbindungen mit mehreren Apps offenhalten. Diese Verbindungen werden jeweils im flüchtigen Speicher der Kommunikationsteilnehmer gehalten. Wird die App oder der Agent neugestartet, müssen die Verbindungen neu aufgebaut werden.
Damit der Schlüsselaustausch und die Verschlüsselung wie beschrieben funktioniert, müssen die Endpunkte ihre geheimen Schlüssel geheim halten. Das bedeutet, sie müssen sie selbst verwalten. Wie dies funktioniert, wird im Zusammenhang mit der Installation des Agenten und der Installation der App beschrieben.