MIDA2025-0003: Establishing Key for End-to-End Encryption

McAiden Research Lab

TitleEstablishing Key for End-to-End Encryption
McAiden Vulnerability No.MIDA2025-0003
ProductMobile Apps/Thick-client Apps
Found2025-04-22
ByMcAiden Research Lab

Introduction

Key exchange is a fundamental step in establishing end-to-end encrypted (E2EE) communication. A secure key exchange ensures that only intended parties can derive and use the shared secret for further encryption and authentication. This advisory recommends practical and secure methods for performing key exchange in E2EE applications, tailored for modern security requirements and aligned with industry standards.

Common Problems and Mistakes in Key Exchange

Many systems fail to achieve true end-to-end security due to the following mistakes, often leading to real-world vulnerabilities

No Authentication of Public Keys

When public keys are exchanged without verifying who they belong to, an attacker can intercept and replace them. This allows the attacker to impersonate one of the parties, leading to a man-in-the-middle (MitM) attack

Background: DH (Diffie-Hellman) is a method where two parties agree on a shared secret over an insecure channel without sending the secret directly. However, if the parameters used in DH (such as the prime number or generator) are not properly authenticated, an attacker can trick both sides into using weak or attacker-chosen parameters, making it easier to compute the shared secret.

Sample vulnerability: CVE-2015-4000 (Logjam Attack). Servers accepted weak or manipulatable DH parameters. Attackers could downgrade the security of a TLS connection and decrypt or alter encrypted traffic.

Use of Static Keys Without Forward Secrecy

If a system always uses the same long-term key pair for multiple sessions (e.g. hardcoded the key), then if that key pair is ever compromised, all past and future communications can also be decrypted by an attacker.

Background: Forward Secrecy is a security property that ensures that even if long-term private keys are later compromised, past communications remain secure. It is achieved by generating ephemeral keys — fresh key pairs for each session — instead of reusing a single static key.

Sample vulnerability: CVE-2018-5383 (Bluetooth ECDH Key Reuse Vulnerability). Bluetooth devices reused the same ECDH key pairs across multiple sessions. This allowed attackers to capture traffic and eventually compute the shared session keys.

Weak Cryptographic Parameters

If weak or outdated algorithms are used, attackers can break the encryption, even if the rest of the system is secure.

Background: Export-grade RSA refers to intentionally weakened RSA key sizes (typically 512 bits) that were created to comply with 1990s-era US export regulations. These keys are very weak by today’s standards and can be easily cracked using modern computing resources.

Sample vulnerability: CVE-2015-0204 (FREAK Attack). Servers supported “export-grade” RSA keys, and attackers could force a downgrade to these weak keys. Once downgraded, attackers could break the encryption and intercept the supposedly secure communication.

What is Key Establishment, Key Exchange, and Key Agreement?

In end-to-end encryption (E2EE), both communicating parties must share a secret key that will be used to encrypt and decrypt messages. However, the key itself must never be sent openly over the network, because an attacker could intercept it. To solve this, cryptographic methods are used to securely establish a shared secret without exposing it. This process is called key establishment.

There are two main approaches forkey establishment:

TermMeaning
Key ExchangeOne party generates a secret and securely sends it to the other party.
Key AgreementBoth parties jointly create a shared secret by exchanging public information (without sending the secret directly).

Why Key Establishment is Critical?

If key establishment is weak:

  • Attackers can steal the session key and decrypt all communication.
  • Man-in-the-middle attacks become possible.
  • Forward secrecy may be lost if long-term keys are compromised.

Therefore, a strong and authenticated key establishment method is mandatory for secure E2EE systems.

Key Derivation Function (KDF)

When two parties exchange information to establish a shared secret (such as through ECDH), the raw shared secret produced is not directly ready for use as a session key. Instead, a Key Derivation Function (KDF) must be applied.

A Key Derivation Function (KDF) is a cryptographic process that takes a shared secret and strengthens it, producing a clean, strong, and usable encryption key.

Why a KDF is Necessary After Key Exchange

Simply using the output of ECDH or other key exchange algorithms directly can introduce serious risks:

