在Laravel中进行编码,Python中进行解码。

3

我正在使用Laravel的encryptString方法在我的网站上加密一些数据。这使用OpenSSL的256位AES-CBC加密,没有任何序列化。现在我正在尝试在Python中解密这些数据,但我一直收到有关密钥长度的错误,并且似乎无法弄清楚原因。

Example data to decrypt: eyJpdiI6ImdxY0VcLzFodmpISFV4allSWmJDdEpRPT0iLCJ2YWx1ZSI6IkxXd0ZJaUd2bTUweW5pNm0wUjQwOFM2N1wvWEs5SlYrNB4xNlR7Qkh1U3FvPSIsIm1hYyI6Ijc5ZWM0YTYxYjljZGFiNzgwNjY2NDU1ZmQ5Yjc1ZmJlOGU4NzBkMjQzMzA3MmVhYzE3NzY4ZmU1MWIyMjZlOTQifQ==

Example Key to use for decryption (from laravel .env):
base64:/AZejP0lh3McL/+Vy5yZcADdTcR65qnx5Jqinuw7raK=

我将这些值进行了更改,因此实际上使用这些值进行解密不会得到任何真实数据,只是为了举例而已。然后我尝试在Python 3.7中解密此数据:

import base64
from Crypto.Cipher import AES

def decrypt(enc, key):
    IV = 16 * '\x00'
    decobj = AES.new(key, AES.MODE_CBC, IV)
    data = decobj.decrypt(base64.b64decode(enc))
    print(str(data.decode()))

if __name__ == "__main__":
    key = b"/AZejP0lh3McL/+Vy5yZcADdTcR65qnx5Jqinuw7raK="
    decrypt("eyJpdiI6ImdxY0VcLzFodmpISFV4allSWmJDdEpRPT0iLCJ2YWx1ZSI6IkxXd0ZJaUd2bTUweW5pNm0wUjQwOFM2N1wvWEs5SlYrNB4xNlR7Qkh1U3FvPSIsIm1hYyI6Ijc5ZWM0YTYxYjljZGFiNzgwNjY2NDU1ZmQ5Yjc1ZmJlOGU4NzBkMjQzMzA3MmVhYzE3NzY4ZmU1MWIyMjZlOTQifQ==", key)

这段代码看起来应该能够工作,但运行时却报错:ValueError: Incorrect AES key length (60 bytes)。我不确定自己做错了什么。我尝试过对数据/密钥进行填充/取消填充,但没有改变任何东西。我在想是否从 Laravel 获得了错误的解密密钥,但从链接的文档中可以看出,这应该只是我的 .env 文件中的 APP_KEY。

如果有人能帮助我或指点一下方向,那就太棒了!

与其他类似问题不同的是,我主要是想弄清楚我是否从 Laravel 获取了正确的 AES 密钥,我实际上并不需要过多的解密帮助,只是认为我从 Laravel 中获取了错误的密钥。

编辑:新的代码似乎可以工作:

import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

def decrypt(enc, key):
    IV = 16 * '\x00'.encode()
    decobj = AES.new(key, AES.MODE_CBC, IV)
    data = decobj.decrypt(pad(base64.b64decode(enc), 16))
    print(base64.b64decode(data))

if __name__ == "__main__":
    key = base64.b64decode(b"/AZejP0lh3McL/+Vy5yZcADdTcR65qnx5Jqinuw7raK=")
    decrypt("eyJpdiI6ImdxY0VcLzFodmpISFV4allSWmJDdEpRPT0iLCJ2YWx1ZSI6IkxXd0ZJaUd2bTUweW5pNm0wUjQwOFM2N1wvWEs5SlYrNB4xNlR7Qkh1U3FvPSIsIm1hYyI6Ijc5ZWM0YTYxYjljZGFiNzgwNjY2NDU1ZmQ5Yjc1ZmJlOGU4NzBkMjQzMzA3MmVhYzE3NzY4ZmU1MWIyMjZlOTQifQ==", key)

现在的打印语句输出了一些字节,但是当我运行.decode()时,出现了错误:UnicodeDecodeError: 'utf-8' codec can't decode byte 0xfa in position 0: invalid start byte,而且似乎无法弄清楚需要做什么才能将其作为字符串打印出来。


