Cryptography

End-to-End Encryption

Communication between STP.Documents.OnPremise Mobile DESK and the On-premise Agent is encrypted. Since data and documents from the secure on-premise system are transmitted through the cloud, they must be encrypted in such a way that only the respective recipients can decrypt them. This encryption mechanism is also referred to as end-to-end encryption, as only the endpoints of the communication know or can restore the plaintext. Other communication participants, such as smartphones of other users or agents of other firms, cannot decrypt the communication contents. Not even STP can see what is transmitted between the app and the agent.

The following algorithms are used:

  • ECDH/ECDSA on curve P-256 for key exchange
  • AES-GCM 256bit for symmetric encryption

The following is ensured:

  • Before the device can establish an encrypted connection, it must authenticate itself on behalf of the user using OpenID Connect (OIDC).
  • Private keys remain at the endpoints, i.e., in the app and in the agent.
  • Only the public keys of the involved endpoints are known to the respective other endpoints.
  • Short-lived encrypted connections are derived from the endpoints’ private keys.
  • Even if a private key were to become known, recorded communication cannot be decrypted (Perfect Forward Secrecy).
  • After 15 minutes or 1000 requests, a new encrypted connection is established, and data from the old one can no longer be restored.
  • Separate keys are used for sending and receiving data.
  • Encrypted requests are only accepted once and cannot be replayed (Replay Attack Protection).
  • Each document version has its own unique private key.
  • The end-to-end encrypted connection between app to cloud and cloud to agent is additionally embedded in a TLS 1.2+ connection.
  • The cryptographic algorithms used are supported by all modern browsers, and even hardware-accelerated on the iPhone.

Connection Establishment

When the app wants to connect to the agent to retrieve data and documents, it must first establish a secure connection. This works as follows:

//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
    ...
}

The first contact between the app and the agent takes place via the LoadAgentInfo method (/api/sessions/agentinfo?api-version=1.0). Here, the public key and some properties of the agent are retrieved to ensure that the app connects to the correct agent in the next step.

If no device has been registered yet, this can be done using the RegisterAs method (/api/sessions/register?api-version=1.0). In this process, the browser’s WebCrypto API is used to generate a new random key pair consisting of a private and a public key. The private key is linked with the agent’s public key and remains on the device. The corresponding public key is sent to the agent. As a result, both the app and the agent know each other’s public keys.

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,
    });

During registration, it must be ensured that the public key of the agent known to the app actually matches the public key of the agent in the on-premises environment. This principle is known as Trust On First Use (TOFU). The user must manually verify this by comparing the fingerprints of the public keys. If this is not ensured, the app could connect to a spoofed agent.

Once the app and agent know each other’s public keys, the key exchange can take place. For this, the login method is called (/api/sessions/start?api-version=1.0). First, it is checked whether the agent still has the public key that was used during device registration. Then, cryptographic connection parameters are derived from the app’s private key and sent to the agent. The agent also derives cryptographic connection parameters from its own private key and returns them to the app. Using these cryptographic parameters, the app and agent can now derive secret key material for the end-to-end encrypted connection using Elliptic-curve Diffie-Hellman on curve P-256 (ECDH-P256).

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);

Connection

The app and agent can now communicate securely through the established end-to-end encrypted connection (/api/requests?api-version=2.0). A request from the app is encrypted using the session key. The agent’s response is then decrypted accordingly.

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);

While the app only needs to maintain a single connection with the agent, the agent must keep multiple parallel connections open with several apps. These connections are each held in the volatile memory of the communication participants. If the app or agent is restarted, the connections must be re-established.

For the key exchange and encryption to work as described, the endpoints must keep their private keys secret. This means they must manage them themselves. How this is done is described in the context of Agent Installation and App Installation.