用RAW键在Python中解密GPG加密文件

发布于 2025-01-26 22:36:25 字数 2225 浏览 2 评论 0原文

我正在尝试使用Python使用RAW键解密GPG加密文件。不是密钥环中的密码,而不是一个格式化的文件,而只是该文件已加密的键的字面原始字节。

我首先创建了一个测试文件:

~$ echo "It would be really cool if this worked" >> PGPDecryptorTest1.txt

然后,我使用AES256加密使用密码“ A”和SHA256密钥推导:

~$ gpg --symmetric --s2k-mode 0 --s2k-digest-algo SHA256 --cipher-algo AES256 PGPDecryptorTest1.txt

我编写了以下简短脚本来解码文件:

import sys 
from Crypto.Cipher import AES 

# With s2k-mode 0 specified, key is just SHA256 hash of passphrase
hash_a = b"\xca\x97\x81\x12\xca\x1b\xbd\xca\xfa\xc2\x31\xb3\x9a\x23\xdc\x4d\xa7\x86\xef\xf8\x14\x7c\x4e\x72\xb9\x80\x77\x85\xaf\xee\x48\xbb"
key = hash_a

def main(filename):
    
    with open(filename, "rb") as f:

        # First 9 bytes are header, ignore them and read the rest
        contents = f.read()[9:]

        # IV is size (block size + 2)
        # AES uses 16-byte (128-bit) blocks
        # Last two bytes are for checksum
        iv = contents[0:16 + 2]

        # Rest of contents should be ciphertext
        ciphertext = contents[16 + 2:]

        # Use openPGP special cipher mode
        cipher = AES.new(key, AES.MODE_OPENPGP, iv=iv)
        plaintext = cipher.decrypt(ciphertext)

        print("Output: " + str(plaintext))

if __name__ == "__main__":
    if (len(sys.argv) > 1): 
        main(sys.argv[1])
    else:
        main(input("Please specify an input file: "))

但是,此程序的输出是无法理解的垃圾。

~$ python3 PGPDecryptor.py PGPDecryptorTest1.txt.gpg
Output: b'\x11\xd6\xf4\x8d\xf7/o.\x13k#D\xd1!\xce\xf5\xf9\xd9\x0b,\xdb\xe4\xd6,\xb8\x80\xcb2N\xd1^\x96\x8chP\xfb\xb0?Z\xb2\xed?\xce==\xfb9\xcf5o{\xb6\x12\xf3\xf7\xc9QC\xc3\xb5\xe4\x95ab?\x17\x9d\xd3\xd3\xc6\xa8j#K\x8cMf\xc6\x00V\x89Y\xe2\xe7~\xc4B\xd5\x1b\x8f\xe9&t'

我已经通过其他方法验证了密钥,所以我相信这是正确的。我必须非常接近适当的解决方案,因为更改IV或键甚至会导致以下错误出现:

ValueError: Failed integrity check for OPENPGP IV

这表明我得到了键并且IV正确。我尝试了一个嵌套的循环来尝试使用密文的开始和终端索引的每种有效组合,以防万一某个地方还有一些额外的垃圾/标头数据,但每种组合都同样无用的输出。

如果有人能告诉我我在做错什么/如何纠正它,我会非常感激。我怀疑错误非常简单,但是问题的性质使得很难进行故障排除。

我目前有一个Janky替代解决方案,涉及修改PGPY库,但是我的问题是导入大型文件处理(〜500MB)需要很长时间(〜20-30分钟)。我也看着gnupg,但它只是一个包装纸 - 它可以用密码酶解密,但不能用原键进行解密。

I'm trying to use Python to decrypt a GPG-encrypted file using the raw key. Not the passphrase, not a nicely formatted file from a keyring, just the literal raw bytes of the key that the file was encrypted with.

I first created a test file:

~$ echo "It would be really cool if this worked" >> PGPDecryptorTest1.txt

I then encrypted the file using AES256 with the passphrase "a" and SHA256 key derivation:

~$ gpg --symmetric --s2k-mode 0 --s2k-digest-algo SHA256 --cipher-algo AES256 PGPDecryptorTest1.txt

I wrote the following short script to decode the file:

import sys 
from Crypto.Cipher import AES 

# With s2k-mode 0 specified, key is just SHA256 hash of passphrase
hash_a = b"\xca\x97\x81\x12\xca\x1b\xbd\xca\xfa\xc2\x31\xb3\x9a\x23\xdc\x4d\xa7\x86\xef\xf8\x14\x7c\x4e\x72\xb9\x80\x77\x85\xaf\xee\x48\xbb"
key = hash_a

