Skip to content
All essays
ArchitectureMarch 27, 202418 min

Asymmetric Encryption: Public Key Cryptography Deep Dive

Master asymmetric encryption, digital signatures, and key exchange protocols for secure communications

Ü
Ümit Uz
Mobile & Full Stack Developer

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:

  1. 1Confidentiality: Anyone can encrypt with your public key, only you can decrypt
  2. 2Digital Signatures: You can sign with your private key, anyone can verify with your public key
  3. 3Key Exchange: Securely establish symmetric keys for bulk encryption
typescript
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

typescript
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

typescript
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:

  1. 1Size Limitation: Can only encrypt data smaller than the key size
  2. 2Slow Performance: Much slower than symmetric encryption
  3. 3Data Size: For 2048-bit RSA, maximum plaintext is ~245 bytes
typescript
// ❌ 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.

typescript
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

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

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

typescript
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.

typescript
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.

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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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.
Enjoyed this essay? Share it.

Related essays

Next essay
Encryption Basics: Protecting Data in the Digital Age