如何在Python中使用密码加密文本?

33
想要在网络上寻找一个直接的答案似乎很困难。
我想收集来自用户消息的文本和代码,例如1PWP7a6xgoYx81VZocrDr5okEEcnqKkyDc hello world 。 然后我希望能够以某种方式使用该文本加密/解密消息,以便我可以将其保存在数据库中并不用担心我的网站被黑客攻击时数据会被泄露。 encrypt('1PWP7a6xgoYx81VZocrDr5okEEcnqKkyDc','hello world') decrypt('1PWP7a6xgoYx81VZocrDr5okEEcnqKkyDc',<encrypted_text>) 请问是否有一种简单的方法可以使用Python实现,并提供/指向示例?
也许可以举一个如何使用类似“1PWP7a6xgoYx81VZocrDr5okEEcnqKkyDc”这样的种子生成公钥/私钥对的示例?
先行致谢 :)
编辑:仅为明确起见,我正在寻找一种以确定性方式加密用户数据的方法,而不是混淆消息。
如果这意味着我必须使用文本1PWP7a6xgoYx81VZocrDr5okEEcnqKkyDc作为种子即时生成PGP / GPG公钥/私钥对,那么可以,但是如何进行操作?

2
“以确定性方式加密我的用户数据” - 不安全。如果相同明文的每个加密都产生相同的密文,那么在您的数据库中发现相同的明文将非常容易。 - user2357112
1
@user2357112,我将使用唯一的文本字符串来加密每个消息,因此不会有相同的明文。 - derrend
即使它不是完全重复的,另一个问题似乎包含了有用的答案。另一个问题的几个答案使用了强大的算法,如AES或DES。那个问题的本质不是混淆而是安全(尽管OP更感兴趣的是混淆)。一个合适的答案似乎是使用pycrypto进行加密本身,然后使用base64来获取结果的字符串表示形式。 - John Coleman
3
即使有一些有用的片段,除非你已经知道自己在做什么,否则很难辨认哪些是有用的并且如何正确使用它们。DES已经被攻破,最佳AES答案使用ECB模式,其后的AES答案未展示正确的IV处理。被采纳的答案使用维吉尼亚密码,这种密码已经被攻破了几个世纪,而且可以用手打破。 - user2357112
2
@derrend:相同的明文块将映射到相同的密文块,因此你会得到像ECB企鹅这样的东西。 - user2357112
显示剩余4条评论
6个回答

57

以下是如何使用CBC模式进行正确操作,包括PKCS#7填充:

import base64
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto import Random

def encrypt(key, source, encode=True):
    key = SHA256.new(key).digest()  # use SHA-256 over our key to get a proper-sized AES key
    IV = Random.new().read(AES.block_size)  # generate IV
    encryptor = AES.new(key, AES.MODE_CBC, IV)
    padding = AES.block_size - len(source) % AES.block_size  # calculate needed padding
    source += bytes([padding]) * padding  # Python 2.x: source += chr(padding) * padding
    data = IV + encryptor.encrypt(source)  # store the IV at the beginning and encrypt
    return base64.b64encode(data).decode("latin-1") if encode else data

def decrypt(key, source, decode=True):
    if decode:
        source = base64.b64decode(source.encode("latin-1"))
    key = SHA256.new(key).digest()  # use SHA-256 over our key to get a proper-sized AES key
    IV = source[:AES.block_size]  # extract the IV from the beginning
    decryptor = AES.new(key, AES.MODE_CBC, IV)
    data = decryptor.decrypt(source[AES.block_size:])  # decrypt
    padding = data[-1]  # pick the padding value from the end; Python 2.x: ord(data[-1])
    if data[-padding:] != bytes([padding]) * padding:  # Python 2.x: chr(padding) * padding
        raise ValueError("Invalid padding...")
    return data[:-padding]  # remove the padding

