Python没有内置的加密方案,没有。您还应该认真对待加密的数据存储;一个开发人员理解为不安全的琐碎加密方案和一个玩具方案很可能会被经验不足的开发人员误认为是安全方案。如果加密,请正确加密。
但是,您不需要做很多工作即可实现适当的加密方案。首先,不要重新发明密码轮,而是使用受信任的密码库为您处理。对于Python 3,该受信任的库为cryptography
。
我还建议对字节进行加密和解密;首先将短信编码为字节;stringvalue.encode()
编码为UTF8,可使用轻松再次还原bytesvalue.decode()
。
最后但并非最不重要的一点是,在加密和解密时,我们谈论的是密钥,而不是密码。密钥不应该让人记忆深刻,它是您存储在一个秘密位置但可以机读的东西,而密码通常可以被人类可读和记住。您可以轻松地从密码派生密钥。
但是,对于在群集中运行的Web应用程序或进程而没有人为注意使其继续运行,则需要使用密钥。密码仅在最终用户需要访问特定信息时使用。即使那样,您通常也可以使用密码保护应用程序安全,然后使用可能是用户帐户附带的密钥交换加密信息。
对称密钥加密
Fernet – AES CBC + HMAC,强烈推荐
该cryptography
库包含Fernet配方,这是使用加密技术的最佳实践配方。Fernet是一个开放标准,可以使用多种编程语言进行现成的实现,并且它为您提供AES CBC加密以及版本信息,时间戳和HMAC签名,以防止篡改消息。
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迭代可以在某种程度上缓解熵的缺乏,因为这会使攻击者蛮横地闯入变得更加昂贵。
只是知道选择一个较短但仍相当安全的密码不会破坏该方案,它只是减少了暴力攻击者必须搜索的可能值的数量。确保为您的安全要求选择足够强大的密码。
备择方案
遮盖
另一种方法是不加密。Vignere表示,不要试图只使用低安全性密码或家庭自用的实现。这些方法没有安全性,但是可能会给经验不足的开发人员提供在将来维护代码的任务,从而产生安全性错觉,这比根本没有安全性还差。
如果您需要的只是晦涩难懂,则只需对数据进行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支持的相同的生存时间用例。
本页的其他方法,使用Python 3
AES CFB- 类似于CBC,但无需填充
这是万事俱备的方法,尽管有误。这是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
可以处理常见的密码构造块,甚至您实际上不应该使用的那些。