Problem / RiskWhy KDF is Needed
Wrong Size – The shared secret may not match the key size needed by encryption algorithms like AES-256.KDF outputs exactly the desired size (e.g., 256 bits for AES-256).
Non-Uniform Entropy – The shared secret might have subtle biases or predictable structure.                KDF extracts full entropy and “smooths out” any irregularities.
Single Key Limitation – Without a KDF, you cannot easily derive multiple independent keys from the same secret.KDF allows deriving multiple keys securely by providing different context (info) fields.
Future Security – Vulnerabilities in the raw shared secret could later be exploited.            KDF isolates derived keys from weaknesses in the key exchange.

Standard KDFs in Secure Systems

Modern protocols such as TLS 1.3, Signal Protocol, and Matrix (Olm/Megolm) all use KDFs after key exchanges to secure their communications. The most common and recommended KDF today is:

  • HKDF (HMAC-based Key Derivation Function), typically used with:
    • SHA-256 or SHA-512 as the underlying hash function.

How to Securely Perform Key Establishment

To perform secure key establishment for end-to-end encryption (E2EE) in a mobile app or thick client app, we recommend using Ephemeral Elliptic Curve Diffie-Hellman (ECDH) combined with Key Derivation Functions (HKDF) and Authenticated Encryption (AES-GCM).

How the Process Works

StepDescription
1. Key Pair GenerationEach side (client and server) generates a fresh ephemeral ECDH key pair.
2. Public Key Exchange        Each side exchanges their public key with the other (securely authenticated if possible).
3. Shared Secret DerivationEach side computes the shared secret by combining their private key with the other party’s public key using ECDH.
4. Key Derivation (HKDF)     The raw shared secret is run through a Key Derivation Function (e.g., HKDF-SHA256) to produce a strong session key.
5. Secure Communication    The session key is used with AES-GCM to encrypt and decrypt messages securely and with integrity protection.

Key Points for Developers:

  • Regenerate ephemeral key pairs for every session.
  • Secure the private keys — keep them only in memory, and destroy after use.
  • Ensure public key exchange is authenticated (e.g., signed public keys, pinned keys, or certificates).
  • Always use a new random nonce for each AES-GCM encryption operation.
  • Use HKDF to strengthen and derive session keys properly.

Recommended Algorithms and Parameters

To ensure secure, efficient, and practical end-to-end encryption suitable for mobile applications, IoT devices, and thick clients, we recommend the following algorithms and parameters for each step of the key establishment and secure communication process:

StepRecommendationReason
1. Key Pair GenerationX25519 (Elliptic Curve Diffie-Hellman over Curve25519)– Fast, lightweight, highly secure – Very efficient for mobile and IoT hardware – Standard in Signal, TLS 1.3
2. Public Key Exchange                 – Sign public keys with Ed25519 (for optional authentication) – Or pre-trust keys if certificates are too heavy– Ed25519 is fast, small, strong – Certificates are optional for smaller systems
3. Shared Secret DerivationUse ECDH based on X25519– Built into X25519 primitive – Very efficient, safe against known attacks
4. Key Derivation (HKDF)     HKDF-SHA256– Standard in modern protocols (TLS 1.3, Signal) – Secure and fast enough even for small devices
5. Secure Communication        AES-256-GCM (or ChaCha20-Poly1305 for low-power devices)– AES-GCM is very fast with hardware acceleration (on ARM, iOS, Android) – ChaCha20-Poly1305 is better for pure software environments or very low-end IoT devices

The following figure show the simple flow of secure key establishment:

Salt Derivation for HKDF

In key derivation, it is strongly recommended to provide a salt input to HKDF.

Salt acts as a nonce to ensure that each derived session key is unique, even if the same shared secret is reused. Without a salt, repeated ephemeral keys or handshake parameters could lead to identical session keys — a serious security risk.

How to generate a salt:

  • Random 32-byte value (simple and effective), or
  • Derive deterministically from the handshake ephemeral keys: salt = SHA256(client_public_key || server_public_key)

This method tightly binds the salt to each unique handshake, ensuring uniqueness without needing additional randomness.

In the following design, because the ephemeral keys are freshly and randomly generated for every session, deriving the salt from the ephemeral keys is efficient and cryptographically sound.

Sample implementation with Python

File: key_establishment.py

from cryptography.hazmat.primitives.asymmetric import x25519, ed25519
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import serialization

import os

# -------------------------------------------------
# 1. Key Pair Generation
# -------------------------------------------------

print("\n# 1. Key Pair Generation")
print("# -------------------------------------------------")