它被设置为使用bytes数据,因此如果您想加密字符串或使用字符串密码,请确保在将其传递给方法之前使用适当的编解码器对其进行encode()。 如果将encode参数保留为True,则encrypt()输出将为base64编码的字符串,并且decrypt()源也应该是base64字符串。

现在,如果您将其测试为:

my_password = b"secret_AES_key_string_to_encrypt/decrypt_with"
my_data = b"input_string_to_encrypt/decrypt"

print("key:  {}".format(my_password))
print("data: {}".format(my_data))
encrypted = encrypt(my_password, my_data)
print("\nenc:  {}".format(encrypted))
decrypted = decrypt(my_password, encrypted)
print("dec:  {}".format(decrypted))
print("\ndata match: {}".format(my_data == decrypted))
print("\nSecond round....")
encrypted = encrypt(my_password, my_data)
print("\nenc:  {}".format(encrypted))
decrypted = decrypt(my_password, encrypted)
print("dec:  {}".format(decrypted))
print("\ndata match: {}".format(my_data == decrypted))

您的输出将类似于:

key:  b'secret_AES_key_string_to_encrypt/decrypt_with'
data: b'input_string_to_encrypt/decrypt'

enc:  7roSO+P/4eYdyhCbZmraVfc305g5P8VhDBOUDGrXmHw8h5ISsS3aPTGfsTSqn9f5
dec:  b'input_string_to_encrypt/decrypt'

data match: True

Second round....

enc:  BQm8FeoPx1H+bztlZJYZH9foI+IKAorCXRsMjbiYQkqLWbGU3NU50OsR+L9Nuqm6
dec:  b'input_string_to_encrypt/decrypt'

data match: True

证明相同的密钥和数据每次生成的密文仍然不同。

现在,这比 ECB 好多了,但是......如果你要用它进行通信-不要!这更多地是为了解释应该如何构建,而不是真正用于生产环境,特别是不用于通信,因为缺少一个关键因素-消息认证。随意玩耍,但是你不应该自己编写密码,有经过审查的协议可以帮助你避免常见陷阱,你应该使用它们。


