secrets_demo.py

Download
python 417 lines 12.9 KB
  1"""
  2시크릿 관리 데모
  3Secrets Management Demo
  4
  5환경 변수 로딩, 시크릿 강도 검증, Git secrets 패턴 스캐너,
  6설정 파일 암호화, .env 파일 파서 등 시크릿 관리 기법을 구현합니다.
  7
  8Demonstrates environment variable loading, secret strength validation,
  9git secrets pattern scanning, config file encryption, and .env file parsing.
 10"""
 11
 12import os
 13import re
 14import string
 15import math
 16import hashlib
 17import hmac
 18import base64
 19import json
 20import tempfile
 21from pathlib import Path
 22
 23
 24# =============================================================================
 25# 1. Environment Variable Loading Simulation (환경 변수 로딩 시뮬레이션)
 26# =============================================================================
 27
 28class EnvLoader:
 29    """
 30    Load configuration from environment variables with defaults and validation.
 31    Simulates best practices for 12-factor app config management.
 32    """
 33
 34    def __init__(self):
 35        self.loaded: dict[str, str] = {}
 36        self.missing: list[str] = []
 37
 38    def get(self, key: str, default: str | None = None,
 39            required: bool = False) -> str | None:
 40        """Retrieve an environment variable with optional default."""
 41        value = os.environ.get(key, default)
 42        if value is not None:
 43            self.loaded[key] = "(set)" if "SECRET" in key or "KEY" in key else value
 44        elif required:
 45            self.missing.append(key)
 46        return value
 47
 48    def report(self) -> str:
 49        lines = ["  Environment Variable Report:"]
 50        for k, v in self.loaded.items():
 51            lines.append(f"    {k:30s} = {v}")
 52        if self.missing:
 53            lines.append(f"\n    MISSING (required): {', '.join(self.missing)}")
 54        return "\n".join(lines)
 55
 56
 57def demo_env_loading():
 58    print("=" * 60)
 59    print("1. Environment Variable Loading")
 60    print("=" * 60)
 61
 62    # Set some simulated env vars for demo
 63    os.environ["APP_DB_HOST"] = "localhost"
 64    os.environ["APP_DB_PORT"] = "5432"
 65    os.environ["APP_SECRET_KEY"] = "demo-secret-abc123"
 66
 67    loader = EnvLoader()
 68    loader.get("APP_DB_HOST", required=True)
 69    loader.get("APP_DB_PORT", default="5432")
 70    loader.get("APP_SECRET_KEY", required=True)
 71    loader.get("APP_DB_PASSWORD", required=True)  # Will be missing
 72    loader.get("APP_DEBUG", default="false")
 73
 74    print(f"\n{loader.report()}")
 75
 76    # Cleanup
 77    for key in ["APP_DB_HOST", "APP_DB_PORT", "APP_SECRET_KEY"]:
 78        os.environ.pop(key, None)
 79    print()
 80
 81
 82# =============================================================================
 83# 2. Secret Strength Validation (시크릿 강도 검증)
 84# =============================================================================
 85
 86def calculate_entropy(secret: str) -> float:
 87    """Calculate Shannon entropy (bits) of a string."""
 88    if not secret:
 89        return 0.0
 90    freq: dict[str, int] = {}
 91    for ch in secret:
 92        freq[ch] = freq.get(ch, 0) + 1
 93    length = len(secret)
 94    entropy = -sum(
 95        (count / length) * math.log2(count / length)
 96        for count in freq.values()
 97    )
 98    return entropy * length  # Total bits
 99