# Client ephemeral key (X25519) and signing key (Ed25519)
client_private_key_x25519 = x25519.X25519PrivateKey.generate()
client_public_key_x25519 = client_private_key_x25519.public_key()

client_signing_key = ed25519.Ed25519PrivateKey.generate()
client_verifying_key = client_signing_key.public_key()

# Server ephemeral key (X25519) and signing key (Ed25519)
server_private_key_x25519 = x25519.X25519PrivateKey.generate()
server_public_key_x25519 = server_private_key_x25519.public_key()

server_signing_key = ed25519.Ed25519PrivateKey.generate()
server_verifying_key = server_signing_key.public_key()

print("[+] Key pairs generated successfully.")

# -------------------------------------------------
# 2. Public Key Exchange
# -------------------------------------------------

print("\n# 2. Public Key Exchange")
print("# -------------------------------------------------")

# 2.1 Client sends public key and signature
client_public_key_bytes = client_public_key_x25519.public_bytes(
    encoding=serialization.Encoding.Raw,
    format=serialization.PublicFormat.Raw
)
client_signature = client_signing_key.sign(client_public_key_bytes)

print("[2.1] Client sends to media:")
print("    Client Public Key (hex):", client_public_key_bytes.hex())
print("    Client Signature (hex):", client_signature.hex())

# --- Simulate transmission over insecure media ---

# 2.2 Server receives and verifies
print("[2.2] Server receives and verifies Client's key...")

try:
    server_verifying_key_of_client = client_verifying_key  # Trusted out-of-band
    server_verifying_key_of_client.verify(client_signature, client_public_key_bytes)
    print("[+] Server verified Client's public key signature.")
except Exception as e:
    print("[-] Verification failed:", str(e))
    exit(1)

# 2.3 Server sends public key and signature
server_public_key_bytes = server_public_key_x25519.public_bytes(
    encoding=serialization.Encoding.Raw,
    format=serialization.PublicFormat.Raw
)
server_signature = server_signing_key.sign(server_public_key_bytes)

print("[2.3] Server sends to media:")
print("    Server Public Key (hex):", server_public_key_bytes.hex())
print("    Server Signature (hex):", server_signature.hex())

# --- Simulate transmission over insecure media ---

# 2.4 Client receives and verifies
print("[2.4] Client receives and verifies Server's key...")

try:
    client_verifying_key_of_server = server_verifying_key  # Trusted out-of-band
    client_verifying_key_of_server.verify(server_signature, server_public_key_bytes)
    print("[+] Client verified Server's public key signature.")
except Exception as e:
    print("[-] Verification failed:", str(e))
    exit(1)

# -------------------------------------------------
# 3. Shared Secret Derivation
# -------------------------------------------------

print("\n# 3. Shared Secret Derivation")
print("# -------------------------------------------------")

# Each side derives the shared secret
server_pub_key_loaded = x25519.X25519PublicKey.from_public_bytes(server_public_key_bytes)
client_shared_secret = client_private_key_x25519.exchange(server_pub_key_loaded)

client_pub_key_loaded = x25519.X25519PublicKey.from_public_bytes(client_public_key_bytes)
server_shared_secret = server_private_key_x25519.exchange(client_pub_key_loaded)

assert client_shared_secret == server_shared_secret, "Shared secret mismatch!"
print("[+] Shared secret established successfully.")
print("Shared secret (hex): ", client_shared_secret.hex())
# -------------------------------------------------
# 4. Key Derivation (HKDF)
# -------------------------------------------------

print("\n# 4. Key Derivation (HKDF)")
print("# -------------------------------------------------")

session_key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,  # 256 bits key for AES-256
    salt=None,
    info=b"handshake data",
).derive(client_shared_secret)

print("[+] Session key derived.")
print("    Session Key (hex):", session_key.hex())

# -------------------------------------------------
# 5. Secure Communication
# -------------------------------------------------

print("\n# 5. Secure Communication")
print("# -------------------------------------------------")

# Encrypt a message
aesgcm = AESGCM(session_key)
nonce = os.urandom(12)  # 96-bit nonce
plaintext = b"Hello from Client to Server!"

ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data=None)

print("[5.1] Client sends encrypted message:")
print("    Nonce (hex):", nonce.hex())
print("    Ciphertext (hex):", ciphertext.hex())

# Decrypt the message
aesgcm_server = AESGCM(session_key)
decrypted_plaintext = aesgcm_server.decrypt(nonce, ciphertext, associated_data=None)

