Cryptography fundamentals including symmetric/asymmetric encryption, hashing, signing, key exchange, and common algorithms
Scope: Symmetric/asymmetric encryption, hashing, digital signatures, key exchange, common algorithms Lines: ~380 Last Updated: 2025-10-27 Format Version: 1.0 (Atomic)
Activate this skill when:
1. Encryption/Decryption → Confidentiality
2. Hashing → Integrity
3. Digital Signatures → Authentication + Integrity
4. Key Exchange → Secure channel establishment
Same key for encryption and decryption:
Plaintext → [Encrypt with Key] → Ciphertext
Ciphertext → [Decrypt with Key] → Plaintext
Characteristics:
AES (Advanced Encryption Standard):
Key sizes: 128, 192, 256 bits
Block size: 128 bits
Modes: GCM (recommended), CBC, CTR
Status: Industry standard, secure
ChaCha20:
Key size: 256 bits
Stream cipher
Often paired with Poly1305 (MAC)
Status: Modern, secure, fast on mobile
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
# Generate key
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
# Encrypt
nonce = os.urandom(12) # 96 bits for GCM
plaintext = b"Secret message"
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
# Decrypt
recovered = aesgcm.decrypt(nonce, ciphertext, None)
assert recovered == plaintext
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"io"
)
func encrypt(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Prepend nonce to ciphertext
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
func decrypt(key, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
return plaintext, err
}
Different keys for encryption and decryption:
Public Key (share freely) → Encrypt
Private Key (keep secret) → Decrypt
Anyone can encrypt with public key
Only holder of private key can decrypt
Characteristics:
RSA:
Key sizes: 2048, 3072, 4096 bits
Use cases: Key exchange, signatures
Status: Widely supported, slower
ECDSA (Elliptic Curve):
Key sizes: 256, 384, 521 bits
Use cases: Signatures (TLS, Bitcoin)
Status: Modern, faster, smaller keys
Ed25519:
Key size: 256 bits
Use cases: Signatures (SSH, crypto)
Status: Modern, fast, secure
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
plaintext = b"Secret message"
ciphertext = public_key.encrypt(
plaintext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Decrypt with private key
recovered = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
assert recovered == plaintext
use ed25519_dalek::{Keypair, Signature, Signer, Verifier};
use rand::rngs::OsRng;
fn main() {
let mut csprng = OsRng{};
let keypair: Keypair = Keypair::generate(&mut csprng);
// Sign
let message = b"Important message";
let signature: Signature = keypair.sign(message);
// Verify
assert!(keypair.public.verify(message, &signature).is_ok());
}
One-way function:
Input (any size) → [Hash Function] → Fixed-size output
Properties:
- Deterministic (same input → same output)
- Fast to compute
- Infeasible to reverse
- Collision-resistant
SHA-256 (Secure Hash Algorithm):
Output: 256 bits (32 bytes)
Use cases: Integrity, signatures, Bitcoin
Status: Industry standard
SHA-3:
Output: 224, 256, 384, 512 bits
Use cases: Same as SHA-256
Status: Modern alternative
BLAKE2:
Output: Configurable
Use cases: General hashing
Status: Faster than SHA-2, secure
import hashlib
# SHA-256
data = b"Data to hash"
hash_digest = hashlib.sha256(data).hexdigest()
print(f"SHA-256: {hash_digest}")
# BLAKE2b (faster)
hash_digest = hashlib.blake2b(data).hexdigest()
print(f"BLAKE2: {hash_digest}")
# File hashing
def hash_file(filename):
sha256 = hashlib.sha256()
with open(filename, 'rb') as f:
while chunk := f.read(8192):
sha256.update(chunk)
return sha256.hexdigest()
Different from general hashing:
Requirements:
- Slow (prevent brute force)
- Salt (prevent rainbow tables)
- Memory-hard (resist GPU attacks)
bcrypt:
import bcrypt
# Hash password
password = b"user_password"
salt = bcrypt.gensalt(rounds=12) # Cost factor
hashed = bcrypt.hashpw(password, salt)
# Verify password
if bcrypt.checkpw(password, hashed):
print("Password correct")
Argon2 (modern, recommended):
from argon2 import PasswordHasher
ph = PasswordHasher()
# Hash password
hashed = ph.hash("user_password")
# Verify password
try:
ph.verify(hashed, "user_password")
print("Password correct")
except:
print("Password incorrect")
scrypt:
import hashlib
password = b"user_password"
salt = os.urandom(16)
# Hash with scrypt (memory-hard)
key = hashlib.scrypt(
password,
salt=salt,
n=2**14, # CPU/memory cost
r=8, # Block size
p=1, # Parallelization
dklen=32 # Key length
)
Authenticate + Integrity:
1. Hash the message
2. Encrypt hash with private key → Signature
3. Anyone can verify with public key
Proves:
- Message came from private key holder
- Message wasn't modified
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()
# Sign message
message = b"Important document"
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# Verify signature
try:
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("Signature valid")
except:
print("Signature invalid")
Agree on shared secret over insecure channel:
Alice Bob
private_a private_b
↓ ↓
public_a → -------→ public_b ← public_b
+ +
public_b public_a
↓ ↓
shared_secret shared_secret
Python Example:
from cryptography.hazmat.primitives.asymmetric import dh
from cryptography.hazmat.primitives import serialization
# Generate parameters (can be reused)
parameters = dh.generate_parameters(generator=2, key_size=2048)
# Alice generates keypair
alice_private = parameters.generate_private_key()
alice_public = alice_private.public_key()
# Bob generates keypair
bob_private = parameters.generate_private_key()
bob_public = bob_private.public_key()
# Both derive same shared secret
alice_shared = alice_private.exchange(bob_public)
bob_shared = bob_private.exchange(alice_public)
assert alice_shared == bob_shared
Hash-based MAC:
import hmac
import hashlib
key = b"shared_secret"
message = b"Message to authenticate"
# Create MAC
mac = hmac.new(key, message, hashlib.sha256).digest()
# Verify MAC
def verify_mac(message, mac, key):
expected_mac = hmac.new(key, message, hashlib.sha256).digest()
return hmac.compare_digest(mac, expected_mac) # Constant-time comparison
Use asymmetric for key exchange, symmetric for data:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
import os
def hybrid_encrypt(public_key, plaintext):
# Generate random symmetric key
symmetric_key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(symmetric_key)
# Encrypt data with symmetric key
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
# Encrypt symmetric key with public key
encrypted_key = public_key.encrypt(
symmetric_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return {
'encrypted_key': encrypted_key,
'nonce': nonce,
'ciphertext': ciphertext
}
def hybrid_decrypt(private_key, encrypted_data):
# Decrypt symmetric key
symmetric_key = private_key.decrypt(
encrypted_data['encrypted_key'],
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Decrypt data
aesgcm = AESGCM(symmetric_key)
plaintext = aesgcm.decrypt(
encrypted_data['nonce'],
encrypted_data['ciphertext'],
None
)
return plaintext
# ✅ Good: AES-GCM (includes authentication)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# ❌ Bad: AES-CBC without MAC (vulnerable)
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
# ✅ Good: Cryptographically secure
import secrets
key = secrets.token_bytes(32)
# ❌ Bad: Predictable
import random
key = random.randbytes(32) # NOT cryptographically secure
# ✅ Good: Use established libraries
from cryptography.fernet import Fernet
# ❌ Bad: Roll your own crypto
def my_custom_encryption(data):
return bytes([b ^ 42 for b in data]) # Insecure!
| Use Case | Algorithm | |----------|-----------| | Bulk encryption | AES-256-GCM | | Key exchange | ECDH (X25519) | | Digital signatures | Ed25519 or ECDSA P-256 | | Password hashing | Argon2id | | General hashing | SHA-256 or BLAKE2 | | Message auth | HMAC-SHA256 |
cryptography-pki-fundamentals - Certificates and PKIcryptography-tls-configuration - TLS implementationcryptography-crypto-best-practices - Security guidelinessecurity-authentication - Auth patternsLast Updated: 2025-10-27