def main(filename):
    
    with open(filename, "rb") as f:

        # First 9 bytes are header, ignore them and read the rest
        contents = f.read()[9:]

        # IV is size (block size + 2)
        # AES uses 16-byte (128-bit) blocks
        # Last two bytes are for checksum
        iv = contents[0:16 + 2]

        # Rest of contents should be ciphertext
        ciphertext = contents[16 + 2:]

        # Use openPGP special cipher mode
        cipher = AES.new(key, AES.MODE_OPENPGP, iv=iv)
        plaintext = cipher.decrypt(ciphertext)

        print("Output: " + str(plaintext))

if __name__ == "__main__":
    if (len(sys.argv) > 1): 
        main(sys.argv[1])
    else:
        main(input("Please specify an input file: "))

However, the output for this program is unintelligible garbage.

~$ python3 PGPDecryptor.py PGPDecryptorTest1.txt.gpg
Output: b'\x11\xd6\xf4\x8d\xf7/o.\x13k#D\xd1!\xce\xf5\xf9\xd9\x0b,\xdb\xe4\xd6,\xb8\x80\xcb2N\xd1^\x96\x8chP\xfb\xb0?Z\xb2\xed?\xce==\xfb9\xcf5o{\xb6\x12\xf3\xf7\xc9QC\xc3\xb5\xe4\x95ab?\x17\x9d\xd3\xd3\xc6\xa8j#K\x8cMf\xc6\x00V\x89Y\xe2\xe7~\xc4B\xd5\x1b\x8f\xe9&t'

I have verified the key by other methods, so I'm confident that it's correct. I must be very close to a proper solution, because changing either the IV or the key even slightly causes the following error to appear:

ValueError: Failed integrity check for OPENPGP IV

This suggests that I'm getting the key and IV correct. I've tried a nested for loop to try every valid combination of start and end indices for the ciphertext, just in case there was some additional garbage/header data somewhere, but with equally useless output for every combination.

If anyone can tell me what I'm doing wrong/how to correct it, I'd be very grateful. I suspect the error is very simple, but the nature of the problem makes it difficult to troubleshoot.

I currently have a janky alternative solution that involves modifying the pgpy library, but my problem with this is that importing large files to process (~500MB) takes a long time (~20-30 minutes). I looked at gnupg as well, but it's just a wrapper--it can decrypt with passphrases, but not with raw keys.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

初见终念 2025-02-02 22:36:25

使用aes.mode_openpgp可能适用于对称加密的数据包(TAG 9),因为它只是包含加密的数据(参考)。

但是,这不是您使用gpg调用所产生的。为了了解我们实际处理的内容,您可以使用- list-packets命令:

$ gpg --list-packets --verbose PGPDecryptorTest1.txt.gpg
gpg: AES256.CFB encrypted data
gpg: pinentry launched (90945 curses 1.1.0 /dev/pts/6 screen -)
gpg: encrypted with 1 passphrase
# off=0 ctb=8c tag=3 hlen=2 plen=4
:symkey enc packet: version 4, cipher 9, aead 0,s2k 0, hash 8
# off=6 ctb=d2 tag=18 hlen=2 plen=112 new-ctb
:encrypted data packet:
        length: 112
        mdc_method: 2
# off=27 ctb=a3 tag=8 hlen=1 plen=0 indeterminate
:compressed packet: algo=1
# off=29 ctb=ac tag=11 hlen=2 plen=66
:literal data packet:
        mode b (62), created 1652044966, name="PGPDecryptorTest1.txt",
        raw data: 39 bytes

两件事要注意:

  • 加密的数据数据包是TAG 18,它是对称加密的完整性受保护的数据包。我们不再仅处理密码的输出,而是以版本#之前的数据和修改的检测代码数据包(参考)。
  • 加密的数据包内容被压缩。

警告:下面的代码只是戳戳的粗略演示
OpenPGP消息格式。这是脆弱的,不应该重复使用。主要要点是,可靠地解析OpenPGP消息并不小,您应该使用经过良好测试的库。

我使用的主要参考文献是:


更容易地证明挖掘此信息,我发出了一个加密消息,并关闭了压缩:

$ cat PGPDecryptorTest1.txt
It would be really cool if this worked

$ gpg --symmetric -o PGPDecryptorTest1.txt.uncompressed.gpg --compress-level 0 --s2k-mode 0 --s2k-digest-algo SHA256 --cipher-algo AES256 PGPDecryptorTest1.txt
gpg: Note: simple S2K mode (0) is strongly discouraged