2
有经过充分验证的协议可以帮助您避免常见的陷阱,您应该使用它们。你的意思是有一种更简单/更强大的方法来实现这个吗?如果是这样,请给我提供链接。 - derrend
1
@TessellatingHeckler - 是的,CBC确实需要它,如果不是随机和唯一的(对使用的密钥),CBC就会容易受到多种攻击。至于Damon的评论,恐怕他不知道自己在说什么 - 在CBC模式中,IV用于XOR第一个“明文”块,由于它是链接模式,如果您无法检索第一个块,则无法检索任何其他块 - 这意味着,如果您使用数据加密IV,则无法解密它。有一种方法可以混淆IV,但这超出了评论部分的大小,并且除此之外,绝对没有理由这样做。 - zwer
3
警告:对于具有非常高熵量的输入字符串,使用SHA作为密钥派生方法(即KBKDF)是可以接受的,但对于普通密码或短语则不可行。CBC适用于存储(例如在数据库中),但**不适用于传输模式安全性:请使用TLS代替。 - Maarten Bodewes
4
这可能是一个不错的解决方案,但看起来过于复杂了。我认为没有理由不使用 cryptography 库(https://cryptography.io/en/latest/),它可以在一行代码中完成加密/解密操作。该软件包是静态链接的,因此所有依赖项都已包含在内。 - expz
1
@Ferris - 因为自己编写加密算法通常是个坏主意。特别是,仅使用此方法来保护数据传输会使您的协议容易受到重放攻击、密钥流替换、填充预言攻击和可能的其他攻击。此外,正如之前在评论中指出的那样,它不包括用于低熵数据的安全密钥派生,因此容易受到暴力攻击。有一整群比我聪明得多的人花了几十年时间来解决细节问题,因此,请在生产环境中参考他们经过验证的协议,例如TLS。 - zwer
显示剩余5条评论

11

基于zwer的答案,但展示了一种尝试处理源文本恰好是16(AES.block_size)的倍数的情况的示例。然而,@zwer在评论中解释了这段代码将不适当地填充源文本,从而破坏了您文本的加密,使您的管道不安全。

代码:

from builtins import bytes
import base64
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto import Random

def encrypt(string, password):
    """
    It returns an encrypted string which can be decrypted just by the 
    password.
    """
    key = password_to_key(password)
    IV = make_initialization_vector()
    encryptor = AES.new(key, AES.MODE_CBC, IV)

    # store the IV at the beginning and encrypt
    return IV + encryptor.encrypt(pad_string(string))

def decrypt(string, password):
    key = password_to_key(password)   
    
    # extract the IV from the beginning
    IV = string[:AES.block_size]  
    decryptor = AES.new(key, AES.MODE_CBC, IV)
    
    string = decryptor.decrypt(string[AES.block_size:])
    return unpad_string(string)

def password_to_key(password):
    """
    Use SHA-256 over our password to get a proper-sized AES key.
    This hashes our password into a 256 bit string. 
    """
    return SHA256.new(password).digest()

def make_initialization_vector():
    """
    An initialization vector (IV) is a fixed-size input to a cryptographic
    primitive that is typically required to be random or pseudorandom.
    Randomization is crucial for encryption schemes to achieve semantic 
    security, a property whereby repeated usage of the scheme under the 
    same key does not allow an attacker to infer relationships 
    between segments of the encrypted message.
    """
    return Random.new().read(AES.block_size)

def pad_string(string, chunk_size=AES.block_size):
    """
    Pad string the peculirarity that uses the first byte
    is used to store how much padding is applied
    """
    assert chunk_size  <= 256, 'We are using one byte to represent padding'
    to_pad = (chunk_size - (len(string) + 1)) % chunk_size
    return bytes([to_pad]) + string + bytes([0] * to_pad)
def unpad_string(string):
    to_pad = string[0]
    return string[1:-to_pad]

def encode(string):
    """
    Base64 encoding schemes are commonly used when there is a need to encode 
    binary data that needs be stored and transferred over media that are 
    designed to deal with textual data.
    This is to ensure that the data remains intact without 
    modification during transport.
    """
    return base64.b64encode(string).decode("latin-1")

def decode(string):
    return base64.b64decode(string.encode("latin-1"))

                              

                                                                                                                                                       

测试:

def random_text(length):
    def rand_lower():
        return chr(randint(ord('a'), ord('z')))
    string = ''.join([rand_lower() for _ in range(length)])
    return bytes(string, encoding='utf-8')

def test_encoding():
    string = random_text(100)
    assert encode(string) != string
    assert decode(encode(string)) == string

def test_padding():
    assert len(pad_string(random_text(14))) == 16
    assert len(pad_string(random_text(15))) == 16
    assert len(pad_string(random_text(16))) == 32

def test_encryption():
    string = random_text(100)
    password = random_text(20)
    assert encrypt(string, password) != string
    assert decrypt(encrypt(string, password), password) == string

1
谢谢您的回答,先生 :) @zwer,您是否介意花点时间检查一下这个新答案是否确实改进了您之前的答案,并涵盖了源是16的倍数的用例?谢谢 :) - derrend
3
@derrend - 抱歉回复晚了,SO没有通知我你的评论,我刚刚才看到。关于源大小,我的示例中没有错误-当它恰好是AES.block_size的倍数时,它将返回AES.block_size作为填充,确保根据规范,存在PKCS#7填充。但是,Ignacio的(chunk_size - (len(string) + 1)) % chunk_size将会对所有AES.block_size - 1(在本例中为15个字节)的倍数进行零填充,这将破坏那些特定大小的源的加密/解密过程。 - zwer
1
@zwer非常感谢您,非常感谢您的回复和澄清 :) Ignacio,您的努力仍然非常受赞赏。 - derrend
谢谢,问题已解决。如果您需要我更新答案,请告诉我。 - Ignacio Tartavull

