Cryptography ๊ธฐ์ด
Cryptography ๊ธฐ์ด¶
์ด์ : 01. Security ๊ธฐ์ด | ๋ค์: 03. Hashing๊ณผ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ
์ํธํ์ ์๋๋ ๋น์ฌ์๋ง ์ ๊ทผํ ์ ์๋๋ก ํต์ ๊ณผ ๋ฐ์ดํฐ๋ฅผ ๋ณดํธํ๋ ๊ดํ์ ๋๋ค. ์ด ๋ ์จ์ ํ๋ ์ํธํ์ ๋ ๊ฐ์ง ์ฃผ์ ๋ถ์ผ์ธ ๋์นญ ๋ฐ ๋น๋์นญ ์ํธํ์ ํค ๊ตํ ํ๋กํ ์ฝ, ๋์งํธ ์๋ช , ๊ทธ๋ฆฌ๊ณ ์ค์ฉ์ ์ธ Python ๊ตฌํ์ ๋ค๋ฃน๋๋ค. ์ด ๋ ์จ์ด ๋๋๋ฉด ์ฃผ์ด์ง ๋ฌธ์ ์ ๋ํด ์ฌ๋ฐ๋ฅธ ์ํธํ ๊ธฐ๋ณธ ์์๋ฅผ ์ ํํ๊ณ ๊ฐ์ฅ ์ผ๋ฐ์ ์ธ ํจ์ ์ ํผํ ์ ์๊ฒ ๋ฉ๋๋ค.
๋์ด๋: โญโญโญ
ํ์ต ๋ชฉํ: - ๋์นญ ๋ฐ ๋น๋์นญ ์ํธํ์ ์ฐจ์ด์ ์ดํดํ๊ธฐ - Python์์ AES-GCM๊ณผ ChaCha20-Poly1305 ์ํธํ ๊ตฌํํ๊ธฐ - ๋น๋์นญ ์ํธํ์ ์ํ RSA, ECDSA, Ed25519 ์ดํดํ๊ธฐ - Diffie-Hellman๊ณผ ECDH ํค ๊ตํ ๊ตฌํํ๊ธฐ - ๋์งํธ ์๋ช ์์ฑ ๋ฐ ๊ฒ์ฆํ๊ธฐ - ์ผ๋ฐ์ ์ธ ์ํธํ์ ํจ์ ์ธ์ํ๊ณ ํผํ๊ธฐ - ํ๋ ์ํธํ ๊ถ์ฅ ์ฌํญ ์ ์ฉํ๊ธฐ
๋ชฉ์ฐจ¶
- ์ํธํ ๊ฐ์
- ๋์นญ ์ํธํ
- ๋ธ๋ก ์ํธ ์ด์ ๋ชจ๋
- AES-GCM: ํ๋ ํ์ค
- ChaCha20-Poly1305
- ๋น๋์นญ ์ํธํ
- RSA
- ํ์ ๊ณก์ ์ํธํ
- ํค ๊ตํ
- ๋์งํธ ์๋ช
- ์ผ๋ฐ์ ์ธ ํจ์ ๊ณผ ํผํ๋ ๋ฐฉ๋ฒ
- ํ๋ ๊ถ์ฅ ์ฌํญ
- ์ฐ์ต ๋ฌธ์
- ์ฐธ๊ณ ์๋ฃ
1. ์ํธํ ๊ฐ์¶
1.1 ์ ์ฒด ๊ตฌ์กฐ¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ํ๋ ์ํธํ ๋ถ๋ฅ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Cryptography โ
โ โโโ Symmetric (๊ณต์ ํค) โ
โ โ โโโ Block Ciphers โ
โ โ โ โโโ AES (128/192/256-bit key) โ
โ โ โ โโโ Modes: ECB, CBC, CTR, GCM, CCM โ
โ โ โ โโโ Legacy: DES, 3DES, Blowfish (ํผํ ๊ฒ) โ
โ โ โโโ Stream Ciphers โ
โ โ โโโ ChaCha20(-Poly1305) โ
โ โ โโโ Legacy: RC4 (๊นจ์ง, ํผํ ๊ฒ) โ
โ โ โ
โ โโโ Asymmetric (๊ณต๊ฐ/๊ฐ์ธ ํค ์) โ
โ โ โโโ Encryption โ
โ โ โ โโโ RSA-OAEP โ
โ โ โ โโโ ECIES (Elliptic Curve Integrated Encryption) โ
โ โ โโโ Digital Signatures โ
โ โ โ โโโ RSA-PSS โ
โ โ โ โโโ ECDSA (secp256r1, secp384r1) โ
โ โ โ โโโ Ed25519 / Ed448 โ
โ โ โโโ Key Exchange โ
โ โ โโโ Diffie-Hellman (DH) โ
โ โ โโโ ECDH (X25519, P-256) โ
โ โ โโโ Post-quantum: ML-KEM (CRYSTALS-Kyber) โ
โ โ โ
โ โโโ Hash Functions (๋ ์จ 03์์ ๋ค๋ฃธ) โ
โ โ โโโ SHA-2 (SHA-256, SHA-512) โ
โ โ โโโ SHA-3 (Keccak) โ
โ โ โโโ BLAKE2/BLAKE3 โ
โ โ โ
โ โโโ Key Derivation Functions โ
โ โโโ HKDF โ
โ โโโ PBKDF2 โ
โ โโโ scrypt / Argon2 (๋น๋ฐ๋ฒํธ ๊ธฐ๋ฐ) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
1.2 ํต์ฌ ๊ฐ๋ ¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ํต์ฌ ์ํธํ ๊ฐ๋
โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ์ฉ์ด โ ์ ์ โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Plaintext โ ์๋ณธ, ์ํธํ๋์ง ์์ ๋ฐ์ดํฐ โ
โ Ciphertext โ ์ํธํ๋ (์ฝ์ ์ ์๋) ์ถ๋ ฅ โ
โ Key โ ์ํธํ/๋ณตํธํ์ ์ฌ์ฉ๋๋ ๋น๋ฐ ๊ฐ โ
โ Nonce / IV โ ํ ๋ฒ๋ง ์ฌ์ฉ๋๋ ์ซ์; ๋์ผํ ํ๋ฌธ์ด โ
โ โ ๋์ผํ ์ํธ๋ฌธ์ ์์ฑํ๋ ๊ฒ์ ๋ฐฉ์ง โ
โ Authenticated enc. โ ๋ฌด๊ฒฐ์ฑ๋ ๊ฒ์ฆํ๋ ์ํธํ (AEAD) โ
โ Key derivation โ ๋น๋ฐ๋ฒํธ๋ ๋ค๋ฅธ ํค๋ก๋ถํฐ โ
โ โ ์ํธํ ํค๋ฅผ ์ ๋ โ
โ Forward secrecy โ ์ฅ๊ธฐ ํค์ ์นจํด๊ฐ ๊ณผ๊ฑฐ โ
โ โ ์ธ์
ํค๋ฅผ ์นจํดํ์ง ์์ โ
โโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
1.3 Python Cryptography ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์ ¶
์ด ๋ ์จ์ ๋ชจ๋ ์ฝ๋ ์์ ๋ ๋์ ์์ค์ ๋ ์ํผ์ ๋ฎ์ ์์ค์ ๊ธฐ๋ณธ ์์๋ฅผ ์ ๊ณตํ๋ cryptography ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
pip install cryptography
# Verify installation
import cryptography
print(f"cryptography version: {cryptography.__version__}")
# The library has two main layers:
# 1. High-level (recipes): cryptography.fernet, cryptography.hazmat.primitives.kdf
# 2. Low-level (hazmat): cryptography.hazmat.primitives.ciphers, asymmetric, etc.
#
# "hazmat" stands for "hazardous materials" - these primitives can be
# misused. Always prefer high-level APIs when available.
2. ๋์นญ ์ํธํ¶
๋์นญ ์ํธํ๋ ์ํธํ์ ๋ณตํธํ ๋ชจ๋์ ๋์ผํ ํค๋ฅผ ์ฌ์ฉํฉ๋๋ค. ๋น ๋ฅด๊ณ ๋๋์ ๋ฐ์ดํฐ๋ฅผ ์ํธํํ๋ ๋ฐ ์ ํฉํฉ๋๋ค.
โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
โPlaintext โโโKeyโโโถโ Encrypt โโโโโโโโโโถโCiphertextโ
โ "Hello" โ โ (AES) โ โ 0xA3F1.. โ
โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
โCiphertextโโโKeyโโโถโ Decrypt โโโโโโโโโโถโPlaintext โ
โ 0xA3F1.. โ โ (AES) โ โ "Hello" โ
โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
๋ ์์
๋ชจ๋ ๋์ผํ ํค ์ฌ์ฉ!
2.1 AES (Advanced Encryption Standard)¶
AES๋ ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ ๋์นญ ์ํธ์ ๋๋ค. 128๋นํธ ๋ธ๋ก์์ ์๋ํ๋ฉฐ 128, 192 ๋๋ 256๋นํธ์ ํค ํฌ๊ธฐ๋ฅผ ์ง์ํฉ๋๋ค.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AES ํค ํฌ๊ธฐ โ
โโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ํค ํฌ๊ธฐ โ ๋ผ์ด๋ โ ๋ณด์ โ ์ฌ์ฉ ์ฌ๋ก โ
โโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 128-bit โ 10 โ ~128 bit โ ๋ฒ์ฉ, ๋น ๋ฆ โ
โ 192-bit โ 12 โ ~192 bit โ ์ค์ ๋ก ๊ฑฐ์ ์ฌ์ฉ๋์ง ์์ โ
โ 256-bit โ 14 โ ~256 bit โ ์ ๋ถ/๊ตฐ์ฌ, ์์ ์ดํ โ
โ โ โ โ ์ ํญ ๋ง์ง โ
โโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
2.2 Fernet: ๋์ ์์ค์ ๋์นญ ์ํธํ¶
๊ฐ๋จํ ์ฌ์ฉ ์ฌ๋ก์ ๊ฒฝ์ฐ Fernet ํด๋์ค๋ ๊ฐ๋จํ API๋ก ์ธ์ฆ๋ ์ํธํ๋ฅผ ์ ๊ณตํฉ๋๋ค.
from cryptography.fernet import Fernet
import base64
# Generate a random key (URL-safe base64 encoded)
key = Fernet.generate_key()
print(f"Key: {key.decode()}")
print(f"Key length: {len(base64.urlsafe_b64decode(key))} bytes = "
f"{len(base64.urlsafe_b64decode(key)) * 8} bits")
# Create cipher
cipher = Fernet(key)
# Encrypt
plaintext = b"Sensitive financial data: account balance $50,000"
ciphertext = cipher.encrypt(plaintext)
print(f"\nPlaintext: {plaintext.decode()}")
print(f"Ciphertext: {ciphertext[:60]}...")
print(f"Ciphertext length: {len(ciphertext)} bytes")
# Decrypt
decrypted = cipher.decrypt(ciphertext)
assert decrypted == plaintext
print(f"Decrypted: {decrypted.decode()}")
# Fernet includes a timestamp - you can set a TTL (time-to-live)
import time
token = cipher.encrypt(b"temporary secret")
# time.sleep(2) # Uncomment to test expiration
try:
cipher.decrypt(token, ttl=60) # Valid for 60 seconds
print("\nToken is still valid")
except Exception as e:
print(f"\nToken expired: {e}")
Fernet์ด ๋ด๋ถ์ ์ผ๋ก ํ๋ ์ผ:
1. ๋๋ค 128๋นํธ IV ์์ฑ
2. AES-128-CBC๋ก ์ํธํ
3. HMAC-SHA256์ผ๋ก ์ธ์ฆ
4. ์ฐ๊ฒฐ: version || timestamp || IV || ciphertext || HMAC
5. ๊ฒฐ๊ณผ๋ฅผ Base64 ์ธ์ฝ๋ฉ
3. ๋ธ๋ก ์ํธ ์ด์ ๋ชจ๋¶
๋ธ๋ก ์ํธ(AES ๊ฐ์)๋ ๊ณ ์ ํฌ๊ธฐ ๋ธ๋ก์ ์ํธํํฉ๋๋ค. ์ด์ ๋ชจ๋๋ ํ ๋ธ๋ก๋ณด๋ค ๊ธด ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ ์ํฉ๋๋ค.
3.1 ECB ๋ชจ๋ (์ ๋ ์ฌ์ฉํ์ง ๋ง์ธ์)¶
ECB(Electronic Codebook)๋ ๊ฐ ๋ธ๋ก์ ๋ ๋ฆฝ์ ์ผ๋ก ์ํธํํฉ๋๋ค. ๋์ผํ ํ๋ฌธ ๋ธ๋ก์ ๋์ผํ ์ํธ๋ฌธ ๋ธ๋ก์ ์์ฑํ์ฌ ํจํด์ ๋ ธ์ถํฉ๋๋ค.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ECB ๋ชจ๋ - ์ ๊นจ์ก๋๊ฐ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ ํ๋ฌธ ๋ธ๋ก: [AAAA] [BBBB] [AAAA] [CCCC] [AAAA] โ
โ โ โ โ โ โ โ
โ AES AES AES AES AES โ
โ โ โ โ โ โ โ
โ ์ํธ๋ฌธ ๋ธ๋ก: [X1X1] [Y2Y2] [X1X1] [Z3Z3] [X1X1] โ
โ โ
โ ๋ฌธ์ : ๋์ผํ ํ๋ฌธ ๋ธ๋ก โ ๋์ผํ ์ํธ๋ฌธ! โ
โ ๊ณต๊ฒฉ์๋ ๋ณตํธํ ์์ด ํจํด์ ๋ณผ ์ ์์ต๋๋ค. โ
โ โ
โ ๊ณ ์ ์ ์: ECB ์ํธํ๋ ๋นํธ๋งต ์ด๋ฏธ์ง๋ ๋ชจ์์ ๋ณด์กดํฉ๋๋ค โ
โ ์ธ์ ํ ๋์ผํ ํฝ์
์ด ๋์ผํ ์ํธ๋ฌธ์ ์์ฑํ๊ธฐ ๋๋ฌธ์
๋๋ค. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Demonstration: ECB leaks patterns
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import os
key = os.urandom(32) # AES-256
# ECB mode (DO NOT USE in production - for demonstration only)
def ecb_encrypt_block(key: bytes, plaintext: bytes) -> bytes:
"""Encrypt a single block with AES-ECB. For demonstration only!"""
cipher = Cipher(algorithms.AES(key), modes.ECB())
encryptor = cipher.encryptor()
return encryptor.update(plaintext) + encryptor.finalize()
# Same plaintext block produces same ciphertext
block1 = b"AAAAAAAAAAAAAAAA" # 16 bytes = 1 AES block
block2 = b"BBBBBBBBBBBBBBBB"
ct1a = ecb_encrypt_block(key, block1)
ct1b = ecb_encrypt_block(key, block1) # Same input
ct2 = ecb_encrypt_block(key, block2)
print("ECB Pattern Leak Demonstration:")
print(f" Block 'AAA...' โ {ct1a.hex()[:32]}...")
print(f" Block 'AAA...' โ {ct1b.hex()[:32]}... (SAME ciphertext!)")
print(f" Block 'BBB...' โ {ct2.hex()[:32]}... (different)")
print(f" ct1a == ct1b: {ct1a == ct1b}") # True - this is the problem
3.2 CBC ๋ชจ๋ (๋ ๊ฑฐ์, ์ฃผ์ํด์ ์ฌ์ฉ)¶
CBC(Cipher Block Chaining)๋ ์ํธํํ๊ธฐ ์ ์ ๊ฐ ํ๋ฌธ ๋ธ๋ก์ ์ด์ ์ํธ๋ฌธ ๋ธ๋ก๊ณผ XORํฉ๋๋ค. ๋๋ค IV๊ฐ ํ์ํฉ๋๋ค.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CBC ๋ชจ๋ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ IV โโโ โ
โ โผ โ
โ P1 โโโถ XOR โโโถ AES โโโถ C1 โ
โ โ โ
โ โผ โ
โ P2 โโโโโโโโโโโถ XOR โโโถ AES โโโถ C2 โ
โ โ โ
โ โผ โ
โ P3 โโโโโโโโโโโโโโโโโโโถ XOR โโโถ AES โโโถ C3 โ
โ โ
โ ๊ฐ ์ํธ๋ฌธ ๋ธ๋ก์ ๋ชจ๋ ์ด์ ๋ธ๋ก์ ์์กดํฉ๋๋ค. โ
โ ๋๋ค IV๋ ๋์ผํ ํ๋ฌธ์ด ๋ค๋ฅด๊ฒ ์ํธํ๋๋๋ก ๋ณด์ฅํฉ๋๋ค. โ
โ โ
โ โ ํจ๋ฉ ํ์ (์: PKCS#7) โ
โ โ ์ธ์ฆ๋์ง ์์ผ๋ฉด ํจ๋ฉ ์ค๋ผํด ๊ณต๊ฒฉ์ ์ทจ์ฝ โ
โ โ ์ํธํ๋ฅผ ๋ณ๋ ฌํํ ์ ์์ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
import os
def aes_cbc_encrypt(key: bytes, plaintext: bytes) -> tuple:
"""AES-CBC encryption with PKCS7 padding. Returns (iv, ciphertext)."""
iv = os.urandom(16) # Random 128-bit IV
# Pad plaintext to block size
padder = padding.PKCS7(128).padder()
padded = padder.update(plaintext) + padder.finalize()
# Encrypt
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
encryptor = cipher.encryptor()
ciphertext = encryptor.update(padded) + encryptor.finalize()
return iv, ciphertext
def aes_cbc_decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes:
"""AES-CBC decryption with PKCS7 unpadding."""
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
decryptor = cipher.decryptor()
padded = decryptor.update(ciphertext) + decryptor.finalize()
# Remove padding
unpadder = padding.PKCS7(128).unpadder()
plaintext = unpadder.update(padded) + unpadder.finalize()
return plaintext
# Usage
key = os.urandom(32) # AES-256
message = b"CBC mode requires padding and a random IV for each message"
iv, ct = aes_cbc_encrypt(key, message)
print(f"IV: {iv.hex()}")
print(f"Ciphertext: {ct.hex()[:64]}...")
pt = aes_cbc_decrypt(key, iv, ct)
print(f"Decrypted: {pt.decode()}")
# Same plaintext, different IV โ different ciphertext
iv2, ct2 = aes_cbc_encrypt(key, message)
print(f"\nSame message, new IV:")
print(f" ct1: {ct.hex()[:32]}...")
print(f" ct2: {ct2.hex()[:32]}...")
print(f" Same? {ct == ct2}") # False - good!
3.3 CTR ๋ชจ๋¶
CTR(Counter) ๋ชจ๋๋ ๋ธ๋ก ์ํธ๋ฅผ ์คํธ๋ฆผ ์ํธ๋ก ๋ณํํฉ๋๋ค. ๋ณ๋ ฌํ ๊ฐ๋ฅํ๋ฉฐ ํจ๋ฉ์ด ํ์ํ์ง ์์ต๋๋ค.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CTR ๋ชจ๋ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Nonce|Counter=0 โโโถ AES โโโถ Keystream0 โโXORโโโถ C0 โ
โ โฒ โ
โ โ โ
โ P0 โ
โ โ
โ Nonce|Counter=1 โโโถ AES โโโถ Keystream1 โโXORโโโถ C1 โ
โ โฒ โ
โ โ โ
โ P1 โ
โ โ
โ โ ๋ณ๋ ฌํ ๊ฐ๋ฅ (์ํธํ ๋ฐ ๋ณตํธํ) โ
โ โ ํจ๋ฉ ํ์ ์์ โ
โ โ ๋ชจ๋ ๋ธ๋ก์ผ๋ก ์ํฌ ๊ฐ๋ฅ โ
โ โ Nonce ์ฌ์ฌ์ฉ์ ์น๋ช
์ (๋ ํ๋ฌธ์ XOR ๋์ถ) โ
โ โ ์ธ์ฆ ์์ (๋์ GCM ์ฌ์ฉ) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
4. AES-GCM: ํ๋ ํ์ค¶
GCM(Galois/Counter Mode)์ CTR ๋ชจ๋ ์ํธํ์ GMAC ์ธ์ฆ์ ๊ฒฐํฉํฉ๋๋ค. ๋จ์ผ ์์ ์ผ๋ก ๊ธฐ๋ฐ์ฑ๊ณผ ๋ฌด๊ฒฐ์ฑ์ ๋ชจ๋ ์ ๊ณตํฉ๋๋ค. ์ด๋ฅผ Authenticated Encryption with Associated Data(AEAD)๋ผ๊ณ ํฉ๋๋ค.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AES-GCM (AEAD) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ ์
๋ ฅ: Key, Nonce (96-bit), Plaintext, AAD (์ ํ) โ
โ ์ถ๋ ฅ: Ciphertext, Authentication Tag (128-bit) โ
โ โ
โ โโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โ
โ โ Nonce โโโโโโถโ AES-CTR โโโโโโถโ Ciphertext โ โ
โ โโโโโโโโโโโ โ Encryption โ โโโโโโโโฌโโโโโโ โ
โ โโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโ โ โ
โ โ AAD โโโโโโโโโโโโ โ โ
โ โ(header) โ โผ โผ โ
โ โโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ GHASH โโโโโโถโ Auth โ โ
โ โ (GMAC) โ โ Tag โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ
โ AAD = Associated Authenticated Data โ
โ - ์ธ์ฆ๋์ง๋ง ์ํธํ๋์ง ์์ โ
โ - ์: ๋ฉ์์ง ํค๋, ํจํท ์ํ์ค ๋ฒํธ โ
โ - AAD ๋ณ์กฐ๋ ์ธ์ฆ ์คํจ๋ฅผ ์ ๋ฐ โ
โ โ
โ โ AEAD: ๊ธฐ๋ฐ์ฑ + ๋ฌด๊ฒฐ์ฑ + ์ง์ ์ฑ โ
โ โ ๋ณ๋ ฌํ ๊ฐ๋ฅ โ
โ โ ํ๋์จ์ด ๊ฐ์ (AES-NI) โ
โ โ Nonce๋ ํค๋น ๊ณ ์ ํด์ผ ํจ (์ ๋ ์ฌ์ฌ์ฉ ๊ธ์ง!) โ
โ โ 96๋นํธ nonce๋ ํค๋น ~2^32 ์ํธํ๋ก ์ ํ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
4.1 AES-GCM ๊ตฌํ¶
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def aes_gcm_encrypt(key: bytes, plaintext: bytes,
aad: bytes = None) -> tuple:
"""
Encrypt with AES-GCM.
Returns (nonce, ciphertext_with_tag).
"""
aesgcm = AESGCM(key)
nonce = os.urandom(12) # 96-bit nonce (NIST recommended)
ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
return nonce, ciphertext
def aes_gcm_decrypt(key: bytes, nonce: bytes, ciphertext: bytes,
aad: bytes = None) -> bytes:
"""
Decrypt with AES-GCM.
Raises InvalidTag if authentication fails.
"""
aesgcm = AESGCM(key)
return aesgcm.decrypt(nonce, ciphertext, aad)
# Generate a 256-bit key
key = AESGCM.generate_key(bit_length=256)
print(f"Key: {key.hex()} ({len(key) * 8} bits)")
# Encrypt a message
message = b"Top secret: The missile codes are 12345"
aad = b"message-id: 42, timestamp: 2026-01-15" # Authenticated but not encrypted
nonce, ciphertext = aes_gcm_encrypt(key, message, aad)
print(f"\nNonce: {nonce.hex()}")
print(f"Ciphertext: {ciphertext.hex()[:64]}...")
print(f"CT length: {len(ciphertext)} bytes "
f"(plaintext: {len(message)} + tag: 16)")
# Decrypt
plaintext = aes_gcm_decrypt(key, nonce, ciphertext, aad)
print(f"Decrypted: {plaintext.decode()}")
# Tamper with ciphertext โ authentication fails
tampered_ct = bytearray(ciphertext)
tampered_ct[0] ^= 0xFF # Flip bits in first byte
try:
aes_gcm_decrypt(key, nonce, bytes(tampered_ct), aad)
print("ERROR: Should have failed!")
except Exception as e:
print(f"\nTamper detected: {type(e).__name__}")
# Tamper with AAD โ authentication fails
try:
aes_gcm_decrypt(key, nonce, ciphertext, b"tampered AAD")
print("ERROR: Should have failed!")
except Exception as e:
print(f"AAD tamper detected: {type(e).__name__}")
4.2 AES-GCM์ ์ฌ์ฉํ ํ์ผ ์ํธํ¶
import os
import struct
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from pathlib import Path
class FileEncryptor:
"""Encrypt/decrypt files using AES-256-GCM."""
CHUNK_SIZE = 64 * 1024 # 64 KB chunks
NONCE_SIZE = 12
TAG_SIZE = 16
def __init__(self, key: bytes):
if len(key) != 32:
raise ValueError("Key must be 256 bits (32 bytes)")
self.aesgcm = AESGCM(key)
def encrypt_file(self, input_path: str, output_path: str) -> dict:
"""
Encrypt a file chunk by chunk.
Format: [nonce (12B)][chunk_count (4B)][encrypted_chunk1][encrypted_chunk2]...
"""
input_file = Path(input_path)
if not input_file.exists():
raise FileNotFoundError(f"Input file not found: {input_path}")
file_size = input_file.stat().st_size
chunks_written = 0
with open(input_path, 'rb') as fin, open(output_path, 'wb') as fout:
# Write file nonce (used as base; each chunk gets nonce + counter)
base_nonce = os.urandom(self.NONCE_SIZE)
fout.write(base_nonce)
# Placeholder for chunk count
chunk_count_pos = fout.tell()
fout.write(struct.pack('<I', 0)) # Will update later
while True:
chunk = fin.read(self.CHUNK_SIZE)
if not chunk:
break
# Derive unique nonce for this chunk
chunk_nonce = self._derive_chunk_nonce(base_nonce, chunks_written)
# AAD includes chunk index to prevent reordering
aad = struct.pack('<I', chunks_written)
encrypted = self.aesgcm.encrypt(chunk_nonce, chunk, aad)
# Write: [length (4B)][encrypted_data]
fout.write(struct.pack('<I', len(encrypted)))
fout.write(encrypted)
chunks_written += 1
# Update chunk count
fout.seek(chunk_count_pos)
fout.write(struct.pack('<I', chunks_written))
return {
"input_size": file_size,
"chunks": chunks_written,
"output_size": Path(output_path).stat().st_size
}
def decrypt_file(self, input_path: str, output_path: str) -> dict:
"""Decrypt a file encrypted with encrypt_file."""
with open(input_path, 'rb') as fin, open(output_path, 'wb') as fout:
base_nonce = fin.read(self.NONCE_SIZE)
chunk_count = struct.unpack('<I', fin.read(4))[0]
for i in range(chunk_count):
chunk_nonce = self._derive_chunk_nonce(base_nonce, i)
aad = struct.pack('<I', i)
enc_len = struct.unpack('<I', fin.read(4))[0]
encrypted = fin.read(enc_len)
decrypted = self.aesgcm.decrypt(chunk_nonce, encrypted, aad)
fout.write(decrypted)
return {"chunks_decrypted": chunk_count}
def _derive_chunk_nonce(self, base_nonce: bytes, chunk_index: int) -> bytes:
"""Derive a unique nonce for each chunk by XORing with chunk index."""
nonce_int = int.from_bytes(base_nonce, 'big') ^ chunk_index
return nonce_int.to_bytes(self.NONCE_SIZE, 'big')
# Usage example
key = AESGCM.generate_key(bit_length=256)
encryptor = FileEncryptor(key)
# Create a test file
test_data = b"Hello, encrypted world! " * 10000 # ~240 KB
with open("/tmp/test_plain.bin", "wb") as f:
f.write(test_data)
# Encrypt
info = encryptor.encrypt_file("/tmp/test_plain.bin", "/tmp/test_encrypted.bin")
print(f"Encrypted: {info}")
# Decrypt
info = encryptor.decrypt_file("/tmp/test_encrypted.bin", "/tmp/test_decrypted.bin")
print(f"Decrypted: {info}")
# Verify
with open("/tmp/test_decrypted.bin", "rb") as f:
decrypted_data = f.read()
assert decrypted_data == test_data
print("Verification: OK - decrypted data matches original")
# Clean up
for f in ["/tmp/test_plain.bin", "/tmp/test_encrypted.bin", "/tmp/test_decrypted.bin"]:
Path(f).unlink(missing_ok=True)
5. ChaCha20-Poly1305¶
ChaCha20-Poly1305๋ ChaCha20 ์คํธ๋ฆผ ์ํธ์ Poly1305 MAC์ ๊ฒฐํฉํ AEAD ์ํธ์ ๋๋ค. AES-GCM์ ์ฃผ์ ๋์์ ๋๋ค.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AES-GCM vs ChaCha20-Poly1305 โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ AES-256-GCM โ ChaCha20-Poly1305 โ
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ํค ํฌ๊ธฐ โ 256 bits โ 256 bits โ
โ Nonce ํฌ๊ธฐ โ 96 bits โ 96 bits โ
โ Tag ํฌ๊ธฐ โ 128 bits โ 128 bits โ
โ ์๋ (HW ๊ฐ์) โ ๋งค์ฐ ๋น ๋ฆ (AES-NI) โ AES-NI๋ก ๋๋ฆผ โ
โ ์๋ (์ํํธ์จ์ด)โ ๋๋ฆผ โ ๋น ๋ฆ (ํน์ HW ๋ถํ์) โ
โ ๋ชจ๋ฐ์ผ/์๋ฒ ๋๋ โ HW ์ง์ ํ์ โ ์ฐ์ (์์ ์ํํธ์จ์ด) โ
โ ์ฌ์ด๋ ์ฑ๋ โ ์ฃผ์ ํ์ (T-ํ
์ด๋ธ) โ ๋ณธ์ง์ ์ผ๋ก ์์ ์๊ฐ โ
โ ์ฌ์ฉ์ฒ โ TLS, IPsec, ๋์คํฌ ์โ TLS (Google/CF), WireGuardโ
โ Nonce ์ค์ฉ โ ์น๋ช
์ โ ์น๋ช
์ โ
โ ์์ ์ดํ โ PQ ์ ํญ ์์ โ PQ ์ ํญ ์์ โ
โ โ (๋จ๋
์ผ๋ก) โ (๋จ๋
์ผ๋ก) โ
โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
5.1 ChaCha20-Poly1305 ๊ตฌํ¶
import os
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
# Generate a 256-bit key
key = ChaCha20Poly1305.generate_key()
chacha = ChaCha20Poly1305(key)
# Encrypt
nonce = os.urandom(12) # 96-bit nonce
message = b"ChaCha20-Poly1305 is great for mobile and embedded devices"
aad = b"metadata: device=mobile, version=1"
ciphertext = chacha.encrypt(nonce, message, aad)
print(f"Plaintext: {message.decode()}")
print(f"Ciphertext: {ciphertext.hex()[:64]}...")
print(f"CT length: {len(ciphertext)} (plaintext {len(message)} + tag 16)")
# Decrypt
plaintext = chacha.decrypt(nonce, ciphertext, aad)
assert plaintext == message
print(f"Decrypted: {plaintext.decode()}")
# Tamper detection
tampered = bytearray(ciphertext)
tampered[-1] ^= 0x01
try:
chacha.decrypt(nonce, bytes(tampered), aad)
except Exception as e:
print(f"Tamper detected: {type(e).__name__}")
5.2 XChaCha20-Poly1305 (ํ์ฅ Nonce)¶
XChaCha20์ 192๋นํธ nonce๋ฅผ ์ฌ์ฉํฉ๋๋ค(96๋นํธ ๋๋น). ์ด๋ ํ์ค์ ์ธ ์ถฉ๋ ์ํ ์์ด ๋ฌด์์๋ก ์์ฑ๋ ์ ์์ ๋งํผ ์ถฉ๋ถํ ํฝ๋๋ค. ์ด๋ nonce ๊ด๋ฆฌ ๋ถ๋ด์ ์ ๊ฑฐํฉ๋๋ค.
# XChaCha20 is available through libsodium bindings (PyNaCl)
# pip install pynacl
import nacl.secret
import nacl.utils
# XChaCha20-Poly1305 with 192-bit random nonce
key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE) # 256-bit
box = nacl.secret.SecretBox(key)
# Encrypt - nonce is generated automatically (192-bit, random-safe)
message = b"With XChaCha20, random nonces are always safe"
encrypted = box.encrypt(message)
print(f"Nonce size: {box.NONCE_SIZE} bytes = {box.NONCE_SIZE * 8} bits")
print(f"Encrypted length: {len(encrypted)} bytes")
# Decrypt
decrypted = box.decrypt(encrypted)
print(f"Decrypted: {decrypted.decode()}")
6. ๋น๋์นญ ์ํธํ¶
๋น๋์นญ(๊ณต๊ฐํค) ์ํธํ๋ ํค ์์ ์ฌ์ฉํฉ๋๋ค: ์ํธํ(๋๋ ๊ฒ์ฆ)๋ฅผ ์ํ ๊ณต๊ฐ ํค์ ๋ณตํธํ(๋๋ ์๋ช )๋ฅผ ์ํ ๊ฐ์ธ ํค์ ๋๋ค.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๋น๋์นญ ์ํธํ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ ํค ์์ฑ โ
โ โโโโโโโโโโโโ โ
โ โ KeyGen() โโโโถ Private Key (๋น๋ฐ๋ก ์ ์ง!) โ
โ โ โโโโถ Public Key (์์ ๋กญ๊ฒ ๊ณต์ ) โ
โ โโโโโโโโโโโโ โ
โ โ
โ ์ํธํ (๋๊ตฌ๋ โ ํค ์์ ์) โ
โ โโโโโโโโโโโโโ Public Key โโโโโโโโโโโโ โ
โ โ Plaintext โโโโโโโโโโโโโโโโถโ Encrypt โโโโถ Ciphertext โ
โ โโโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ
โ ๋ณตํธํ (ํค ์์ ์๋ง) โ
โ โโโโโโโโโโโโโ Private Key โโโโโโโโโโโโ โ
โ โCiphertext โโโโโโโโโโโโโโโโถโ Decrypt โโโโถ Plaintext โ
โ โโโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ
โ ์๋ช
(ํค ์์ ์ โ ๋๊ตฌ๋ ๊ฒ์ฆ ๊ฐ๋ฅ) โ
โ โโโโโโโโโโโโโ Private Key โโโโโโโโโโโโ โ
โ โ Message โโโโโโโโโโโโโโโโถโ Sign โโโโถ Signature โ
โ โโโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ
โ ๊ฒ์ฆ (๊ณต๊ฐ ํค๋ฅผ ๊ฐ์ง ๋๊ตฌ๋) โ
โ โโโโโโโโโโโโโ Public Key โโโโโโโโโโโโ โ
โ โ Message + โโโโโโโโโโโโโโโโถโ Verify โโโโถ Valid / Invalid โ
โ โ Signature โ โโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
6.1 ํ์ด๋ธ๋ฆฌ๋ ์ํธํ¶
๋น๋์นญ ์ํธํ๋ ๋๋ฆฌ๊ณ ์ํธํํ ์ ์๋ ๋ฐ์ดํฐ ์์ด ์ ํ๋ฉ๋๋ค. ์ค์ ๋ก๋ ํ์ด๋ธ๋ฆฌ๋ ์ํธํ๋ฅผ ์ฌ์ฉํฉ๋๋ค: ๋์นญ ํค๋ก ๋ฐ์ดํฐ๋ฅผ ์ํธํํ ๋ค์ ์์ ์์ ๊ณต๊ฐ ํค๋ก ๋์นญ ํค๋ฅผ ์ํธํํฉ๋๋ค.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ํ์ด๋ธ๋ฆฌ๋ ์ํธํ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ ๋ฐ์ ์: โ
โ 1. ๋๋ค ๋์นญ ํค ์์ฑ (์: AES-256) โ
โ 2. ๋์นญ ํค๋ก ๋ฐ์ดํฐ ์ํธํ (AES-GCM) โ
โ 3. ์์ ์์ ๊ณต๊ฐ ํค๋ก ๋์นญ ํค ์ํธํ (RSA) โ
โ 4. ์ ์ก: [encrypted_key] + [encrypted_data] โ
โ โ
โ ์์ ์: โ
โ 1. ๊ฐ์ธ ํค๋ก ๋์นญ ํค ๋ณตํธํ (RSA) โ
โ 2. ๋์นญ ํค๋ก ๋ฐ์ดํฐ ๋ณตํธํ (AES-GCM) โ
โ โ
โ ์ด๊ฒ์ ๋ค์์ ์ ๊ณตํฉ๋๋ค: โ
โ - ๋๋ ๋ฐ์ดํฐ๋ฅผ ์ํ ๋์นญ ์ํธํ์ ์๋ โ
โ - ํค ๋ฐฐํฌ๋ฅผ ์ํ ๊ณต๊ฐ ํค ์ํธํ์ ํธ๋ฆฌํจ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
7. RSA¶
RSA(Rivest-Shamir-Adleman)๋ ๊ฐ์ฅ ๋๋ฆฌ ๋ฐฐํฌ๋ ๋น๋์นญ ์๊ณ ๋ฆฌ์ฆ์ ๋๋ค. ๋ณด์์ ํฐ ์ ์๋ฅผ ์ธ์๋ถํดํ๋ ์ด๋ ค์์ ๊ธฐ๋ฐํฉ๋๋ค.
7.1 RSA ํค ์์ฑ ๋ฐ ์ํธํ¶
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
# Generate RSA key pair
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096, # 2048 minimum, 4096 recommended
)
public_key = private_key.public_key()
# Display key info
print(f"Key size: {private_key.key_size} bits")
print(f"Public exponent: {private_key.private_numbers().public_numbers.e}")
# Serialize keys (PEM format)
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.BestAvailableEncryption(b"my-password")
)
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
print(f"\nPublic key (first 80 chars):\n{public_pem.decode()[:80]}...")
# Encrypt with public key (using OAEP padding - ALWAYS use OAEP, never PKCS1v15)
message = b"Secret message for RSA encryption"
ciphertext = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print(f"\nCiphertext: {ciphertext.hex()[:64]}...")
print(f"CT length: {len(ciphertext)} bytes")
# Decrypt with private key
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print(f"Decrypted: {plaintext.decode()}")
7.2 RSA ํ์ด๋ธ๋ฆฌ๋ ์ํธํ¶
import os
import json
import base64
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
class HybridEncryptor:
"""RSA + AES-GCM hybrid encryption."""
def __init__(self, public_key=None, private_key=None):
self.public_key = public_key
self.private_key = private_key
@classmethod
def generate_keypair(cls):
"""Generate a new RSA key pair and return an encryptor."""
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
)
return cls(
public_key=private_key.public_key(),
private_key=private_key,
)
def encrypt(self, plaintext: bytes) -> dict:
"""Encrypt data using hybrid encryption."""
if not self.public_key:
raise ValueError("Public key required for encryption")
# 1. Generate random AES-256 key
aes_key = AESGCM.generate_key(bit_length=256)
# 2. Encrypt data with AES-GCM
nonce = os.urandom(12)
aesgcm = AESGCM(aes_key)
encrypted_data = aesgcm.encrypt(nonce, plaintext, None)
# 3. Encrypt AES key with RSA public key
encrypted_key = self.public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# 4. Package everything together
return {
"encrypted_key": base64.b64encode(encrypted_key).decode(),
"nonce": base64.b64encode(nonce).decode(),
"ciphertext": base64.b64encode(encrypted_data).decode(),
}
def decrypt(self, package: dict) -> bytes:
"""Decrypt hybrid-encrypted data."""
if not self.private_key:
raise ValueError("Private key required for decryption")
# 1. Decrypt AES key with RSA private key
encrypted_key = base64.b64decode(package["encrypted_key"])
aes_key = self.private_key.decrypt(
encrypted_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# 2. Decrypt data with AES-GCM
nonce = base64.b64decode(package["nonce"])
ciphertext = base64.b64decode(package["ciphertext"])
aesgcm = AESGCM(aes_key)
return aesgcm.decrypt(nonce, ciphertext, None)
# Usage
encryptor = HybridEncryptor.generate_keypair()
# Encrypt a large message
large_message = b"A" * 100000 # 100 KB - too large for raw RSA
package = encryptor.encrypt(large_message)
print("Hybrid Encryption Package:")
print(f" Encrypted key length: {len(base64.b64decode(package['encrypted_key']))} bytes")
print(f" Nonce: {package['nonce']}")
print(f" Ciphertext length: {len(base64.b64decode(package['ciphertext']))} bytes")
# Decrypt
decrypted = encryptor.decrypt(package)
assert decrypted == large_message
print(f"\nDecrypted successfully: {len(decrypted)} bytes match original")
8. ํ์ ๊ณก์ ์ํธํ¶
ECC๋ ํจ์ฌ ์์ ํค ํฌ๊ธฐ๋ก RSA์ ๋์ผํ ๋ณด์์ ์ ๊ณตํ์ฌ ๋ ๋น ๋ฅด๊ณ ํจ์จ์ ์ ๋๋ค.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ RSA vs ํ์ ๊ณก์ ํค ํฌ๊ธฐ โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๋ณด์ ์์ค โ RSA ํค ํฌ๊ธฐ โ ECC ํค ํฌ๊ธฐ โ
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 128-bit โ 3072 bits โ 256 bits (P-256/secp256r1) โ
โ 192-bit โ 7680 bits โ 384 bits (P-384/secp384r1) โ
โ 256-bit โ 15360 bits โ 521 bits (P-521/secp521r1) โ
โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ECC๋ ๋๋ฑํ ๋ณด์์ ์ํด ~10-15๋ฐฐ ์์ต๋๋ค! โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
8.1 ECDSA (ํ์ ๊ณก์ ๋์งํธ ์๋ช ์๊ณ ๋ฆฌ์ฆ)¶
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
# Generate key pair using P-256 curve (NIST recommended)
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
print(f"Curve: {private_key.curve.name}")
print(f"Key size: {private_key.curve.key_size} bits")
# Sign a message
message = b"This message is signed with ECDSA"
signature = private_key.sign(
message,
ec.ECDSA(hashes.SHA256())
)
print(f"\nSignature: {signature.hex()[:64]}...")
print(f"Signature length: {len(signature)} bytes") # ~70-72 bytes for P-256
# Verify
try:
public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
print("Signature valid!")
except Exception:
print("Signature invalid!")
# Tampered message fails
try:
public_key.verify(signature, b"tampered message", ec.ECDSA(hashes.SHA256()))
print("ERROR: Should have failed!")
except Exception:
print("Tampered message: signature verification failed (expected)")
8.2 Ed25519 (ํ๋ ์๋ช ์๊ณ ๋ฆฌ์ฆ)¶
Ed25519๋ Curve25519๋ฅผ ์ฌ์ฉํ๋ ํ๋ EdDSA ์๋ช ์คํด์ ๋๋ค. ๊ฒฐ์ ๋ก ์ ์ด๊ณ (์๋ช ์ค ๋๋ค nonce ๋ถํ์), ๋น ๋ฅด๋ฉฐ, ์ฌ์ด๋ ์ฑ๋ ๊ณต๊ฒฉ์ ๊ฐํฉ๋๋ค.
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
# Generate Ed25519 key pair
private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()
# Sign (no hash algorithm parameter needed - it uses SHA-512 internally)
message = b"Ed25519 is the recommended signature algorithm for new systems"
signature = private_key.sign(message)
print(f"Signature: {signature.hex()}")
print(f"Signature length: {len(signature)} bytes") # Always 64 bytes
# Verify
try:
public_key.verify(signature, message)
print("Ed25519 signature valid!")
except Exception as e:
print(f"Invalid: {e}")
# Key sizes are small
from cryptography.hazmat.primitives.serialization import (
Encoding, PublicFormat, PrivateFormat, NoEncryption
)
pub_bytes = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
priv_bytes = private_key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
print(f"\nPublic key: {len(pub_bytes)} bytes ({len(pub_bytes) * 8} bits)")
print(f"Private key: {len(priv_bytes)} bytes ({len(priv_bytes) * 8} bits)")
print(f"Signature: {len(signature)} bytes ({len(signature) * 8} bits)")
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ์๋ช
์๊ณ ๋ฆฌ์ฆ ๋น๊ต โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโค
โ โ RSA-2048 โECDSA P256โ Ed25519 โ Ed448 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโค
โ ๊ณต๊ฐ ํค ํฌ๊ธฐ โ 256 B โ 64 B โ 32 B โ 57 B โ
โ ์๋ช
ํฌ๊ธฐ โ 256 B โ ~72 B โ 64 B โ 114 B โ
โ ์๋ช
์๋ โ ๋๋ฆผ โ ๋น ๋ฆ โ ๋งค์ฐ ๋น ๋ฆโ ๋น ๋ฆ โ
โ ๊ฒ์ฆ ์๋ โ ๋น ๋ฆ โ ๋ณดํต โ ๋น ๋ฆ โ ๋น ๋ฆ โ
โ ๊ฒฐ์ ๋ก ์ โ No โ No* โ Yes โ Yes โ
โ ์ฌ์ด๋ ์ฑ๋ โ ์ฃผ์ โ ์ฃผ์ โ ๋ณธ์ง์ โ ๋ณธ์ง์ โ
โ ์ ํญ โ ํ์ โ ํ์ โ โ โ
โ ํ์ค โ PKCS#1 โ FIPS โ RFC 8032 โ RFC 8032 โ
โโโโโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโค
โ * RFC 6979๋ ๊ฒฐ์ ๋ก ์ ECDSA๋ฅผ ์ ๊ณตํ์ง๋ง ๋ชจ๋ ๊ตฌํ์ด ์ฌ์ฉํ์ง ์์ โ
โ ๊ถ์ฅ ์ฌํญ: ์ ์์คํ
์๋ Ed25519 ์ฌ์ฉ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
9. ํค ๊ตํ¶
ํค ๊ตํ ํ๋กํ ์ฝ์ ๋ ๋น์ฌ์๊ฐ ์์ ํ์ง ์์ ์ฑ๋์ ํตํด ๊ณต์ ๋น๋ฐ์ ์ค์ ํ ์ ์๋๋ก ํฉ๋๋ค.
9.1 Diffie-Hellman ํค ๊ตํ¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Diffie-Hellman ํค ๊ตํ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Alice Bob โ
โ โโโโโ โโโ โ
โ โ
โ 1. ๊ฐ์ธ ํค ์ ํ: a 1. ๊ฐ์ธ ํค ์ ํ: b โ
โ 2. ๊ณ์ฐ: A = g^a mod p 2. ๊ณ์ฐ: B = g^b โ
โ mod p โ
โ โ
โ 3. A ์ ์ก โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโถ A ์์ โ
โ B ์์ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ 4. B ์ ์ก โ
โ โ
โ 5. ๊ณ์ฐ: 5. ๊ณ์ฐ: โ
โ shared = B^a mod p shared = A^b mod pโ
โ = (g^b)^a mod p = (g^a)^b โ
โ = g^(ab) mod p mod p โ
โ = g^(ab) โ
โ mod p โ
โ โ
โ ๋ ๋น์ฌ์ ๋ชจ๋ ๋์ผํ ๊ณต์ ๋น๋ฐ์ ๋๋ฌ: g^(ab) mod p โ
โ โ
โ Eve๊ฐ ๋ณด๋ ๊ฒ: g, p, A=g^a, B=g^b โ
โ A๋ก๋ถํฐ a๋ฅผ ๊ณ์ฐํ๋ ค๋ฉด ์ด์ฐ ๋ก๊ทธ ๋ฌธ์ ๋ฅผ ํ์ด์ผ ํ๋ฉฐ โ
โ ํฐ ์์์ ๋ํด ๊ณ์ฐ์ ์ผ๋ก ๋ถ๊ฐ๋ฅํ๋ค๊ณ ๋ฏฟ์ด์ง๋๋ค. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
9.2 ECDH (ํ์ ๊ณก์ Diffie-Hellman)¶
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
def ecdh_key_exchange():
"""
Demonstrate ECDH key exchange between Alice and Bob.
Uses X25519-equivalent (P-256 shown here for compatibility).
"""
# Alice generates her key pair
alice_private = ec.generate_private_key(ec.SECP256R1())
alice_public = alice_private.public_key()
# Bob generates his key pair
bob_private = ec.generate_private_key(ec.SECP256R1())
bob_public = bob_private.public_key()
# Alice computes shared secret using her private key + Bob's public key
alice_shared = alice_private.exchange(ec.ECDH(), bob_public)
# Bob computes shared secret using his private key + Alice's public key
bob_shared = bob_private.exchange(ec.ECDH(), alice_public)
# Both shared secrets are identical
assert alice_shared == bob_shared
print(f"ECDH shared secret: {alice_shared.hex()[:32]}...")
print(f"Shared secret length: {len(alice_shared)} bytes")
# Derive actual encryption key from shared secret using HKDF
# (raw ECDH output should NOT be used directly as an encryption key)
alice_key = HKDF(
algorithm=hashes.SHA256(),
length=32, # 256-bit key for AES-256
salt=None,
info=b"ecdh-derived-key-v1",
).derive(alice_shared)
bob_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=b"ecdh-derived-key-v1",
).derive(bob_shared)
assert alice_key == bob_key
print(f"Derived AES key: {alice_key.hex()}")
return alice_key
derived_key = ecdh_key_exchange()
9.3 X25519 ํค ๊ตํ¶
X25519๋ ํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ถ์ฅ๋๋ ECDH ๊ณก์ ์ ๋๋ค(TLS 1.3, WireGuard, Signal์์ ์ฌ์ฉ).
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
# Alice
alice_private = X25519PrivateKey.generate()
alice_public = alice_private.public_key()
# Bob
bob_private = X25519PrivateKey.generate()
bob_public = bob_private.public_key()
# Exchange
alice_shared = alice_private.exchange(bob_public)
bob_shared = bob_private.exchange(alice_public)
assert alice_shared == bob_shared
print(f"X25519 shared secret: {alice_shared.hex()}")
# Derive keys using HKDF
def derive_keys(shared_secret: bytes, context: bytes) -> dict:
"""Derive separate keys for encryption and MAC from shared secret."""
# Encryption key
enc_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=context + b"-enc",
).derive(shared_secret)
# MAC key (for additional message authentication if needed)
mac_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=context + b"-mac",
).derive(shared_secret)
return {"encryption_key": enc_key, "mac_key": mac_key}
keys = derive_keys(alice_shared, b"session-2026-01-15")
print(f"Encryption key: {keys['encryption_key'].hex()}")
print(f"MAC key: {keys['mac_key'].hex()}")
10. ๋์งํธ ์๋ช ¶
๋์งํธ ์๋ช ์ ์ธ์ฆ, ๋ฌด๊ฒฐ์ฑ ๋ฐ ๋ถ์ธ ๋ฐฉ์ง๋ฅผ ์ ๊ณตํฉ๋๋ค. ๋๊ตฌ๋ ๋ฉ์์ง๊ฐ ํน์ ๊ฐ์ธ ํค์ ์์ ์์ ์ํด ์์ฑ๋์์ผ๋ฉฐ ์์ ๋์ง ์์์์ ํ์ธํ ์ ์์ต๋๋ค.
10.1 ๋์งํธ ์๋ช ์๋ ๋ฐฉ์¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๋์งํธ ์๋ช
ํ๋ก์ธ์ค โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ ์๋ช
(์์ฑ์๊ฐ ๊ฐ์ธ ํค๋ฅผ ์ฌ์ฉํ์ฌ): โ
โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโโ โ
โ โ Message โโโโโโถโ Hash โโโโโโถโ Sign โโโโโโถโ Signature โ โ
โ โ โ โ (SHA-256)โ โ(Private โ โ โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โ Key) โ โโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโ โ
โ โ
โ ๊ฒ์ฆ (์์ฑ์์ ๊ณต๊ฐ ํค๋ฅผ ์ฌ์ฉํ์ฌ ๋๊ตฌ๋): โ
โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ Message โโโโโโถโ Hash โโโโโโโ โ
โ โโโโโโโโโโโโ โ (SHA-256)โ โ โ
โ โโโโโโโโโโโโ โผ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโ โ Verify โโโโโโถโ Valid / โ โ
โ โ Signature โโโโโโโโโโโโโโโโโโถโ(Public โ โ Invalid โ โ
โ โโโโโโโโโโโโโ โ Key) โ โโโโโโโโโโโโ โ
โ โโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
10.2 ์ค์ต: ๋ฌธ์ ์๋ช ์์คํ ¶
import json
import time
import base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import (
Encoding, PublicFormat, PrivateFormat, NoEncryption
)
from dataclasses import dataclass, asdict
from typing import Optional
@dataclass
class SignedDocument:
"""A document with a digital signature."""
content: str
author: str
timestamp: float
public_key: str # Base64-encoded public key
signature: str # Base64-encoded signature
class DocumentSigner:
"""Sign and verify documents using Ed25519."""
def __init__(self):
self.private_key = Ed25519PrivateKey.generate()
self.public_key = self.private_key.public_key()
def get_public_key_b64(self) -> str:
"""Get base64-encoded public key for sharing."""
raw = self.public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
return base64.b64encode(raw).decode()
def sign_document(self, content: str, author: str) -> SignedDocument:
"""Sign a document and return it with the signature."""
timestamp = time.time()
# Create canonical message to sign
message = self._canonical_message(content, author, timestamp)
# Sign
signature = self.private_key.sign(message)
return SignedDocument(
content=content,
author=author,
timestamp=timestamp,
public_key=self.get_public_key_b64(),
signature=base64.b64encode(signature).decode(),
)
@staticmethod
def verify_document(doc: SignedDocument) -> dict:
"""Verify a signed document. Returns verification result."""
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
try:
# Reconstruct the canonical message
message = DocumentSigner._canonical_message(
doc.content, doc.author, doc.timestamp
)
# Decode public key and signature
pub_bytes = base64.b64decode(doc.public_key)
signature = base64.b64decode(doc.signature)
# Load public key
public_key = Ed25519PublicKey.from_public_bytes(pub_bytes)
# Verify
public_key.verify(signature, message)
return {
"valid": True,
"author": doc.author,
"signed_at": time.ctime(doc.timestamp),
"public_key": doc.public_key[:20] + "...",
}
except Exception as e:
return {
"valid": False,
"error": str(e),
}
@staticmethod
def _canonical_message(content: str, author: str, timestamp: float) -> bytes:
"""Create a canonical byte representation for signing."""
canonical = json.dumps({
"content": content,
"author": author,
"timestamp": timestamp,
}, sort_keys=True, separators=(',', ':')).encode()
return canonical
# Create signers for Alice and Bob
alice_signer = DocumentSigner()
bob_signer = DocumentSigner()
# Alice signs a document
doc = alice_signer.sign_document(
content="I, Alice, hereby agree to pay Bob $100.",
author="Alice"
)
print("Signed Document:")
print(f" Content: {doc.content}")
print(f" Author: {doc.author}")
print(f" Timestamp: {time.ctime(doc.timestamp)}")
print(f" Signature: {doc.signature[:40]}...")
# Anyone can verify using Alice's public key (embedded in document)
result = DocumentSigner.verify_document(doc)
print(f"\nVerification: {result}")
# Tamper with the document
tampered_doc = SignedDocument(
content="I, Alice, hereby agree to pay Bob $10000.", # Changed!
author=doc.author,
timestamp=doc.timestamp,
public_key=doc.public_key,
signature=doc.signature,
)
tamper_result = DocumentSigner.verify_document(tampered_doc)
print(f"\nTampered verification: {tamper_result}")
11. ์ผ๋ฐ์ ์ธ ํจ์ ๊ณผ ํผํ๋ ๋ฐฉ๋ฒ¶
11.1 ์ํธํ์ ์น๋ช ์ ์ธ ์ฃ์ ¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ์ฃผ์ ์ํธํ์ ํจ์ (๋ฐ ํผํ๋ ๋ฐฉ๋ฒ) โ
โโโโโโฌโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ # โ ํจ์ โ ์ฌ๋ฐ๋ฅธ ์ ๊ทผ๋ฒ โ
โโโโโโผโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 1 โ ECB ๋ชจ๋ ์ฌ์ฉ โ AES-GCM ๋๋ ChaCha20-Poly1305 ์ฌ์ฉ โ
โโโโโโผโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 2 โ ๋์ผํ ํค๋ก โ ๋ฉ์์ง๋น ๋๋ค nonce (๋๋ ์นด์ดํฐ) โ
โ โ nonces/IV ์ฌ์ฌ์ฉ โ ๋๋ค ์์ nonce๋ฅผ ์ํด XChaCha20 ์ฌ์ฉ โ
โโโโโโผโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 3 โ ์ธ์ฆ ์์ด ์ํธํ โ ํญ์ AEAD ์ฌ์ฉ (GCM, Poly1305) โ
โ โ โ ์์ CBC/CTR ์ฌ์ฉ ๊ธ์ง โ
โโโโโโผโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 4 โ ์์ฒด ์ํธํ ๊ตฌํ โ ํ๋ฆฝ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ (cryptography, โ
โ โ โ libsodium/NaCl, OpenSSL) โ
โโโโโโผโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 5 โ ์ฝํ๊ฑฐ๋ ์์ธก โ os.urandom() ๋๋ secrets ๋ชจ๋ ์ฌ์ฉ โ
โ โ ๊ฐ๋ฅํ ๋์ โ ์ํธํ์ random.random() ์ ๋ ์ฌ์ฉ ๊ธ์ง โ
โโโโโโผโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 6 โ ์์ค ์ฝ๋์ โ ํค ๊ด๋ฆฌ ์ฌ์ฉ (AWS KMS, Vault) โ
โ โ ํ๋์ฝ๋ฉ๋ ํค โ ์ต์ํ ํ๊ฒฝ ๋ณ์ โ
โโโโโโผโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 7 โ ๋น๋ฐ๋ฒํธ์ โ ํด์์๋ SHA-256+, ๋น๋ฐ๋ฒํธ์๋ โ
โ โ MD5/SHA-1 ์ฌ์ฉ โ bcrypt/argon2 ์ฌ์ฉ โ
โโโโโโผโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 8 โ PKCS#1 v1.5 โ ์ํธํ์๋ RSA-OAEP ์ฌ์ฉ โ
โ โ ํจ๋ฉ์ผ๋ก RSA ์ฌ์ฉ โ ์๋ช
์๋ RSA-PSS ์ฌ์ฉ โ
โโโโโโผโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 9 โ ==๋ก MAC ๋น๊ต โ ์์ ์๊ฐ ๋น๊ต๋ฅผ ์ํด โ
โ โ โ hmac.compare_digest() ์ฌ์ฉ โ
โโโโโโผโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 10 โ ํค ๋กํ
์ด์
์์ โ ํค ๋กํ
์ด์
์ผ์ ๊ตฌํ โ
โ โ โ ํค ๋ฒ์ ๊ด๋ฆฌ ์ฌ์ฉ โ
โโโโโโดโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
11.2 Nonce ์ฌ์ฌ์ฉ ์ฌ์¶
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
# Demonstration: Why nonce reuse with CTR/GCM is catastrophic
key = os.urandom(32)
nonce = os.urandom(16) # Same nonce used twice - BAD!
def ctr_encrypt(key, nonce, plaintext):
cipher = Cipher(algorithms.AES(key), modes.CTR(nonce))
encryptor = cipher.encryptor()
return encryptor.update(plaintext) + encryptor.finalize()
# Two messages encrypted with the SAME key and nonce
msg1 = b"Attack at dawn!!!" # 17 bytes
msg2 = b"Retreat at night!" # 17 bytes
ct1 = ctr_encrypt(key, nonce, msg1)
ct2 = ctr_encrypt(key, nonce, msg2)
# XOR of two ciphertexts = XOR of two plaintexts!
# In CTR mode: ct = plaintext XOR keystream
# So: ct1 XOR ct2 = (msg1 XOR keystream) XOR (msg2 XOR keystream)
# = msg1 XOR msg2 (keystream cancels out!)
xor_result = bytes(a ^ b for a, b in zip(ct1, ct2))
expected = bytes(a ^ b for a, b in zip(msg1, msg2))
print("Nonce Reuse Attack Demonstration:")
print(f" ct1 XOR ct2: {xor_result.hex()}")
print(f" msg1 XOR msg2: {expected.hex()}")
print(f" Match: {xor_result == expected}")
print()
print(" An attacker who knows msg1 can recover msg2:")
recovered = bytes(a ^ b for a, b in zip(xor_result, msg1))
print(f" Recovered msg2: {recovered.decode()}")
print()
print(" LESSON: Never reuse a nonce with the same key!")
11.3 ์์ ํ ๋์ vs ์์ ํ์ง ์์ ๋์¶
import random
import secrets
import os
# INSECURE - never use for cryptography
# random.random() uses Mersenne Twister (MT19937)
# It is deterministic and predictable if you observe enough outputs
insecure_key = bytes([random.randint(0, 255) for _ in range(32)])
print(f"INSECURE key: {insecure_key.hex()}")
print(f" Source: random.random() - Mersenne Twister (PREDICTABLE)")
# SECURE - use for cryptography
secure_key = os.urandom(32)
print(f"\nSECURE key: {secure_key.hex()}")
print(f" Source: os.urandom() - OS CSPRNG (/dev/urandom)")
# ALSO SECURE - Python 3.6+ secrets module
secure_token = secrets.token_bytes(32)
print(f"\nSECURE key: {secure_token.hex()}")
print(f" Source: secrets.token_bytes() - wraps os.urandom()")
# For URL-safe tokens
url_token = secrets.token_urlsafe(32)
print(f"\nURL-safe token: {url_token}")
# For comparison tokens (timing-safe)
a = secrets.token_bytes(32)
b = secrets.token_bytes(32)
print(f"\nConstant-time comparison: {secrets.compare_digest(a, b)}")
12. ํ๋ ๊ถ์ฅ ์ฌํญ¶
12.1 ์๊ณ ๋ฆฌ์ฆ ์ ํ ๊ฐ์ด๋ (2025+)¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ํ๋ ์ํธํ ๊ถ์ฅ ์ฌํญ โ
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ์ฌ์ฉ ์ฌ๋ก โ ๊ถ์ฅ ์๊ณ ๋ฆฌ์ฆ โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๋์นญ ์ํธํ โ AES-256-GCM (ํ๋์จ์ด ์ฌ์ฉ) ๋๋ โ
โ โ ChaCha20-Poly1305 (ํ๋์จ์ด ์์ / ๋ชจ๋ฐ์ผ) โ
โ โ ๋๋ค nonce ํ์ ์ XChaCha20-Poly1305 โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ํค ๊ตํ โ X25519 (Curve25519์ ECDH) โ
โ โ ML-KEM (์์ ์ดํ, X25519์ ํ์ด๋ธ๋ฆฌ๋) โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๋์งํธ ์๋ช
โ Ed25519 (๋ฒ์ฉ) โ
โ โ Ed448 (๋ ๋์ ๋ณด์ ๋ง์ง) โ
โ โ ECDSA P-256 (๋ ๊ฑฐ์ ํธํ์ฑ) โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ํด์ฑ โ SHA-256 / SHA-3-256 (๋ฒ์ฉ) โ
โ โ BLAKE3 (์๋ ์ค์) โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๋น๋ฐ๋ฒํธ ํด์ โ Argon2id (์ ํธ) โ
โ โ bcrypt (๋๋ฆฌ ์ง์๋จ) โ
โ โ scrypt (๋ฉ๋ชจ๋ฆฌ ํ๋ ๋์) โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ํค ์ ๋ โ HKDF-SHA256 (๊ณ ์ํธ๋กํผ ์
๋ ฅ์์) โ
โ โ Argon2id (๋น๋ฐ๋ฒํธ์์) โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ TLS โ TLS 1.3 with X25519 + AES-256-GCM โ
โ โ ๋๋ ChaCha20-Poly1305 โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ํผํด์ผ ํ ๊ฒ โ MD5, SHA-1, DES, 3DES, RC4, RSA-1024, โ
โ โ ECB ๋ชจ๋, PKCS#1 v1.5, ์ฌ์ฉ์ ์ ์ ์๊ณ ๋ฆฌ์ฆ โ
โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
12.2 ํค ํฌ๊ธฐ ๊ถ์ฅ ์ฌํญ¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ์ต์ ํค ํฌ๊ธฐ (NIST / ANSSI 2025+) โ
โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ์๊ณ ๋ฆฌ์ฆ โ ์ต์ ํค ํฌ๊ธฐ โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ AES โ 128-bit (์์ ์ดํ ๋ง์ง์ ์ํด 256-bit) โ
โ RSA (ํ์ ์) โ 3072-bit (4096 ๊ถ์ฅ) โ
โ ECDSA / ECDH โ P-256 / Curve25519 (256-bit) โ
โ EdDSA โ Ed25519 (256-bit) โ
โ ํด์ ์ถ๋ ฅ โ 256-bit (SHA-256, SHA-3-256, BLAKE2b-256) โ
โ HMAC ํค โ ํด์ ์ถ๋ ฅ ํฌ๊ธฐ์ ๋์ผ (256-bit) โ
โโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
12.3 ์์ ์ดํ ์ํธํ¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ์์ ์ดํ ์ํธํ (PQC) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ ๋ฌธ์ : ์์ ์ปดํจํฐ (Shor ์๊ณ ๋ฆฌ์ฆ)๋ ๋ค์์ ๊นจ๋จ๋ฆด ๊ฒ์
๋๋ค: โ
โ - RSA (์ธ์๋ถํด) โ
โ - ECDSA/ECDH (ํ์ ๊ณก์ ์ด์ฐ ๋ก๊ทธ) โ
โ - DH (์ด์ฐ ๋ก๊ทธ) โ
โ โ
โ ์์์ ์ํฅ๋ฐ์ง ์์: โ
โ - AES (Grover ์๊ณ ๋ฆฌ์ฆ์ด ์ ํจ ํค ํฌ๊ธฐ๋ฅผ ์ ๋ฐ์ผ๋ก: โ
โ AES-256 โ ~128๋นํธ ๋ณด์, ์ฌ์ ํ ์์ ) โ
โ - SHA-256 (์ ์ฌํ ์ ๋ฐ, ์ฌ์ ํ ์ ์ ํจ) โ
โ โ
โ NIST PQC ํ์ค (2024 ํ์ ): โ
โ โโโ ML-KEM (CRYSTALS-Kyber) โ ํค ์บก์ํ โ
โ โโโ ML-DSA (CRYSTALS-Dilithium) โ ๋์งํธ ์๋ช
โ
โ โโโ SLH-DSA (SPHINCS+) โ ํด์ ๊ธฐ๋ฐ ์๋ช
โ
โ โโโ FN-DSA (FALCON) โ ๊ฒฉ์ ๊ธฐ๋ฐ ์๋ช
โ
โ โ
โ ํ์ฌ ๊ถ์ฅ ์ฌํญ: ํ์ด๋ธ๋ฆฌ๋ ๋ชจ๋ โ
โ - ํค ๊ตํ์ X25519 + ML-KEM์ ํจ๊ป ์ฌ์ฉ โ
โ - ๊ณ ์ ๋๋ PQ ์๊ณ ๋ฆฌ์ฆ์ด ๊นจ์ ธ๋ ์ฌ์ ํ ์์ โ
โ - Chrome, Firefox, Cloudflare๊ฐ ์ด๋ฏธ ํ์ด๋ธ๋ฆฌ๋ PQ ์ง์ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
13. ์ฐ์ต ๋ฌธ์ ¶
์ฐ์ต ๋ฌธ์ 1: ๋์นญ ์ํธํ (์ด๊ธ)¶
๋ค์์ ์ํํ๋ Python ํจ์๋ฅผ ์์ฑํ์ธ์: 1. ํ๋ฌธ ๋ฌธ์์ด๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅ์ผ๋ก ๋ฐ๊ธฐ 2. PBKDF2๋ฅผ ์ฌ์ฉํ์ฌ ๋น๋ฐ๋ฒํธ๋ก๋ถํฐ AES-256 ํค ์ ๋ (๋๋ค salt ์ฌ์ฉ) 3. AES-GCM์ผ๋ก ํ๋ฌธ ์ํธํ 4. salt + nonce + ciphertext๋ฅผ ํฌํจํ๋ ๋จ์ผ base64 ์ธ์ฝ๋ฉ ๋ฌธ์์ด ๋ฐํ 5. ํด๋นํ๋ ๋ณตํธํ ํจ์ ์์ฑ
ํํธ:
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
# Use iterations=600000, salt_size=16, nonce_size=12
์ฐ์ต ๋ฌธ์ 2: ํ์ด๋ธ๋ฆฌ๋ ์ํธํ (์ค๊ธ)¶
PGP์ ์ ์ฌํ ์ํธํ์ ๊ฐ๋จํ ๋ฒ์ ์ ๊ตฌํํ์ธ์: 1. Alice๊ฐ RSA-4096 ํค ์ ์์ฑ 2. Bob์ด RSA-4096 ํค ์ ์์ฑ 3. Alice๊ฐ Bob์๊ฒ ์๋ช ๋๊ณ ์ํธํ๋ ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ ค๊ณ ํจ: - Alice์ ๊ฐ์ธ ํค๋ก ๋ฉ์์ง ์๋ช - ๋๋ค AES-256 ํค ์์ฑ - AES-GCM์ผ๋ก ๋ฉ์์ง ์ํธํ - Bob์ ๊ณต๊ฐ RSA ํค๋ก AES ํค ์ํธํ - ํจํค์ง: encrypted_key + nonce + ciphertext + signature + alice_public_key 4. Bob์ด ํจํค์ง๋ฅผ ๋ฐ์์: - ์์ ์ ๊ฐ์ธ RSA ํค๋ก AES ํค ๋ณตํธํ - AES-GCM์ผ๋ก ๋ฉ์์ง ๋ณตํธํ - Alice์ ๊ณต๊ฐ ํค๋ฅผ ์ฌ์ฉํ์ฌ ์๋ช ๊ฒ์ฆ
์ฐ์ต ๋ฌธ์ 3: ํค ๊ตํ ํ๋กํ ์ฝ (์ค๊ธ)¶
Alice์ Bob ๊ฐ์ ์์ ํ ์ฑํ ์๋ฎฌ๋ ์ด์ : 1. ๋ ๋น์ฌ์ ๋ชจ๋ X25519 ํค ๊ตํ ์ํ 2. HKDF๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ ๋ฐฉํฅ์ ๋ํ ๋ณ๋ ํค ์ ๋ (Alice-to-Bob, Bob-to-Alice) 3. ๊ฐ ๋ฉ์์ง๋ ๊ณ ์ ํ nonce๋ฅผ ๋ฐ์ (์นด์ดํฐ ์ฌ์ฉ) 4. ๋ฉ์์ง๋ ChaCha20-Poly1305๋ก ์ํธํ๋จ 5. ์ฌ์ ๊ณต๊ฒฉ์ ๊ฐ์งํ๊ธฐ ์ํ ๋ฉ์์ง ์นด์ดํฐ ํฌํจ
์ฐ์ต ๋ฌธ์ 4: Nonce ์ฌ์ฌ์ฉ ๊ณต๊ฒฉ (๊ณ ๊ธ)¶
๋์ผํ ํค์ nonce๋ฅผ ์ฌ์ฉํ์ฌ AES-CTR๋ก ์ํธํ๋ ๋ ๊ฐ์ ์ํธ๋ฌธ์ด ์ฃผ์ด์ก์ ๋:
ct1 = bytes.fromhex("a1b2c3d4e5f6071829")
ct2 = bytes.fromhex("b4a3d2c5f4e7162738")
๊ทธ๋ฆฌ๊ณ plaintext1์ด b"plaintext"์์ ์ ๋:
1. plaintext2 ๋ณต๊ตฌ
2. ์ด ๊ณต๊ฒฉ์ด ์ํ์ ์ผ๋ก ์ ์๋ํ๋์ง ์ค๋ช
3. ์ด ์ทจ์ฝ์ ์ ๋ฐฉ์งํ๋ ๋ฐฉ๋ฒ ์ค๋ช
์ฐ์ต ๋ฌธ์ 5: ๋์งํธ ์๋ช ๊ฒ์ฆ (๊ณ ๊ธ)¶
๊ฐ๋จํ ์ฝ๋ ์๋ช ์์คํ ๊ตฌ์ถ: 1. "๋ฐํ์"๊ฐ Ed25519๋ก Python ์คํฌ๋ฆฝํธ ์๋ช 2. "์คํ์"๊ฐ ์คํฌ๋ฆฝํธ ์คํ ์ ์ ์๋ช ๊ฒ์ฆ 3. ์ ๋ขฐํ ์ ์๋ ๊ณต๊ฐ ํค ๋ ์ง์คํธ๋ฆฌ ์ ์ง 4. ํค ๋กํ ์ด์ ์ฒ๋ฆฌ (์ด์ ์๋ช ์ ์ด์ ํค๋ก ์ฌ์ ํ ์ ํจํด์ผ ํจ) 5. ํ์์คํฌํ ๊ฒ์ฆ ์ถ๊ฐ (30์ผ ์ด์ ๋ ์๋ช ๊ฑฐ๋ถ)
์ฐ์ต ๋ฌธ์ 6: ์ํธํ์ ๊ฐ์ฌ (๊ณ ๊ธ)¶
๋ค์ ์ฝ๋๋ฅผ ๊ฒํ ํ๊ณ ๋ชจ๋ ์ํธํ์ ์ทจ์ฝ์ ์ ์๋ณํ์ธ์. ์ต์ 8๊ฐ์ ๋ฌธ์ ๊ฐ ์์ต๋๋ค:
import hashlib
import base64
from Crypto.Cipher import AES # PyCryptodome
def encrypt_message(password, message):
key = hashlib.md5(password.encode()).digest() # 128-bit key from MD5
iv = b'\x00' * 16 # Static IV
cipher = AES.new(key, AES.MODE_CBC, iv)
# Manual padding
pad_len = 16 - (len(message) % 16)
padded = message + chr(pad_len) * pad_len
encrypted = cipher.encrypt(padded.encode())
return base64.b64encode(encrypted).decode()
def verify_password(stored_hash, password):
return hashlib.sha256(password.encode()).hexdigest() == stored_hash
๊ฐ ์ทจ์ฝ์ ์ ๋ํด ๋ค์์ ์ค๋ช ํ์ธ์: - ๋ฌด์์ด ์๋ชป๋์๋์ง - ์ ์ํํ์ง - ์ด๋ป๊ฒ ์์ ํ๋์ง
14. ์ฐธ๊ณ ์๋ฃ¶
- Ferguson, Schneier, Kohno. Cryptography Engineering. Wiley, 2010.
- Bernstein, D.J. "Curve25519: New Diffie-Hellman Speed Records". 2006.
- NIST SP 800-175B: Guideline for Using Cryptographic Standards
- NIST Post-Quantum Cryptography - https://csrc.nist.gov/projects/post-quantum-cryptography
- Python
cryptographylibrary docs - https://cryptography.io/ - Latacora, "Cryptographic Right Answers" (2018, updated regularly)
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA)
- RFC 8439: ChaCha20 and Poly1305 for IETF Protocols
์ด์ : 01. Security ๊ธฐ์ด | ๋ค์: 03. Hashing๊ณผ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