cryptographyaessecurity

Cryptography Fundamentals for Developers

· Cosyslabs

Cryptography provides the mathematical foundations that protect digital communication, data storage, and software systems. As a developer, you don't need to implement cryptographic algorithms from scratch — but you must understand when to apply each primitive, which algorithms are safe, and how they compose into secure systems. Getting cryptography wrong is one of the most dangerous mistakes in software.

Core Security Properties

Every cryptographic system aims to provide one or more of these properties:

  • Confidentiality: only authorized parties can read the data
  • Integrity: data has not been modified in transit
  • Authenticity: the sender is who they claim to be
  • Non-repudiation: a sender cannot deny having sent the message

Hash Functions

A cryptographic hash function takes arbitrary input and produces a fixed-size digest. The same input always produces the same output, but you cannot reverse the process to recover the input.

// WebCrypto API (browser + Node.js 18+)
async function sha256(message) {
  const msgBuffer = new TextEncoder().encode(message);
  const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
}

const hash = await sha256("Hello, World!");
// "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986d"

Algorithm guidance:

  • SHA-256: general data integrity, digital signatures, HMAC
  • SHA-512: higher security margin for sensitive applications
  • Argon2id / bcrypt: password hashing only (use these, not SHA-256, for passwords)
  • MD5 / SHA-1: broken, do not use for security

Symmetric Encryption: AES

Symmetric encryption uses the same key for encryption and decryption. AES (Advanced Encryption Standard) is the standard — use AES-256-GCM which provides both encryption and authentication.

// AES-256-GCM with WebCrypto
async function encrypt(plaintext, key) {
  const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit IV for GCM
  const data = new TextEncoder().encode(plaintext);
  const ciphertext = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    key,
    data
  );
  return { ciphertext, iv };
}

async function decrypt(ciphertext, iv, key) {
  const plaintext = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv },
    key,
    ciphertext
  );
  return new TextDecoder().decode(plaintext);
}

// Generate a key
const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"]
);

Key rules for AES-GCM:

  • Never reuse an IV with the same key — generate a fresh random IV every time
  • The GCM authentication tag protects against tampering
  • Transmit the IV alongside the ciphertext (it is not secret)

Asymmetric Encryption: RSA and EC

Asymmetric cryptography uses a key pair: a public key for encryption (or signature verification) and a private key for decryption (or signing). The private key never leaves its owner.

// Generate RSA-OAEP key pair (for encryption)
const { publicKey, privateKey } = await crypto.subtle.generateKey(
  {
    name: "RSA-OAEP",
    modulusLength: 2048,
    publicExponent: new Uint8Array([1, 0, 1]),
    hash: "SHA-256",
  },
  true,
  ["encrypt", "decrypt"]
);

// Encrypt with public key
const ciphertext = await crypto.subtle.encrypt(
  { name: "RSA-OAEP" },
  publicKey,
  new TextEncoder().encode("secret message")
);

// Decrypt with private key
const plaintext = await crypto.subtle.decrypt(
  { name: "RSA-OAEP" },
  privateKey,
  ciphertext
);

RSA is limited to encrypting small payloads (shorter than the key size minus padding). In practice, use hybrid encryption: encrypt the data with AES, then encrypt the AES key with RSA.

Digital Signatures

Digital signatures prove that a message was created by the holder of a specific private key. They provide authenticity and non-repudiation.

// ECDSA with P-256 curve (common in JWT with ES256)
const { publicKey, privateKey } = await crypto.subtle.generateKey(
  { name: "ECDSA", namedCurve: "P-256" },
  true,
  ["sign", "verify"]
);

const data = new TextEncoder().encode("document to sign");

// Sign with private key
const signature = await crypto.subtle.sign(
  { name: "ECDSA", hash: "SHA-256" },
  privateKey,
  data
);

// Verify with public key
const isValid = await crypto.subtle.verify(
  { name: "ECDSA", hash: "SHA-256" },
  publicKey,
  signature,
  data
);

HMAC: Message Authentication Codes

HMAC combines a hash function with a secret key to create a message authentication code. Unlike signatures, HMAC requires both parties to share the secret key.

// HMAC-SHA256
const key = await crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode("shared-secret-key"),
  { name: "HMAC", hash: "SHA-256" },
  false,
  ["sign", "verify"]
);

const mac = await crypto.subtle.sign(
  "HMAC",
  key,
  new TextEncoder().encode("message to authenticate")
);

// Verify (compare in constant time to prevent timing attacks)
const valid = await crypto.subtle.verify(
  "HMAC",
  key,
  mac,
  new TextEncoder().encode("message to authenticate")
);

HMAC is used in JWTs (HS256), webhook signature verification, and API request signing.

Key Derivation: PBKDF2 and HKDF

Never store passwords as plain hashes. Use key derivation functions designed to be slow (PBKDF2, bcrypt, Argon2) or to derive keys from shared secrets (HKDF).

// PBKDF2: derive a key from a password
async function deriveKey(password, salt) {
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(password),
    "PBKDF2",
    false,
    ["deriveKey"]
  );
  return crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt: salt,           // 16 random bytes, store alongside the hash
      iterations: 310_000,  // OWASP recommended minimum for SHA-256
      hash: "SHA-256",
    },
    keyMaterial,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"]
  );
}

Common Mistakes to Avoid

MistakeCorrect Approach
Using MD5 or SHA-1 for securityUse SHA-256 or SHA-512
Hashing passwords with SHA-256Use Argon2id or bcrypt
Reusing IV/nonce in AES-GCMGenerate random IV per encryption
Rolling your own cipherUse AES-GCM from a trusted library
Storing private keys in codeUse environment variables or key vaults
Using Math.random() for keysUse crypto.getRandomValues()

Tools