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
| Mistake | Correct Approach |
|---|---|
| Using MD5 or SHA-1 for security | Use SHA-256 or SHA-512 |
| Hashing passwords with SHA-256 | Use Argon2id or bcrypt |
| Reusing IV/nonce in AES-GCM | Generate random IV per encryption |
| Rolling your own cipher | Use AES-GCM from a trusted library |
| Storing private keys in code | Use environment variables or key vaults |
Using Math.random() for keys | Use crypto.getRandomValues() |
Tools
- Hash Generator — compute SHA-256, SHA-512, MD5 hashes
- HMAC Generator — generate and verify HMAC signatures
- JWT Decoder — inspect JWT headers, claims, and signatures
- AES Encrypt/Decrypt — AES-256-GCM encryption in the browser