Skip to content

暗号化パターン ​

この記事は英語版から翻訳されました。最新版は英語版をご覧ください。

TL;DR ​

暗号化はデータの機密性を保護します。転送中のデータにはTLS、保存データにはAES-256を使用し、鍵管理が最も難しい部分であることを理解してください。適切な鍵管理を伴わない暗号化はセキュリティシアター(見せかけのセキュリティ)です。


暗号化の基礎 ​

共通鍵暗号と公開鍵暗号 ​

Symmetric Encryption (AES):
- Same key encrypts and decrypts
- Fast, efficient for large data
- Challenge: How to share the key securely?
Asymmetric Encryption (RSA, ECC):
- Public key encrypts, private key decrypts
- Slower, used for key exchange and signatures
- Anyone can encrypt, only private key holder decrypts

ハイブリッド暗号化(TLSの仕組み) ​

1. Asymmetric exchange of symmetric key
2. Symmetric encryption for actual data

転送中のデータ(TLS) ​

TLS 1.3 ハンドシェイク ​

TLS設定のベストプラクティス ​

python
import ssl

def create_secure_ssl_context():
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)

    # Minimum TLS 1.2, prefer 1.3
    context.minimum_version = ssl.TLSVersion.TLSv1_2
    context.maximum_version = ssl.TLSVersion.TLSv1_3

    # Strong cipher suites only
    context.set_ciphers(
        'ECDHE+AESGCM:DHE+AESGCM:ECDHE+CHACHA20:DHE+CHACHA20'
    )

    # Load certificate chain
    context.load_cert_chain(
        certfile='/path/to/cert.pem',
        keyfile='/path/to/key.pem'
    )

    # Enable certificate verification for client connections
    context.verify_mode = ssl.CERT_REQUIRED
    context.check_hostname = True

    return context

mTLS(相互TLS) ​

クライアントとサーバーの双方が証明書を提示します。

Use cases:
- Service mesh (Istio, Linkerd)
- Zero trust architectures
- API security between trusted services

保存データ ​

暗号化の層 ​

アプリケーションレベルの暗号化 ​

python
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

class FieldEncryption:
    """Encrypt sensitive fields before database storage"""

    def __init__(self, key: bytes):
        # AES-256 requires 32-byte key
        assert len(key) == 32
        self.aesgcm = AESGCM(key)

    def encrypt(self, plaintext: str) -> bytes:
        """Encrypt with random nonce"""
        nonce = os.urandom(12)  # 96-bit nonce for GCM
        ciphertext = self.aesgcm.encrypt(
            nonce,
            plaintext.encode(),
            associated_data=None
        )
        # Return nonce + ciphertext (need nonce for decryption)
        return nonce + ciphertext

    def decrypt(self, data: bytes) -> str:
        """Decrypt, extracting nonce from data"""
        nonce = data[:12]
        ciphertext = data[12:]
        plaintext = self.aesgcm.decrypt(
            nonce,
            ciphertext,
            associated_data=None
        )
        return plaintext.decode()

# Usage
encryption = FieldEncryption(key=os.urandom(32))

# Before storing
encrypted_ssn = encryption.encrypt("123-45-6789")
db.store(user_id, encrypted_ssn)

# When retrieving
encrypted_data = db.get(user_id)
ssn = encryption.decrypt(encrypted_data)

検索可能な暗号化 ​

問題:暗号化されたデータは復号しないと検索できません。

python
# Approach 1: Deterministic encryption for exact match
import hashlib
import hmac

class SearchableEncryption:
    def __init__(self, search_key: bytes, encryption_key: bytes):
        self.search_key = search_key
        self.encryption = FieldEncryption(encryption_key)

    def store(self, plaintext: str):
        # Create searchable blind index
        blind_index = hmac.new(
            self.search_key,
            plaintext.lower().encode(),
            hashlib.sha256
        ).hexdigest()[:16]  # Truncate to limit leakage

        # Store encrypted value + blind index
        return {
            'encrypted': self.encryption.encrypt(plaintext),
            'search_index': blind_index
        }

    def search(self, search_term: str):
        # Generate same blind index
        blind_index = hmac.new(
            self.search_key,
            search_term.lower().encode(),
            hashlib.sha256
        ).hexdigest()[:16]

        # Search by blind index
        return db.find({'search_index': blind_index})

# Trade-off: Leaks equality (same plaintext = same index)

鍵管理 ​

鍵階層 ​

エンベロープ暗号化 ​