print("[5.2] Server decrypts the message:")
print("    Decrypted Plaintext:", decrypted_plaintext.decode())

print("\n[+] Secure transmission and decryption completed.")

Sample implementation with Dart

File: key_establishment.dart

import 'dart:convert';
import 'dart:typed_data';
import 'dart:math';
import 'package:cryptography/cryptography.dart';

void main() async {
  // -------------------------------------------------
  // 1. Key Pair Generation
  // -------------------------------------------------
print('\n# -------------------------------------------------');
  print('# 1. Key Pair Generation');
  print('# -------------------------------------------------');

  final x25519 = X25519();
  final ed25519 = Ed25519();

  // Client generates ephemeral X25519 and signing Ed25519 key pairs
  final clientEphemeralKeyPair = await x25519.newKeyPair();
  final clientSigningKeyPair = await ed25519.newKeyPair();

  // Server generates ephemeral X25519 and signing Ed25519 key pairs
  final serverEphemeralKeyPair = await x25519.newKeyPair();
  final serverSigningKeyPair = await ed25519.newKeyPair();

  print('[+] Key pairs generated successfully.');

  // -------------------------------------------------
  // 2. Public Key Exchange
  // -------------------------------------------------
print('\n# -------------------------------------------------');
  print('# 2. Public Key Exchange');
  print('# -------------------------------------------------');

  // 2.1 Client sends public key and signature
  final clientPublicKeyBytes = await clientEphemeralKeyPair.extractPublicKey().then((k) => k.bytes);
  final clientSignature = await ed25519.sign(
    clientPublicKeyBytes,
    keyPair: clientSigningKeyPair,
  );

  print('[2.1] Client sends to media:');
  print('\tClient Public Key (hex): ${_toHex(clientPublicKeyBytes)}');
  print('\tClient Signature (hex): ${_toHex(clientSignature.bytes)}');

  // --- Simulate transmission ---

  // 2.2 Server receives and verifies
  print('[2.2] Server receives and verifies Client\'s key...');

  final clientVerifyingKey = await clientSigningKeyPair.extractPublicKey();
  try {
    await ed25519.verify(
      clientPublicKeyBytes,
      signature: Signature(clientSignature.bytes, publicKey: clientVerifyingKey),
    );
    print('[+] Server verified Client\'s public key signature.');
  } catch (e) {
    print('[-] Verification failed: $e');
    return;
  }

  // 2.3 Server sends public key and signature
  final serverPublicKeyBytes = await serverEphemeralKeyPair.extractPublicKey().then((k) => k.bytes);
  final serverSignature = await ed25519.sign(
    serverPublicKeyBytes,
    keyPair: serverSigningKeyPair,
  );

  print('[2.3] Server sends to media:');
  print('\tServer Public Key (hex): ${_toHex(serverPublicKeyBytes)}');
  print('\tServer Signature (hex): ${_toHex(serverSignature.bytes)}');

  // --- Simulate transmission ---

  // 2.4 Client receives and verifies
  print('[2.4] Client receives and verifies Server\'s key...');

  final serverVerifyingKey = await serverSigningKeyPair.extractPublicKey();
  try {
    await ed25519.verify(
      serverPublicKeyBytes,
      signature: Signature(serverSignature.bytes, publicKey: serverVerifyingKey),
    );
    print('[+] Client verified Server\'s public key signature.');
  } catch (e) {
    print('[-] Verification failed: $e');
    return;
  }

// -------------------------------------------------
// 3. Shared Secret Derivation
// -------------------------------------------------
print('\n# -------------------------------------------------');
print('# 3. Shared Secret Derivation');
print('# -------------------------------------------------');

// Correct shared secret extraction
final clientSharedSecretKey = await x25519.sharedSecretKey(
  keyPair: clientEphemeralKeyPair,
  remotePublicKey: SimplePublicKey(serverPublicKeyBytes, type: KeyPairType.x25519),
);
final serverSharedSecretKey = await x25519.sharedSecretKey(
  keyPair: serverEphemeralKeyPair,
  remotePublicKey: SimplePublicKey(clientPublicKeyBytes, type: KeyPairType.x25519),
);

// Extract bytes
final clientSharedSecretBytes = await clientSharedSecretKey.extractBytes();
final serverSharedSecretBytes = await serverSharedSecretKey.extractBytes();

// Check
assert(_toHex(clientSharedSecretBytes) == _toHex(serverSharedSecretBytes), 'Shared secret mismatch!');
print('[+] Shared secret established successfully.');
print('\tShared Secret (hex): ${_toHex(clientSharedSecretBytes)}');

// -------------------------------------------------
// 4. Key Derivation (HKDF)
// -------------------------------------------------
print('\n# -------------------------------------------------');
print('# 4. Key Derivation (HKDF)');
print('# -------------------------------------------------');

final hkdf = Hkdf(
  hmac: Hmac.sha256(),
  outputLength: 32,
);

// Derive HKDF nonce securely:
final nonceInput = <int>[];
nonceInput.addAll(clientPublicKeyBytes);
nonceInput.addAll(serverPublicKeyBytes);

final hash = await Sha256().hash(nonceInput);
final hkdfNonce = hash.bytes.sublist(0, 12); // Take first 12 bytes

final sessionKey = await hkdf.deriveKey(
  secretKey: SecretKey(clientSharedSecretBytes),
  nonce: hkdfNonce,
  info: utf8.encode('handshake data'), // constant agreed string
);

final sessionKeyBytes = await sessionKey.extractBytes();

print('[+] Session key derived.');
print('\tSession Key (hex): ${_toHex(sessionKeyBytes)}');

  // -------------------------------------------------
  // 5. Secure Communication
  // -------------------------------------------------
  print('\n# -------------------------------------------------');
  print('# 5. Secure Communication');
  print('# -------------------------------------------------');

  final aesGcm = AesGcm.with256bits();
  final nonce = _randomBytes(12);
  final plaintext = utf8.encode('Hello from Client to Server!');

  final ciphertext = await aesGcm.encrypt(
    plaintext,
    secretKey: sessionKey,
    nonce: nonce,
  );

  print('[5.1] Client sends encrypted message:');
  print('\tNonce (hex): ${_toHex(nonce)}');
  print('\tCiphertext (hex): ${_toHex(ciphertext.cipherText)}');

  final decryptedPlaintext = await aesGcm.decrypt(
    SecretBox(ciphertext.cipherText, nonce: nonce, mac: ciphertext.mac),
    secretKey: sessionKey,
  );

  print('[5.2] Server decrypts the message:');
  print('\tDecrypted Plaintext: ${utf8.decode(decryptedPlaintext)}');

  print('\n[+] Secure transmission and decryption completed.');
}

