Python não tem esquemas de criptografia embutidos, não. Você também deve levar a sério o armazenamento de dados criptografados; esquemas de criptografia triviais que um desenvolvedor entende como inseguros e um esquema de brinquedo pode muito bem ser confundido com um esquema seguro por um desenvolvedor menos experiente. Se você criptografar, criptografe corretamente.
No entanto, você não precisa fazer muito trabalho para implementar um esquema de criptografia adequado. Em primeiro lugar, não reinvente a roda da criptografia , use uma biblioteca de criptografia confiável para lidar com isso para você. Para Python 3, essa biblioteca confiável écryptography
.
Eu também recomendo que a criptografia e descriptografia se apliquem a bytes ; codifique as mensagens de texto em bytes primeiro; stringvalue.encode()
codifica para UTF8, facilmente revertido novamente usando bytesvalue.decode()
.
Por último, mas não menos importante, ao criptografar e descriptografar, falamos sobre chaves , não senhas. Uma chave não deve ser memorável por humanos, é algo que você armazena em um local secreto, mas legível por máquina, ao passo que uma senha geralmente pode ser lida e memorizada por humanos. Você pode derivar uma chave de uma senha, com um pouco de cuidado.
Mas para um aplicativo da web ou processo em execução em um cluster sem atenção humana para mantê-lo em execução, você deseja usar uma chave. As senhas são para quando apenas um usuário final precisa de acesso às informações específicas. Mesmo assim, você geralmente protege o aplicativo com uma senha e, em seguida, troca informações criptografadas usando uma chave, talvez uma anexada à conta do usuário.
Criptografia de chave simétrica
Fernet - AES CBC + HMAC, fortemente recomendado
A cryptography
biblioteca inclui a receita Fernet , uma receita de práticas recomendadas para usar criptografia. Fernet é um padrão aberto , com implementações prontas em uma ampla gama de linguagens de programação e pacotes de criptografia AES CBC para você com informações de versão, um carimbo de data / hora e uma assinatura HMAC para evitar adulteração de mensagens.
Fernet torna muito fácil criptografar e descriptografar mensagens e mantê-lo seguro. É o método ideal para criptografar dados com um segredo.
Eu recomendo que você use Fernet.generate_key()
para gerar uma chave segura. Você também pode usar uma senha (próxima seção), mas uma chave secreta completa de 32 bytes (16 bytes para criptografar, mais outros 16 para a assinatura) será mais segura do que a maioria das senhas que você possa imaginar.
A chave que o Fernet gera é um bytes
objeto com URL e caracteres de base64 de arquivo seguro, portanto, imprimível:
from cryptography.fernet import Fernet
key = Fernet.generate_key() # store in a secure location
print("Key:", key.decode())
Para criptografar ou descriptografar mensagens, crie uma Fernet()
instância com a chave fornecida e chame Fernet.encrypt()
ou Fernet.decrypt()
, a mensagem de texto simples a ser criptografada e o token criptografado são bytes
objetos.
encrypt()
e as decrypt()
funções seriam semelhantes a:
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)
Demo:
>>> 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 com senha - chave derivada de senha, enfraquece um pouco a segurança
Você pode usar uma senha em vez de uma chave secreta, desde que use um método de derivação de chave forte . Em seguida, você deve incluir o salt e a contagem de iteração HMAC na mensagem, para que o valor criptografado não seja mais compatível com Fernet sem primeiro separar salt, count e token 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)
Demo:
>>> message = 'John Doe'
>>> password = 'mypass'
>>> password_encrypt(message.encode(), password)
b'9Ljs-w8IRM3XT1NDBbSBuQABhqCAAAAAAFyJdhiCPXms2vQHO7o81xZJn5r8_PAtro8Qpw48kdKrq4vt-551BCUbcErb_GyYRz8SVsu8hxTXvvKOn9QdewRGDfwx'
>>> token = _
>>> password_decrypt(token, password).decode()
'John Doe'
Incluir o salt na saída torna possível usar um valor salt aleatório, o que, por sua vez, garante que a saída criptografada seja totalmente aleatória, independentemente da reutilização da senha ou da repetição da mensagem. Incluir a contagem de iterações garante que você possa ajustar os aumentos de desempenho da CPU com o tempo, sem perder a capacidade de descriptografar mensagens mais antigas.
Uma senha sozinha pode ser tão segura quanto uma chave aleatória Fernet de 32 bytes, desde que você gere uma senha aleatória apropriada de um pool de tamanho semelhante. 32 bytes fornece 256 ^ 32 números de chaves, portanto, se você usar um alfabeto de 74 caracteres (26 superior, 26 inferior, 10 dígitos e 12 símbolos possíveis), sua senha deve ter pelo menos math.ceil(math.log(256 ** 32, 74))
== 42 caracteres. No entanto, um número maior e bem selecionado de iterações HMAC pode mitigar um pouco a falta de entropia, pois isso torna muito mais caro para um invasor entrar com força bruta.
Saiba que escolher uma senha mais curta, mas ainda razoavelmente segura, não prejudicará esse esquema, apenas reduzirá o número de valores possíveis que um invasor de força bruta teria que pesquisar; certifique-se de escolher uma senha forte o suficiente para seus requisitos de segurança .
Alternativas
Obscurecedor
Uma alternativa é não criptografar . Não fique tentado a usar apenas uma cifra de baixa segurança ou uma implementação caseira do, digamos Vignere. Não há segurança nessas abordagens, mas podem dar a um desenvolvedor inexperiente que recebe a tarefa de manter seu código no futuro a ilusão de segurança, que é pior do que nenhuma segurança.
Se tudo o que você precisa é obscuridade, apenas base64 os dados; para requisitos seguros de URL, a base64.urlsafe_b64encode()
função é adequada. Não use uma senha aqui, apenas codifique e pronto. No máximo, adicione alguma compressão (como 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))
Isso se transforma b'Hello world!'
em b'eNrzSM3JyVcozy_KSVEEAB0JBF4='
.
Integridade apenas
Se tudo o que você precisa é uma maneira de se certificar de que os dados podem ser confiáveis e inalterados após terem sido enviados a um cliente não confiável e recebidos de volta, então você deseja assinar os dados, você pode usar a hmac
biblioteca para isso com SHA1 (ainda considerado seguro para assinatura HMAC ) ou melhor:
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)
Use-o para assinar dados e, em seguida, anexe a assinatura aos dados e envie-a ao cliente. Ao receber os dados de volta, divida os dados e a assinatura e verifique. Eu defini o algoritmo padrão para SHA256, então você precisará de uma chave de 32 bytes:
key = secrets.token_bytes(32)
Você pode querer dar uma olhada na itsdangerous
biblioteca , que empacota tudo isso com serialização e desserialização em vários formatos.
Usando criptografia AES-GCM para fornecer criptografia e integridade
Fernet baseia-se no AEC-CBC com uma assinatura HMAC para garantir a integridade dos dados criptografados; um invasor mal-intencionado não pode alimentar seu sistema com dados absurdos para manter seu serviço ocupado em execução em círculos com entrada incorreta, porque o texto cifrado está assinado.
A cifra de bloco do modo Galois / Contador produz texto cifrado e uma etiqueta para servir ao mesmo propósito, portanto, pode ser usada para os mesmos fins. A desvantagem é que, ao contrário do Fernet, não existe uma receita única e fácil de usar para reutilizar em outras plataformas. O AES-GCM também não usa preenchimento, portanto, esse texto cifrado de criptografia corresponde ao comprimento da mensagem de entrada (enquanto o Fernet / AES-CBC criptografa as mensagens em blocos de comprimento fixo, obscurecendo um pouco o comprimento da mensagem).
AES256-GCM usa o segredo usual de 32 bytes como uma chave:
key = secrets.token_bytes(32)
então use
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()
Incluí um carimbo de data / hora para oferecer suporte aos mesmos casos de uso de tempo de vida que o Fernet oferece.
Outras abordagens nesta página, em Python 3
AES CFB - como CBC, mas sem a necessidade de preenchimento
Esta é a abordagem que All Іѕ Vаиітy segue, embora incorretamente. Esta é a cryptography
versão, mas observe que incluo o IV no texto cifrado , ele não deve ser armazenado como um global (reutilizar um IV enfraquece a segurança da chave e armazená-lo como um módulo global significa que ele será regenerado a próxima invocação do Python, tornando todo o texto cifrado indecifrável):
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()
Isso não tem a proteção adicional de uma assinatura HMAC e não há carimbo de data / hora; você mesmo teria que adicioná-los.
O texto acima também ilustra como é fácil combinar blocos de construção básicos de criptografia incorretamente; O manuseio incorreto do valor IV de All Іѕ Vаиітy pode levar a uma violação de dados ou a ilegibilidade de todas as mensagens criptografadas porque o IV foi perdido. Em vez disso, usar o Fernet protege você de tais erros.
AES ECB - não seguro
Se você implementou a criptografia AES ECB anteriormente e ainda precisa suportá-la no Python 3, você ainda pode fazer isso com cryptography
também. As mesmas ressalvas se aplicam, o ECB não é seguro o suficiente para aplicativos da vida real . Reimplementando essa resposta para Python 3, adicionando tratamento automático de preenchimento:
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()
Novamente, falta a assinatura HMAC e você não deve usar o ECB de qualquer maneira. O texto acima existe apenas para ilustrar que cryptography
pode lidar com os blocos de construção criptográficos comuns, mesmo aqueles que você não deveria realmente usar.