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)