Skip to content

This textbook is in beta – content is actively being refined. Report issues or suggestions

Section 15.2: Cryptography & Data Protection

Learning Objectives

By the end of this section, you will be able to:

  • Understand hashing vs encryption and determine when to use each technique

  • Implement key management fundamentals for secure cryptographic operations

  • Create secure password storage using hashing with salting techniques

  • Perform basic encryption/decryption operations using Python cryptography libraries

  • Apply certificate and TLS basics for secure communication

Why Cryptography Matters

Cryptography is the foundation of modern digital security, protecting data whether it’s stored on disk, transmitted over networks, or processed in applications. Understanding when and how to use different cryptographic techniques is essential for building secure systems.

Without proper cryptography:

  • Passwords stored in plain text are immediately compromised in data breaches

  • Sensitive data transmitted over networks can be intercepted and read

  • Stored files containing personal information are vulnerable to unauthorized access

  • Digital communications lack authenticity and can be impersonated

Hashing vs Encryption: When to Use Each

Hashing: One-Way Data Fingerprinting

Hashing creates a fixed-size “fingerprint” of data that cannot be reversed to recover the original data.

Use hashing for:

  • Password verification

  • Data integrity checking

  • Digital signatures

  • Proof of data existence without revealing content

Key characteristics:

  • One-way function: Cannot be reversed to get original data

  • Deterministic: Same input always produces same hash

  • Fixed output size: Hash is always the same length regardless of input size

  • Avalanche effect: Small input changes produce completely different hashes

import hashlib
import secrets
import time

class SecureHashingManager:
    def __init__(self):
        self.supported_algorithms = ['sha256', 'sha3_256', 'blake2b']

    def hash_password(self, password, salt=None, algorithm='sha256', iterations=100000):
        """Securely hash password with salt and key stretching"""

        if salt is None:
            salt = secrets.token_bytes(32)  # 256-bit salt
        elif isinstance(salt, str):
            salt = salt.encode('utf-8')

        # Use PBKDF2 for key stretching (makes brute force attacks slower)
        password_hash = hashlib.pbkdf2_hmac(
            algorithm,
            password.encode('utf-8'),
            salt,
            iterations
        )

        return {
            'hash': password_hash.hex(),
            'salt': salt.hex(),
            'algorithm': algorithm,
            'iterations': iterations
        }

    def verify_password(self, password, stored_hash_data):
        """Verify password against stored hash"""

        # Recreate hash with same parameters
        verification_hash = hashlib.pbkdf2_hmac(
            stored_hash_data['algorithm'],
            password.encode('utf-8'),
            bytes.fromhex(stored_hash_data['salt']),
            stored_hash_data['iterations']
        )

        # Constant-time comparison to prevent timing attacks
        return secrets.compare_digest(
            verification_hash.hex(),
            stored_hash_data['hash']
        )

    def hash_file_for_integrity(self, file_path, algorithm='sha256'):
        """Calculate file hash for integrity verification"""

        hash_obj = hashlib.new(algorithm)

        try:
            with open(file_path, 'rb') as f:
                # Read file in chunks to handle large files
                for chunk in iter(lambda: f.read(4096), b""):
                    hash_obj.update(chunk)

            return {
                'file_path': file_path,
                'hash': hash_obj.hexdigest(),
                'algorithm': algorithm,
                'timestamp': time.time()
            }

        except FileNotFoundError:
            raise FileNotFoundError(f"File not found: {file_path}")

    def verify_file_integrity(self, file_path, expected_hash, algorithm='sha256'):
        """Verify file hasn't been modified"""

        current_hash_data = self.hash_file_for_integrity(file_path, algorithm)

        return secrets.compare_digest(
            current_hash_data['hash'],
            expected_hash
        )

    def demonstrate_avalanche_effect(self, base_text="Hello World"):
        """Show how small changes create completely different hashes"""

        texts = [
            base_text,
            base_text + "!",  # Add exclamation
            base_text.replace("l", "L"),  # Change case
            base_text + " "  # Add space
        ]

        results = []
        for text in texts:
            hash_value = hashlib.sha256(text.encode()).hexdigest()
            results.append({
                'input': repr(text),
                'hash': hash_value,
                'length': len(hash_value)
            })

        return results

# Example usage for password security
def demonstrate_secure_password_hashing():
    """Demonstrate secure password handling"""

    hash_manager = SecureHashingManager()

    print("=== Secure Password Hashing Demo ===")

    # Hash a password
    password = "SecureStudentPassword123!"
    hash_data = hash_manager.hash_password(password)

    print(f"Original password: {password}")
    print(f"Salt: {hash_data['salt'][:16]}... (truncated)")
    print(f"Hash: {hash_data['hash'][:16]}... (truncated)")
    print(f"Algorithm: {hash_data['algorithm']}")
    print(f"Iterations: {hash_data['iterations']}")

    # Verify correct password
    is_valid = hash_manager.verify_password(password, hash_data)
    print(f"Correct password verification: {is_valid}")

    # Verify incorrect password
    is_valid_wrong = hash_manager.verify_password("WrongPassword", hash_data)
    print(f"Wrong password verification: {is_valid_wrong}")

    print("\n=== Avalanche Effect Demo ===")
    avalanche_results = hash_manager.demonstrate_avalanche_effect()

    for result in avalanche_results:
        print(f"Input: {result['input']}")
        print(f"Hash:  {result['hash']}")
        print()

Encryption: Reversible Data Protection

Encryption transforms data into an unreadable format that can be reversed with the correct key.

Use encryption for:

  • Protecting sensitive data in storage

  • Securing data transmission

  • Confidential file storage

  • Database field protection

Key characteristics:

  • Two-way function: Can be reversed with the correct key

  • Key-dependent: Different keys produce different encrypted output

  • Preserves data: Original data can be fully recovered

  • Variable output size: Encrypted data is usually larger than original

from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
import os

class SecureEncryptionManager:
    def __init__(self):
        self.symmetric_keys = {}
        self.asymmetric_keys = {}

    def generate_symmetric_key(self, key_id="default"):
        """Generate symmetric encryption key"""
        key = Fernet.generate_key()
        self.symmetric_keys[key_id] = key
        return key

    def derive_key_from_password(self, password, salt=None):
        """Derive encryption key from password"""
        if salt is None:
            salt = os.urandom(16)

        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=100000,
        )

        key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
        return key, salt

    def encrypt_data(self, data, key_id="default"):
        """Encrypt data using symmetric encryption"""

        if key_id not in self.symmetric_keys:
            self.generate_symmetric_key(key_id)

        key = self.symmetric_keys[key_id]
        fernet = Fernet(key)

        if isinstance(data, str):
            data = data.encode('utf-8')

        encrypted_data = fernet.encrypt(data)

        return {
            'encrypted_data': encrypted_data,
            'key_id': key_id,
            'algorithm': 'Fernet (AES-128)'
        }

    def decrypt_data(self, encrypted_data, key_id="default"):
        """Decrypt data using symmetric encryption"""

        if key_id not in self.symmetric_keys:
            raise ValueError(f"Key '{key_id}' not found")

        key = self.symmetric_keys[key_id]
        fernet = Fernet(key)

        if isinstance(encrypted_data, dict):
            encrypted_data = encrypted_data['encrypted_data']

        try:
            decrypted_data = fernet.decrypt(encrypted_data)
            return decrypted_data.decode('utf-8')
        except Exception as e:
            raise ValueError(f"Decryption failed: {e}")

    def generate_asymmetric_keypair(self, key_id="default"):
        """Generate RSA public/private key pair"""

        private_key = rsa.generate_private_key(
            public_exponent=65537,
            key_size=2048,
        )
        public_key = private_key.public_key()

        self.asymmetric_keys[key_id] = {
            'private_key': private_key,
            'public_key': public_key
        }

        return {
            'public_key': public_key,
            'private_key': private_key,
            'key_id': key_id
        }

    def encrypt_with_public_key(self, data, key_id="default"):
        """Encrypt data using RSA public key"""

        if key_id not in self.asymmetric_keys:
            self.generate_asymmetric_keypair(key_id)

        public_key = self.asymmetric_keys[key_id]['public_key']

        if isinstance(data, str):
            data = data.encode('utf-8')

        # RSA can only encrypt small amounts of data
        if len(data) > 190:  # RSA-2048 limit minus padding
            raise ValueError("Data too large for RSA encryption")

        encrypted_data = public_key.encrypt(
            data,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None
            )
        )

        return encrypted_data

    def decrypt_with_private_key(self, encrypted_data, key_id="default"):
        """Decrypt data using RSA private key"""

        if key_id not in self.asymmetric_keys:
            raise ValueError(f"Key '{key_id}' not found")

        private_key = self.asymmetric_keys[key_id]['private_key']

        try:
            decrypted_data = private_key.decrypt(
                encrypted_data,
                padding.OAEP(
                    mgf=padding.MGF1(algorithm=hashes.SHA256()),
                    algorithm=hashes.SHA256(),
                    label=None
                )
            )
            return decrypted_data.decode('utf-8')
        except Exception as e:
            raise ValueError(f"Decryption failed: {e}")

# Example usage for student record encryption
def demonstrate_encryption_scenarios():
    """Demonstrate different encryption use cases"""

    encryption_manager = SecureEncryptionManager()

    print("=== Symmetric Encryption Demo ===")

    # Encrypt student personal information
    student_info = {
        'student_id': 'S12345',
        'name': 'Alice Johnson',
        'ssn': '123-45-6789',
        'address': '123 School St, Education City',
        'grade_level': 11
    }

    # Convert to string for encryption
    student_data = str(student_info)

    # Encrypt
    encrypted_result = encryption_manager.encrypt_data(student_data, "student_records")
    print(f"Original data length: {len(student_data)} characters")
    print(f"Encrypted data length: {len(encrypted_result['encrypted_data'])} bytes")
    print(f"Algorithm: {encrypted_result['algorithm']}")

    # Decrypt
    decrypted_data = encryption_manager.decrypt_data(encrypted_result, "student_records")
    print(f"Decryption successful: {decrypted_data == student_data}")

    print("\n=== Asymmetric Encryption Demo ===")

    # Generate keypair for secure communication
    keypair = encryption_manager.generate_asymmetric_keypair("communication")

    # Encrypt small sensitive message
    message = "Grade change: Student S12345 Math grade updated to 95"

    try:
        encrypted_message = encryption_manager.encrypt_with_public_key(message, "communication")
        print(f"Message encrypted successfully")
        print(f"Original message: {message}")

        # Decrypt message
        decrypted_message = encryption_manager.decrypt_with_private_key(encrypted_message, "communication")
        print(f"Decrypted message: {decrypted_message}")
        print(f"Decryption successful: {message == decrypted_message}")

    except ValueError as e:
        print(f"Encryption error: {e}")

Key Management Fundamentals

Proper key management is crucial for cryptographic security. Keys must be generated securely, stored safely, and rotated regularly.

import json
import os
from datetime import datetime, timedelta
from cryptography.fernet import Fernet
import secrets

class CryptographicKeyManager:
    def __init__(self, key_store_path="keystore"):
        self.key_store_path = key_store_path
        self.keys = {}
        self.key_metadata = {}

        # Create secure key storage directory
        os.makedirs(key_store_path, exist_ok=True, mode=0o700)  # Owner only

        self.load_existing_keys()

    def generate_key(self, key_id, key_type="symmetric", purpose="general", 
                    expiry_days=365):
        """Generate new cryptographic key with metadata"""

        if key_type == "symmetric":
            key = Fernet.generate_key()
        else:
            raise ValueError(f"Unsupported key type: {key_type}")

        # Store key metadata
        metadata = {
            'key_id': key_id,
            'key_type': key_type,
            'purpose': purpose,
            'created_at': datetime.now().isoformat(),
            'expires_at': (datetime.now() + timedelta(days=expiry_days)).isoformat(),
            'algorithm': 'Fernet (AES-128)',
            'status': 'active'
        }

        # Store key securely
        self.keys[key_id] = key
        self.key_metadata[key_id] = metadata

        # Persist to secure storage
        self._save_key_to_storage(key_id, key, metadata)

        return {
            'key_id': key_id,
            'metadata': metadata,
            'key_created': True
        }

    def get_key(self, key_id):
        """Retrieve key with validation"""

        if key_id not in self.keys:
            raise ValueError(f"Key '{key_id}' not found")

        metadata = self.key_metadata[key_id]

        # Check if key has expired
        expiry_date = datetime.fromisoformat(metadata['expires_at'])
        if datetime.now() > expiry_date:
            raise ValueError(f"Key '{key_id}' has expired")

        # Check if key is active
        if metadata['status'] != 'active':
            raise ValueError(f"Key '{key_id}' is not active")

        return self.keys[key_id]

    def rotate_key(self, old_key_id, new_key_id=None):
        """Rotate encryption key"""

        if new_key_id is None:
            new_key_id = f"{old_key_id}_rotated_{int(datetime.now().timestamp())}"

        # Get old key metadata for reference
        old_metadata = self.key_metadata.get(old_key_id, {})

        # Generate new key with same purpose
        new_key_result = self.generate_key(
            new_key_id,
            key_type=old_metadata.get('key_type', 'symmetric'),
            purpose=old_metadata.get('purpose', 'general')
        )

        # Mark old key as deprecated (don't delete immediately for data recovery)
        if old_key_id in self.key_metadata:
            self.key_metadata[old_key_id]['status'] = 'deprecated'
            self.key_metadata[old_key_id]['deprecated_at'] = datetime.now().isoformat()
            self.key_metadata[old_key_id]['replaced_by'] = new_key_id

        return {
            'old_key_id': old_key_id,
            'new_key_id': new_key_id,
            'rotation_completed': True
        }

    def list_keys(self, include_deprecated=False):
        """List all keys with their status"""

        key_list = []
        for key_id, metadata in self.key_metadata.items():
            if not include_deprecated and metadata['status'] == 'deprecated':
                continue

            key_info = {
                'key_id': key_id,
                'purpose': metadata['purpose'],
                'status': metadata['status'],
                'created_at': metadata['created_at'],
                'expires_at': metadata['expires_at']
            }

            # Check expiry status
            expiry_date = datetime.fromisoformat(metadata['expires_at'])
            if datetime.now() > expiry_date:
                key_info['expired'] = True

            key_list.append(key_info)

        return key_list

    def _save_key_to_storage(self, key_id, key, metadata):
        """Securely save key to filesystem"""

        # In production, use HSM or secure key management service
        key_file = os.path.join(self.key_store_path, f"{key_id}.key")
        metadata_file = os.path.join(self.key_store_path, f"{key_id}.metadata")

        # Save key (in production, encrypt this with master key)
        with open(key_file, 'wb') as f:
            f.write(key)
        os.chmod(key_file, 0o600)  # Owner read/write only

        # Save metadata
        with open(metadata_file, 'w') as f:
            json.dump(metadata, f, indent=2)
        os.chmod(metadata_file, 0o600)

    def load_existing_keys(self):
        """Load keys from secure storage on startup"""

        if not os.path.exists(self.key_store_path):
            return

        for filename in os.listdir(self.key_store_path):
            if filename.endswith('.metadata'):
                key_id = filename[:-9]  # Remove .metadata extension

                try:
                    # Load metadata
                    metadata_file = os.path.join(self.key_store_path, filename)
                    with open(metadata_file, 'r') as f:
                        metadata = json.load(f)

                    # Load key
                    key_file = os.path.join(self.key_store_path, f"{key_id}.key")
                    with open(key_file, 'rb') as f:
                        key = f.read()

                    self.keys[key_id] = key
                    self.key_metadata[key_id] = metadata

                except (FileNotFoundError, json.JSONDecodeError) as e:
                    print(f"Warning: Could not load key {key_id}: {e}")

# Example usage for school system key management
def demonstrate_key_management():
    """Demonstrate key management for school system"""

    key_manager = CryptographicKeyManager()

    print("=== Key Management Demo ===")

    # Generate keys for different purposes
    purposes = [
        ("student_records", "student record encryption"),
        ("grade_data", "grade database encryption"),
        ("communication", "secure messaging")
    ]

    for key_id, purpose in purposes:
        result = key_manager.generate_key(key_id, purpose=purpose)
        print(f"Generated key: {result['key_id']} for {purpose}")

    # List current keys
    print("\n=== Current Keys ===")
    keys = key_manager.list_keys()
    for key_info in keys:
        print(f"Key: {key_info['key_id']}")
        print(f"  Purpose: {key_info['purpose']}")
        print(f"  Status: {key_info['status']}")
        print(f"  Expires: {key_info['expires_at'][:10]}")
        print()

    # Demonstrate key rotation
    print("=== Key Rotation Demo ===")
    rotation_result = key_manager.rotate_key("student_records")
    print(f"Rotated {rotation_result['old_key_id']} to {rotation_result['new_key_id']}")

    # Show updated key list
    print("\n=== Keys After Rotation ===")
    keys = key_manager.list_keys(include_deprecated=True)
    for key_info in keys:
        status_indicator = "🔑" if key_info['status'] == 'active' else "⚠️"
        print(f"{status_indicator} {key_info['key_id']} ({key_info['status']})")

Guided Example: Secure Password Manager

Let’s build a complete password manager that demonstrates all cryptographic concepts.

import json
import getpass
from datetime import datetime
import secrets
import base64

class SecurePasswordManager:
    def __init__(self, master_password=None):
        self.encryption_manager = SecureEncryptionManager()
        self.hash_manager = SecureHashingManager()
        self.key_manager = CryptographicKeyManager()

        self.vault_file = "password_vault.enc"
        self.master_key_id = "master_vault_key"
        self.vault_data = {}

        if master_password:
            self._initialize_vault(master_password)

    def _initialize_vault(self, master_password):
        """Initialize password vault with master password"""

        # Create master encryption key from password
        master_key, salt = self.encryption_manager.derive_key_from_password(master_password)

        # Store the key securely
        self.encryption_manager.symmetric_keys[self.master_key_id] = master_key

        # Store salt for future key derivation
        self.vault_salt = salt

        # Try to load existing vault
        self._load_vault()

    def unlock_vault(self, master_password):
        """Unlock vault with master password"""

        try:
            # Derive key from password and stored salt
            master_key, _ = self.encryption_manager.derive_key_from_password(
                master_password, self.vault_salt
            )

            self.encryption_manager.symmetric_keys[self.master_key_id] = master_key

            # Try to decrypt vault to verify password
            self._load_vault()
            return True, "Vault unlocked successfully"

        except Exception as e:
            return False, "Invalid master password"

    def add_password(self, service, username, password, notes=""):
        """Add password entry to vault"""

        # Generate entry ID
        entry_id = secrets.token_hex(8)

        # Create password entry
        entry = {
            'service': service,
            'username': username,
            'password': password,
            'notes': notes,
            'created_at': datetime.now().isoformat(),
            'last_modified': datetime.now().isoformat()
        }

        # Add to vault
        self.vault_data[entry_id] = entry

        # Save vault
        self._save_vault()

        return entry_id

    def get_password(self, entry_id):
        """Retrieve password entry from vault"""

        if entry_id not in self.vault_data:
            raise ValueError("Entry not found")

        return self.vault_data[entry_id]

    def list_entries(self):
        """List all password entries (without passwords)"""

        entries = []
        for entry_id, entry in self.vault_data.items():
            entries.append({
                'entry_id': entry_id,
                'service': entry['service'],
                'username': entry['username'],
                'created_at': entry['created_at']
            })

        return entries

    def generate_secure_password(self, length=16, include_symbols=True):
        """Generate cryptographically secure password"""

        # Character sets
        lowercase = 'abcdefghijklmnopqrstuvwxyz'
        uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
        digits = '0123456789'
        symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?'

        # Build character set
        charset = lowercase + uppercase + digits
        if include_symbols:
            charset += symbols

        # Generate password ensuring at least one character from each set
        password = []

        # Ensure minimum complexity
        password.append(secrets.choice(lowercase))
        password.append(secrets.choice(uppercase))
        password.append(secrets.choice(digits))

        if include_symbols:
            password.append(secrets.choice(symbols))

        # Fill remaining length
        for _ in range(length - len(password)):
            password.append(secrets.choice(charset))

        # Shuffle the password
        secrets.SystemRandom().shuffle(password)

        return ''.join(password)

    def _save_vault(self):
        """Save encrypted vault to disk"""

        # Convert vault to JSON
        vault_json = json.dumps(self.vault_data, indent=2)

        # Encrypt vault data
        encrypted_vault = self.encryption_manager.encrypt_data(
            vault_json, self.master_key_id
        )

        # Save to file
        vault_container = {
            'encrypted_data': base64.b64encode(encrypted_vault['encrypted_data']).decode(),
            'salt': base64.b64encode(self.vault_salt).decode(),
            'created_at': datetime.now().isoformat()
        }

        with open(self.vault_file, 'w') as f:
            json.dump(vault_container, f, indent=2)

    def _load_vault(self):
        """Load and decrypt vault from disk"""

        try:
            with open(self.vault_file, 'r') as f:
                vault_container = json.load(f)

            # Extract encrypted data and salt
            encrypted_data = base64.b64decode(vault_container['encrypted_data'])
            self.vault_salt = base64.b64decode(vault_container['salt'])

            # Decrypt vault
            decrypted_json = self.encryption_manager.decrypt_data(
                encrypted_data, self.master_key_id
            )

            # Load vault data
            self.vault_data = json.loads(decrypted_json)

        except FileNotFoundError:
            # New vault
            self.vault_data = {}
        except Exception as e:
            raise ValueError(f"Could not load vault: {e}")

