Implementing End-to-End Encryption in a Distributed System

Implementing End-to-End Encryption in a Distributed System
Photo by Towfiqu barbhuiya / Unsplash

In the current threat landscape, "encryption at rest" and "encryption in transit" (TLS) are merely table stakes. For high-compliance environments—fintech, healthcare, and enterprise SaaS—they are often insufficient. If your load balancer or database administrator can read user data in plaintext, your system is not truly secure.

The gold standard is End-to-End Encryption (E2EE). In a distributed system, E2EE ensures that data is encrypted on the client device and remains opaque until it reaches the intended recipient, effectively turning your backend microservices into blind relays.

As a global product engineering firm, 4Geeks frequently partners with CTOs to upgrade legacy architectures into zero-trust ecosystems. This article outlines the architectural patterns and technical implementation details required to build E2EE in a modern distributed environment.

Product Engineering Services

Work with our in-house Project Managers, Software Engineers and QA Testers to build your new custom software product or to support your current workflow, following Agile, DevOps and Lean methodologies.

Build with 4Geeks

The Core Architecture: The "Blind" Backend

In a traditional distributed system, the backend terminates TLS, processes plaintext data, and writes to a database. In an E2EE architecture, the backend manages metadata and key exchange, but never sees the payload.

Key Components:

  1. Identity Service (PKI): specific microservice handling public key storage.
  2. Relay/Store Service: Handles encrypted blobs (ciphertexts).
  3. Clients (Edge): Mobile/Web apps where encryption/decryption actually occurs.

The fundamental cryptographic primitive we will use is Authenticated Encryption with Associated Data (AEAD), specifically using X25519 for key exchange and AES-256-GCM (or ChaCha20-Poly1305) for payload encryption.

Step 1: Identity and Key Generation

Before any data flows, we must establish identity. Unlike TLS, where a Certificate Authority validates the server, E2EE requires clients to validate each other. We use an asynchronous key exchange pattern (inspired by the Signal Protocol's X3DH).

Each user device generates a long-term Identity Key Pair.

Implementation (Go)

We use the standard golang.org/x/crypto libraries. This snippet demonstrates generating a Curve25519 key pair, which is standard for modern ECDH (Elliptic Curve Diffie-Hellman).

package main

import (
	"crypto/rand"
	"fmt"
	"io"

	"golang.org/x/crypto/curve25519"
)

// GenerateKeyPair creates a new X25519 private/public key pair
func GenerateKeyPair(rng io.Reader) ([32]byte, [32]byte, error) {
	var privateKey [32]byte
	var publicKey [32]byte

	// 1. Generate random private key
	if _, err := io.ReadFull(rng, privateKey[:]); err != nil {
		return privateKey, publicKey, err
	}

	// 2. Derive public key from private key
	curve25519.ScalarBaseMult(&publicKey, &privateKey)

	return privateKey, publicKey, nil
}

func main() {
	priv, pub, err := GenerateKeyPair(rand.Reader)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Public Key: %x\n", pub)
}

Note: In a production system, you would also generate signed "PreKeys" (one-time use keys) to allow offline messaging, uploading these public keys to your Key Server.

Step 2: The Key Exchange (ECDH)

When User A wants to send data to User B, User A fetches User B's public key from the Key Server. User A then performs a Diffie-Hellman exchange using their own private key and User B's public key to derive a Shared Secret.

Crucially, the server never knows this secret because it never possesses the private keys.

// DeriveSharedSecret computes the X25519 shared secret
func DeriveSharedSecret(privateKey, peerPublicKey [32]byte) ([32]byte, error) {
	var sharedSecret [32]byte
	
	// ScalarMult calculates the shared secret: s = priv * pub
	curve25519.ScalarMult(&sharedSecret, &privateKey, &peerPublicKey)
	
	return sharedSecret, nil
}

Security Note: never use the raw result of ECDH as your encryption key. Always pass it through a Key Derivation Function (KDF) like HKDF to generate a cryptographically strong session key.

Step 3: The Encryption Pipeline (AEAD)

Once the shared session key is established, we use AES-GCM. GCM (Galois/Counter Mode) is critical because it provides both confidentiality (encryption) and integrity (authentication). If the server or a man-in-the-middle attempts to tamper with the encrypted payload, the decryption will fail.

Encrypting the Payload

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/hex"
	"io"
)