python
class EnvelopeEncryption:
    """
    1. Generate unique data key for each encryption
    2. Encrypt data with data key
    3. Encrypt data key with master key
    4. Store encrypted data + encrypted data key
    """

    def __init__(self, kms_client):
        self.kms = kms_client

    def encrypt(self, plaintext: bytes, master_key_id: str) -> dict:
        # 1. Generate data key (KMS returns plaintext + encrypted versions)
        data_key_response = self.kms.generate_data_key(
            KeyId=master_key_id,
            KeySpec='AES_256'
        )

        plaintext_key = data_key_response['Plaintext']
        encrypted_key = data_key_response['CiphertextBlob']

        # 2. Encrypt data with plaintext key
        aesgcm = AESGCM(plaintext_key)
        nonce = os.urandom(12)
        ciphertext = aesgcm.encrypt(nonce, plaintext, None)

        # 3. Securely delete plaintext key from memory
        # (In practice, use secure memory handling)
        del plaintext_key

        # 4. Return encrypted data + encrypted key
        return {
            'ciphertext': nonce + ciphertext,
            'encrypted_data_key': encrypted_key
        }

    def decrypt(self, encrypted_bundle: dict) -> bytes:
        # 1. Decrypt data key using KMS
        data_key = self.kms.decrypt(
            CiphertextBlob=encrypted_bundle['encrypted_data_key']
        )['Plaintext']

        # 2. Decrypt data
        ciphertext = encrypted_bundle['ciphertext']
        nonce = ciphertext[:12]
        actual_ciphertext = ciphertext[12:]

        aesgcm = AESGCM(data_key)
        plaintext = aesgcm.decrypt(nonce, actual_ciphertext, None)

        del data_key
        return plaintext

鍵ローテーション ​

python
class KeyRotation:
    """
    Key rotation strategy:
    1. Generate new key version
    2. New encryptions use new key
    3. Old data still decryptable with old key
    4. Gradually re-encrypt old data
    5. Retire old key after all data migrated
    """

    def __init__(self, kms):
        self.kms = kms

    def rotate_master_key(self, key_id: str):
        # Create new key version (old version still usable)
        self.kms.rotate_key(KeyId=key_id)

    def re_encrypt_data(self, encrypted_bundle: dict,
                        old_key_id: str, new_key_id: str) -> dict:
        # Decrypt with old key
        plaintext = self.decrypt(encrypted_bundle, old_key_id)

        # Encrypt with new key
        new_bundle = self.encrypt(plaintext, new_key_id)

        return new_bundle

    def batch_re_encrypt(self, table_name: str,
                         old_key_id: str, new_key_id: str):
        """Re-encrypt table in batches"""
        cursor = None

        while True:
            # Get batch of records
            records, cursor = db.scan(
                table_name,
                limit=100,
                cursor=cursor
            )

            if not records:
                break

            for record in records:
                new_encrypted = self.re_encrypt_data(
                    record['encrypted_data'],
                    old_key_id,
                    new_key_id
                )

                db.update(
                    table_name,
                    record['id'],
                    {'encrypted_data': new_encrypted}
                )

クラウドKMSサービス ​

AWS KMS ​

python
import boto3

class AWSKMS:
    def __init__(self):
        self.client = boto3.client('kms')

    def create_key(self, description: str):
        response = self.client.create_key(
            Description=description,
            KeyUsage='ENCRYPT_DECRYPT',
            KeySpec='SYMMETRIC_DEFAULT',  # AES-256-GCM
            MultiRegion=False
        )
        return response['KeyMetadata']['KeyId']

    def encrypt(self, key_id: str, plaintext: bytes):
        response = self.client.encrypt(
            KeyId=key_id,
            Plaintext=plaintext,
            EncryptionAlgorithm='SYMMETRIC_DEFAULT'
        )
        return response['CiphertextBlob']

    def decrypt(self, ciphertext: bytes):
        response = self.client.decrypt(
            CiphertextBlob=ciphertext,
            EncryptionAlgorithm='SYMMETRIC_DEFAULT'
        )
        return response['Plaintext']

    def generate_data_key(self, key_id: str):
        """Generate data key for envelope encryption"""
        response = self.client.generate_data_key(
            KeyId=key_id,
            KeySpec='AES_256'
        )
        return {
            'plaintext': response['Plaintext'],
            'encrypted': response['CiphertextBlob']
        }

HashiCorp Vault ​

python
import hvac

