Skip to content

7 Cryptography Concepts Every Developer Should Know

Source: YouTube - Fireship
Tutorial: Fireship.io
Repository: GitHub
Date: October 28, 2021
Duration: 11 minutes


Overview

Cryptography forms the backbone of internet security. This tutorial covers seven essential cryptography concepts that every developer should understand, implemented using Node.js's built-in crypto module. You don't need to understand the underlying mathematics, but knowing these concepts is crucial for building secure applications.


1. Hash

Concept

A hash function takes an input of any length and produces a fixed-length output. The term "hash" comes from culinary roots meaning to "chop and mix" - exactly what the function does to data.

Key Properties

  • Deterministic: Same input always produces the same output
  • Fast to compute: But computationally expensive to reverse
  • Unique: Small probability of collision
  • One-way: Cannot derive the original input from the hash

Use Cases

  • Password storage
  • Data integrity verification
  • Comparing values for equality

Implementation

const { createHash } = require('crypto');

// Create a string hash
function hash(str) {
    return createHash('sha256').update(str).digest('hex');
}

// Compare two hashed passwords
let password = 'hi-mom!';
const hash1 = hash(password);
console.log(hash1);

// Later verification
password = 'hi-mom';
const hash2 = hash(password);
const match = hash1 === hash2;

console.log(match ? '✔️  good password' : '❌  password does not match');

Common Algorithms

  • SHA-256: Secure Hashing Algorithm (256-bit)
  • MD5: Older, less secure (not recommended for security)
  • SHA-512: More secure variant with longer output

2. Salt

Concept

A salt is a random string added to input before hashing. This prevents attackers from using precomputed rainbow tables to crack common passwords.

Why Salt?

Users often choose weak passwords like "password123". Without salt: - Identical passwords produce identical hashes - Attackers can use rainbow tables (precomputed hash databases) - Database compromises expose all users with same password

With salt: - Same password + different salt = different hash - Rainbow tables become useless - Each password needs individual cracking

Implementation

const { scryptSync, randomBytes, timingSafeEqual } = require('crypto');

function signup(email, password) {
    // Generate random salt (16 bytes)
    const salt = randomBytes(16).toString('hex');

    // Hash password with salt using scrypt
    const hashedPassword = scryptSync(password, salt, 64).toString('hex');

    // Store salt:hash format
    const user = { 
        email, 
        password: `${salt}:${hashedPassword}` 
    };

    users.push(user);
    return user;
}

function login(email, password) {
    const user = users.find(v => v.email === email);

    // Split stored password into salt and hash
    const [salt, key] = user.password.split(':');

    // Hash the login attempt with stored salt
    const hashedBuffer = scryptSync(password, salt, 64);

    const keyBuffer = Buffer.from(key, 'hex');

    // Timing-safe comparison to prevent timing attacks
    const match = timingSafeEqual(hashedBuffer, keyBuffer);

    if (match) {
        return 'login success!';
    } else {
        return 'login fail!';
    }
}

// Usage
const users = [];
const user = signup('foo@bar.com', 'pa$$word');
console.log(user);

const result = login('foo@bar.com', 'pa$$word');
console.log(result);