1
  1. 如果您要使用提到的数据库来授权用户,应该使用用户密码的哈希值或消息摘要,而不是双向加密算法,这将使您的数据在发生数据库泄露时难以使用。
  2. 您无法使用以上方法来保护需要在某一点上解密的数据,但即便如此,您也可以使用比仅使用某个固定密钥加密用户密码更安全的方法(这是最糟糕的方法)。请查看OWASP的密码存储备忘单

由于您写道“我想能够加密/解密消息”,因此我附上了一个使用Blowfish进行加密/解密的简单Python源代码(在2.7下测试)。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import os
from Crypto.Cipher import Blowfish     # pip install pycrypto

BS = 8
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 
unpad = lambda s : s[0:-ord(s[-1])]

def doEncrypt(phrase, key):
    c1  = Blowfish.new(key, Blowfish.MODE_ECB)
    return c1.encrypt(pad(phrase))

def doDecrypt(phrase, key):
    c1  = Blowfish.new(key, Blowfish.MODE_ECB)
    return unpad(c1.decrypt(phrase))

def testing123(phrase, key):
    encrypted = doEncrypt(phrase, key)
    decrypted = doDecrypt(encrypted, key)
    assert phrase == decrypted, "Blowfish ECB enc/dec verification failed"
    print ("Blowfish ECB enc/dec verified ok")
    print ('phrase/key(hex)/enc+dec: {}/{}/{}'.format(phrase, key.encode('hex'), decrypted))

if __name__== "__main__":
    phrase= 'Ala ma kota, a kot ma AIDS.'
    key= os.urandom(32)
    testing123(phrase, key)

1
ECB?Blowfish?不安全! - Maarten Bodewes

0

你考虑过使用密码学包吗?这里有一个简单的例子,使用Fernet加密,来自他们的README:

from cryptography.fernet import Fernet

key = Fernet.generate_key()
f = Fernet(key)
token = f.encrypt(b"A secret message")
f.decrypt(token)

根据this answer,以下AES256-GCM解决方案更安全,尽管需要一个nonce:
import secrets
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

# Generate a random secret key (AES256 needs 32 bytes)
key = secrets.token_bytes(32)

# Encrypt a message
nonce = secrets.token_bytes(12)  # GCM mode needs 12 fresh bytes every time
ciphertext = nonce + AESGCM(key).encrypt(nonce, b"Message", b"")

# Decrypt (raises InvalidTag if using wrong key or corrupted ciphertext)
msg = AESGCM(key).decrypt(ciphertext[:12], ciphertext[12:], b"")

安装加密包的方法如下:

pip install cryptography

祝好,Cocco


0

你可以使用标准 Python 库中的两个内置函数来实现。第一个函数是ord()函数,它接收一个 Unicode 字符串字符作为单个输入参数,并将其转换为相应的 Unicode 代码(一个整数)。下面提供了这个函数的两个简单示例:

>>> ord('a')
    97

>>> ord('b')
    98

接下来,您还有ord()的反函数:chr()。正如您所想象的那样,它可以完全相反地工作:它以Unicode代码作为输入(整数),并获取相应的Unicode字符(字符串):

>>> chr(97)
    'a'

>>> chr(98)
    'b'

然后,您可以通过添加或减去一些任意整数来进行简单的加密... 在这种情况下,数字2:

注意:不要使用非常大的值,否则如果达到负数,例如,您将会得到错误。

def encrypt(message):
    newS=''
    for car in message:
        newS=newS+chr(ord(car)+2)
    return newS


print(encrypt('hello world'))

得到的结果如下:

jgnnq"yqtnf

现在你可以复制并粘贴相同的函数,生成解密函数。在这种情况下,显然需要减去2:

def decrypt(message):
    newS=''
    for car in message:
        newS=newS+chr(ord(car)-2)
    return newS


print(decrypt('jgnnq"yqtnf'))

结果将会是原始消息:

'hello world'

这是一种加密消息以便非程序员阅读的好方法。然而,任何有一点编程知识的人都可以编写一个程序,改变我们使用的整数,直到他们发现我们只是添加了(2)到Unicode字符来加密代码...

为了避免这种情况,我提出了两个更复杂的替代方案。

1. 第一个是最简单的:它包括根据字符的位置应用不同的和值到chr函数上(例如,在字符串中占据偶数位置时将每个Unicode代码加2,在奇数位置时减去3)。

2. 第二个将产生最大的安全性。它将包括为每个字符随机生成一个数字来添加或减去每个Unicode代码。它需要存储一个值数组来解密消息。请确保这个值数组不可供第三方使用。

这里是可能的解决方案1

def encryptHard(message):
newS=''
for i in range(len(message)):
  if i%2==0:
    newS=newS+chr(ord(message[i])+2)
  else:
    newS=newS+chr(ord(message[i])-3)
return newS


print(encryptHard('hello world'))

结果会是:

jbniqyltif

根据提供的信息,解密脚本显而易见,所以我不会麻烦你复制、粘贴和更改两个值。

最后,让我们深入分析第二种更复杂的替代方案。通过这种方法,我们可以说加密将几乎无法被破解。其思想是通过介于0和255之间的随机数来变化我们添加或减去到每个Unicode代码的值(这是chr()函数允许的数字范围,因此不要尝试使用其他数字,否则您肯定会出现错误)。

在这种情况下,我的建议还随机化操作(求和或减去),并避免最终数字为0(即我们将得到一个原始字符)。最后,它还返回一个列表,其中包含已减去的数字,这是您需要用来解密消息的。