$ python3 solution.py PGPDecryptorTest1.txt.uncompressed.gpg
contents(len: 117): b'8c0404090008d26d017795712a4686d1a176a0f150a33b9c972d876948df739b1058a513f916ef8094c80ae65ed022c30e1108d20dbeaeee70285e8736e8184520ceb0c435feafdd856051eb166e96e32e82ba51a3af4d230174e97a8f3a3529606b6558fce716bf3b0e9b856d442f5104f3647af0'
decrypted_iv(len: 16): b'49355c68e8e3eba7cc5ccb529d158a2c'
first_block(len: 16): b'8a2cac42621550475044656372797074'
decrypted_data (first block)(len: 14): b'ac42621550475044656372797074'
decrypted_data(len: 68): b'ac426215504750446563727970746f7254657374312e74787462783732497420776f756c64206265207265616c6c7920636f6f6c206966207468697320776f726b65640a'
plaintext(len: 39): b'497420776f756c64206265207265616c6c7920636f6f6c206966207468697320776f726b65640a'
It would be really cool if this worked

这是解决方案实现:

import binascii
import hashlib
import sys
from Cryptodome.Cipher import AES

def print_bytes(name, data):
    print("%s(len: %d): %s" % (name, len(data), str(binascii.hexlify(data))))

def main(filename):
    # Generate key material from the passphrase.
    passphrase = b"a"
    m = hashlib.sha256()
    m.update(passphrase)
    key = m.digest()

    # Get file contents.
    with open(filename, "rb") as f:
        contents = f.read()
    print_bytes("contents", contents)

    # Constants
    header_len = 9  # including the 1-octet type-19 version identifier
    block_size = 16 # alogorithm details should normally be extracted from the header
    segment_size = block_size * 8
    iv_len = block_size
    iv_tag_len = 2
    mdc_len = 22

    # "Manually" decrypting to adhere to
    # https://datatracker.ietf.org/doc/html/rfc4880#section-5.13
    # Doing it this way helps with the integrity check, which I ended
    # skipping.
    cipher = AES.new(key, AES.MODE_CFB, iv=b"\x00" * block_size, segment_size=segment_size)

    offset = header_len

    decrypted_iv = cipher.decrypt(contents[offset:offset+block_size])
    print_bytes("decrypted_iv", decrypted_iv)

    decrypted_data = bytearray()

    offset += block_size

    first_block = cipher.decrypt(contents[offset:offset+block_size])
    print_bytes("first_block", first_block)

    offset += block_size

    if first_block[:2] != decrypted_iv[-2:]:
        print("IV check failed")
        sys.exit(1)

    decrypted_data.extend(first_block[2:])
    print_bytes("decrypted_data (first block)", decrypted_data)

    padding = block_size - (len(contents)-offset) % block_size
    contents += b"\x00" * padding

    decrypted_data.extend(cipher.decrypt(contents[offset:]))

    # Here is where you should parse the MDC packet and use it for integrity
    # checking. Instead, skipping the check and discarding the packet for 
    # brevity.
    decrypted_data = decrypted_data[:-(mdc_len+padding)]
    print_bytes("decrypted_data", decrypted_data)

    # Extract filename length so we can find the plaintext offset.
    # see https://datatracker.ietf.org/doc/html/rfc4880#section-5.9
    filename_len = decrypted_data[3]
    plaintext_offset = (
        2  # header
        + 1  # file type
        + 1  # filename length
        + filename_len  # filename contents
        + 4  # timestamp
    )
    plaintext = decrypted_data[plaintext_offset:]
    print_bytes("plaintext", plaintext)
    print(plaintext.decode())


if __name__ == "__main__":
    if (len(sys.argv) > 1):
        main(sys.argv[1])
    else:
        print("first argument must be file to decrypt")
        sys.exit(1)

Using AES.MODE_OPENPGP would probably work for a Symmetrically Encrypted Data packet (tag 9), as it simply contains the encrypted data (reference).

However, that's not what you've produced with your gpg invocation. To get some insight into what we're actually dealing with, you can use the --list-packets command:

$ gpg --list-packets --verbose PGPDecryptorTest1.txt.gpg
gpg: AES256.CFB encrypted data
gpg: pinentry launched (90945 curses 1.1.0 /dev/pts/6 screen -)
gpg: encrypted with 1 passphrase
# off=0 ctb=8c tag=3 hlen=2 plen=4
:symkey enc packet: version 4, cipher 9, aead 0,s2k 0, hash 8
# off=6 ctb=d2 tag=18 hlen=2 plen=112 new-ctb
:encrypted data packet:
        length: 112
        mdc_method: 2
# off=27 ctb=a3 tag=8 hlen=1 plen=0 indeterminate
:compressed packet: algo=1
# off=29 ctb=ac tag=11 hlen=2 plen=66
:literal data packet:
        mode b (62), created 1652044966, name="PGPDecryptorTest1.txt",
        raw data: 39 bytes

