Asymmetric encryption, also known as public key cryptography, revolutionized secure communication by solving the key distribution problem. Unlike symmetric encryption, which requires sharing a secret key, asymmetric encryption uses key pairs—enabling secure communication without prior secret sharing.
Understanding Asymmetric Encryption
Asymmetric encryption uses two mathematically related keys:
- Public Key: Freely distributed and used for encryption
- Private Key: Kept secret and used for decryption
This elegant design enables several critical security operations:
- 1Confidentiality: Anyone can encrypt with your public key, only you can decrypt
- 2Digital Signatures: You can sign with your private key, anyone can verify with your public key
- 3Key Exchange: Securely establish symmetric keys for bulk encryption
import { generateKeyPairSync, publicEncrypt, privateDecrypt, privateEncrypt, publicDecrypt } from 'crypto';
// Generate RSA key pair
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
},
});
// Encrypt with public key (confidentiality)
function encryptWithPublicKey(message: string, publicKey: string): string {
const buffer = Buffer.from(message, 'utf8');
const encrypted = publicEncrypt(publicKey, buffer);
return encrypted.toString('base64');
}
// Decrypt with private key
function decryptWithPrivateKey(encrypted: string, privateKey: string): string {
const buffer = Buffer.from(encrypted, 'base64');
const decrypted = privateDecrypt(privateKey, buffer);
return decrypted.toString('utf8');
}
const message = 'Secret message';
const encrypted = encryptWithPublicKey(message, publicKey);
const decrypted = decryptWithPrivateKey(encrypted, privateKey);
console.log('Original:', message);
console.log('Encrypted:', encrypted);
console.log('Decrypted:', decrypted);RSA Algorithm
RSA is the most widely used asymmetric encryption algorithm, based on the mathematical difficulty of factoring large prime numbers.
Key Generation
import { generateKeyPairSync } from 'crypto';
// Generate different key sizes
const rsa2048 = generateKeyPairSync('rsa', {
modulusLength: 2048, // Minimum recommended size
});
const rsa4096 = generateKeyPairSync('rsa', {
modulusLength: 4096, // More secure, slower
});
// For production systems, use 4096 bits
const productionKey = generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});Encryption and Decryption
import { publicEncrypt, privateDecrypt, constants } from 'crypto';
interface EncryptedData {
ciphertext: string;
algorithm: string;
}
function rsaEncrypt(plaintext: string, publicKey: string): EncryptedData {
const buffer = Buffer.from(plaintext, 'utf8');
const encrypted = publicEncrypt({
key: publicKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
}, buffer);
return {
ciphertext: encrypted.toString('base64'),
algorithm: 'RSA-OAEP-256',
};
}
function rsaDecrypt(data: EncryptedData, privateKey: string): string {
const buffer = Buffer.from(data.ciphertext, 'base64');
const decrypted = privateDecrypt({
key: privateKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
}, buffer);
return decrypted.toString('utf8');
}
// Usage
const sensitiveData = 'Credit card: 4532-1234-5678-9010';
const encrypted = rsaEncrypt(sensitiveData, publicKey);
const decrypted = rsaDecrypt(encrypted, privateKey);RSA Limitations
RSA has significant limitations:
- 1Size Limitation: Can only encrypt data smaller than the key size
- 2Slow Performance: Much slower than symmetric encryption
- 3Data Size: For 2048-bit RSA, maximum plaintext is ~245 bytes
// ❌ This will fail
const largeData = 'A'.repeat(300); // Too large for RSA-2048
const encrypted = rsaEncrypt(largeData, publicKey); // Error!
// ✅ Solution: Hybrid encryption (use RSA to encrypt symmetric key)Digital Signatures
Digital signatures provide authentication and non-repudiation. You sign data with your private key, and anyone can verify using your public key.
import { createSign, createVerify } from 'crypto';
interface SignedData {
data: string;
signature: string;
algorithm: string;
}
function signData(data: string, privateKey: string): SignedData {
const sign = createSign('SHA256');
sign.update(data);
sign.end();
const signature = sign.sign(privateKey, 'base64');
return {
data,
signature,
algorithm: 'RSA-SHA256',
};
}
function verifySignature(signedData: SignedData, publicKey: string): boolean {
const verify = createVerify('SHA256');
verify.update(signedData.data);
verify.end();
return verify.verify(publicKey, signedData.signature, 'base64');
}
// Usage
const document = 'Important contract text';
const signed = signData(document, privateKey);
// Verification
const isValid = verifySignature(signed, publicKey);
console.log('Signature valid:', isValid);Signing Files
import { createReadStream } from 'fs';
import { createSign } from 'crypto';
import { pipeline } from 'stream/promises';
async function signFile(filePath: string, privateKey: string): Promise<string> {
const sign = createSign('SHA256');
const stream = createReadStream(filePath);
await pipeline(stream, sign);
return sign.sign(privateKey, 'base64');
}
async function verifyFile(filePath: string, signature: string, publicKey: string): Promise<boolean> {
const verify = createVerify('SHA256');
const stream = createReadStream(filePath);
await pipeline(stream, verify);
return verify.verify(publicKey, signature, 'base64');
}Elliptic Curve Cryptography (ECC)
ECC provides similar security to RSA with much smaller key sizes and better performance.
ECDSA (Elliptic Curve Digital Signature Algorithm)
import { generateKeyPairSync, createSign, createVerify } from 'crypto';
// Generate ECDSA key pair
const { publicKey: ecPublicKey, privateKey: ecPrivateKey } = generateKeyPairSync('ec', {
namedCurve: 'secp256k1', // Bitcoin's curve
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});
function signWithECDSA(data: string, privateKey: string): string {
const sign = createSign('SHA256');
sign.update(data);
sign.end();
return sign.sign(privateKey, 'base64');
}
function verifyWithECDSA(data: string, signature: string, publicKey: string): boolean {
const verify = createVerify('SHA256');
verify.update(data);
verify.end();
return verify.verify(publicKey, signature, 'base64');
}ECIES (Elliptic Curve Integrated Encryption Scheme)
import { generateKeyPairSync, randomBytes, createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt } from 'crypto';
// ECIES is not directly supported in Node.js crypto module
// Here's a simplified implementation using ECDH + AES
import { createECDH } from 'crypto';
function eciesEncrypt(plaintext: string, publicKey: string): { ephemeralPublicKey: string; ciphertext: string; iv: string; tag: string } {
// Generate ephemeral key pair
const ephemeral = createECDH('prime256v1');
const ephemeralPublicKey = ephemeral.getPublicKey();
// Compute shared secret
const theirPublicKey = Buffer.from(publicKey, 'hex');
const sharedSecret = ephemeral.computeSecret(theirPublicKey);
// Derive AES key from shared secret
const key = createHash('sha256').update(sharedSecret).digest();
// Encrypt with AES-GCM
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
return {
ephemeralPublicKey: ephemeralPublicKey.toString('hex'),
ciphertext: encrypted,
iv: iv.toString('hex'),
tag: tag.toString('hex'),
};
}Hybrid Encryption
Because RSA is slow and has size limitations, the standard approach is hybrid encryption: use RSA to encrypt a symmetric key, then use that symmetric key to encrypt the actual data.
import { generateKeyPairSync, publicEncrypt, privateDecrypt, randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
interface HybridEncrypted {
encryptedKey: string;
iv: string;
ciphertext: string;
authTag: string;
}
function hybridEncrypt(plaintext: string, publicKey: string): HybridEncrypted {
// Generate random symmetric key
const symmetricKey = randomBytes(32);
const iv = randomBytes(12);
// Encrypt data with AES-GCM
const cipher = createCipheriv('aes-256-gcm', symmetricKey, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Encrypt symmetric key with RSA
const encryptedKey = publicEncrypt(publicKey, symmetricKey);
return {
encryptedKey: encryptedKey.toString('base64'),
iv: iv.toString('hex'),
ciphertext: encrypted,
authTag: authTag.toString('hex'),
};
}
function hybridDecrypt(data: HybridEncrypted, privateKey: string): string {
// Decrypt symmetric key with RSA
const symmetricKey = privateDecrypt(privateKey, Buffer.from(data.encryptedKey, 'base64'));
// Decrypt data with AES-GCM
const decipher = createDecipheriv('aes-256-gcm', symmetricKey, Buffer.from(data.iv, 'hex'));
decipher.setAuthTag(Buffer.from(data.authTag, 'hex'));
let decrypted = decipher.update(data.ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Usage
const largeData = 'A'.repeat(10000); // Large data
const encrypted = hybridEncrypt(largeData, publicKey);
const decrypted = hybridDecrypt(encrypted, privateKey);Key Exchange Protocols
Diffie-Hellman Key Exchange
Diffie-Hellman allows two parties to establish a shared secret over an insecure channel.
import { createDiffieHellman, randomBytes } from 'crypto';
// Generate prime and generator (in production, use standardized parameters)
const dh = createDiffieHellman(2048);
const prime = dh.getPrime();
const generator = dh.getGenerator();
// Alice's side
const aliceDh = createDiffieHellman(prime, generator);
const alicePublicKey = aliceDh.generateKeys();
const alicePrivateKey = aliceDh.getPrivateKey();
// Bob's side
const bobDh = createDiffieHellman(prime, generator);
const bobPublicKey = bobDh.generateKeys();
const bobPrivateKey = bobDh.getPrivateKey();
// Both compute shared secret independently
const aliceShared = aliceDh.computeSecret(bobPublicKey);
const bobShared = bobDh.computeSecret(alicePublicKey);
// Secrets match!
console.log('Shared secrets match:', aliceShared.equals(bobShared));ECDH (Elliptic Curve Diffie-Hellman)
import { createECDH } from 'crypto';
// Alice's ECDH
const alice = createECDH('prime256v1');
const alicePublicKey = alice.generateKeys();
// Bob's ECDH
const bob = createECDH('prime256v1');
const bobPublicKey = bob.generateKeys();
// Compute shared secrets
const aliceShared = alice.computeSecret(bobPublicKey);
const bobShared = bob.computeSecret(alicePublicKey);
console.log('Shared secrets match:', aliceShared.equals(bobShared));Practical Applications
Secure API Authentication
import { createSign, createVerify } from 'crypto';
interface APIRequest {
method: string;
path: string;
body: any;
timestamp: number;
}
class APIClient {
constructor(private privateKey: string, private publicKey: string) {}
signRequest(request: APIRequest): string {
const payload = JSON.stringify({
method: request.method,
path: request.path,
body: request.body,
timestamp: request.timestamp,
});
const sign = createSign('SHA256');
sign.update(payload);
return sign.sign(this.privateKey, 'base64');
}
verifyRequest(request: APIRequest, signature: string): boolean {
const payload = JSON.stringify({
method: request.method,
path: request.path,
body: request.body,
timestamp: request.timestamp,
});
const verify = createVerify('SHA256');
verify.update(payload);
return verify.verify(this.publicKey, signature, 'base64');
}
async makeAuthenticatedRequest(url: string, body: any) {
const request: APIRequest = {
method: 'POST',
path: url,
body,
timestamp: Date.now(),
};
const signature = this.signRequest(request);
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Signature': signature,
'X-Timestamp': request.timestamp.toString(),
},
body: JSON.stringify(body),
});
}
}JWT (JSON Web Tokens) with Asymmetric Keys
import { sign, verify } from 'jsonwebtoken';
import { readFileSync } from 'fs';
// Sign JWT with private key
function generateJWT(payload: any, privateKey: string): string {
return sign(payload, privateKey, {
algorithm: 'RS256',
expiresIn: '1h',
});
}
// Verify JWT with public key
function verifyJWT(token: string, publicKey: string): any {
return verify(token, publicKey, {
algorithms: ['RS256'],
});
}
// Usage
const payload = { userId: 123, role: 'admin' };
const token = generateJWT(payload, privateKey);
try {
const decoded = verifyJWT(token, publicKey);
console.log('Valid token:', decoded);
} catch (error) {
console.log('Invalid token');
}End-to-End Encrypted Messaging
interface EncryptedMessage {
senderPublicKey: string;
encryptedKey: string;
iv: string;
ciphertext: string;
signature: string;
}
class SecureMessaging {
constructor(
private publicKey: string,
private privateKey: string
) {}
encryptMessage(message: string, recipientPublicKey: string): EncryptedMessage {
// Generate symmetric key
const symmetricKey = randomBytes(32);
const iv = randomBytes(12);
// Encrypt message
const cipher = createCipheriv('aes-256-gcm', symmetricKey, iv);
let encrypted = cipher.update(message, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Encrypt symmetric key with recipient's public key
const encryptedKey = publicEncrypt(recipientPublicKey, symmetricKey);
// Sign with private key
const sign = createSign('SHA256');
sign.update(encrypted);
sign.end();
const signature = sign.sign(this.privateKey, 'base64');
return {
senderPublicKey: this.publicKey,
encryptedKey: encryptedKey.toString('base64'),
iv: iv.toString('hex'),
ciphertext: encrypted,
signature,
};
}
decryptMessage(message: EncryptedMessage): string {
// Verify signature
const verify = createVerify('SHA256');
verify.update(message.ciphertext);
verify.end();
if (!verify.verify(message.senderPublicKey, message.signature, 'base64')) {
throw new Error('Invalid signature');
}
// Decrypt symmetric key
const symmetricKey = privateDecrypt(this.privateKey, Buffer.from(message.encryptedKey, 'base64'));
// Decrypt message
const decipher = createDecipheriv('aes-256-gcm', symmetricKey, Buffer.from(message.iv, 'hex'));
let decrypted = decipher.update(message.ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}Key Management Best Practices
Secure Key Storage
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
class KeyManager {
private client: SecretsManager;
private cache: Map<string, string> = new Map();
constructor() {
this.client = new SecretsManager({});
}
async getPrivateKey(keyId: string): Promise<string> {
// Check cache first
if (this.cache.has(keyId)) {
return this.cache.get(keyId)!;
}
// Fetch from AWS Secrets Manager
const secret = await this.client.getSecretValue({
SecretId: keyId,
});
const privateKey = secret.SecretString!;
this.cache.set(keyId, privateKey);
return privateKey;
}
async getPublicKey(keyId: string): Promise<string> {
// Public keys can be stored in a less secure location
// or fetched from a public directory
return await fetchPublicKey(keyId);
}
clearCache() {
this.cache.clear();
}
}Key Rotation
`` ypescript class KeyRotation { private activeKeyId: string = 'key-2024-01'; private previousKeys: string[] = [];
async getCurrentKey(): Promise<string> { return await this.keyManager.getPrivateKey(this.activeKeyId); }
async rotateKey() { // Generate new key pair const { publicKey, privateKey } = await generateKeyPair();
// Store new key await this.keyManager.storePrivateKey('key-2024-02', privateKey);
// Update active key this.previousKeys.push(this.activeKeyId); this.activeKeyId = 'key-2024-02'; }
async decryptWithAnyKey(encrypted: any): Promise<string> { // Try current key first try { return decrypt(encrypted, await this.getCurrentKey()); } catch (error) { // Try previous keys for (const keyId of this.previousKeys) { try { return decrypt(encrypted, await this.keyManager.getPrivateKey(keyId)); } catch (error) { continue; } } throw new Error('Unable to decrypt with any available key'); } } }
## Performance Considerations
### Key Size Comparison
| Algorithm | Key Size | Security Level | Performance |
|-----------|----------|----------------|-------------|
| RSA-2048 | 2048 bits | ~112 bits | Slow |
| RSA-4096 | 4096 bits | ~140 bits | Very Slow |
| ECC P-256 | 256 bits | ~128 bits | Fast |
| ECC P-384 | 384 bits | ~192 bits | Medium |
### When to Use Each
// Use RSA for: Key exchange, signatures const encryptedKey = publicEncrypt(publicKey, symmetricKey);
// Use ECC for: Better performance, smaller keys const signature = signWithECDSA(data, ecPrivateKey);
// Use Hybrid for: Large data encryption const encrypted = hybridEncrypt(largeData, publicKey);
## Common Pitfalls
### 1. Reusing Key Pairs
// ❌ BAD: Using same key pair for everything const universalKey = generateKeyPair();
// ✅ GOOD: Separate keys for different purposes const encryptionKey = generateKeyPair(); const signatureKey = generateKeyPair(); const authKey = generateKeyPair();
### 2. Insufficient Key Size
// ❌ BAD: 1024-bit RSA is deprecated const weakKey = generateKeyPairSync('rsa', { modulusLength: 1024 });
// ✅ GOOD: Use at least 2048 bits const strongKey = generateKeyPairSync('rsa', { modulusLength: 2048 });
### 3. Not Validating Public Keys
Always validate public keys before using them.
function validatePublicKey(key: string): boolean { try { // Try to use the key const test = 'test'; publicEncrypt(key, Buffer.from(test)); return true; } catch (error) { return false; } }
## Conclusion
Asymmetric encryption is a powerful tool for secure communication, digital signatures, and key exchange. By understanding the strengths and limitations of RSA, ECC, and hybrid approaches, you can build secure systems that protect data while maintaining good performance.
Remember to follow best practices: use appropriate key sizes, implement proper key management, and choose the right algorithm for your use case. Combined with symmetric encryption for bulk data, asymmetric cryptography forms the foundation of modern secure systems.