class VaultEncryption:
    def __init__(self, vault_url: str, token: str):
        self.client = hvac.Client(url=vault_url, token=token)

    def encrypt(self, key_name: str, plaintext: str):
        """Use Vault's transit secrets engine"""
        response = self.client.secrets.transit.encrypt_data(
            name=key_name,
            plaintext=base64.b64encode(plaintext.encode()).decode()
        )
        return response['data']['ciphertext']

    def decrypt(self, key_name: str, ciphertext: str):
        response = self.client.secrets.transit.decrypt_data(
            name=key_name,
            ciphertext=ciphertext
        )
        return base64.b64decode(response['data']['plaintext']).decode()

    def rotate_key(self, key_name: str):
        """Rotate encryption key"""
        self.client.secrets.transit.rotate_key(name=key_name)

    def rewrap_data(self, key_name: str, ciphertext: str):
        """Re-encrypt with latest key version without exposing plaintext"""
        response = self.client.secrets.transit.rewrap_data(
            name=key_name,
            ciphertext=ciphertext
        )
        return response['data']['ciphertext']

ハッシュ化と暗号化の違い ​

Encryption (Reversible):
- Plaintext ──[key]──► Ciphertext ──[key]──► Plaintext
- Use for: Data you need to read later

Hashing (One-way):
- Plaintext ──────────► Hash
- Cannot reverse: Hash ───X───► Plaintext
- Use for: Passwords, integrity verification

# Password storage
password_hash = bcrypt.hashpw(password, bcrypt.gensalt())

# Data integrity
file_hash = hashlib.sha256(file_content).hexdigest()
# Verify: recalculate hash and compare

認証のためのHMAC ​

python
import hmac
import hashlib

def create_signed_url(url: str, secret_key: bytes, expiry: int) -> str:
    """Create URL with HMAC signature"""
    message = f"{url}|{expiry}"
    signature = hmac.new(
        secret_key,
        message.encode(),
        hashlib.sha256
    ).hexdigest()

    return f"{url}?expires={expiry}&signature={signature}"

def verify_signed_url(url: str, secret_key: bytes) -> bool:
    """Verify URL signature"""
    # Parse URL and extract signature
    parsed = parse_url(url)
    provided_signature = parsed['signature']
    expiry = parsed['expires']
    base_url = parsed['base_url']

    # Check expiry
    if int(expiry) < time.time():
        return False

    # Verify signature
    message = f"{base_url}|{expiry}"
    expected_signature = hmac.new(
        secret_key,
        message.encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(provided_signature, expected_signature)

よくある落とし穴 ​

1. ECBモード(使用禁止) ​

ECB: Same plaintext block = same ciphertext block!
Problem: Patterns in plaintext visible in ciphertext
Solution: Use GCM, CBC with random IV, or CTR mode

2. ノンスの再利用 ​

python
# CATASTROPHIC with GCM/CTR modes
key = os.urandom(32)
nonce = b'static_nonce'  # WRONG!

# If same nonce used twice with same key:
# XOR of ciphertexts = XOR of plaintexts
# This completely breaks confidentiality

# CORRECT: Always use random nonce
nonce = os.urandom(12)  # New random nonce per encryption

3. 暗号文の認証なし ​

python
# Encryption without authentication (vulnerable to tampering)
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(plaintext)
# Attacker can flip bits in ciphertext!

# Use authenticated encryption (AES-GCM)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
# Tampering detected during decryption

4. ハードコードされた鍵 ​

python
# NEVER do this
ENCRYPTION_KEY = "my-super-secret-key-12345"

# Load from secure source
ENCRYPTION_KEY = os.environ.get('ENCRYPTION_KEY')
# Or use KMS
ENCRYPTION_KEY = kms.get_key('production/encryption')

コンプライアンスの考慮事項 ​

PCI-DSS要件 ​

For credit card data:
□ Use strong cryptography (AES-256)
□ Document key management procedures
□ Implement key rotation
□ Protect keys from unauthorized access
□ Maintain audit logs of key usage
□ Split knowledge for key custodians

GDPR要件 ​

For personal data:
□ Encryption as appropriate technical measure
□ Pseudonymization where possible
□ Consider encryption for data portability
□ Key management supports right to erasure

ベストプラクティスチェックリスト ​

Algorithm Selection:
□ AES-256-GCM for symmetric encryption
□ RSA-2048+ or ECC P-256+ for asymmetric
□ SHA-256 or SHA-3 for hashing
□ Argon2 or bcrypt for passwords

Key Management:
□ Use a KMS (cloud or Vault)
□ Implement key hierarchy
□ Automate key rotation
□ Never hardcode keys
□ Audit key access

Implementation:
□ Use authenticated encryption (GCM)
□ Never reuse nonces
□ Use cryptographic random number generator
□ Verify library is maintained and audited
□ Keep dependencies updated

Transit:
□ TLS 1.2+ only
□ Strong cipher suites
□ Certificate validation
□ HSTS enabled

At Rest:
□ Encrypt sensitive fields
□ Consider storage-layer encryption
□ Envelope encryption for scalability
□ Secure key storage separate from data

参考文献 ​

MITライセンスの下で公開。Babushkaiコミュニティが構築。