End-to-End Encryption
Building a secure chat application with ECDH and AES-GCM
End-to-End Encryption ensures that data is encrypted on the sender's device and only decrypted on the recipient's device. The server only sees encrypted blobs and cannot read the messages.
In this guide, we will implement a secure chat flow using ECDH ( Elliptic Curve Diffie-Hellman) for key exchange and AES-256-GCM for message encryption.
The Architecture
Implementation
Generate Identity Keys
Each user needs a permanent (or session) Key Pair. We use X25519 (Curve25519) because it is highly efficient and secure for key exchange.
import QuickCrypto from 'react-native-quick-crypto';
// user-1.ts
const alice = QuickCrypto.createECDH('curve25519');
const aliceKey = alice.generateKeys(); // Public Key to send to server
// user-2.ts
const bob = QuickCrypto.createECDH('curve25519');
const bobKey = bob.generateKeys(); // Public Key to send to serverDerive Shared Secret
Once Alice has Bob's public key (retrieved from your server), she can derive the shared secret. Bob does the same with Alice's public key.
// Alice's device
const aliceSecret = alice.computeSecret(bobKey);
// Bob's device
const bobSecret = bob.computeSecret(aliceKey);
console.log(aliceSecret.equals(bobSecret)); // trueThe derived secret is usually too long or not uniformly random enough to be used directly as an AES key. Always pass it through a KDF (Key Derivation Function) like HKDF first.
Key Derivation (HKDF)
Turn the raw Diffie-Hellman secret into a cryptographically strong Encryption Key.
// Derive a 32-byte (256-bit) AES key
const encryptionKey = QuickCrypto.hkdfSync(
'sha256',
aliceSecret, // Input Key Material (Shared Secret)
'chat-app-v1', // Salt (application specific constant)
'aes-key', // Info (context)
32 // Output length
);Encrypt Message
Use AES-256-GCM to encrypt the message. GCM provides both confidentiality (they can't read it) and integrity (they can't modify it).
const message = "Hello, Bob! Secret message 🤫";
const iv = QuickCrypto.randomBytes(12); // NIST recommends 12 bytes for GCM
const cipher = QuickCrypto.createCipheriv('aes-256-gcm', encryptionKey, iv);
let encrypted = cipher.update(message, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Send this package to Bob
const payload = {
iv: iv.toString('hex'),
content: encrypted,
authTag: authTag.toString('hex')
};Decrypt Message
Bob receives the payload and decrypts it using the same derived encryptionKey.
const decipher = QuickCrypto.createDecipheriv(
'aes-256-gcm',
encryptionKey,
Buffer.from(payload.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(payload.authTag, 'hex'));
let decrypted = decipher.update(payload.content, 'hex', 'utf8');
decrypted += decipher.final('utf8');
console.log(decrypted); // "Hello, Bob! Secret message 🤫"Security Considerations
- Man-in-the-Middle (MITM): If the server is malicious, it could swap Bob's public key with its own. To prevent this, implement Key Verification (e.g., comparing a "Safety Number" fingerprint derived from the keys) out-of-band (QR code scan).
- Forward Secrecy: The example above uses static keys. If a key is compromised, all past messages are compromised. Real-world apps (Signal, WhatsApp) use the Double Ratchet Algorithm to rotate keys with every message.
- Storage: Never store the Private Key (
alice.getPrivateKey()) in plain text. Use Secure Storage (Keychain/Keystore).
Production Readiness
This guide demonstrates core cryptographic primitives and concepts. A production-grade secure messaging application should implement a comprehensive protocol (like Signal Protocol) which includes:
- Double Ratchet Algorithm for Forward Secrecy.
- X3DH for asynchronous key exchange.
- Pre-Keys to allow offline messaging.