파이썬에는 내장 된 암호화 체계가 없습니다. 또한 암호화 된 데이터 저장소를 중요하게 생각해야합니다. 한 개발자가 안전하지 않다고 이해하는 사소한 암호화 체계와 경험이 적은 개발자는 장난감 체계를 안전한 체계로 오인 할 수 있습니다. 암호화하는 경우 올바르게 암호화하십시오.
그러나 적절한 암호화 체계를 구현하기 위해 많은 작업을 할 필요는 없습니다. 우선 , 암호화 휠을 재발 명하지 말고 신뢰할 수있는 암호화 라이브러리를 사용하여이를 처리하십시오. Python 3의 경우 신뢰할 수있는 라이브러리는cryptography
.
또한 암호화 및 암호 해독을 바이트에 적용하는 것이 좋습니다 . 먼저 문자 메시지를 바이트로 인코딩합니다. stringvalue.encode()
UTF8로 인코딩하고 다음을 사용하여 쉽게 되돌릴 수 있습니다.bytesvalue.decode()
.
마지막으로 암호화 및 복호화시 암호가 아닌 키 에 대해 이야기 합니다. 키는 사람이 기억할 수 없어야합니다. 암호는 사람이 읽을 수 있고 기억할 수있는 경우가 많지만 비밀 위치에 저장하지만 컴퓨터에서 읽을 수있는 것입니다. 당신 은 할 수 있습니다약간의주의를 기울여 암호에서 키를 추출 .
그러나 웹 애플리케이션 또는 클러스터에서 실행되는 프로세스를 사람의주의없이 계속 실행하려면 키를 사용하려고합니다. 암호는 최종 사용자 만 특정 정보에 액세스해야하는 경우에 사용됩니다. 그럼에도 불구하고 일반적으로 암호로 응용 프로그램을 보호 한 다음 사용자 계정에 연결된 키를 사용하여 암호화 된 정보를 교환합니다.
대칭 키 암호화
Fernet – AES CBC + HMAC, 적극 권장
cryptography
라이브러리가 포함 Fernet 조리법 , 암호화를 사용하기위한 모범 사례 조리법을. Fernet은 개방형 표준입니다. 광범위한 프로그래밍 언어로 즉시 구현 이며 메시지 변조를 방지하기 위해 버전 정보, 타임 스탬프 및 HMAC 서명과 함께 AES CBC 암호화를 패키지화합니다.
Fernet 암호화하고 해독 메시지를 매우 쉽게 그것을하게 하고 안전하게 보호 할 수 있습니다. 비밀로 데이터를 암호화하는 데 이상적인 방법입니다.
Fernet.generate_key()
보안 키를 생성하는 데 사용 하는 것이 좋습니다 . 암호도 사용할 수 있지만 (다음 섹션), 전체 32 바이트 비밀 키 (암호화에 16 바이트, 서명에 16 바이트)가 생각할 수있는 대부분의 암호보다 더 안전합니다.
Fernet이 생성하는 키는 bytes
URL 및 파일 안전 base64 문자 가있는 객체이므로 인쇄 할 수 있습니다.
from cryptography.fernet import Fernet
key = Fernet.generate_key() # store in a secure location
print("Key:", key.decode())
암호화 또는 암호 해독 메시지하려면 만들 Fernet()
지정된 키를 가진 인스턴스를, 및 호출 Fernet.encrypt()
하거나 Fernet.decrypt()
, 암호화에 일반 텍스트 메시지와 암호화 된 토큰 모두는 bytes
객체.
encrypt()
및 decrypt()
기능과 같을 것이다 :
from cryptography.fernet import Fernet
def encrypt(message: bytes, key: bytes) -> bytes:
return Fernet(key).encrypt(message)
def decrypt(token: bytes, key: bytes) -> bytes:
return Fernet(key).decrypt(token)
데모:
>>> key = Fernet.generate_key()
>>> print(key.decode())
GZWKEhHGNopxRdOHS4H4IyKhLQ8lwnyU7vRLrM3sebY=
>>> message = 'John Doe'
>>> encrypt(message.encode(), key)
'gAAAAABciT3pFbbSihD_HZBZ8kqfAj94UhknamBuirZWKivWOukgKQ03qE2mcuvpuwCSuZ-X_Xkud0uWQLZ5e-aOwLC0Ccnepg=='
>>> token = _
>>> decrypt(token, key).decode()
'John Doe'
암호가있는 Fernet – 암호에서 파생 된 키로 보안이 다소 약화됩니다.
강력한 키 파생 방법 을 사용하는 경우 비밀 키 대신 비밀번호를 사용할 수 있습니다 . 그런 다음 메시지에 salt 및 HMAC 반복 횟수를 포함해야하므로 암호화 된 값은 먼저 salt, count 및 Fernet 토큰을 분리하지 않고는 더 이상 Fernet과 호환되지 않습니다.
import secrets
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
backend = default_backend()
iterations = 100_000
def _derive_key(password: bytes, salt: bytes, iterations: int = iterations) -> bytes:
"""Derive a secret key from a given password and salt"""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(), length=32, salt=salt,
iterations=iterations, backend=backend)
return b64e(kdf.derive(password))
def password_encrypt(message: bytes, password: str, iterations: int = iterations) -> bytes:
salt = secrets.token_bytes(16)
key = _derive_key(password.encode(), salt, iterations)
return b64e(
b'%b%b%b' % (
salt,
iterations.to_bytes(4, 'big'),
b64d(Fernet(key).encrypt(message)),
)
)
def password_decrypt(token: bytes, password: str) -> bytes:
decoded = b64d(token)
salt, iter, token = decoded[:16], decoded[16:20], b64e(decoded[20:])
iterations = int.from_bytes(iter, 'big')
key = _derive_key(password.encode(), salt, iterations)
return Fernet(key).decrypt(token)
데모:
>>> message = 'John Doe'
>>> password = 'mypass'
>>> password_encrypt(message.encode(), password)
b'9Ljs-w8IRM3XT1NDBbSBuQABhqCAAAAAAFyJdhiCPXms2vQHO7o81xZJn5r8_PAtro8Qpw48kdKrq4vt-551BCUbcErb_GyYRz8SVsu8hxTXvvKOn9QdewRGDfwx'
>>> token = _
>>> password_decrypt(token, password).decode()
'John Doe'
출력에 솔트를 포함하면 임의의 솔트 값을 사용할 수 있으므로 암호 재사용이나 메시지 반복에 관계없이 암호화 된 출력이 완전히 무작위로 보장됩니다. 반복 횟수를 포함하면 오래된 메시지를 해독하는 기능을 잃지 않고 시간이 지남에 따라 CPU 성능 향상을 조정할 수 있습니다.
비슷한 크기의 풀에서 적절하게 임의의 암호를 생성한다면 암호만으로도 Fernet 32 바이트 임의 키만큼 안전 할 수 있습니다. 32 바이트는 256 ^ 32 개의 키를 제공하므로 74 자 (대문자 26 개, 하위 26 개, 10 자리 및 12 개의 가능한 기호)의 알파벳을 사용하는 경우 암호는 최소 math.ceil(math.log(256 ** 32, 74))
== 42 자 여야합니다 . 그러나 잘 선택된 더 많은 수의 HMAC 반복 은 공격자가 무차별 대입하는 데 훨씬 더 많은 비용이 들기 때문에 엔트로피 부족을 다소 완화 할 수 있습니다.
더 짧지 만 여전히 합리적으로 안전한 암호를 선택한다고해서이 체계가 무력화되는 것은 아니며 무차별 대입 공격자가 검색해야하는 가능한 값의 수를 줄입니다. 보안 요구 사항에 대해 충분히 강력한 암호 를 선택하십시오. .
대안
모호함
대안은 암호화하지 않는 것 입니다. 보안 수준이 낮은 암호 나 집에서 만든 구현을 사용하려는 유혹을받지 마십시오. 이러한 접근 방식에는 보안이 없지만, 미래에 코드를 유지 관리하는 작업을 맡은 경험이없는 개발자에게 보안이 전혀없는 것보다 더 나쁜 보안의 환상을 줄 수 있습니다.
모호함 만 있으면 데이터를 base64로 지정하십시오. URL 안전 요구 사항의 경우 base64.urlsafe_b64encode()
기능 이 좋습니다. 여기에서 비밀번호를 사용하지 말고 인코딩 만하면됩니다. 기껏해야 압축을 추가하십시오 (예 zlib
:).
import zlib
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d
def obscure(data: bytes) -> bytes:
return b64e(zlib.compress(data, 9))
def unobscure(obscured: bytes) -> bytes:
return zlib.decompress(b64d(obscured))
이 회전 b'Hello world!'
에 b'eNrzSM3JyVcozy_KSVEEAB0JBF4='
.
무결성 만
신뢰할 수없는 클라이언트로 전송되고 다시 수신 된 후 데이터가 변경되지 않도록 신뢰할 수 있는지 확인하는 방법 만 있으면 데이터 에 서명하고이를 위해 hmac
라이브러리 를 SHA1과 함께 사용할 수 있습니다 (여전히 HMAC 서명에 대해 안전한 것으로 간주 됨 ) 이상 :
import hmac
import hashlib
def sign(data: bytes, key: bytes, algorithm=hashlib.sha256) -> bytes:
assert len(key) >= algorithm().digest_size, (
"Key must be at least as long as the digest size of the "
"hashing algorithm"
)
return hmac.new(key, data, algorithm).digest()
def verify(signature: bytes, data: bytes, key: bytes, algorithm=hashlib.sha256) -> bytes:
expected = sign(data, key, algorithm)
return hmac.compare_digest(expected, signature)
이를 사용하여 데이터에 서명 한 다음 데이터와 함께 서명을 첨부하여 클라이언트에 보냅니다. 데이터를 다시 받으면 데이터와 서명을 분할하고 확인하십시오. 기본 알고리즘을 SHA256으로 설정 했으므로 32 바이트 키가 필요합니다.
key = secrets.token_bytes(32)
다양한 형식의 직렬화 및 역 직렬화로이 모든 것을 패키징 하는 itsdangerous
라이브러리 를 살펴볼 수 있습니다 .
AES-GCM 암호화를 사용하여 암호화 및 무결성 제공
Fernet은 암호화 된 데이터의 무결성을 보장하기 위해 HMAC 서명으로 AEC-CBC를 기반으로합니다. 악의적 인 공격자는 암호문이 서명되어 있기 때문에 잘못된 입력으로 서클에서 서비스를 계속 실행하기 위해 시스템에 말도 안되는 데이터를 제공 할 수 없습니다.
갈루아 / 카운터 모드의 블록 암호는 암호문을 생성하고 태그 때문에 동일한 목적을 제공하기 위해 사용될 수 있고, 동일한 목적을 제공 할 수있다. 단점은 Fernet과 달리 다른 플랫폼에서 재사용 할 수있는 사용하기 쉬운 한 가지 방법이 없다는 것입니다. AES-GCM은 또한 패딩을 사용하지 않으므로이 암호화 암호문은 입력 메시지의 길이와 일치합니다 (Fernet / AES-CBC는 메시지를 고정 길이의 블록으로 암호화하여 메시지 길이를 다소가립니다).
AES256-GCM은 일반적인 32 바이트 비밀을 키로 사용합니다.
key = secrets.token_bytes(32)
그런 다음 사용
import binascii, time
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidTag
backend = default_backend()
def aes_gcm_encrypt(message: bytes, key: bytes) -> bytes:
current_time = int(time.time()).to_bytes(8, 'big')
algorithm = algorithms.AES(key)
iv = secrets.token_bytes(algorithm.block_size // 8)
cipher = Cipher(algorithm, modes.GCM(iv), backend=backend)
encryptor = cipher.encryptor()
encryptor.authenticate_additional_data(current_time)
ciphertext = encryptor.update(message) + encryptor.finalize()
return b64e(current_time + iv + ciphertext + encryptor.tag)
def aes_gcm_decrypt(token: bytes, key: bytes, ttl=None) -> bytes:
algorithm = algorithms.AES(key)
try:
data = b64d(token)
except (TypeError, binascii.Error):
raise InvalidToken
timestamp, iv, tag = data[:8], data[8:algorithm.block_size // 8 + 8], data[-16:]
if ttl is not None:
current_time = int(time.time())
time_encrypted, = int.from_bytes(data[:8], 'big')
if time_encrypted + ttl < current_time or current_time + 60 < time_encrypted:
# too old or created well before our current time + 1 h to account for clock skew
raise InvalidToken
cipher = Cipher(algorithm, modes.GCM(iv, tag), backend=backend)
decryptor = cipher.decryptor()
decryptor.authenticate_additional_data(timestamp)
ciphertext = data[8 + len(iv):-16]
return decryptor.update(ciphertext) + decryptor.finalize()
Fernet이 지원하는 동일한 TTL (Time-to-Live) 사용 사례를 지원하기 위해 타임 스탬프를 포함했습니다.
이 페이지의 다른 접근 방식, Python 3
AES CFB- CBC와 비슷하지만 패딩 할 필요 없음
이것은 잘못되었지만 All Іѕ Vаиітy가 따르는 접근 방식입니다 . 이것은 cryptography
버전이지만 암호문에 IV를 포함하므로 전역으로 저장해서는 안됩니다 (IV를 재사용하면 키의 보안이 약화되고 모듈 전역으로 저장하면 다시 생성됨을 의미 함). 다음 Python 호출, 모든 암호문을 해독 할 수 없도록 렌더링) :
import secrets
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
backend = default_backend()
def aes_cfb_encrypt(message, key):
algorithm = algorithms.AES(key)
iv = secrets.token_bytes(algorithm.block_size // 8)
cipher = Cipher(algorithm, modes.CFB(iv), backend=backend)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(message) + encryptor.finalize()
return b64e(iv + ciphertext)
def aes_cfb_decrypt(ciphertext, key):
iv_ciphertext = b64d(ciphertext)
algorithm = algorithms.AES(key)
size = algorithm.block_size // 8
iv, encrypted = iv_ciphertext[:size], iv_ciphertext[size:]
cipher = Cipher(algorithm, modes.CFB(iv), backend=backend)
decryptor = cipher.decryptor()
return decryptor.update(encrypted) + decryptor.finalize()
여기에는 HMAC 서명의 추가 아머 링이 없으며 타임 스탬프가 없습니다. 직접 추가해야합니다.
위의 내용은 기본 암호화 빌딩 블록을 잘못 결합하는 것이 얼마나 쉬운 지 보여줍니다. 모든 Іѕ Vаиітy의 IV 값을 잘못 처리하면 IV가 손실되어 데이터 유출 또는 모든 암호화 된 메시지를 읽을 수 없게 될 수 있습니다. 대신 Fernet을 사용하면 이러한 실수로부터 보호됩니다.
AES ECB – 안전하지 않음
이전에 AES ECB 암호화를 구현 했고 Python 3에서이를 계속 지원해야하는 경우에도 여전히 그렇게 할 수 cryptography
있습니다. 동일한 경고가 적용되며 ECB는 실제 애플리케이션에 대해 충분히 안전하지 않습니다 . Python 3에 대한 답변을 다시 구현하여 패딩 자동 처리를 추가합니다.
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend
backend = default_backend()
def aes_ecb_encrypt(message, key):
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend)
encryptor = cipher.encryptor()
padder = padding.PKCS7(cipher.algorithm.block_size).padder()
padded = padder.update(msg_text.encode()) + padder.finalize()
return b64e(encryptor.update(padded) + encryptor.finalize())
def aes_ecb_decrypt(ciphertext, key):
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend)
decryptor = cipher.decryptor()
unpadder = padding.PKCS7(cipher.algorithm.block_size).unpadder()
padded = decryptor.update(b64d(ciphertext)) + decryptor.finalize()
return unpadder.update(padded) + unpadder.finalize()
다시 말하지만 여기에는 HMAC 서명이 없기 때문에 ECB를 사용해서는 안됩니다. 위의 내용은 cryptography
실제로 사용해서는 안되는 공통 암호화 빌딩 블록을 처리 할 수있는 것을 설명하기 위한 것입니다.