Skip to content
StackPractices
intermediate By Mathias Paulenko

Cryptography Basics — Encryption, Hashing, and Signing

A developer's guide to cryptography: symmetric and asymmetric encryption, hashing, digital signatures, and key management with practical code examples.

Topics: security

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Overview

Cryptography is the foundation of digital security. Whether you are storing passwords, transmitting data over TLS, or signing API requests, you are using cryptography. Understanding the primitives — encryption, hashing, and signing — and when to use each prevents a class of vulnerabilities that no framework can protect against. This guide walks through the essential concepts every developer needs without requiring a mathematics degree.

When to Use

  • For alternatives, see Encryption at Rest: AES-256, KMS, Envelope Encryption.

  • You need to protect data at rest or in transit

  • You are implementing authentication or authorization

  • You need to verify the integrity or origin of data

  • You are choosing between cryptographic libraries or algorithms

Symmetric Encryption

The same key encrypts and decrypts. Fast and suitable for bulk data.

AES (Advanced Encryption Standard)

from cryptography.fernet import Fernet

# Generate a key
key = Fernet.generate_key()
cipher = Fernet(key)

# Encrypt
token = cipher.encrypt(b"sensitive data")

# Decrypt
data = cipher.decrypt(token)
ModeUse CaseSecurity
AES-GCMMost applicationsAuthenticated encryption
AES-CBCLegacy compatibilityNeeds HMAC for integrity
AES-CTRStreaming dataNeeds careful IV handling

Key Management

  • Never hardcode keys; use a key management service (KMS)
  • Rotate keys periodically (annually or on suspected compromise)
  • Separate keys by environment and purpose

Asymmetric Encryption

Two keys: a public key encrypts, a private key decrypts. Used for key exchange and digital signatures.

RSA

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes

# Generate key pair
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()

# Encrypt with public key
encrypted = public_key.encrypt(
    b"secret message",
    padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)

# Decrypt with private key
decrypted = private_key.decrypt(
    encrypted,
    padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)

Elliptic Curve (ECDH/ECDSA)

Faster and smaller keys than RSA at equivalent security.

from cryptography.hazmat.primitives.asymmetric import ec

private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()

Hashing

One-way functions that produce a fixed-size fingerprint. Used for passwords, data integrity, and checksums.

Secure Hashing Algorithms

AlgorithmOutput SizeStatus
SHA-256256 bitsRecommended
SHA-3VariableRecommended
BLAKE3256 bitsFast, modern
MD5128 bitsBroken, do not use
SHA-1160 bitsBroken, do not use
import hashlib

# SHA-256
digest = hashlib.sha256(b"data").hexdigest()

# For passwords: use Argon2id, bcrypt, or scrypt — not SHA-256

Password Hashing

import bcrypt

hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))

Digital Signatures

Prove authenticity and integrity of a message.

from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import hashes, serialization

# Sign
signature = private_key.sign(
    message,
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256()
)

# Verify
public_key.verify(
    signature,
    message,
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256()
)

Transport Layer Security (TLS)

TLS uses asymmetric encryption for key exchange, then symmetric encryption for the session.

Client                           Server
  │                                 │
  │ ─────── Client Hello ───────▶ │
  │ ◀────── Server Hello ─────── │
  │ ◀──── Certificate + Key Exchange
  │ ───── Client Key Exchange ───▶ │
  │ ───── [Encrypted Handshake]──▶│
  │                                 │
  │ ←──── Symmetric Session ─────▶ │

What works:

  • Use TLS 1.3; disable TLS 1.0 and 1.1
  • Enable HSTS (HTTP Strict Transport Security)
  • Use certificate pinning for mobile apps
  • Monitor certificate expiry (30, 14, 7 days before)

Common Mistakes

  • Rolling your own crypto — use well-vetted libraries (libsodium, OpenSSL, Bouncy Castle)
  • Using ECB mode — patterns in plaintext leak through ciphertext
  • Reusing IVs/nonces — destroys confidentiality in stream modes
  • Storing keys with data — keys should be in a separate trust boundary
  • Ignoring side-channel attacks — timing and power analysis can leak keys

Troubleshooting

  • Authentication bypass in tests: ensure test users cannot reach production endpoints. Use separate credentials and environments for CI.
  • False positives in scanning tools: tune rules against the risk profile. Distinguish between reachable vulnerabilities and theoretical issues.
  • Secrets appear in logs: configure log filters to redact tokens, passwords, and keys. Audit log sinks for sensitive patterns.
  • CSP breaks legitimate functionality: use report-only mode first, then enforce. Iterate on allowed sources based on real violations.
  • Incident response stalls: run tabletop exercises. Document escalation paths, evidence collection steps, and communication templates in advance.

FAQ

What is the difference between encryption and hashing? Encryption is reversible (two-way); hashing is one-way. You encrypt data you need to read later; you hash data you only need to compare (passwords).