func EncryptPayload(key []byte, plaintext []byte) (string, error) {
	// 1. Create Cipher Block
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	// 2. Wrap in GCM (Galois Counter Mode) for Authenticated Encryption
	aesGCM, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	// 3. Create a Nonce (Number used once)
	// Standard nonce size for GCM is 12 bytes
	nonce := make([]byte, aesGCM.NonceSize())
	if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
		return "", err
	}

	// 4. Encrypt and Seal
	// Seal appends the ciphertext and the authentication tag to the nonce
	ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil)

	return hex.EncodeToString(ciphertext), nil
}

In this distributed architecture, the backend receives this hex string. It stores it, replicates it, and backs it up, but it cannot read it.

Product Engineering Services

Work with our in-house Project Managers, Software Engineers and QA Testers to build your new custom software product or to support your current workflow, following Agile, DevOps and Lean methodologies.

Build with 4Geeks

Challenges in Distributed E2EE Systems

Implementing this at the scale of a global product engineering firm introduces distinct distributed system challenges that go beyond simple cryptography.

1. The "Searchability" Paradox

If the backend cannot read the data, it cannot query it. You cannot run SELECT * FROM messages WHERE content LIKE '%hello%'.

  • Solution: Implement Blind Indexing. The client hashes searchable keywords (e.g., HMAC("hello", search_key)) and uploads these hashes alongside the encrypted payload. The server searches for matching hashes without knowing the underlying keyword.

2. Multi-Device Synchronization

Users expect to see their data on mobile and desktop.

  • Solution: The "sender" must encrypt the message $N$ times, once for every device the recipient owns. This requires the Key Service to return a list of active device keys (Device Fan-out).

3. Key Rotation and Revocation

If a device is stolen, the key must be revoked immediately.

  • Solution: Implement a "Ratchet" mechanism (like the Double Ratchet Algorithm). This ensures Forward Secrecy: even if a key is compromised later, past messages remain secure because the key changes with every message.

Conclusion

End-to-End Encryption transforms your distributed system from a vulnerability into a fortress. It shifts the trust boundary from the cloud provider to the user's edge device. However, it significantly increases complexity regarding data availability, search, and client-side logic.

For organizations navigating this transition, 4Geeks offers specialized product engineering services. Whether you are building secure communication platforms or HIPAA-compliant data pipelines, our team provides the architectural expertise to implement these cryptographic primitives correctly and scalably.

Product Engineering Services

Work with our in-house Project Managers, Software Engineers and QA Testers to build your new custom software product or to support your current workflow, following Agile, DevOps and Lean methodologies.

Build with 4Geeks

FAQs

What is the difference between End-to-End Encryption (E2EE) and standard TLS?

While Transport Layer Security (TLS) protects data "in transit" between a client and a server, the server typically decrypts and processes the data in plaintext. In contrast, End-to-End Encryption (E2EE) ensures that data is encrypted on the client device and remains opaque until it reaches the final recipient. This architecture treats the backend as a "blind relay" that manages metadata and storage but never has access to the actual data payload or the encryption keys.

How can a system search for data when it is End-to-End Encrypted?

One of the primary challenges in E2EE is the inability to run standard database queries (like SQL SELECT) on encrypted text. To resolve this, systems often implement Blind Indexing. In this approach, the client creates a hashed version of searchable keywords (using a secure method like HMAC) and uploads these hashes alongside the encrypted content. The server can then search for matching hashes to retrieve the correct records without ever deciphering the underlying information.

How does E2EE handle data synchronization across multiple devices?

Modern users expect their data to be accessible on both mobile and desktop environments. To support this in an E2EE ecosystem, the system must utilize Device Fan-out. When a user sends a message or saves data, the client encrypts the payload multiple times—once for every active device key associated with the recipient (or themselves). This ensures that every authorized device can independently decrypt the content using its own unique private key.

Read more