Claude Code for Cryptography: Practical Patterns for Developers — Claude Skills 360 Blog
Blog / Security / Claude Code for Cryptography: Practical Patterns for Developers
Security

Claude Code for Cryptography: Practical Patterns for Developers

Published: September 20, 2026
Read time: 9 min read
By: Claude Skills 360

Cryptography is easy to get wrong in subtle ways that create serious vulnerabilities without obvious failures. Using ECB mode instead of GCM. Comparing tokens with === instead of timing-safe equality. Storing encryption keys in the same database as the ciphertext. Claude Code generates cryptographic code that uses battle-tested libraries correctly — applying modern best practices while flagging common pitfalls.

The Core Rule: Don’t Implement Primitives

Should I implement AES encryption from scratch? I understand the algorithm.

No. Use battle-tested libraries: Web Crypto API (browser), node:crypto (Node), ring (Rust), cryptography (Python). Cryptographic implementations are extraordinarily sensitive to timing attacks, side channels, and specification edge cases. Write application code using these libraries; let them handle the primitives.

Password Hashing

We store user passwords. How should we hash them?
Our team is debating bcrypt vs Argon2.
// src/lib/passwords.ts — Argon2id is the current best practice
import argon2 from 'argon2';

// Argon2id parameters (OWASP recommended for 2024+)
const HASH_OPTIONS = {
  type: argon2.argon2id,  // 'id' variant is resistant to both timing and GPU attacks
  memoryCost: 65536,      // 64 MB — increase as hardware improves
  timeCost: 3,            // 3 iterations
  parallelism: 4,         // 4 threads
};

export async function hashPassword(password: string): Promise<string> {
  // argon2 automatically generates a unique salt and embeds it in the hash string
  return argon2.hash(password, HASH_OPTIONS);
}

export async function verifyPassword(password: string, hash: string): Promise<boolean> {
  try {
    return await argon2.verify(hash, password);
  } catch {
    return false; // Invalid hash format
  }
}

// Upgrade bcrypt hashes to Argon2 on next login
export async function isUpgradeNeeded(hash: string): Promise<boolean> {
  return hash.startsWith('$2') || hash.startsWith('$2b'); // bcrypt prefix
}

export async function rehashOnLogin(
  password: string,
  oldHash: string,
): Promise<string | null> {
  // Verify with old algorithm first
  const bcrypt = await import('bcryptjs');
  if (await bcrypt.compare(password, oldHash)) {
    // Verified — rehash with Argon2id
    return hashPassword(password);
  }
  return null; // Wrong password
}

Why Argon2id over bcrypt:

  • Argon2 uses configurable memory — makes GPU cracking much more expensive
  • bcrypt is capped at 72 bytes (silently truncates longer passwords)
  • Argon2 is the PHC winner and OWASP recommendation since 2022

Symmetric Encryption (AES-GCM)

We need to encrypt sensitive user data at rest.
Patients' health records stored in our database.
// src/lib/encryption.ts — AES-256-GCM with authenticated encryption
// AES-GCM provides both confidentiality AND integrity (AEAD)

const ALGORITHM = 'AES-GCM';
const KEY_LENGTH = 256; // bits
const IV_LENGTH = 12;   // bytes — 96 bits is optimal for GCM

// Generate or load the data encryption key (DEK)
async function generateDEK(): Promise<CryptoKey> {
  return crypto.subtle.generateKey(
    { name: ALGORITHM, length: KEY_LENGTH },
    true,  // extractable = true (needed for key export/storage)
    ['encrypt', 'decrypt'],
  );
}

// Export key as raw bytes for storage (e.g., in AWS KMS or secrets manager)
async function exportKey(key: CryptoKey): Promise<Uint8Array> {
  const raw = await crypto.subtle.exportKey('raw', key);
  return new Uint8Array(raw);
}

// Import key from raw bytes
async function importKey(raw: Uint8Array): Promise<CryptoKey> {
  return crypto.subtle.importKey(
    'raw', raw,
    { name: ALGORITHM, length: KEY_LENGTH },
    false, // extractable = false for imported keys (security: can't be re-exported)
    ['encrypt', 'decrypt'],
  );
}

export async function encrypt(
  plaintext: string,
  key: CryptoKey,
): Promise<{ ciphertext: string; iv: string }> {
  // Fresh random IV for every encryption — NEVER reuse an IV with the same key
  const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
  
  const encoded = new TextEncoder().encode(plaintext);
  
  const ciphertextBuffer = await crypto.subtle.encrypt(
    { name: ALGORITHM, iv },
    key,
    encoded,
  );
  
  // Encode as base64 for storage
  return {
    ciphertext: Buffer.from(ciphertextBuffer).toString('base64'),
    iv: Buffer.from(iv).toString('base64'),
  };
}

export async function decrypt(
  ciphertext: string,
  iv: string,
  key: CryptoKey,
): Promise<string> {
  const ciphertextBuffer = Buffer.from(ciphertext, 'base64');
  const ivBuffer = Buffer.from(iv, 'base64');
  
  // GCM automatically validates the authentication tag — throws if tampered
  const decryptedBuffer = await crypto.subtle.decrypt(
    { name: ALGORITHM, iv: ivBuffer },
    key,
    ciphertextBuffer,
  );
  
  return new TextDecoder().decode(decryptedBuffer);
}