100
101def validate_secret_strength(secret: str) -> dict:
102    """Evaluate the strength of a secret/password/API key."""
103    checks = {
104        "length >= 16": len(secret) >= 16,
105        "has uppercase": bool(re.search(r'[A-Z]', secret)),
106        "has lowercase": bool(re.search(r'[a-z]', secret)),
107        "has digits": bool(re.search(r'[0-9]', secret)),
108        "has special chars": bool(re.search(r'[^a-zA-Z0-9]', secret)),
109        "no common patterns": not re.search(
110            r'(password|secret|admin|12345|qwerty)', secret, re.IGNORECASE
111        ),
112    }
113    entropy = calculate_entropy(secret)
114
115    passed = sum(checks.values())
116    total = len(checks)
117
118    if passed == total and entropy >= 60:
119        grade = "STRONG"
120    elif passed >= 4 and entropy >= 40:
121        grade = "MODERATE"
122    else:
123        grade = "WEAK"
124
125    return {"checks": checks, "entropy_bits": entropy, "grade": grade}
126
127
128def demo_secret_strength():
129    print("=" * 60)
130    print("2. Secret Strength Validation")
131    print("=" * 60)
132
133    test_secrets = [
134        "password123",
135        "MyS3cret!",
136        "xK9#mPq2$vL7nR4@wB6j",
137        "aaaaaaaaaaaaaaaa",
138        "Tr0ub4dor&3",
139    ]
140
141    for secret in test_secrets:
142        result = validate_secret_strength(secret)
143        display = secret if len(secret) <= 25 else secret[:22] + "..."
144        print(f"\n  Secret: {display}")
145        print(f"    Entropy: {result['entropy_bits']:.1f} bits | Grade: {result['grade']}")
146        for check, passed in result["checks"].items():
147            symbol = "OK" if passed else "--"
148            print(f"      [{symbol}] {check}")
149    print()
150
151
152# =============================================================================
153# 3. Git Secrets Pattern Scanner (Git 시크릿 패턴 스캐너)
154# =============================================================================
155
156# Patterns that indicate leaked secrets in code
157SECRET_PATTERNS = [
158    (r'(?i)(api[_-]?key|apikey)\s*[=:]\s*["\']?[A-Za-z0-9_\-]{16,}',
159     "API Key"),
160    (r'(?i)(secret[_-]?key|secret)\s*[=:]\s*["\']?[A-Za-z0-9_\-]{8,}',
161     "Secret Key"),
162    (r'(?i)(password|passwd|pwd)\s*[=:]\s*["\']?[^\s"\']{4,}',
163     "Password"),
164    (r'(?i)aws[_-]?access[_-]?key[_-]?id\s*[=:]\s*["\']?AKIA[A-Z0-9]{16}',
165     "AWS Access Key"),
166    (r'(?i)ghp_[A-Za-z0-9]{36}',
167     "GitHub Personal Access Token"),
168    (r'(?i)sk-[A-Za-z0-9]{32,}',
169     "OpenAI API Key Pattern"),
170    (r'-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----',
171     "Private Key"),
172    (r'(?i)(jdbc|mysql|postgres)://[^\s]+:[^\s]+@',
173     "Database Connection String with Credentials"),
174]
175
176
177def scan_text_for_secrets(text: str, filename: str = "<input>") -> list[dict]:
178    """Scan text content for potential secret patterns."""
179    findings = []
180    for line_num, line in enumerate(text.splitlines(), 1):
181        for pattern, label in SECRET_PATTERNS:
182            if re.search(pattern, line):
183                findings.append({
184                    "file": filename,
185                    "line": line_num,
186                    "type": label,
187                    "content": line.strip()[:80],
188                })
189    return findings
190
191
192def demo_git_secrets_scanner():
193    print("=" * 60)
194    print("3. Git Secrets Pattern Scanner")
195    print("=" * 60)
196
197    sample_code = '''
198# config.py
199DATABASE_URL = "postgres://admin:SuperSecret123@db.example.com/mydb"
200API_KEY = "sk-abc123def456ghi789jkl012mno345pqr678"
201DEBUG = True
202
203# Safe reference (no actual secret)
204password = os.environ.get("DB_PASSWORD")
205
206# Dangerous: hardcoded AWS key
207AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
208ghp_EXAMPLE_TOKEN_NOT_REAL_REPLACE_ME_1234
209
210-----BEGIN RSA PRIVATE KEY-----
211MIIEowIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF...
212    '''
213
214    print("\n  Scanning sample code for secret patterns...\n")
215    findings = scan_text_for_secrets(sample_code, "config.py")
216
217    if findings:
218        for f in findings:
219            print(f"    [{f['type']}]")
220            print(f"      File: {f['file']}  Line: {f['line']}")
221            content = f['content'] if len(f['content']) <= 60 else f['content'][:57] + "..."
222            print(f"      Content: {content}\n")
223        print(f"  Total findings: {len(findings)}")
224    else:
225        print("  No secrets detected.")
226    print()
227
228
229# =============================================================================
230# 4. Config File Encryption (설정 파일 암호화)
231# =============================================================================
232# Uses a simple XOR-based approach as a stdlib-only fallback.
233# In production, use the 'cryptography' library with Fernet.
234
235def derive_key(password: str, salt: bytes) -> bytes:
236    """Derive a 32-byte key from password using PBKDF2."""
237    return hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100_000)
238
239
240def encrypt_config(config_data: dict, password: str) -> bytes:
241    """Encrypt a config dictionary using PBKDF2 + HMAC-authenticated XOR stream."""
242    salt = os.urandom(16)
243    key = derive_key(password, salt)
244
245    plaintext = json.dumps(config_data).encode()
246
247    # Generate keystream via SHA-256 counter mode
248    ciphertext = bytearray()
249    for i in range(0, len(plaintext), 32):
250        block_key = hashlib.sha256(key + i.to_bytes(4, "big")).digest()
251        chunk = plaintext[i:i + 32]
252        ciphertext.extend(b ^ k for b, k in zip(chunk, block_key))
253
254    # HMAC for integrity
255    mac = hmac.new(key, bytes(ciphertext), hashlib.sha256).digest()
256
257    # Format: salt (16) + mac (32) + ciphertext
258    return salt + mac + bytes(ciphertext)
259
260
261def decrypt_config(encrypted: bytes, password: str) -> dict | None:
262    """Decrypt an encrypted config. Returns None if integrity check fails."""
263    salt = encrypted[:16]
264    stored_mac = encrypted[16:48]
265    ciphertext = encrypted[48:]
266
267    key = derive_key(password, salt)
268
269    # Verify HMAC first
270    computed_mac = hmac.new(key, ciphertext, hashlib.sha256).digest()
271    if not hmac.compare_digest(stored_mac, computed_mac):
272        return None  # Integrity check failed
273
274    # Decrypt
275    plaintext = bytearray()
276    for i in range(0, len(ciphertext), 32):
277        block_key = hashlib.sha256(key + i.to_bytes(4, "big")).digest()
278        chunk = ciphertext[i:i + 32]
279        plaintext.extend(b ^ k for b, k in zip(chunk, block_key))
280
281    return json.loads(bytes(plaintext))
282
283
284def demo_config_encryption():
285    print("=" * 60)
286    print("4. Config File Encryption (stdlib-only)")
287    print("=" * 60)
288
289    config = {
290        "database_url": "postgres://user:pass@host/db",
291        "api_secret": "my-super-secret-key-12345",
292        "smtp_password": "mail_pass_789",
293    }
294
295    password = "master-encryption-password"
296
297    print(f"\n  Original config keys: {list(config.keys())}")
298    encrypted = encrypt_config(config, password)
299    print(f"  Encrypted size: {len(encrypted)} bytes")
300    print(f"  Encrypted (b64, first 60 chars): {base64.b64encode(encrypted).decode()[:60]}...")
301
302    # Decrypt with correct password
303    decrypted = decrypt_config(encrypted, password)
304    print(f"\n  Decrypted with correct password: {decrypted is not None}")
305    if decrypted:
306        print(f"    Keys recovered: {list(decrypted.keys())}")
307        print(f"    Values match: {decrypted == config}")
308
309    # Decrypt with wrong password
310    bad_result = decrypt_config(encrypted, "wrong-password")
311    print(f"  Decrypted with wrong password:  {bad_result is not None} (integrity check)")
312    print()
313
314
315# =============================================================================
316# 5. .env File Parser (.env 파일 파서)
317# =============================================================================
318
319def parse_env_file(content: str) -> dict[str, str]:
320    """
321    Parse a .env file content into a dictionary.
322    Supports comments, quoted values, and export prefix.
323    """
324    env_vars: dict[str, str] = {}
325
326    for line_num, line in enumerate(content.splitlines(), 1):
327        line = line.strip()
328
329        # Skip empty lines and comments
330        if not line or line.startswith("#"):
331            continue
332
333        # Remove optional 'export ' prefix
334        if line.startswith("export "):
335            line = line[7:]
336
337        # Split on first '='
338        if "=" not in line:
339            continue
340
341        key, value = line.split("=", 1)
342        key = key.strip()
343        value = value.strip()
344
345        # Remove surrounding quotes
346        if (value.startswith('"') and value.endswith('"')) or \
347           (value.startswith("'") and value.endswith("'")):
348            value = value[1:-1]
349
350        # Validate key format
351        if re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', key):
352            env_vars[key] = value
353
354    return env_vars
355
356
357def demo_env_parser():
358    print("=" * 60)
359    print("5. .env File Parser")
360    print("=" * 60)
361
362    sample_env = """
363# Application Configuration
364APP_NAME="My Secure App"
365APP_ENV=production
366APP_DEBUG=false
367
368# Database
369export DB_HOST=localhost
370DB_PORT=5432
371DB_USER='admin'
372DB_PASSWORD="s3cur3_p@ss!"
373
374# API Keys
375API_KEY=abc123def456
376SECRET_KEY="my-secret-key-value"
377
378# Invalid lines (skipped)
379not a valid line
380123INVALID=starts_with_digit
381"""
382
383    print(f"\n  Parsing sample .env file...\n")
384    parsed = parse_env_file(sample_env)
385
386    for key, value in parsed.items():
387        # Mask sensitive values
388        if any(s in key.upper() for s in ["PASSWORD", "SECRET", "KEY"]):
389            display = value[:3] + "*" * (len(value) - 3)
390        else:
391            display = value
392        print(f"    {key:20s} = {display}")
393
394    print(f"\n  Total variables parsed: {len(parsed)}")
395    print()
396
397
398# =============================================================================
399# Main
400# =============================================================================
401
402if __name__ == "__main__":
403    print("\n" + "=" * 60)
404    print("  Secrets Management Demo")
405    print("  시크릿 관리 데모")
406    print("=" * 60 + "\n")
407
408    demo_env_loading()
409    demo_secret_strength()
410    demo_git_secrets_scanner()
411    demo_config_encryption()
412    demo_env_parser()
413
414    print("=" * 60)
415    print("  Demo complete. All examples use stdlib only.")
416    print("=" * 60)