根据密码编码字符串的简单方法?
Python有没有简单的方法,可以用密码来加密和解密字符串呢?
比如说,像这样:
>>> encode('John Doe', password = 'mypass')
'sjkl28cn2sx0'
>>> decode('sjkl28cn2sx0', password = 'mypass')
'John Doe'
那么字符串“John Doe”就会被加密成'sjkl28cn2sx0'。要想得到原来的字符串,我需要用一个密码'mypass'来“解锁”这个字符串,这个密码就在我的代码里。我希望能用这种方式来给Word文档加密和解密。
我想把这些加密后的字符串用作网址参数。我的目标是让内容变得模糊,而不是追求强安全性;我并不在意编码的内容是否非常重要。我知道可以用数据库表来存储密码和对应的值,但我想尽量简单一点。
22 个回答
既然你明确表示你想要的是模糊性而不是安全性,我们就不批评你所建议的做法有多弱了 :)
那么,使用PyCrypto的话:
import base64
from Crypto.Cipher import AES
msg_text = b'test some plain text here'.rjust(32)
secret_key = b'1234567890123456'
cipher = AES.new(secret_key,AES.MODE_ECB) # never use ECB in strong systems obviously
encoded = base64.b64encode(cipher.encrypt(msg_text))
print(encoded)
decoded = cipher.decrypt(base64.b64decode(encoded))
print(decoded)
如果有人拿到了你的数据库和代码,他们就能解密你加密的数据。所以一定要好好保护你的 secret_key
!
Python没有内置的加密方案。你需要认真对待加密数据的存储;一些简单的加密方法可能对一个开发者来说不安全,但对另一个经验不足的开发者来说可能看起来是安全的。如果你要加密,就要好好加密。
不过,实施一个合适的加密方案其实并不复杂。首先,不要重新发明轮子,使用一个可信赖的加密库来处理这些事情。对于Python 3,那个可信赖的库就是cryptography
。
我还建议加密和解密操作应该针对字节进行;首先把文本消息编码成字节;stringvalue.encode()
可以编码成UTF8,使用bytesvalue.decode()
可以轻松恢复。
最后但同样重要的是,在加密和解密时,我们谈论的是密钥,而不是密码。密钥不应该是人类容易记住的,它应该存储在一个秘密的地方,但可以被机器读取,而密码通常是人类可读并且可以记住的。你可以通过密码推导出一个密钥,但需要小心。
但是对于一个网络应用或在集群中运行的进程,如果没有人来持续关注它,你应该使用密钥。密码是为了让最终用户访问特定信息而设计的。即便如此,通常你会用密码来保护应用程序,然后使用密钥交换加密信息,可能是与用户账户相关联的密钥。
对称密钥加密
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
# PRINTING FOR DEMO PURPOSES ONLY, don't do this in production code
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'
>>> token = encrypt(message.encode(), key)
>>> print(token)
'gAAAAABciT3pFbbSihD_HZBZ8kqfAj94UhknamBuirZWKivWOukgKQ03qE2mcuvpuwCSuZ-X_Xkud0uWQLZ5e-aOwLC0Ccnepg=='
>>> decrypt(token, key).decode()
'John Doe'
使用密码的Fernet – 从密码推导出的密钥,安全性稍弱
你可以使用密码代替秘密密钥,前提是你使用强大的密钥推导方法。这样你就需要在消息中包含盐和HMAC迭代次数,因此加密值不再兼容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基于AEC-CBC和HMAC签名,以确保加密数据的完整性;恶意攻击者无法向你的系统输入无意义的数据,让你的服务陷入忙碌,因为密文是被签名的。
Galois / 计数模式块密码生成密文和一个标签来实现相同的目的,因此可以用于相同的用途。缺点是,与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但不需要填充
这是All Іѕ Vаниtу所遵循的方法,尽管不正确。这是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签名的额外保护,并且没有时间戳;你需要自己添加这些。
上述内容还说明了如何错误地组合基本的加密构建块;All Іѕ Vаниtу对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
可以处理常见的加密构建块,即使是那些你实际上不应该使用的。
假设你只是想要一些简单的混淆方法,让普通人看不懂,而不想使用第三方库。我推荐你可以试试维吉尼亚密码。这是一种古老的简单密码中比较强的。
这个密码实现起来既快又简单。像这样:
import base64
def encode(key, string):
encoded_chars = []
for i in xrange(len(string)):
key_c = key[i % len(key)]
encoded_c = chr(ord(string[i]) + ord(key_c) % 256)
encoded_chars.append(encoded_c)
encoded_string = "".join(encoded_chars)
return base64.urlsafe_b64encode(encoded_string)
解码的过程基本上是一样的,只不过你需要把密钥减去。
如果你编码的字符串比较短,或者很难猜到用的密码长度,那么破解起来就会更难。
如果你想要更强的加密,PyCrypto可能是个不错的选择,不过之前的回答有些细节没提到:在PyCrypto中,ECB模式要求你的消息长度必须是16的倍数。所以,你需要进行填充。此外,如果你想把它们用作URL参数,建议使用 base64.urlsafe_b64_encode()
,而不是标准的那种。这个方法会把base64字母表中的一些字符替换成URL安全的字符(正如它的名字所暗示的那样)。
不过,在使用之前,你一定要确保这种非常薄弱的混淆层能满足你的需求。我链接的维基百科文章提供了详细的破解方法,所以只要有一定决心的人都能轻松破解它。