String _toHex(List<int> bytes) {
  return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}

List<int> _randomBytes(int length) {
  final rand = Random.secure();
  return List<int>.generate(length, (_) => rand.nextInt(256));
}

In Dart implementation, we use cryptography: ^2.7.0

Bonus: When to Prefer ChaCha20-Poly1305 Over AES-GCM

In environments where hardware acceleration for AES is unavailable, it is recommended to use ChaCha20-Poly1305 instead of AES-GCM. ChaCha20-Poly1305 is specifically designed for high performance in pure software implementations and provides strong security with excellent speed, even on low-power or resource-constrained devices. Typical scenarios where ChaCha20-Poly1305 is preferred:

  • Low-end Android devices without AES hardware acceleration
  • IoT devices such as ESP32-based hardware
  • Older embedded systems or microcontrollers with limited cryptographic support

In all other cases, where AES hardware acceleration (e.g., ARM Cryptography Extensions, Intel AES-NI) is available, AES-256-GCM remains the preferred choice due to its hardware-optimized performance.

Summary

This advisory outlines a secure and practical method for establishing cryptographic keys in end-to-end encryption (E2EE) systems, particularly for mobile and thick-client applications. By using fresh ephemeral keys (X25519), authenticated public key exchange (Ed25519), strong key derivation (HKDF-SHA256), and authenticated encryption (AES-256-GCM or ChaCha20-Poly1305), the recommended approach provides:

  • Forward Secrecy: Each session generates independent keys, protecting past communications even if long-term keys are compromised.
  • Integrity and Authenticity: Public key signatures ensure that keys cannot be tampered with during exchange.
  • Efficiency: All recommended algorithms are optimized for both modern smartphones and low-power IoT devices.
  • Strong Cryptographic Separation: Salt derived from ephemeral public keys ensures that each handshake produces unique session keys, even without additional randomness.

More Information can be found at: