Skip to main content

Python Secrets Management Practice Problems & Exercises

Practice: Secrets Management

11 problems4 Easy4 Medium3 Hard50–65 min
← Back to lesson

Easy

#1Load Config from os.environEasy
os.environconfigenvironment-variables

Read database connection settings from environment variables and return them as a typed dictionary.

Solution
import os

os.environ['DB_HOST'] = 'localhost'
os.environ['DB_PORT'] = '5432'
os.environ['DB_NAME'] = 'myapp'

def load_db_config() -> dict:
return {
'host': os.environ.get('DB_HOST', '127.0.0.1'),
'port': int(os.environ.get('DB_PORT', '5432')),
'name': os.environ.get('DB_NAME', 'app'),
}

config = load_db_config()
print(f"host={config['host']}")
print(f"port={config['port']}")
print(f"name={config['name']}")

Why os.environ.get over direct index access: os.environ['KEY'] raises KeyError if the variable is missing, which crashes the app at startup — often in production when a deployment step forgot to inject a secret. Using .get() with a safe default means the process starts and emits a clear error elsewhere rather than an unhandled exception.

import os

# Simulate environment variables being set
os.environ['DB_HOST'] = 'localhost'
os.environ['DB_PORT'] = '5432'
os.environ['DB_NAME'] = 'myapp'

def load_db_config() -> dict:
  # Read DB_HOST, DB_PORT (as int), DB_NAME from env
  # Provide sensible defaults if not set
  pass

config = load_db_config()
print(f"host={config['host']}")
print(f"port={config['port']}")
print(f"name={config['name']}")
Expected Output
host=localhost
port=5432
name=myapp
Hints

Hint 1: Use os.environ.get(key, default) to avoid KeyError when a variable is absent.

Hint 2: Convert port to int with int() — environment variables are always strings.

Hint 3: Never hard-code defaults that look like real credentials — use safe placeholders.


#2Load .env File with python-dotenvEasy
dotenvpython-dotenvenv-fileconfig

Implement a minimal .env file parser that replicates the core behaviour of python-dotenv.

Solution
ENV_CONTENT = """
API_KEY=sk-your-key-here
REDIS_URL=redis://localhost:6379
DEBUG=false
"""

def parse_dotenv(content: str) -> dict:
result = {}
for line in content.splitlines():
line = line.strip()
# Skip blank lines and comments
if not line or line.startswith('#'):
continue
if '=' not in line:
continue
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
# Strip optional surrounding quotes
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
value = value[1:-1]
result[key] = value
return result

env = parse_dotenv(ENV_CONTENT)
print(f"API_KEY={env.get('API_KEY')}")
print(f"REDIS_URL={env.get('REDIS_URL')}")
print(f"DEBUG={env.get('DEBUG')}")

Real usage with python-dotenv: from dotenv import load_dotenv; load_dotenv() loads .env into os.environ automatically. The .env file must be in .gitignore — it exists only on developer machines and CI, never in version control. Production secrets come from the deployment platform (AWS Secrets Manager, Vault, etc.).

# Simulate what python-dotenv does internally
# (We'll implement a minimal .env parser since we can't write real files here)

ENV_CONTENT = """
API_KEY=sk-your-key-here
REDIS_URL=redis://localhost:6379
DEBUG=false
"""

def parse_dotenv(content: str) -> dict:
  # Parse key=value lines, skip comments (#) and blank lines
  # Strip surrounding whitespace and optional quotes from values
  pass

env = parse_dotenv(ENV_CONTENT)
print(f"API_KEY={env.get('API_KEY')}")
print(f"REDIS_URL={env.get('REDIS_URL')}")
print(f"DEBUG={env.get('DEBUG')}")
Expected Output
API_KEY=sk-your-key-here
REDIS_URL=redis://localhost:6379
DEBUG=false
Hints

Hint 1: Split each non-blank, non-comment line on the first = only (use split("=", 1)).

Hint 2: Strip leading/trailing whitespace from both key and value.

Hint 3: python-dotenv skips lines starting with # — handle that case first.


#3Never Log Secrets — Redact Sensitive KeysEasy
loggingredactionsecretssecurity

Write a safe_log_config function that logs configuration values but redacts secrets.

Solution
import logging

logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
logger = logging.getLogger(__name__)

SENSITIVE_KEYS = {'password', 'api_key', 'secret', 'token', 'private_key'}

def safe_log_config(config: dict) -> None:
for key, value in config.items():
if key.lower() in SENSITIVE_KEYS:
logger.info(f"{key}=***REDACTED***")
else:
logger.info(f"{key}={value}")

config = {
'host': 'db.example.com',
'port': 5432,
'password': 'super-secret-password',
'api_key': 'sk-live-abc123',
'timeout': 30,
}

safe_log_config(config)

Why logs are a prime leak vector: Log aggregators (Datadog, Splunk, ELK) store logs for months. A single logger.info(f"Connecting with config: {config}") that exposes a raw config dict can leak credentials to everyone with log-read access — often far more people than those who should see secrets. Always audit log statements in code review.

import logging

logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
logger = logging.getLogger(__name__)

SENSITIVE_KEYS = {'password', 'api_key', 'secret', 'token', 'private_key'}

def safe_log_config(config: dict) -> None:
  # Log each key-value pair, but redact values whose keys are in SENSITIVE_KEYS
  pass

config = {
  'host': 'db.example.com',
  'port': 5432,
  'password': 'super-secret-password',
  'api_key': 'sk-live-abc123',
  'timeout': 30,
}

safe_log_config(config)
Expected Output
INFO: host=db.example.com
INFO: port=5432
INFO: password=***REDACTED***
INFO: api_key=***REDACTED***
INFO: timeout=30
Hints

Hint 1: Check if each key (lowercased) is in SENSITIVE_KEYS before logging.

Hint 2: Replace the actual value with a fixed placeholder like "***REDACTED***".

Hint 3: Partial masking (showing last 4 chars) is fine for debugging — full redaction is safest for logs.


#4Generate Secure Tokens with secrets ModuleEasy
secretstokenurandomcryptography

Implement three secure token generators using Python's secrets module.

Solution
import secrets

def generate_url_safe_token(nbytes: int = 32) -> str:
return secrets.token_urlsafe(nbytes)

def generate_api_key(prefix: str = 'sk') -> str:
return f"{prefix}-{secrets.token_hex(16)}"

def generate_otp(length: int = 6) -> str:
# Pad with leading zeros if randbelow produces a short number
return str(secrets.randbelow(10 ** length)).zfill(length)

token = generate_url_safe_token()
print(f"URL token (32 bytes): {token[:20]}...")
print(f"URL token length >= 43: {len(token) >= 43}")
print(f"API key starts with sk-: {generate_api_key().startswith('sk-')}")
otp = generate_otp()
print(f"OTP is 6 digits: {otp.isdigit() and len(otp) == 6}")

secrets vs random: random uses a Mersenne Twister PRNG — its output is predictable if an attacker observes enough values. secrets uses os.urandom() (CSPRNG backed by the OS entropy pool) — cryptographically unpredictable. Always use secrets for tokens, session IDs, OTPs, and API keys. Never use random for anything security-related.

import secrets
import string

def generate_url_safe_token(nbytes: int = 32) -> str:
  # Generate a URL-safe base64 token using secrets module
  pass

def generate_api_key(prefix: str = 'sk') -> str:
  # Generate an API key like: sk-<32 hex chars>
  pass

def generate_otp(length: int = 6) -> str:
  # Generate a numeric OTP of given length (cryptographically secure)
  pass

print(f"URL token (32 bytes): {generate_url_safe_token()[:20]}...")
print(f"URL token length >= 43: {len(generate_url_safe_token()) >= 43}")
print(f"API key starts with sk-: {generate_api_key().startswith('sk-')}")
print(f"OTP is 6 digits: {generate_otp().isdigit() and len(generate_otp()) == 6}")
Expected Output
URL token (32 bytes): (first 20 chars of base64url token)...
URL token length >= 43: True
API key starts with sk-: True
OTP is 6 digits: True
Hints

Hint 1: secrets.token_urlsafe(nbytes) returns a URL-safe base64 string — 32 bytes gives ~43 chars.

Hint 2: secrets.token_hex(nbytes) returns a hex string — 16 bytes gives 32 hex chars.

Hint 3: For OTP: use secrets.choice() in a loop, or str(secrets.randbelow(10**length)).zfill(length).


Medium

#5Vault-Style In-Memory Secret StoreMedium
vaultsecret-storeaccess-controlsecrets

Build a minimal in-memory secret store with TTL expiry and access logging, inspired by HashiCorp Vault.

Solution
import time
from typing import Optional

class SecretStore:
def __init__(self) -> None:
self._store: dict[str, dict] = {}
self._log: list[dict] = []

def set(self, key: str, value: str, ttl_seconds: Optional[int] = None) -> None:
expires_at = time.time() + ttl_seconds if ttl_seconds is not None else None
self._store[key] = {
'value': value,
'expires_at': expires_at,
'created_at': time.time(),
}

def get(self, key: str, accessor: str = 'system') -> Optional[str]:
entry = self._store.get(key)
if entry is None:
self._log.append({'key': key, 'accessor': accessor, 'time': time.time(), 'result': 'miss'})
return None
if entry['expires_at'] is not None and time.time() > entry['expires_at']:
del self._store[key]
self._log.append({'key': key, 'accessor': accessor, 'time': time.time(), 'result': 'expired'})
return None
self._log.append({'key': key, 'accessor': accessor, 'time': time.time(), 'result': 'hit'})
return entry['value']

def delete(self, key: str) -> None:
self._store.pop(key, None)

def access_log(self) -> list[dict]:
return list(self._log)

store = SecretStore()
store.set('db_password', 'hunter2', ttl_seconds=3600)
store.set('api_key', 'sk-live-abc', ttl_seconds=1)

val = store.get('db_password', accessor='app-service')
print(f"db_password: {val}")

time.sleep(1.1)
expired = store.get('api_key', accessor='app-service')
print(f"api_key after expiry: {expired}")

print(f"access log entries: {len(store.access_log())}")

Real Vault: HashiCorp Vault adds lease management, dynamic credential generation (e.g., temporary DB passwords), audit backends, and policy-based access control on top of this basic pattern. The access log here is the minimal version of Vault's audit log, which compliance frameworks like SOC 2 require.

import time
from typing import Optional

class SecretStore:
  """
  Minimal in-memory secret store with:
  - set/get/delete operations
  - TTL (time-to-live) for auto-expiry
  - Access tracking (who accessed what, when)
  """

  def __init__(self) -> None:
      pass

  def set(self, key: str, value: str, ttl_seconds: Optional[int] = None) -> None:
      """Store a secret with optional expiry."""
      pass

  def get(self, key: str, accessor: str = 'system') -> Optional[str]:
      """Retrieve a secret; return None if missing or expired. Log access."""
      pass

  def delete(self, key: str) -> None:
      pass

  def access_log(self) -> list[dict]:
      pass

store = SecretStore()
store.set('db_password', 'hunter2', ttl_seconds=3600)
store.set('api_key', 'sk-live-abc', ttl_seconds=1)  # expires in 1s

val = store.get('db_password', accessor='app-service')
print(f"db_password: {val}")

import time; time.sleep(1.1)
expired = store.get('api_key', accessor='app-service')
print(f"api_key after expiry: {expired}")

print(f"access log entries: {len(store.access_log())}")
Expected Output
db_password: hunter2
api_key after expiry: None
access log entries: 2
Hints

Hint 1: Store each secret as a dict with keys: value, expires_at (float or None), created_at.

Hint 2: In get(), compare time.time() against expires_at — if expired, return None and optionally delete.

Hint 3: Append a dict with key, accessor, timestamp, and hit/miss to a list for the access log.


#6Secret RotationMedium
secret-rotationvaultversioningsecrets

Implement a secret store that supports versioning and a grace period, so old credentials remain valid briefly after rotation.

Solution
import time
from typing import Optional

class RotatingSecretStore:
def __init__(self, grace_seconds: int = 30) -> None:
self._grace = grace_seconds
self._store: dict[str, list[dict]] = {}

def set(self, key: str, value: str) -> int:
versions = self._store.setdefault(key, [])
new_version = len(versions) + 1
versions.append({
'version': new_version,
'value': value,
'created_at': time.time(),
})
return new_version

def get_current(self, key: str) -> Optional[str]:
versions = self._store.get(key)
if not versions:
return None
return versions[-1]['value']

def get_version(self, key: str, version: int) -> Optional[str]:
versions = self._store.get(key, [])
for entry in versions:
if entry['version'] == version:
is_current = entry is versions[-1]
within_grace = (time.time() - entry['created_at']) <= self._grace
if is_current or within_grace:
return entry['value']
return None

def versions(self, key: str) -> list[int]:
return [e['version'] for e in self._store.get(key, [])]

store = RotatingSecretStore(grace_seconds=5)
v1 = store.set('db_password', 'old-password')
print(f"version 1: {v1}")
v2 = store.set('db_password', 'new-password')
print(f"version 2: {v2}")
print(f"current: {store.get_current('db_password')}")
print(f"v1 still valid: {store.get_version('db_password', v1) is not None}")
print(f"versions: {store.versions('db_password')}")

Why grace periods matter: When you rotate a database password, there is a window where old connections (connection pool) still hold the previous credential. A zero-grace rotation would break live traffic. Vault's previous_version_count and AWS Secrets Manager's rotation lambda use the same pattern — keep N old versions valid for a short window while connections drain.

import time
from typing import Optional

class RotatingSecretStore:
  """
  Secret store that keeps previous versions during rotation,
  allowing a grace period where both old and new are valid.
  """

  def __init__(self, grace_seconds: int = 30) -> None:
      pass

  def set(self, key: str, value: str) -> int:
      """Store a new version of a secret. Returns the new version number."""
      pass

  def get_current(self, key: str) -> Optional[str]:
      """Return the current (latest) version value."""
      pass

  def get_version(self, key: str, version: int) -> Optional[str]:
      """Return a specific version (if still within grace period or is current)."""
      pass

  def versions(self, key: str) -> list[int]:
      """List available version numbers for a key."""
      pass

store = RotatingSecretStore(grace_seconds=5)
v1 = store.set('db_password', 'old-password')
print(f"version 1: {v1}")
v2 = store.set('db_password', 'new-password')
print(f"version 2: {v2}")
print(f"current: {store.get_current('db_password')}")
print(f"v1 still valid: {store.get_version('db_password', v1) is not None}")
print(f"versions: {store.versions('db_password')}")
Expected Output
version 1: 1
version 2: 2
current: new-password
v1 still valid: True
versions: [1, 2]
Hints

Hint 1: Store each secret key as a list of (version, value, created_at) tuples.

Hint 2: get_current returns the entry with the highest version number.

Hint 3: get_version checks if the version is the current one OR was created within grace_seconds.


#7Validate Required Env Vars on StartupMedium
env-validationstartupfail-fastconfiguration

Write a startup validator that checks all required environment variables are present and raises a single error listing every missing one.

Solution
import os
from dataclasses import dataclass
from typing import Optional

@dataclass
class EnvSpec:
name: str
required: bool = True
default: Optional[str] = None
description: str = ''

REQUIRED_ENV = [
EnvSpec('DATABASE_URL', required=True, description='PostgreSQL connection string'),
EnvSpec('SECRET_KEY', required=True, description='App signing key (min 32 chars)'),
EnvSpec('REDIS_URL', required=False, default='redis://localhost:6379', description='Cache URL'),
EnvSpec('LOG_LEVEL', required=False, default='INFO', description='Logging verbosity'),
]

def validate_env(specs: list[EnvSpec]) -> dict[str, str]:
missing = []
config = {}
for spec in specs:
value = os.environ.get(spec.name)
if value is None:
if spec.required:
missing.append(f"{spec.name} ({spec.description})")
else:
config[spec.name] = spec.default or ''
else:
config[spec.name] = value
if missing:
raise EnvironmentError(
"Missing required environment variables: " + ", ".join(missing)
)
return config

os.environ.pop('DATABASE_URL', None)
os.environ.pop('SECRET_KEY', None)

try:
config = validate_env(REQUIRED_ENV)
except EnvironmentError as e:
print(f"Startup failed: {e}")

Fail-fast configuration: A process that starts without its secrets will fail later — often mid-request, in production, under load. Validating all required env vars at startup (before accepting any traffic) converts a runtime crash into a deployment-time error, which is vastly easier to debug. Libraries like pydantic-settings provide this pattern with type coercion built in.

import os
from dataclasses import dataclass
from typing import Optional

@dataclass
class EnvSpec:
  name: str
  required: bool = True
  default: Optional[str] = None
  description: str = ''

REQUIRED_ENV = [
  EnvSpec('DATABASE_URL', required=True, description='PostgreSQL connection string'),
  EnvSpec('SECRET_KEY', required=True, description='App signing key (min 32 chars)'),
  EnvSpec('REDIS_URL', required=False, default='redis://localhost:6379', description='Cache URL'),
  EnvSpec('LOG_LEVEL', required=False, default='INFO', description='Logging verbosity'),
]

def validate_env(specs: list[EnvSpec]) -> dict[str, str]:
  """
  Validate all required env vars are present.
  Collect ALL missing vars before raising (don't stop at first error).
  Apply defaults for optional vars.
  Raise EnvironmentError listing every missing required var.
  """
  pass

# Test: missing DATABASE_URL and SECRET_KEY
os.environ.pop('DATABASE_URL', None)
os.environ.pop('SECRET_KEY', None)

try:
  config = validate_env(REQUIRED_ENV)
except EnvironmentError as e:
  print(f"Startup failed: {e}")
Expected Output
Startup failed: Missing required environment variables: DATABASE_URL (PostgreSQL connection string), SECRET_KEY (App signing key (min 32 chars))
Hints

Hint 1: Collect all missing required vars into a list first — report them all at once.

Hint 2: For optional vars with defaults, use os.environ.setdefault(name, default) or os.environ.get.

Hint 3: Raise EnvironmentError (not ValueError) — it is semantically correct for missing env config.


#8Masked Secret Repr — Prevent Accidental ExposureMedium
reprmaskingsecretsdataclass

Create a Secret class that wraps a sensitive string and prevents it from being accidentally logged or printed.

Solution
from dataclasses import dataclass

class Secret:
def __init__(self, value: str) -> None:
# Use object.__setattr__ to bypass any potential __setattr__ overrides
object.__setattr__(self, '_value', value)

def __repr__(self) -> str:
return "Secret('***')"

def __str__(self) -> str:
return '***'

def reveal(self) -> str:
return object.__getattribute__(self, '_value')

def __eq__(self, other: object) -> bool:
if isinstance(other, Secret):
return self.reveal() == other.reveal()
return NotImplemented

api_key = Secret('sk-live-super-secret-key-abc123')
print(f"repr: {repr(api_key)}")
print(f"str: {str(api_key)}")
print(f"revealed: {api_key.reveal()}")
print(f"equality works: {api_key == Secret('sk-live-super-secret-key-abc123')}")
print(f"different values: {api_key == Secret('other')}")

Production usage: Pydantic's SecretStr implements exactly this pattern — str(secret) returns **********, secret.get_secret_value() returns the real value. Using SecretStr in your Pydantic models means secrets never appear in model .dict() output, log lines using f"{model}", or Sentry error reports that capture local variables.

from dataclasses import dataclass

@dataclass
class Secret:
  """
  A wrapper for sensitive string values that:
  - Masks the value in __repr__ and __str__
  - Provides .reveal() to get the actual value
  - Is never accidentally printed in full
  """
  _value: str

  def __init__(self, value: str) -> None:
      pass

  def __repr__(self) -> str:
      pass

  def __str__(self) -> str:
      pass

  def reveal(self) -> str:
      pass

  def __eq__(self, other: object) -> bool:
      pass

api_key = Secret('sk-live-super-secret-key-abc123')
print(f"repr: {repr(api_key)}")
print(f"str: {str(api_key)}")
print(f"revealed: {api_key.reveal()}")
print(f"equality works: {api_key == Secret('sk-live-super-secret-key-abc123')}")
print(f"different values: {api_key == Secret('other')}")
Expected Output
repr: Secret('***')
str: ***
revealed: sk-live-super-secret-key-abc123
equality works: True
different values: False
Hints

Hint 1: Store the real value in a private attribute (e.g., _value) in __init__.

Hint 2: __repr__ should return Secret("***") — never the real value.

Hint 3: reveal() simply returns self._value — the only intentional exposure point.


Hard

#9Encrypted Secrets File with FernetHard
fernetencryptioncryptographysecrets-file

Implement an encrypted secrets file using Fernet symmetric encryption with PBKDF2 key derivation.

Solution
from cryptography.fernet import Fernet
import json
import base64
import hashlib

class EncryptedSecretsFile:
def __init__(self, passphrase: str) -> None:
self._fernet = self._make_fernet(passphrase)

def _make_fernet(self, passphrase: str) -> Fernet:
key_bytes = hashlib.pbkdf2_hmac(
'sha256',
passphrase.encode(),
b'fixed-salt-for-demo',
iterations=100_000,
dklen=32,
)
return Fernet(base64.urlsafe_b64encode(key_bytes))

def encrypt(self, secrets: dict) -> bytes:
plaintext = json.dumps(secrets).encode('utf-8')
return self._fernet.encrypt(plaintext)

def decrypt(self, ciphertext: bytes) -> dict:
plaintext = self._fernet.decrypt(ciphertext).decode('utf-8')
return json.loads(plaintext)

store = EncryptedSecretsFile(passphrase='my-master-password')
secrets = {'db_password': 'hunter2', 'api_key': 'sk-live-abc123'}

encrypted = store.encrypt(secrets)
print(f"encrypted type: {type(encrypted).__name__}")
print(f"is bytes: {isinstance(encrypted, bytes)}")

decrypted = store.decrypt(encrypted)
print(f"db_password: {decrypted['db_password']}")
print(f"api_key: {decrypted['api_key']}")

try:
wrong_store = EncryptedSecretsFile(passphrase='wrong-password')
wrong_store.decrypt(encrypted)
print("should not reach here")
except Exception:
print(f"wrong key rejected: True")

Production notes: In real systems, store the PBKDF2 salt alongside the ciphertext (not hardcoded). Tools like sops and age use this pattern: the salt and encrypted content travel together as a single blob. Fernet uses AES-128-CBC with HMAC-SHA256 — it provides authenticated encryption, so tampering is detected before decryption even begins.

from cryptography.fernet import Fernet
import json
import base64

class EncryptedSecretsFile:
  """
  Encrypt/decrypt a JSON secrets file using Fernet symmetric encryption.
  The key is derived from a passphrase using PBKDF2.
  """

  def __init__(self, passphrase: str) -> None:
      self._fernet = self._make_fernet(passphrase)

  def _make_fernet(self, passphrase: str) -> Fernet:
      # Derive a 32-byte key from the passphrase using PBKDF2-HMAC-SHA256
      # Use a fixed salt for this exercise (in production, store salt alongside ciphertext)
      import hashlib
      key_bytes = hashlib.pbkdf2_hmac(
          'sha256',
          passphrase.encode(),
          b'fixed-salt-for-demo',  # production: random salt stored with ciphertext
          iterations=100_000,
          dklen=32,
      )
      return Fernet(base64.urlsafe_b64encode(key_bytes))

  def encrypt(self, secrets: dict) -> bytes:
      """Serialize secrets dict to JSON and encrypt."""
      pass

  def decrypt(self, ciphertext: bytes) -> dict:
      """Decrypt ciphertext and deserialize back to dict."""
      pass

# Usage
store = EncryptedSecretsFile(passphrase='my-master-password')
secrets = {'db_password': 'hunter2', 'api_key': 'sk-live-abc123'}

encrypted = store.encrypt(secrets)
print(f"encrypted type: {type(encrypted).__name__}")
print(f"is bytes: {isinstance(encrypted, bytes)}")

decrypted = store.decrypt(encrypted)
print(f"db_password: {decrypted['db_password']}")
print(f"api_key: {decrypted['api_key']}")

# Wrong key should fail
try:
  wrong_store = EncryptedSecretsFile(passphrase='wrong-password')
  wrong_store.decrypt(encrypted)
  print("should not reach here")
except Exception as e:
  print(f"wrong key rejected: True")
Expected Output
encrypted type: bytes
is bytes: True
db_password: hunter2
api_key: sk-live-abc123
wrong key rejected: True
Hints

Hint 1: Fernet.encrypt(data) takes bytes — encode your JSON string with .encode("utf-8") first.

Hint 2: Fernet.decrypt(token) returns bytes — decode with .decode("utf-8") then json.loads.

Hint 3: A wrong key raises cryptography.fernet.InvalidToken — catch Exception broadly for the test.


#10SOPS-Style Envelope EncryptionHard
envelope-encryptionkmssopsdata-encryption-key

Implement envelope encryption: a Data Encryption Key (DEK) encrypts the payload, and the DEK itself is wrapped by a Key Encryption Key (KEK).

Solution
from cryptography.fernet import Fernet
import json

class EnvelopeEncryption:
def __init__(self, kek: bytes) -> None:
self._kek_fernet = Fernet(kek)

def encrypt(self, payload: dict) -> dict:
# Step 1: Generate a random DEK
dek = Fernet.generate_key()
dek_fernet = Fernet(dek)

# Step 2: Encrypt the payload with the DEK
plaintext = json.dumps(payload).encode('utf-8')
encrypted_payload = dek_fernet.encrypt(plaintext)

# Step 3: Encrypt the DEK with the KEK
encrypted_dek = self._kek_fernet.encrypt(dek)

return {
'encrypted_dek': encrypted_dek.decode('utf-8'),
'encrypted_payload': encrypted_payload.decode('utf-8'),
}

def decrypt(self, envelope: dict) -> dict:
# Step 1: Decrypt DEK using KEK
dek = self._kek_fernet.decrypt(envelope['encrypted_dek'].encode('utf-8'))
dek_fernet = Fernet(dek)

# Step 2: Decrypt payload using DEK
plaintext = dek_fernet.decrypt(envelope['encrypted_payload'].encode('utf-8'))
return json.loads(plaintext.decode('utf-8'))

kek = Fernet.generate_key()
engine = EnvelopeEncryption(kek)

secrets = {'db_pass': 's3cret', 'token': 'abc123'}
envelope = engine.encrypt(secrets)
print(f"envelope keys: {sorted(envelope.keys())}")
print(f"encrypted_dek present: {'encrypted_dek' in envelope}")
print(f"encrypted_payload present: {'encrypted_payload' in envelope}")

recovered = engine.decrypt(envelope)
print(f"db_pass: {recovered['db_pass']}")
print(f"token: {recovered['token']}")

Why envelope encryption: The KEK (master key) never leaves KMS — only the tiny encrypted DEK travels with the data. To re-encrypt with a new master key (key rotation), you only re-encrypt the DEK — not the potentially huge payload. SOPS, AWS Secrets Manager, GCP Secret Manager, and Kubernetes Secrets (with KMS plugin) all use this pattern.

from cryptography.fernet import Fernet
import json
import base64

# Envelope encryption: a Data Encryption Key (DEK) encrypts the payload,
# and the DEK itself is encrypted with a Key Encryption Key (KEK).
# This models AWS KMS / GCP KMS / SOPS behaviour.

class EnvelopeEncryption:
  def __init__(self, kek: bytes) -> None:
      """kek: Key Encryption Key (Fernet key, simulating a KMS master key)"""
      self._kek_fernet = Fernet(kek)

  def encrypt(self, payload: dict) -> dict:
      """
      1. Generate a random DEK (Fernet key)
      2. Encrypt the payload with the DEK
      3. Encrypt the DEK with the KEK
      4. Return: {encrypted_dek, encrypted_payload}
      """
      pass

  def decrypt(self, envelope: dict) -> dict:
      """
      1. Decrypt the DEK using the KEK
      2. Decrypt the payload using the DEK
      3. Return the original payload dict
      """
      pass

# Simulate a KEK (would be stored in KMS, never locally)
kek = Fernet.generate_key()
engine = EnvelopeEncryption(kek)

secrets = {'db_pass': 's3cret', 'token': 'abc123'}
envelope = engine.encrypt(secrets)
print(f"envelope keys: {sorted(envelope.keys())}")
print(f"encrypted_dek present: {'encrypted_dek' in envelope}")
print(f"encrypted_payload present: {'encrypted_payload' in envelope}")

recovered = engine.decrypt(envelope)
print(f"db_pass: {recovered['db_pass']}")
print(f"token: {recovered['token']}")
Expected Output
envelope keys: ['encrypted_dek', 'encrypted_payload']
encrypted_dek present: True
encrypted_payload present: True
db_pass: s3cret
token: abc123
Hints

Hint 1: Generate a fresh DEK with Fernet.generate_key() on every encrypt call.

Hint 2: Use one Fernet instance (with DEK) to encrypt the payload, another (with KEK) to encrypt the DEK.

Hint 3: Store both as base64 strings (or raw bytes) in the returned envelope dict.


#11Secret Scanner — Detect Hardcoded Credentials in CodeHard
secret-scanningregexbanditstatic-analysis

Build a secret scanner that detects hardcoded credentials in Python source code using a set of regex rules, similar to detect-secrets or truffleHog.