# Demonstration of complete password manager
def demonstrate_password_manager():
    """Demonstrate secure password manager functionality"""

    print("=== Secure Password Manager Demo ===")

    # Create password manager
    master_password = "SuperSecureMasterPassword123!"
    pm = SecurePasswordManager(master_password)

    # Generate and store secure passwords
    print("\n=== Adding Password Entries ===")

    entries = [
        ("School Email", "student@school.edu", ""),
        ("Learning Management System", "student123", ""),
        ("Library System", "student123", "Main campus library")
    ]

    for service, username, notes in entries:
        # Generate secure password
        secure_password = pm.generate_secure_password(length=12)

        # Add to vault
        entry_id = pm.add_password(service, username, secure_password, notes)

        print(f"Added {service}: {username}")
        print(f"  Password: {secure_password}")
        print(f"  Entry ID: {entry_id}")
        print()

    print("=== Vault Contents ===")
    entries = pm.list_entries()
    for entry in entries:
        print(f"Service: {entry['service']}")
        print(f"Username: {entry['username']}")
        print(f"Entry ID: {entry['entry_id']}")
        print()

    # Demonstrate vault security
    print("=== Testing Vault Security ===")

    # Save current vault
    pm._save_vault()

    # Create new manager instance (simulating restart)
    pm2 = SecurePasswordManager()

    # Try with wrong password
    unlock_result = pm2.unlock_vault("WrongPassword")
    print(f"Wrong password unlock: {unlock_result}")

    # Try with correct password
    unlock_result = pm2.unlock_vault(master_password)
    print(f"Correct password unlock: {unlock_result}")

    if unlock_result[0]:
        retrieved_entries = pm2.list_entries()
        print(f"Retrieved {len(retrieved_entries)} entries after unlock")

Summary

Cryptography provides the tools to protect data confidentiality and integrity:

Hashing applications:

  • Password verification: Store password hashes, never plain text passwords

  • Data integrity: Detect unauthorized file or data modifications

  • Digital signatures: Verify authenticity and non-repudiation

  • Proof of existence: Demonstrate data existed at a specific time

Encryption applications:

  • Data at rest: Protect stored files, databases, and backups

  • Data in transit: Secure network communications and API calls

  • Confidential storage: Encrypt sensitive application data

  • Secure messaging: Protect private communications

Key management essentials:

  • Secure generation: Use cryptographically secure random number generators

  • Safe storage: Protect keys with appropriate access controls

  • Regular rotation: Change keys periodically to limit exposure

  • Proper destruction: Securely delete old keys when no longer needed

Implementation best practices:

  • Use established libraries: Never implement cryptography from scratch

  • Salt all hashes: Prevent rainbow table attacks

  • Authenticate then encrypt: Verify integrity before processing

  • Plan for key recovery: Ensure data isn’t lost if keys are compromised

Understanding cryptography enables developers to make informed decisions about data protection and implement security controls that effectively protect sensitive information throughout its lifecycle.