Two things of note:

  • The encrypted data packet is tag 18, which is a Symmetrically Encrypted Integrity Protected Data packet. We're no longer dealing with only the output of the cipher, but data preceded with a version # and suffixed with with a Modification Detection Code packet (reference).
  • The encrypted data packets contents are compressed.

WARNING: The code below is just a rough demonstration of poking around
OpenPGP message format. It is brittle and shouldn't be reused. The main takeaway is that reliably parsing OpenPGP messages isn't trivial and you should use a well tested library.

The main references I used are:


To more easily demonstrate digging into this, I've produced an encrypted message with compression turned off:

$ cat PGPDecryptorTest1.txt
It would be really cool if this worked

$ gpg --symmetric -o PGPDecryptorTest1.txt.uncompressed.gpg --compress-level 0 --s2k-mode 0 --s2k-digest-algo SHA256 --cipher-algo AES256 PGPDecryptorTest1.txt
gpg: Note: simple S2K mode (0) is strongly discouraged

$ python3 solution.py PGPDecryptorTest1.txt.uncompressed.gpg
contents(len: 117): b'8c0404090008d26d017795712a4686d1a176a0f150a33b9c972d876948df739b1058a513f916ef8094c80ae65ed022c30e1108d20dbeaeee70285e8736e8184520ceb0c435feafdd856051eb166e96e32e82ba51a3af4d230174e97a8f3a3529606b6558fce716bf3b0e9b856d442f5104f3647af0'
decrypted_iv(len: 16): b'49355c68e8e3eba7cc5ccb529d158a2c'
first_block(len: 16): b'8a2cac42621550475044656372797074'
decrypted_data (first block)(len: 14): b'ac42621550475044656372797074'
decrypted_data(len: 68): b'ac426215504750446563727970746f7254657374312e74787462783732497420776f756c64206265207265616c6c7920636f6f6c206966207468697320776f726b65640a'
plaintext(len: 39): b'497420776f756c64206265207265616c6c7920636f6f6c206966207468697320776f726b65640a'
It would be really cool if this worked

Here's the solution implementation:

import binascii
import hashlib
import sys
from Cryptodome.Cipher import AES

def print_bytes(name, data):
    print("%s(len: %d): %s" % (name, len(data), str(binascii.hexlify(data))))

def main(filename):
    # Generate key material from the passphrase.
    passphrase = b"a"
    m = hashlib.sha256()
    m.update(passphrase)
    key = m.digest()

    # Get file contents.
    with open(filename, "rb") as f:
        contents = f.read()
    print_bytes("contents", contents)

    # Constants
    header_len = 9  # including the 1-octet type-19 version identifier
    block_size = 16 # alogorithm details should normally be extracted from the header
    segment_size = block_size * 8
    iv_len = block_size
    iv_tag_len = 2
    mdc_len = 22

    # "Manually" decrypting to adhere to
    # https://datatracker.ietf.org/doc/html/rfc4880#section-5.13
    # Doing it this way helps with the integrity check, which I ended
    # skipping.
    cipher = AES.new(key, AES.MODE_CFB, iv=b"\x00" * block_size, segment_size=segment_size)

    offset = header_len

    decrypted_iv = cipher.decrypt(contents[offset:offset+block_size])
    print_bytes("decrypted_iv", decrypted_iv)

    decrypted_data = bytearray()

    offset += block_size

    first_block = cipher.decrypt(contents[offset:offset+block_size])
    print_bytes("first_block", first_block)

    offset += block_size

    if first_block[:2] != decrypted_iv[-2:]:
        print("IV check failed")
        sys.exit(1)

    decrypted_data.extend(first_block[2:])
    print_bytes("decrypted_data (first block)", decrypted_data)

    padding = block_size - (len(contents)-offset) % block_size
    contents += b"\x00" * padding

    decrypted_data.extend(cipher.decrypt(contents[offset:]))

    # Here is where you should parse the MDC packet and use it for integrity
    # checking. Instead, skipping the check and discarding the packet for 
    # brevity.
    decrypted_data = decrypted_data[:-(mdc_len+padding)]
    print_bytes("decrypted_data", decrypted_data)

    # Extract filename length so we can find the plaintext offset.
    # see https://datatracker.ietf.org/doc/html/rfc4880#section-5.9
    filename_len = decrypted_data[3]
    plaintext_offset = (
        2  # header
        + 1  # file type
        + 1  # filename length
        + filename_len  # filename contents
        + 4  # timestamp
    )
    plaintext = decrypted_data[plaintext_offset:]
    print_bytes("plaintext", plaintext)
    print(plaintext.decode())


if __name__ == "__main__":
    if (len(sys.argv) > 1):
        main(sys.argv[1])
    else:
        print("first argument must be file to decrypt")
        sys.exit(1)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文