可能是 Python中AES-128 CBC解密 的重复问题。 - stovfl
@stovfl,那个问题看起来可能有助于回答我的问题,但我觉得我的问题主要涉及Laravel/环境文件,而不仅仅是尝试修复错误。在我的问题中,我问是否有人知道我是否从Laravel中获取了正确的AES密钥,而不仅仅是如何修复Python错误。 - Pecans
我明白你的意思。你为什么认为你拿到了错误的密钥?php artisan key:generate 命令会生成什么? - stovfl
@stovfl 根据 https://dev59.com/x1cP5IYBdhLWcg3wH20Z#44839772 生成用于加密的密钥,我也尝试重新运行它以查看会发生什么,但是当我将其输入我的 Python 代码时,仍然是相同错误的位数。 - Pecans
您未能对密钥进行Base64解码。 - President James K. Polk
3个回答

4

问题: ...我试图在Python中解密该数据,但我一直收到关于密钥长度的错误。

我可以在链接答案的代码中使用您的key,在执行.b64decode(...后。示例代码.encode(...decode(...按预期工作。
结论: 您的密钥没有任何问题!

key = b"/AZejP0lh3McL/+Vy5yZcADdTcR65qnx5Jqinuw7raK="
key = base64.b64decode(key)

But with your code, I got TypeError, related to the IV parameter:

  expect_byte_string(iv)
File "/usr/local/lib/python3.4/dist-packages/Crypto/Util/_raw_api.py", line 172, in expect_byte_string
  TypeError: Only byte strings can be passed to C code

Fixed with IV = 16 * '\x00'.encode(), results in ValueError, related to enc:

  data = decobj.decrypt(base64.b64decode(enc))
File "/usr/local/lib/python3.4/dist-packages/Crypto/Cipher/_mode_cbc.py", line 209, in decrypt
  ValueError: Error 3 while decrypting in CBC mode
这导致Github问题:10

错误3表示“ERR_NOT_ENOUGH_DATA”

根据链接的GitHub页面,您需要重新阅读关于填充数据的文档,同时进行编码
来自GitHub的工作示例:
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

key = b"/AZejP0lh3McL/+Vy5yZcADdTcR65qnx5Jqinuw7raK="
key = base64.b64decode(key)

BLOCK_SIZE = 32
encryption_suite = AES.new(key, AES.MODE_CBC, b'This is an IV...')
cipher_text = encryption_suite.encrypt(pad(b'A really secret message...', BLOCK_SIZE))

decryption_suite = AES.new(key, AES.MODE_CBC, b'This is an IV...')
print(unpad(decryption_suite.decrypt(cipher_text), BLOCK_SIZE).decode())
>>> A really secret message...

测试过的Python版本: 3.4.2


嗯,我把你的代码加到我的代码里,已经成功打印出字节,但是当我尝试解码时出现了错误。我会在原问题中更新新代码。感谢迄今为止的帮助! - Pecans
@Pecans:更新了我的答案,并提供了一个可行的示例。注意:在加密之前必须进行pad(...,在解密后进行unpad(...utf-8错误是由于您在编码之后进行了pad(... 之后 的结果。 - stovfl

3

带MAC验证的完整工作代码

在Gonzalo的提交中,Laravel的加密消息是一个由以下键值对组成的base64 json编码数组:

[
      'iv' => 'generated initialization vector (iv)',
      'value' => 'encrypted, base64ed, signed value',
      'mac'  => 'message authentication code (mac)'
]

'value'的值使用消息认证码(MAC)进行签名,以验证在传输过程中该值是否被更改。

从有效负载(加密消息)中提取的MAC应与从'value'中提取的MAC进行检查(可以使用密钥、iv和value来完成此操作)。Laravel的加密方案可以在GitHub上找到:src/Illuminate/Encryption/Encrypter.php

参考Laravel讨论线程,我在Github上追踪到了部分解决方案:orian/crypt.py(这是fideloper/crypt.py的一个分支)。

我已经从Orian的代码上派生出来并修复了输入参数问题。只要加密密钥(作为输入传递给decrypt())进行base64解码,而不包括通常会添加到.env文件中的APP_KEY环境变量字符串赋值之前的“base64:”,代码即按预期工作。

解决方法: pilatuspc12ng/crypt.py

下面是crypt.py的代码片段:

import base64
import json
from Crypto.Cipher import AES
from phpserialize import loads
import hashlib
import hmac


def decrypt(payload, key):
    """
    Decrypt strings that have been encrypted using Laravel's encrypter (AES-256 encryption).
    Plain text is encrypted in Laravel using the following code:
    >>> ciphertext = Crypt::encrypt('hello world');
    The ciphertext is a base64's json-encoded array consisting of the following keys:
       [
          'iv' => 'generated initialization vector (iv)',
          'value' => 'encrypted, base64ed, signed value',
          'mac'  => 'message authentication code (mac)'
       ]
    The 'value' is signed using a message authentication code (MAC) so verify that the value has not changed during
    transit.
    Parameters:
    payload (str): Laravel encrypted text.
    key (str): Encryption key (base64 decoded). Make sure 'base64:' has been removed from string.
    Returns:
    str: plaintext
    """

    data = json.loads(base64.b64decode(payload))
    if not valid_mac(key, data):
        return None

    value = base64.b64decode(data['value'])
    iv = base64.b64decode(data['iv'])

    return unserialize(mcrypt_decrypt(value, iv, key)).decode("utf-8")

def mcrypt_decrypt(value, iv, key):
    AES.key_size=128
    crypt_object=AES.new(key=key,mode=AES.MODE_CBC,IV=iv)
    return crypt_object.decrypt(value)

def unserialize(serialized):
    return loads(serialized)

def valid_mac(key, data):
    dig = hmac.new(key, digestmod=hashlib.sha256)
    dig.update(data['iv'].encode('utf8'))
    dig.update(data['value'].encode('utf8'))
    dig = dig.hexdigest()
    return dig==data['mac']

1
我注意到这个地方有一段时间没有活跃了,但我正在尝试做同样的事情,似乎无法使其工作。
我注意到Laravel存储的编码密码字符串是一个base 64编码的JSON对象,这一点在原始问题中没有被考虑到。
pass_obj = base64.b64decode('eyJpdiI6ImdxY0VcLzFodmpISFV4allSWmJDdEpRPT0iLCJ2YWx1ZSI6IkxXd0ZJaUd2bTUweW5pNm0wUjQwOFM2N1wvWEs5SlYrNB4xNlR7Qkh1U3FvPSIsIm1hYyI6Ijc5ZWM0YTYxYjljZGFiNzgwNjY2NDU1ZmQ5Yjc1ZmJlOGU4NzBkMjQzMzA3MmVhYzE3NzY4ZmU1MWIyMjZlOTQifQ==')
print(pass_obj)
>>> b'{"iv":"gqcE\\/1hvjHHUxjYRZbCtJQ==","value":"LWwFIiGvm50yni6m0R408S67\\/XK9JV+4\x1e16T{BHuSqo=","mac":"79ec4a61b9cdab780666455fd9b75fbe8e870d2433072eac17768fe51b226e94"}'

从中可以获取IV和加密后的值,两者似乎都是base64编码。但是最后我仍然会遇到解码错误,如下所示;

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xfa in position 0: invalid start byte

这是我的完整代码;

password = 'eyJpdiI6ImJGNDNNZjN3YWtpcDQ5VEJVXC9IazF3PT0iLCJ2YWx1ZSI6IkNVRW1VQUY1dXArYlFkU3NlY1pnZUE9PSIsIm1hYyI6ImM3ODk0NWQ0NjgxMzM4YjE0M2JhN2MzZWRmOWEwMWJiMjI2Y2FhYmUxYjFhYzAyYjY4YWZkZGE3N2EyMDYwNWYifQ=='
key = 'some secret key that i can't share'.encode()
p_obj = json.loads(base64.b64decode(password).decode())
decobj = AES.new(key, AES.MODE_CBC, base64.b64decode(p_obj['iv']))
data = decobj.decrypt(base64.b64decode(p_obj['value']))
print(data)
>>> b'l\xee:f\x9eZ\x90rP\x99\xca&@\x1d1\x9f'
data.decode()
>>> Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xee in position 1: invalid continuation byte

@Pecans 你最终解决了这个问题吗?

谢谢。


完整的可工作代码编辑

我解决了问题,最初我的密钥存在问题。因此,以下是完整的代码片段,供将来参考;

import base64
import json
from phpserialize import unserialize
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

key = b'my secret key'
enc_pass = 'eyJpdiI6ImJGNDNNZjN3YWtpcDQ5VEJVXC9IazF3PT0iLCJ2YWx1ZSI6IkNVRW1VQUY1dXArYlFkU3NlY1pnZUE9PSIsIm1hYyI6ImM3ODk0NWQ0NjgxMzM4YjE0M2JhN2MzZWRmOWEwMWJiMjI2Y2FhYmUxYjFhYzAyYjY4YWZkZGE3N2EyMDYwNWYifQ=='

p_obj = json.loads(base64.b64decode(password).decode())
decobj = AES.new(key, AES.MODE_CBC, base64.b64decode(p_obj['iv']))
data = decobj.decrypt(base64.b64decode(p_obj['value']))
dec_pass = unserialize(unpad(data, 16)).decode()

你将在dec_pass中获得解密后的密码。
请注意,有时Laravel会以base64格式生成密钥。在这种情况下,字符串将类似于base64:sdfsdjfhjsdf32,然后您需要先进行解码。
干杯!

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