Envelope Encryption with AWS KMS

The encryption key can't be stored alongside the data it protects.
How do we manage keys properly?

Envelope encryption: data is encrypted with a Data Encryption Key (DEK), and the DEK is encrypted with a Key Encryption Key (KEK) in KMS. Only the encrypted DEK is stored with the data.

// src/lib/envelope-encryption.ts
import { KMSClient, GenerateDataKeyCommand, DecryptCommand } from '@aws-sdk/client-kms';

const kms = new KMSClient({ region: 'us-east-1' });
const KEY_ARN = process.env.KMS_KEY_ARN!;

interface EncryptedRecord {
  ciphertext: string;
  iv: string;
  encryptedDEK: string; // Encrypted data encryption key
}

export async function encryptRecord(plaintext: string): Promise<EncryptedRecord> {
  // Generate a data key — KMS gives us both plaintext and encrypted versions
  const { Plaintext: rawDEK, CiphertextBlob: encryptedDEK } = await kms.send(
    new GenerateDataKeyCommand({
      KeyId: KEY_ARN,
      KeySpec: 'AES_256',
    })
  );
  
  // Use plaintext DEK to encrypt data locally (faster than KMS for large data)
  const dek = await crypto.subtle.importKey(
    'raw', rawDEK!,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt'],
  );
  
  const encrypted = await encrypt(plaintext, dek);
  
  // Store the encrypted DEK alongside the ciphertext
  // The plaintext DEK is never stored — it's gone after this function
  return {
    ...encrypted,
    encryptedDEK: Buffer.from(encryptedDEK!).toString('base64'),
  };
}

export async function decryptRecord(record: EncryptedRecord): Promise<string> {
  // Ask KMS to decrypt the DEK
  const { Plaintext: rawDEK } = await kms.send(
    new DecryptCommand({
      KeyId: KEY_ARN,
      CiphertextBlob: Buffer.from(record.encryptedDEK, 'base64'),
    })
  );
  
  const dek = await crypto.subtle.importKey(
    'raw', rawDEK!,
    { name: 'AES-GCM', length: 256 },
    false,
    ['decrypt'],
  );
  
  return decrypt(record.ciphertext, record.iv, dek);
}

Secure Token Comparison

We compare webhook signatures with ===. Is that safe?

No. String comparison in most languages short-circuits — it stops at the first differing character. This creates a timing oracle: an attacker can guess a token character by character by measuring response times.

// ❌ Vulnerable to timing attack
if (providedSignature === expectedSignature) { ... }

// ✅ Constant-time comparison — always compares all bytes
import { timingSafeEqual } from 'node:crypto';

function verifyWebhookSignature(payload: Buffer, signature: string, secret: string): boolean {
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  
  const providedBuf = Buffer.from(signature, 'hex');
  const expectedBuf = Buffer.from(expectedSig, 'hex');
  
  // Lengths must match — if not, short-circuit is unavoidable, 
  // but at least we don't leak character position
  if (providedBuf.length !== expectedBuf.length) return false;
  
  return timingSafeEqual(providedBuf, expectedBuf);
}

Key Rotation

Our encryption keys are 3 years old. 
How do we rotate them without downtime?
// Key versioning: every DEK tracks which KEK version encrypted it
interface EncryptedRecord {
  ciphertext: string;
  iv: string;
  encryptedDEK: string;
  keyVersion: string; // e.g., "v2026-09"
}

// On rotation: old keys still available for decrypt, only new for encrypt
const KEY_ARNS: Record<string, string> = {
  'v2024-01': 'arn:aws:kms:us-east-1:123:key/old-key-id',
  'v2026-09': 'arn:aws:kms:us-east-1:123:key/new-key-id', // current
};

const CURRENT_KEY_VERSION = 'v2026-09';

// Decrypt uses the version from the record (works with any historical key)
async function decryptWithVersion(record: EncryptedRecord): Promise<string> {
  const keyArn = KEY_ARNS[record.keyVersion];
  if (!keyArn) throw new Error(`Unknown key version: ${record.keyVersion}`);
  // ... decrypt using the versioned key
}

// Background job: re-encrypt old records with new key
async function reEncryptOldRecords(batchSize = 100): Promise<void> {
  const oldRecords = await db('health_records')
    .where('key_version', '!=', CURRENT_KEY_VERSION)
    .limit(batchSize);
  
  for (const record of oldRecords) {
    const plaintext = await decryptWithVersion(record);
    const reEncrypted = await encryptRecord(plaintext); // Uses current key
    await db('health_records').where('id', record.id).update(reEncrypted);
  }
}

For applying these cryptographic patterns in security testing to verify your implementation is correct, see the security testing guide. For storing secrets (encryption keys, API keys) in production, the Kubernetes guide covers External Secrets Operator patterns. The Claude Skills 360 bundle includes security skill sets with cryptographic code templates and key management patterns. Start with the free tier to try security implementation patterns.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free