Best Practices

  • Use cryptographically secure random salt generation
  • Store salt alongside the hash (it's not secret)
  • Use timing-safe comparison to prevent timing attacks
  • Consider using bcrypt or argon2 for production

3. HMAC (Hash-based Message Authentication Code)

Concept

HMAC is a keyed hash - essentially a hash with a password. It verifies both the authenticity and the originator of data.

Key Properties

  • Requires a secret key to generate
  • Only someone with the key can create an authentic hash
  • Different keys produce different outputs
  • Verifies data hasn't been tampered with

Use Cases

  • API authentication
  • Message integrity verification
  • JWT signatures
  • Webhook verification

Implementation

const { createHmac } = require('crypto');

const password = 'super-secret!';
const message = '🎃 hello jack';

const hmac = createHmac('sha256', password)
    .update(message)
    .digest('hex');

console.log(hmac);

// To verify: recreate HMAC with same key and compare

Difference from Regular Hash

  • Hash: No key, anyone can verify
  • HMAC: Requires secret key, proves authenticity

4. Symmetric Encryption

Concept

Encryption makes a message confidential while allowing it to be reversed (decrypted) with the proper key. In symmetric encryption, the same key is used for both encryption and decryption.

Key Properties

  • Same input produces different outputs each time (unlike hashes)
  • Can be reversed with the correct key
  • Fast and efficient
  • Requires secure key exchange

Components

  • Key: Secret used for encryption/decryption (32 bytes for AES-256)
  • IV (Initialization Vector): Random value to ensure same plaintext encrypts differently each time (16 bytes)
  • Algorithm: AES-256 is current standard

Implementation

const { createCipheriv, randomBytes, createDecipheriv } = require('crypto');

// Cipher setup
const message = 'i like turtles';
const key = randomBytes(32);  // 256-bit key
const iv = randomBytes(16);   // Initialization vector

// Encrypt
const cipher = createCipheriv('aes256', key, iv);
const encryptedMessage = cipher.update(message, 'utf8', 'hex') + cipher.final('hex');
console.log(`Encrypted: ${encryptedMessage}`);

// Decrypt
const decipher = createDecipheriv('aes256', key, iv);
const decryptedMessage = decipher.update(encryptedMessage, 'hex', 'utf-8') + decipher.final('utf8');
console.log(`Deciphered: ${decryptedMessage}`);

Use Cases

  • File encryption
  • Database encryption
  • Encrypted messaging (when both parties have the key)

Challenge

Key Distribution Problem: How do both parties securely agree on the key over an insecure network? This is solved by asymmetric encryption.


5. Keypairs (Public/Private Keys)

Concept

Instead of sharing a single key, cryptographic keypairs consist of: - Public Key: Can be shared freely with anyone - Private Key: Must be kept secret

These are mathematically related but it's computationally infeasible to derive the private key from the public key.

Key Properties

  • Public key can be distributed openly
  • Private key must remain secret
  • Operations with one key require the other to reverse
  • Based on RSA, ECDSA, or similar algorithms

Implementation

const { generateKeyPairSync } = require('crypto');

const { privateKey, publicKey } = generateKeyPairSync('rsa', {
    modulusLength: 2048, // Key length in bits (2048 or 4096 recommended)
    publicKeyEncoding: {
        type: 'spki',    // Recommended by Node.js docs
        format: 'pem',   // Privacy-Enhanced Mail format (text)
    },
    privateKeyEncoding: {
        type: 'pkcs8',   // Recommended by Node.js docs
        format: 'pem',
    },
});

console.log(publicKey);   // Safe to share
console.log(privateKey);  // Keep secret!

Output Format

  • PEM Format: Base64-encoded text with headers
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
    -----END PUBLIC KEY-----
    

Common Algorithms

  • RSA: Most common, good for encryption and signing
  • ECDSA: Elliptic Curve, smaller keys with same security
  • Ed25519: Modern, fast, secure

6. Asymmetric Encryption

Concept

Uses keypair for encryption: - Encrypt with public key - Decrypt with private key

This solves the key distribution problem of symmetric encryption.

Real-World Usage

HTTPS/TLS: Every time you visit a secure website: 1. Browser retrieves server's public key (SSL certificate) 2. Browser encrypts session data with public key 3. Server decrypts with its private key 4. Secure connection established

Implementation

const { publicEncrypt, privateDecrypt } = require('crypto');
const { publicKey, privateKey } = require('./keypair');

const secretMessage = 'The eagle flies at midnight';

// Encrypt with PUBLIC key
const encryptedData = publicEncrypt(
    publicKey,
    Buffer.from(secretMessage)
);

console.log('Encrypted:', encryptedData.toString('hex'));

// Decrypt with PRIVATE key
const decryptedData = privateDecrypt(
    privateKey,
    encryptedData
);

console.log('Decrypted:', decryptedData.toString('utf-8'));

Key Characteristics

  • Slower than symmetric encryption
  • Used to exchange symmetric keys securely
  • Public key can be distributed without security risk
  • Private key compromise breaks all security

Typical Workflow

  1. Use asymmetric encryption to securely exchange a symmetric key
  2. Use symmetric encryption for actual data (much faster)
  3. This is how TLS/SSL works

7. Signing (Digital Signatures)

Concept

Digital signing proves: - Authenticity: Message came from claimed sender - Integrity: Message hasn't been modified - Non-repudiation: Sender can't deny sending it

Process

  1. Signing: Hash the message, then encrypt hash with private key
  2. Verification: Decrypt signature with public key, compare with message hash

Implementation

const { createSign, createVerify } = require('crypto');
const { publicKey, privateKey } = require('./keypair');

const data = 'this data must be signed';

// === SIGN (by sender) ===
const signer = createSign('rsa-sha256');
signer.update(data);
const signature = signer.sign(privateKey, 'hex');

console.log('Signature:', signature);

// === VERIFY (by recipient) ===
const verifier = createVerify('rsa-sha256');
verifier.update(data);
const isVerified = verifier.verify(publicKey, signature, 'hex');

console.log('Verified:', isVerified);  // true if authentic

Use Cases

  • Code Signing: Verify software hasn't been tampered with
  • SSL Certificates: Prove website identity
  • Git Commits: Verify commit author
  • JWT Tokens: Authenticate API requests
  • Blockchain: Transaction verification

Why It Works

  • Only the private key holder can create valid signatures
  • Anyone with the public key can verify
  • Tampering with the message invalidates the signature

Encryption vs Signing

Operation Encryption Signing
Purpose Confidentiality Authenticity
Encrypt with Public Key Private Key
Decrypt/Verify with Private Key Public Key
Protects against Eavesdropping Tampering/Forgery

Key Takeaways

When to Use Each

  1. Hash: Storing passwords, comparing values, data integrity
  2. Salt: Always use with password hashing
  3. HMAC: API authentication, verifying message source
  4. Symmetric Encryption: Fast encryption when both parties have key
  5. Keypairs: Foundation for asymmetric operations
  6. Asymmetric Encryption: Secure key exchange, HTTPS
  7. Signing: Prove authenticity and integrity

Security Best Practices

  1. Never roll your own crypto - use established libraries
  2. Keep private keys private - never commit to git
  3. Use timing-safe comparisons - prevent timing attacks
  4. Generate strong random values - use cryptographically secure RNG
  5. Keep algorithms updated - avoid deprecated algorithms (MD5, SHA-1)
  6. Use appropriate key lengths - RSA 2048+, AES 256
  7. Salt all password hashes - use unique salt per password

Common Pitfalls

  • ❌ Using hash without salt for passwords
  • ❌ Reusing the same IV for symmetric encryption
  • ❌ Sharing private keys
  • ❌ Using weak algorithms (MD5, DES)
  • ❌ Storing keys in source code
  • ❌ Not validating signatures before trusting data

Additional Resources

  • Node.js Crypto Documentation: https://nodejs.org/api/crypto.html
  • Source Code: https://github.com/fireship-io/node-crypto-examples
  • Further Learning:
  • Try the hacking challenge in src/hack.js
  • Implement authentication system using these concepts
  • Study TLS/SSL handshake process
  • Learn about bcrypt, argon2 for password hashing