Solution
import re
from dataclasses import dataclass

@dataclass
class SecretFinding:
line_number: int
rule_name: str
matched_text: str
severity: str

SECRET_RULES = [
('aws_access_key', r'AKIA[0-9A-Z]{16}', 'high'),
('generic_api_key', r'(?i)(api[_-]?key|apikey)\s*=\s*["\'][^"\']{8,}["\']', 'high'),
('generic_password', r'(?i)(password|passwd|pwd)\s*=\s*["\'][^"\']{4,}["\']', 'medium'),
('private_key_header',r'-----BEGIN (RSA |EC )?PRIVATE KEY-----', 'high'),
('jwt_token', r'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}', 'medium'),
]

def scan_code(source: str) -> list[SecretFinding]:
findings = []
for line_number, line in enumerate(source.splitlines(), 1):
for rule_name, pattern, severity in SECRET_RULES:
if re.search(pattern, line):
findings.append(SecretFinding(
line_number=line_number,
rule_name=rule_name,
matched_text=line.strip(),
severity=severity,
))
return findings

SAMPLE_CODE = '''
import os

# Good — from environment
db_host = os.environ.get('DB_HOST')

# Bad — hardcoded password
password = "MySecretPass123"

# Bad — AWS key
aws_key = "AKIAIOSFODNN7EXAMPLE"

# Bad — API key
api_key = "sk-live-abcdefghijklmnop"

# OK — placeholder
secret = os.environ['SECRET_KEY']
'''

findings = scan_code(SAMPLE_CODE)
for f in findings:
print(f"Line {f.line_number}: [{f.severity.upper()}] {f.rule_name}{f.matched_text[:40]}")

Production secret scanning: Tools like detect-secrets, truffleHog, gitleaks, and GitHub's push protection use entropy analysis on top of regex — strings with Shannon entropy above a threshold are flagged even without known patterns. Integrate scanning into pre-commit hooks and CI so secrets are caught before they ever reach the repo, not after.

import re
from dataclasses import dataclass, field

@dataclass
class SecretFinding:
  line_number: int
  rule_name: str
  matched_text: str
  severity: str  # 'high' | 'medium' | 'low'

SECRET_RULES = [
  # (rule_name, pattern, severity)
  ('aws_access_key',    r'AKIA[0-9A-Z]{16}',               'high'),
  ('generic_api_key',   r'(?i)(api[_-]?key|apikey)s*=s*["'][^"']{8,}["']', 'high'),
  ('generic_password',  r'(?i)(password|passwd|pwd)s*=s*["'][^"']{4,}["']', 'medium'),
  ('private_key_header',r'-----BEGIN (RSA |EC )?PRIVATE KEY-----',               'high'),
  ('jwt_token',         r'eyJ[A-Za-z0-9_-]{10,}.[A-Za-z0-9_-]{10,}.[A-Za-z0-9_-]{10,}', 'medium'),
]

def scan_code(source: str) -> list[SecretFinding]:
  """
  Scan source code line by line for secret patterns.
  Return a list of SecretFinding for every match found.
  """
  pass

SAMPLE_CODE = '''
import os

# Good — from environment
db_host = os.environ.get('DB_HOST')

# Bad — hardcoded password
password = "MySecretPass123"

# Bad — AWS key
aws_key = "AKIAIOSFODNN7EXAMPLE"

# Bad — API key
api_key = "sk-live-abcdefghijklmnop"

# OK — placeholder
secret = os.environ['SECRET_KEY']
'''

findings = scan_code(SAMPLE_CODE)
for f in findings:
  print(f"Line {f.line_number}: [{f.severity.upper()}] {f.rule_name} — {f.matched_text[:40]}")
Expected Output
Line 8: [MEDIUM] generic_password — password = "MySecretPass123"
Line 11: [HIGH] aws_access_key — AKIAIOSFODNN7EXAMPLE
Line 14: [HIGH] generic_api_key — api_key = "sk-live-abcdefghijklmnop"
Hints

Hint 1: Iterate over source.splitlines() with enumerate(lines, 1) to get 1-based line numbers.

Hint 2: For each line, check every rule using re.search(pattern, line).

Hint 3: matched_text should be line.strip() — the whole trimmed line, not just the regex match group.

© 2026 EngineersOfAI. All rights reserved.