Should I use AES-256 or AES-128? AES-128 is secure for most purposes. AES-256 adds a margin of safety against quantum computing advances but is slightly slower.

What is authenticated encryption? Authenticated encryption (like AES-GCM) provides both confidentiality and integrity. Without it, attackers can tamper with ciphertext.

How do I get started with this in an existing project?

Start with a small, isolated part of your codebase. Apply the concepts from this guide to one module or service. Measure the impact, then expand to other areas.

What tools do I need?

The tools mentioned throughout this guide are listed in each section. Most are open-source and widely adopted. Check the related resources for setup instructions.

How do I measure success after implementing this?

Define clear metrics before starting: performance benchmarks, error rates, or maintainability indicators. Compare before and after. Iterate based on the data, not on assumptions.

Advanced Topics

Scenario: Data Encryption in a Fintech App

System: Fintech app, handles transactions and PII
Requirements: Encryption in transit + at rest + field-level

Encryption layers:
  | Layer | Technology | Purpose |
  |-------|-----------|---------|
  | Transit | TLS 1.3 | Encrypt network communication |
  | At rest (DB) | AES-256-GCM | Encrypt disk/volume |
  | Field-level | AES-256-GCM + envelope | Encrypt sensitive fields |
  | Backups | AES-256 + KMS | Encrypt backups |
  | Secrets | KMS + rotation | Rotate keys automatically |

Envelope encryption (field-level):
  1. Generate random Data Encryption Key (DEK) (256 bits)
  2. Encrypt sensitive field with DEK (AES-256-GCM)
  3. Encrypt DEK with Key Encryption Key (KEK) via KMS
  4. Store: ciphertext + encrypted DEK
  5. To decrypt: ask KMS to decrypt DEK, then decrypt field

```javascript
// Envelope encryption with AWS KMS (Node.js)
const { KMSClient, EncryptCommand, DecryptCommand } = require("@aws-sdk/client-kms");
const crypto = require("crypto");

async function encryptField(plaintext, kmsKeyId) {
  // 1. Generate DEK
  const dek = crypto.randomBytes(32);
  const iv = crypto.randomBytes(12);

  // 2. Encrypt data with DEK (AES-256-GCM)
  const cipher = crypto.createCipheriv("aes-256-gcm", dek, iv);
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
  const authTag = cipher.getAuthTag();

  // 3. Encrypt DEK with KMS (KEK)
  const kms = new KMSClient();
  const response = await kms.send(new EncryptCommand({
    KeyId: kmsKeyId,
    Plaintext: dek,
  }));

  // 4. Store: encrypted DEK + IV + authTag + ciphertext
  return {
    encryptedDek: response.CiphertextBlob.toString("base64"),
    iv: iv.toString("base64"),
    authTag: authTag.toString("base64"),
    ciphertext: ciphertext.toString("base64"),
  };
}

async function decryptField(encrypted) {
  // 1. Decrypt DEK via KMS
  const kms = new KMSClient();
  const response = await kms.send(new DecryptCommand({
    CiphertextBlob: Buffer.from(encrypted.encryptedDek, "base64"),
  }));
  const dek = response.Plaintext;

  // 2. Decrypt data with DEK
  const decipher = crypto.createDecipheriv(
    "aes-256-gcm", dek, Buffer.from(encrypted.iv, "base64")
  );
  decipher.setAuthTag(Buffer.from(encrypted.authTag, "base64"));
  const plaintext = Buffer.concat([
    decipher.update(Buffer.from(encrypted.ciphertext, "base64")),
    decipher.final()
  ]);
  return plaintext.toString("utf8");
}

Lessons:

  • Never use the same key for everything
  • Envelope encryption: DEK for data, KEK for DEK
  • KMS rotates KEKs automatically
  • AES-256-GCM: authenticated encryption (confidentiality + integrity)
  • Never store DEK in plaintext
  • IV must be unique per encryption (never reuse)

### When do I use symmetric vs asymmetric encryption?

Use symmetric (AES) for encrypting data at rest and large volumes: it is fast and secure. Use asymmetric (RSA, ECC) for key exchange, digital signatures, and authentication: it does not require sharing a secret key. In practice, they are combined: asymmetric to exchange DEK, symmetric to encrypt data (envelope encryption).

## Common Production Pitfalls

- Treating the guide as a checklist to complete once rather than a practice to evolve.
- Adopting every recommendation at once instead of starting with one measured change.
- Skipping the maturity assessment and forcing advanced practices on an unprepared team.
- Not updating runbooks and on-call expectations as new practices are introduced.
- Ignoring real incident data when prioritizing which parts of the guide to apply first.
- Failing to assign an owner who reviews decisions quarterly.
- Copying examples without adapting them to the team's actual tooling and constraints.
- Forgetting to measure outcomes before adding the next improvement.