如果您使用相同长度为n的消息两次调用此函数,则获得相同加密消息的机会接近于255^n...所以不用担心(我说“接近”,因为创建的算法实际上会在值的低端或高端范围内生成更多重复值,例如,如果最常见的字符未集中在此分布Unicode字符集(从0到255)中,则是这种情况。但是,尽管程序并不完美,但它可以无缝运行并保护信息。

import random as r
def encryptSuperHard(message):
  newS=''
  l_trans=[]
  for car in message:
    code=ord(car)
    add_subtract=r.choice([True,False])
    if add_subtract:
      transpose=r.randint(0,code-1)
      newS=newS+chr(code-transpose)
      l_trans=l_trans+[-transpose]
    else:
      transpose=r.randint(code+1,255)
      newS=newS+chr(code+transpose)
      l_trans=l_trans+[transpose]
  return newS, l_trans

print(encryptSuperHard('hello world'))

在这种情况下,我制作的这个随机加密脚本返回了一个包含两个值的元组,第一个值是加密后的消息,第二个值是将每个字符按出现顺序"转置"的值。
('A0ŤłY\x10řG;,à', [-39, -53, 248, 214, -22, -16,     226, -40, -55, -64, 124])

在这种情况下解密需要取出加密的消息和列表,然后按照以下步骤进行:
def decryptSuperHard(encriptedS,l):
  newS=''
  for i in range(len(l)):
    newS=newS+chr(ord(encriptedS[i])-l[i])
  return newS

print(decryptSuperHard('A0ŤłY\x10řG;,à', [-39,-53,248,214,-22,-16,226,-40,-55,-64,124]))

结果返回到:

你好,世界

print(deccryptSuperHard('A0ŤłY\x10řG;,à', [-39, -53, 248, 214, -22, -16,     226, -40, -55, -64, 124])

2
我很感激你花费时间和精力来创建这个答案,但我非常确定这将是对我的原始问题一个可怕且极不安全的解决方案。例如,必须存储用于解密文本所需的值数组是不现实的,并且会引入进一步的复杂性。尽管如此,还是谢谢你的尝试 :) - derrend
6
恭喜你,你刚刚发现了一个滚动凯撒密码,在两千年后。 - zwer
-1,很明显这不是安全的。大多数人需要整整15分钟才能反向工程并解密此“加密”算法使用的任何内容。 - Matt Messersmith
@derrend,不用谢。很抱歉我想出的更复杂的解决方案不可行。我不是计算机科学家,也不是网络安全专家,但我认为它可能会帮助你产生一些想法。感谢你阅读它所花费的时间,我很享受思考它! - blackcub3s

-2

这是我的解决方案,供有兴趣的人参考:

from Crypto.Cipher import AES  # pip install pycrypto
import base64

def cypher_aes(secret_key, msg_text, encrypt=True):
    # an AES key must be either 16, 24, or 32 bytes long
    # in this case we make sure the key is 32 bytes long by adding padding and/or slicing if necessary
    remainder = len(secret_key) % 16
    modified_key = secret_key.ljust(len(secret_key) + (16 - remainder))[:32]
    print(modified_key)

    # input strings must be a multiple of 16 in length
    # we achieve this by adding padding if necessary
    remainder = len(msg_text) % 16
    modified_text = msg_text.ljust(len(msg_text) + (16 - remainder))
    print(modified_text)

    cipher = AES.new(modified_key, AES.MODE_ECB)  # use of ECB mode in enterprise environments is very much frowned upon

    if encrypt:
        return base64.b64encode(cipher.encrypt(modified_text)).strip()

    return cipher.decrypt(base64.b64decode(modified_text)).strip()


encrypted = cypher_aes(b'secret_AES_key_string_to_encrypt/decrypt_with', b'input_string_to_encrypt/decrypt', encrypt=True)
print(encrypted)
print()
print(cypher_aes(b'secret_AES_key_string_to_encrypt/decrypt_with', encrypted, encrypt=False))

结果:

b'secret_AES_key_string_to_encrypt'
b'input_string_to_encrypt/decrypt '
b'+IFU4e4rFWEkUlOU6sd+y8JKyyRdRbPoT/FvDBCFeuY='

b'secret_AES_key_string_to_encrypt'
b'+IFU4e4rFWEkUlOU6sd+y8JKyyRdRbPoT/FvDBCFeuY=    '
b'input_string_to_encrypt/decrypt'

2
不要使用ECB模式。永远不要!在企业环境中,它不仅会被看作是不好的做法,而且在任何需要安全性的场合都是如此。ECB会泄漏大量数据,并容易受到许多攻击,以至于一些库甚至拒绝将其作为选项加入。 - zwer
1
顺便提一下,这里是如何使用CBC(在文件上,但删除文件流,您也可以将同样的代码用于内存加密):https://stackoverflow.com/a/44126075/7553525 - zwer
@zwer,我不是Python专家,您是否介意提供一个修改后的示例,以使用CBC以安全的方式实现相同的功能?我非常感激 :) 我会支付您100个声望点。 - derrend
@zwer - AES KeyWrap 使用 ECB - https://tools.ietf.org/html/rfc5649 - 第4.1.2节中有说明。AES KeyWrap 受到反对吗?苹果在 iOS 设备上使用它,在 LUKS 和 TrueCrypt 上用于保护加密文件系统的密钥,显然是这样的 - https://security.stackexchange.com/a/40073/1427 评论。 - TessellatingHeckler
@TessellatingHeckler - KeyWrap从不加密相同的块,也不在多个块上使用相同的密钥,因此ECB在这里是完全可以的 - 但这是由于包装设计本身,而不是因为ECB作为一种通用加密模式是一个好主意。此外,你可能会称其为“Joe”,而不是ECB,因为它是块链默认操作模式,并且永远不会消失 - 但库创建者正在尝试让终端用户难以使用,因为使用它非常容易出错。 - zwer
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接