Cryptography Implementation Guide
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/security/references/appsec/cryptography-implementation.md |
| Description | Not specified |
Source Content
Cryptography Implementation Guide
Practical cryptographic patterns for securing data at rest, in transit, and in use.
Table of Contents
- Cryptographic Primitives
- Symmetric Encryption
- Asymmetric Encryption
- Hashing and Password Storage
- Key Management
- Common Cryptographic Mistakes
Cryptographic Primitives
Algorithm Selection Guide
| Use Case | Recommended Algorithm | Avoid |
|---|---|---|
| Symmetric encryption | AES-256-GCM, ChaCha20-Poly1305 | DES, 3DES, AES-ECB, RC4 |
| Asymmetric encryption | RSA-OAEP (2048+), ECIES | RSA-PKCS1v1.5 |
| Digital signatures | Ed25519, ECDSA P-256, RSA-PSS | RSA-PKCS1v1.5 |
| Key exchange | X25519, ECDH P-256 | RSA key transport |
| Password hashing | Argon2id, bcrypt, scrypt | MD5, SHA-1, plain SHA-256 |
| Message authentication | HMAC-SHA256, Poly1305 | MD5, SHA-1 |
| Random generation | OS CSPRNG | Math.random(), time-based |
Security Strength Comparison
| Key Size | Security Level | Equivalent Symmetric |
|---|---|---|
| RSA 2048 | 112 bits | AES-128 |
| RSA 3072 | 128 bits | AES-128 |
| RSA 4096 | 152 bits | AES-192 |
| ECDSA P-256 | 128 bits | AES-128 |
| ECDSA P-384 | 192 bits | AES-192 |
| Ed25519 | 128 bits | AES-128 |
Symmetric Encryption
AES-256-GCM Implementation
from cryptography.hazmat.primitives.ciphers.aead import AESGCMimport os
class AESGCMEncryption: """ AES-256-GCM authenticated encryption.
Provides both confidentiality and integrity. GCM mode prevents tampering with authentication tag. """
def __init__(self, key: bytes = None): if key is None: key = AESGCM.generate_key(bit_length=256) if len(key) != 32: raise ValueError("Key must be 32 bytes (256 bits)") self.key = key self.aesgcm = AESGCM(key)
def encrypt(self, plaintext: bytes, associated_data: bytes = None) -> bytes: """ Encrypt with random nonce.
Returns: nonce (12 bytes) + ciphertext + tag (16 bytes) """ nonce = os.urandom(12) # 96-bit nonce for GCM ciphertext = self.aesgcm.encrypt(nonce, plaintext, associated_data) return nonce + ciphertext
def decrypt(self, ciphertext: bytes, associated_data: bytes = None) -> bytes: """ Decrypt and verify authentication tag.
Raises InvalidTag if tampered. """ nonce = ciphertext[:12] actual_ciphertext = ciphertext[12:] return self.aesgcm.decrypt(nonce, actual_ciphertext, associated_data)
# Usageencryptor = AESGCMEncryption()plaintext = b"Sensitive data to encrypt"aad = b"user_id:12345" # Authenticated but not encrypted
ciphertext = encryptor.encrypt(plaintext, associated_data=aad)decrypted = encryptor.decrypt(ciphertext, associated_data=aad)ChaCha20-Poly1305 Implementation
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305import os
class ChaChaEncryption: """ ChaCha20-Poly1305 authenticated encryption.
Faster than AES on systems without hardware AES support. Resistant to timing attacks (constant-time implementation). """
def __init__(self, key: bytes = None): if key is None: key = ChaCha20Poly1305.generate_key() self.key = key self.chacha = ChaCha20Poly1305(key)
def encrypt(self, plaintext: bytes, associated_data: bytes = None) -> bytes: """Encrypt with random 96-bit nonce.""" nonce = os.urandom(12) ciphertext = self.chacha.encrypt(nonce, plaintext, associated_data) return nonce + ciphertext
def decrypt(self, ciphertext: bytes, associated_data: bytes = None) -> bytes: """Decrypt and verify Poly1305 authentication tag.""" nonce = ciphertext[:12] actual_ciphertext = ciphertext[12:] return self.chacha.decrypt(nonce, actual_ciphertext, associated_data)Envelope Encryption Pattern
"""Envelope Encryption: Encrypt data with a Data Encryption Key (DEK),then encrypt DEK with a Key Encryption Key (KEK).
Benefits:- KEK can be rotated without re-encrypting data- DEK can be stored alongside encrypted data- Enables per-record encryption with different DEKs"""
from cryptography.hazmat.primitives.ciphers.aead import AESGCMfrom cryptography.hazmat.primitives import serializationfrom cryptography.hazmat.primitives.asymmetric import paddingfrom cryptography.hazmat.primitives import hashesimport osimport jsonimport base64
class EnvelopeEncryption: def __init__(self, kek_public_key, kek_private_key=None): self.kek_public = kek_public_key self.kek_private = kek_private_key
def encrypt(self, plaintext: bytes) -> dict: """ 1. Generate random DEK 2. Encrypt plaintext with DEK 3. Encrypt DEK with KEK 4. Return encrypted DEK + encrypted data """ # Generate Data Encryption Key dek = AESGCM.generate_key(bit_length=256) aesgcm = AESGCM(dek)
# Encrypt data with DEK nonce = os.urandom(12) encrypted_data = aesgcm.encrypt(nonce, plaintext, None)
# Encrypt DEK with KEK (RSA-OAEP) encrypted_dek = self.kek_public.encrypt( dek, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) )
return { 'encrypted_dek': base64.b64encode(encrypted_dek).decode(), 'nonce': base64.b64encode(nonce).decode(), 'ciphertext': base64.b64encode(encrypted_data).decode() }
def decrypt(self, envelope: dict) -> bytes: """ 1. Decrypt DEK with KEK 2. Decrypt data with DEK """ if self.kek_private is None: raise ValueError("Private key required for decryption")
# Decrypt DEK encrypted_dek = base64.b64decode(envelope['encrypted_dek']) dek = self.kek_private.decrypt( encrypted_dek, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) )
# Decrypt data aesgcm = AESGCM(dek) nonce = base64.b64decode(envelope['nonce']) ciphertext = base64.b64decode(envelope['ciphertext'])
return aesgcm.decrypt(nonce, ciphertext, None)Asymmetric Encryption
RSA Key Generation and Usage
from cryptography.hazmat.primitives.asymmetric import rsa, paddingfrom cryptography.hazmat.primitives import hashes, serialization
def generate_rsa_keypair(key_size=4096): """Generate RSA key pair for encryption/signing.""" private_key = rsa.generate_private_key( public_exponent=65537, key_size=key_size ) public_key = private_key.public_key()
return private_key, public_key
def serialize_keys(private_key, public_key, password=None): """Serialize keys for storage.""" # Private key (encrypted with password) if password: encryption = serialization.BestAvailableEncryption(password.encode()) else: encryption = serialization.NoEncryption()
private_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=encryption )
# Public key public_pem = public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo )
return private_pem, public_pem
def rsa_encrypt(public_key, plaintext: bytes) -> bytes: """RSA-OAEP encryption (for small data like keys).""" return public_key.encrypt( plaintext, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) )
def rsa_decrypt(private_key, ciphertext: bytes) -> bytes: """RSA-OAEP decryption.""" return private_key.decrypt( ciphertext, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) )Digital Signatures (Ed25519)
from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, Ed25519PublicKey)
class Ed25519Signer: """ Ed25519 digital signatures.
Fast, secure, and deterministic. 256-bit keys provide 128-bit security. """
def __init__(self, private_key=None): if private_key is None: private_key = Ed25519PrivateKey.generate() self.private_key = private_key self.public_key = private_key.public_key()
def sign(self, message: bytes) -> bytes: """Create digital signature.""" return self.private_key.sign(message)
def verify(self, message: bytes, signature: bytes) -> bool: """Verify digital signature.""" try: self.public_key.verify(signature, message) return True except Exception: return False
def get_public_key_bytes(self) -> bytes: """Export public key for verification.""" return self.public_key.public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw )
# Usage for message signingsigner = Ed25519Signer()message = b"Important document content"signature = signer.sign(message)
# Verification (can be done with public key only)is_valid = signer.verify(message, signature)ECDH Key Exchange
from cryptography.hazmat.primitives.asymmetric import x25519from cryptography.hazmat.primitives.kdf.hkdf import HKDFfrom cryptography.hazmat.primitives import hashes
class X25519KeyExchange: """ X25519 Diffie-Hellman key exchange.
Used to establish shared secrets over insecure channels. """
def __init__(self): self.private_key = x25519.X25519PrivateKey.generate() self.public_key = self.private_key.public_key()
def get_public_key_bytes(self) -> bytes: """Get public key to send to peer.""" return self.public_key.public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw )
def derive_shared_key(self, peer_public_key_bytes: bytes, info: bytes = b"") -> bytes: """ Derive shared encryption key from peer's public key.
Uses HKDF to derive a proper encryption key. """ peer_public_key = x25519.X25519PublicKey.from_public_bytes( peer_public_key_bytes )
shared_secret = self.private_key.exchange(peer_public_key)
# Derive encryption key using HKDF derived_key = HKDF( algorithm=hashes.SHA256(), length=32, salt=None, info=info, ).derive(shared_secret)
return derived_key
# Key exchange examplealice = X25519KeyExchange()bob = X25519KeyExchange()
# Exchange public keys (can be done over insecure channel)alice_public = alice.get_public_key_bytes()bob_public = bob.get_public_key_bytes()
# Both derive the same shared keyalice_shared = alice.derive_shared_key(bob_public, info=b"session-key")bob_shared = bob.derive_shared_key(alice_public, info=b"session-key")
assert alice_shared == bob_shared # Same key!Hashing and Password Storage
Password Hashing with Argon2
import argon2from argon2 import PasswordHasher, Type
class SecurePasswordHasher: """ Argon2id password hashing.
Argon2id combines resistance to: - GPU attacks (memory-hard) - Side-channel attacks (data-independent) """
def __init__(self): # OWASP recommended parameters self.hasher = PasswordHasher( time_cost=3, # Iterations memory_cost=65536, # 64 MB parallelism=4, # Threads hash_len=32, # Output length type=Type.ID # Argon2id variant )
def hash_password(self, password: str) -> str: """ Hash password for storage.
Returns encoded string with algorithm parameters and salt. """ return self.hasher.hash(password)
def verify_password(self, password: str, hash: str) -> bool: """ Verify password against stored hash.
Automatically handles timing-safe comparison. """ try: self.hasher.verify(hash, password) return True except argon2.exceptions.VerifyMismatchError: return False
def needs_rehash(self, hash: str) -> bool: """Check if hash needs upgrading to current parameters.""" return self.hasher.check_needs_rehash(hash)
# Usagehasher = SecurePasswordHasher()
# During registrationpassword = "user_password_123!"password_hash = hasher.hash_password(password)# Store password_hash in database
# During loginstored_hash = password_hash # From databaseif hasher.verify_password("user_password_123!", stored_hash): print("Login successful")
# Check if hash needs upgrading if hasher.needs_rehash(stored_hash): new_hash = hasher.hash_password(password) # Update stored hashBcrypt Alternative
import bcrypt
class BcryptHasher: """ Bcrypt password hashing.
Well-established, widely supported. Use when Argon2 is not available. """
def __init__(self, rounds=12): self.rounds = rounds
def hash_password(self, password: str) -> str: salt = bcrypt.gensalt(rounds=self.rounds) return bcrypt.hashpw(password.encode(), salt).decode()
def verify_password(self, password: str, hash: str) -> bool: return bcrypt.checkpw(password.encode(), hash.encode())HMAC for Message Authentication
import hmacimport hashlibimport secrets
def create_hmac(key: bytes, message: bytes) -> bytes: """Create HMAC-SHA256 authentication tag.""" return hmac.new(key, message, hashlib.sha256).digest()
def verify_hmac(key: bytes, message: bytes, tag: bytes) -> bool: """Verify HMAC in constant time.""" expected = hmac.new(key, message, hashlib.sha256).digest() return hmac.compare_digest(expected, tag)
# API request signing exampledef sign_api_request(secret_key: bytes, method: str, path: str, body: bytes, timestamp: str) -> str: """Sign API request for authentication.""" message = f"{method}\n{path}\n{timestamp}\n".encode() + body signature = create_hmac(secret_key, message) return signature.hex()Key Management
Key Derivation Functions
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMACfrom cryptography.hazmat.primitives.kdf.scrypt import Scryptfrom cryptography.hazmat.primitives import hashesimport os
def derive_key_pbkdf2(password: str, salt: bytes = None, iterations: int = 600000) -> tuple: """ Derive encryption key from password using PBKDF2.
NIST recommends minimum 600,000 iterations for PBKDF2-SHA256. """ if salt is None: salt = os.urandom(16)
kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt, iterations=iterations )
key = kdf.derive(password.encode()) return key, salt
def derive_key_scrypt(password: str, salt: bytes = None) -> tuple: """ Derive key using scrypt (memory-hard).
More resistant to hardware attacks than PBKDF2. """ if salt is None: salt = os.urandom(16)
kdf = Scrypt( salt=salt, length=32, n=2**17, # CPU/memory cost r=8, # Block size p=1 # Parallelization )
key = kdf.derive(password.encode()) return key, saltKey Rotation Strategy
from datetime import datetime, timedeltafrom typing import Dict, Optionalimport json
class KeyManager: """ Manage encryption key lifecycle.
Supports key rotation without data re-encryption. """
def __init__(self, storage_backend): self.storage = storage_backend
def generate_key(self, key_id: str, algorithm: str = 'AES-256-GCM') -> dict: """Generate and store new encryption key.""" key_material = os.urandom(32)
key_metadata = { 'key_id': key_id, 'algorithm': algorithm, 'created_at': datetime.utcnow().isoformat(), 'expires_at': (datetime.utcnow() + timedelta(days=365)).isoformat(), 'status': 'active' }
self.storage.store_key(key_id, key_material, key_metadata) return key_metadata
def rotate_key(self, old_key_id: str) -> dict: """ Rotate encryption key.
1. Mark old key as 'decrypt-only' 2. Generate new key as 'active' 3. Old key can still decrypt, new key encrypts """ # Mark old key as decrypt-only old_metadata = self.storage.get_key_metadata(old_key_id) old_metadata['status'] = 'decrypt-only' self.storage.update_key_metadata(old_key_id, old_metadata)
# Generate new key new_key_id = f"{old_key_id.rsplit('_', 1)[0]}_{datetime.utcnow().strftime('%Y%m%d')}" return self.generate_key(new_key_id)
def get_encryption_key(self) -> tuple: """Get current active key for encryption.""" return self.storage.get_active_key()
def get_decryption_key(self, key_id: str) -> bytes: """Get specific key for decryption.""" return self.storage.get_key(key_id)Hardware Security Module Integration
# AWS CloudHSM / KMS integration patternimport boto3
class AWSKMSProvider: """ AWS KMS integration for key management.
Keys never leave AWS infrastructure. """
def __init__(self, key_id: str, region: str = 'us-east-1'): self.kms = boto3.client('kms', region_name=region) self.key_id = key_id
def encrypt(self, plaintext: bytes) -> bytes: """Encrypt using KMS master key.""" response = self.kms.encrypt( KeyId=self.key_id, Plaintext=plaintext ) return response['CiphertextBlob']
def decrypt(self, ciphertext: bytes) -> bytes: """Decrypt using KMS master key.""" response = self.kms.decrypt( KeyId=self.key_id, CiphertextBlob=ciphertext ) return response['Plaintext']
def generate_data_key(self) -> tuple: """Generate data encryption key.""" response = self.kms.generate_data_key( KeyId=self.key_id, KeySpec='AES_256' ) return response['Plaintext'], response['CiphertextBlob']Common Cryptographic Mistakes
Mistake 1: Using ECB Mode
# BAD: ECB mode reveals patternsfrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
def bad_ecb_encrypt(key, plaintext): cipher = Cipher(algorithms.AES(key), modes.ECB()) encryptor = cipher.encryptor() return encryptor.update(plaintext) + encryptor.finalize()
# GOOD: Use authenticated encryption (GCM)from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def good_gcm_encrypt(key, plaintext): aesgcm = AESGCM(key) nonce = os.urandom(12) return nonce + aesgcm.encrypt(nonce, plaintext, None)Mistake 2: Reusing Nonces
# BAD: Static noncenonce = b"fixed_nonce!" # NEVER DO THIS
# GOOD: Random nonce per encryptionnonce = os.urandom(12)
# ALSO GOOD: Counter-based nonce (if you can guarantee no repeats)class NonceCounter: def __init__(self): self.counter = 0
def get_nonce(self): self.counter += 1 return self.counter.to_bytes(12, 'big')Mistake 3: Rolling Your Own Crypto
# BAD: Custom "encryption"def bad_encrypt(data, key): return bytes([b ^ k for b, k in zip(data, key * len(data))])
# GOOD: Use established librariesfrom cryptography.fernet import Fernet
def good_encrypt(data, key): f = Fernet(key) return f.encrypt(data)Mistake 4: Weak Random Generation
import randomimport secrets
# BAD: Predictable randomdef bad_generate_token(): return ''.join(random.choices('abcdef0123456789', k=32))
# GOOD: Cryptographically securedef good_generate_token(): return secrets.token_hex(16)Mistake 5: Timing Attacks in Comparison
# BAD: Early exit reveals lengthdef bad_compare(a, b): if len(a) != len(b): return False for x, y in zip(a, b): if x != y: return False return True
# GOOD: Constant-time comparisonimport hmac
def good_compare(a, b): return hmac.compare_digest(a, b)Quick Reference Card
| Operation | Algorithm | Key Size | Notes |
|---|---|---|---|
| Symmetric encryption | AES-256-GCM | 256 bits | Use random 96-bit nonce |
| Alternative encryption | ChaCha20-Poly1305 | 256 bits | Faster on non-AES hardware |
| Asymmetric encryption | RSA-OAEP | 2048+ bits | Only for small data/keys |
| Key exchange | X25519 | 256 bits | Derive key with HKDF |
| Digital signature | Ed25519 | 256 bits | Fast, deterministic |
| Password hashing | Argon2id | - | 64MB memory, 3 iterations |
| Message authentication | HMAC-SHA256 | 256 bits | Use for API signing |
| Key derivation | PBKDF2-SHA256 | - | 600,000+ iterations |