如何在Python中使用PGP(生成密钥,加密/解密)

44

我正在用Python编写一个程序,将其通过安装程序分发给Windows用户。

该程序需要每天使用用户的公钥下载加密文件,然后解密该文件。

因此,我需要找到一个Python库,它可以让我生成公共和私有PGP密钥,并且还可以解密使用公钥加密的文件。

pyCrypto是否能胜任此项任务(文档不明确)? 是否有其他纯Python库? 任何语言中都有独立的命令行工具吗?

到目前为止,我所见过的只有GNUPG,但在Windows上安装它会对注册表进行更改并抛出dll文件,然后我必须担心用户是否已经安装了它,如何备份他们现有的密钥环等。 我宁愿只使用python库或命令行工具,并自己管理密钥。

更新:pyME可能可行,但似乎与我必须使用的Python 2.4不兼容。


请查看此文章 - https://medium.com/@almirx101/pgp-key-pair-generation-and-encryption-and-decryption-examples-in-python-3-a72f56477c22 - Robert Ranjan
9个回答

43

你不需要 PyCrypto 或者 PyMe,尽管这些包可能很好,但在 Windows 平台下构建时会遇到各种问题。相反,为什么不像我一样避免这些麻烦呢?使用 gnupg 1.4.9。你不需要在最终用户的机器上进行完整安装,只需从发行版中获取 gpg.exeiconv.dll 就足够了,并且你只需将它们放在路径中的某个位置或者从你的 Python 代码中使用完整路径名访问它们。无需更改注册表,如果需要,所有可执行文件和数据文件都可以限制在单个文件夹中。

有一个模块 GPG.py 最初是由 Andrew Kuchling 编写的,由 Richard Jones 进行了改进,然后由 Steve Traugott 进一步改进。它可以在这里找到,但是因为它使用了 os.fork(),所以原样使用不适合 Windows 平台。尽管最初是 PyCrypto 的一部分,但它完全独立于 PyCrypto 的其他部分,只需 gpg.exe/iconv.dll 就可以工作

我有一个版本 (gnupg.py) 是从 Traugott 的 GPG.py 派生的,它使用了 subprocess 模块。它在 Windows 平台下工作正常,至少对于我的目的来说是这样的 - 我用它来执行以下操作:

  • 密钥管理 - 生成、列出、导出等
  • 从外部源导入密钥 (例如从合作伙伴公司接收到的公钥)
  • 加密和解密数据
  • 签名和验证签名

我目前拥有的模块不太理想,因为它包含了一些不应该存在的其他东西——这意味着我目前不能将其原样发布。也许在接下来的几周中,我希望能够整理它,添加一些更多的单元测试(例如,我没有对签名/验证进行任何单元测试),然后发布它(可能使用原始的PyCrypto许可证或类似商业友好的许可证)。如果你等待不了,那就使用Traugott的模块并自行修改-使用subprocess模块使其工作并不难。

这种方法比其他方法(如基于SWIG的解决方案或需要使用MinGW/MSYS进行构建的解决方案)要少痛苦得多,我曾考虑和尝试过其他方法。我已经在使用其他语言编写的系统中,例如C#,使用相同的(gpg.exe/iconv.dll)方法取得了同样轻松的结果。

P.S. 它适用于Python 2.4以及Python 2.5及更高版本。虽然未经测试其他版本,但我不预见任何问题。


谢谢,这听起来非常完美。我不知道我可以直接从gpg中提取exe文件。它会把密钥环放在哪里?如果用户已经安装了gpg,这种方法会引起任何问题吗?我能不能看一下你的代码如何生成密钥和导入密钥? - Greg
当你发布最终代码时,请务必在此更新我们。 - Greg
当您调用gpg时,请使用--homedir参数指定您想要存储密钥环的路径。(使用Traugott的模块已经为您完成了此操作 - 有一个GPG类,其构造函数接受gnupghome参数,您可以将其设置为此目录。)pubring.gpg、secring.gpg和trustdb.gpg将在由--homedir指定的文件夹中创建。如果用户已经安装了gpg,我不认为这会导致问题 - --homedir参数应该覆盖注册表中的任何值。 - Vinay Sajip
请访问http://paste.pocoo.org/show/125608/以查看该模块的单元测试摘录。其中展示了密钥生成和导入。感谢接受答案。发布代码时,我会在这里发表评论。 - Vinay Sajip
1
@VinaySajip 你好,Vinay,感谢你在gnupg上的工作。你能否评论一下这个模块和 Github 上 python-gnupg 项目之间的关系?考虑到这个答案现在已经有7年的历史了 - 现在情况如何,新用户应该选哪一个?(两个项目都似乎得到了很好的维护和文档支持,所以选择并不明显) - wim
显示剩余5条评论

39

经过很多挖掘,我找到了一个适合我的软件包。虽然它被说可以支持密钥的生成,但我没有测试过。不过,我成功地解密了使用GPG公钥加密的消息。这个软件包的优点是不需要在机器上安装GPG可执行文件,而是基于Python实现OpenPGP(而不是封装可执行文件)。我使用GPG4win和kleopatra for windows创建了私钥和公钥。以下是我的代码。

import pgpy
emsg = pgpy.PGPMessage.from_file(<path to the file from the client that was encrypted using your public key>)
key,_  = pgpy.PGPKey.from_file(<path to your private key>)
with key.unlock(<your private key passpharase>):
    print (key.decrypt(emsg).message)

虽然这个问题很旧,但我希望这可以帮助未来的用户。


1
谢谢提供信息。这个能实际使用公钥进行加密吗?另外,包的链接可能会有帮助。 - lazyList
1
谢谢@Roee Anuar。已经使用它构建了一个基于PGP的密码保护程序。感谢您的帮助。 - lazyList
有没有类似的示例,可以使用公钥(pgpy)加密文件? - Prabhash Jha
添加了一个带有代码示例的答案,用于使用仅公钥导出文件的pgpy和gpg。 - Antonio Bardazzi
2
@lazyList 这里是包含功能和安装说明的文档。https://pgpy.readthedocs.io/en/latest/index.html - Steven Magana-Zook

7

PyCrypto支持PGP - 不过您应该测试一下以确保它符合您的规格要求。

尽管文档很难找到,但如果您浏览Util/test.py(模块测试脚本),您可以找到一个基本的PGP支持示例:

if verbose: print '  PGP mode:',
obj1=ciph.new(password, ciph.MODE_PGP, IV)
obj2=ciph.new(password, ciph.MODE_PGP, IV)
start=time.time()
ciphertext=obj1.encrypt(str)
plaintext=obj2.decrypt(ciphertext)
end=time.time()
if (plaintext!=str):
    die('Error in resulting plaintext from PGP mode')
print_timing(256, end-start, verbose)
del obj1, obj2

此外,PublicKey/pubkey.py提供以下相关方法:
def encrypt(self, plaintext, K)
def decrypt(self, ciphertext):
def sign(self, M, K):
def verify (self, M, signature):
def can_sign (self):
    """can_sign() : bool
    Return a Boolean value recording whether this algorithm can
    generate signatures.  (This does not imply that this
    particular key object has the private information required to
    to generate a signature.)
    """
    return 1

2
我看不到生成密钥或接受另一方的公钥以加密数据的方法。有任何想法吗? - Greg
4
不要使用PyCrypto中的MODE_PGP,它是一个过时的实验性代码,可能从未正常工作。请参阅https://bugs.launchpad.net/pycrypto/+bug/996814。我们很快会将其从PyCrypto中移除。 - dlitz
1
那么,@dlitz,你有什么建议? - raylu

3

PyMe声称完全兼容Python 2.4,我引用一下:

截至本文撰写时,最新版本的PyMe是v0.8.0。它的Debian二进制发行版是使用SWIG v1.3.33和GCC v4.2.3编译的,用于GPGME v1.1.6和Python v2.3.5、v2.4.4和v2.5.2(在当时提供的“不稳定”版本中)。它的Windows二进制发行版是使用SWIG v1.3.29和MinGW v4.1编译的,用于GPGME v1.1.6和Python v2.5.2(尽管同一个二进制文件也可以安装并在v2.4.2中正常工作)。

我不确定您为什么说“它似乎与我必须使用的Python 2.4不兼容”——请具体说明?

是的,它确实存在作为GPGME的半Pythonic(SWIGd)包装器——这是一种开发Python扩展的流行方式,一旦您有了基本完成工作的C库。

PyPgp采用了更简单的方法——这就是为什么它只有一个简单的Python脚本:基本上它仅仅是“外壳”到命令行PGP命令。例如,解密只需要:

def decrypt(data):
    "Decrypt a string - if you have the right key."
    pw,pr = os.popen2('pgpv -f')
    pw.write(data)
    pw.close()
    ptext = pr.read()
    return ptext

即,将加密的密文写入pgpv -f的标准输入,读取pgpv的标准输出作为解密后的明文。
PyPgp也是一个非常古老的项目,尽管它的简单性意味着使其与现代Python(例如,使用subprocess而不是现在已弃用的os.popen2)配合工作并不难。但您仍然需要安装PGP,否则PyPgp将无法执行任何操作;-)。

PyMe的问题在这里:http://stackoverflow.com/questions/1030297/how-to-make-a-pyme-python-library-run-in-python-2-4-on-windows - Greg
是的,他们声称我引用的内容(关于Windows,具体来说)是没有根据的(在Windows中,只有扩展需要为特定版本的Python编译,因此为2.5编译的扩展将无法在2.4上工作)。你所需要做的就是重新编译(这需要MSVC 6.0)。 - Alex Martelli

3

M2Crypto有PGP模块,但我实际上从未尝试使用它。如果您尝试并且有效,请告诉我(我是当前的M2Crypto维护者)。以下是一些链接:

更新: PGP模块不提供生成密钥的方法,但可以使用较低级别的RSADSA等模块创建这些密钥。我不了解PGP内部情况,所以您需要深入了解细节。此外,如果您知道如何使用openssl命令行命令生成这些密钥,将其转换为M2Crypto调用应该相对容易。


嗯,看起来很有前途,但我不知道如何生成密钥。 - Greg
您可以使用较低级别的RSA、DSA等模块生成密钥。 - Heikki Toivonen

3
如其他人所指出的,PyMe是这个问题的标准解决方案,因为它基于GpgME,而GpgME是GnuPG生态系统的一部分。
对于Windows,我强烈建议使用Gpg4win作为GnuPG发行版,有两个原因:
它基于GnuPG 2,其中包括gpg2.exe,可以(最终,我想补充一下 :)按需启动gpg-agent.exe(gpg v1.x不能)。
其次,这是GnuPG开发人员唯一的官方Windows版本。例如,它完全从Linux交叉编译到Windows,因此在准备它时没有使用任何非自由软件(对于安全套件非常重要:)。

2
这是一个完整的脚本,它将会:
  1. 尝试解密给定文件夹中使用您的公钥加密的所有文件。
  2. 将新文件写入指定文件夹。
  3. 将加密文件移动到指定文件夹。

该脚本还包含了创建和存储自己的私钥和公钥所需的一切内容,请查看下面的“首次设置”部分。

这个想法是您可以根据需要安排此脚本定期运行,它会自动解密找到的数据并为您存储起来。

希望这对某人有所帮助,这是一个棘手的项目需要解决。

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~ Introduction, change log and table of contents
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Purpose: This script is used to encrypt and decrypt files using the PGP (Pretty Good Privacy) standard..
#
# Change date   Changed by      Description
# 2022-10-03    Ryan Bradley    Initial draft
# 2022-10-12    Ryan Bradley    Cleaned up some comments and table of contents. 
#
# Table of Contents
# [1.0] Hard-coded variables
# [1.1] Load packages and custom functions
# [1.3] First time set up
# [1.4] Define custom functions
# [2.0] Load keys and decrypt files
#
# Sources used to create this script, and for further reading:
# https://github.com/SecurityInnovation/PGPy/
# https://dev59.com/6XNA5IYBdhLWcg3wS7wm
# https://pypi.org/project/PGPy/
# https://betterprogramming.pub/creating-a-pgp-encryption-tool-with-python-19bae51b7fd
# https://pgpy.readthedocs.io/en/latest/examples.html

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~ [1.1] Load packages 
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import glob
import pgpy
import shutil
import io

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~ [1.2] Hard-coded variables
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

# Define the paths to public and private keys
path_public_key = r'YOUR PATH HERE'
path_private_key = r'YOUR PATH HERE'

# Define paths to files you want to try decrypting
path_original_files = r'YOUR PATH HERE'
path_decrypted_files = r'YOUR PATH HERE'
path_encrypted_files= r'YOUR PATH HERE'

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~ [1.3] First time set up
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# IMPORTANT WARNINGS!!!!
#  - Do NOT share your private key with anyone else. 
#  - You MUST have the associated private key that is is generated along with a public key 
#       if you want to be able to decrypt anything that is encryped with that public key. Do
#       not overwrite the existing keys unless you will never need any of the previously 
#       encryped data. 
#   - Do not generate new public and private keys unless you have a good reason to. 
#
# The following steps will walk you through how to create and write public and private keys to
# a network location. Be very careful where you store this information. Anyone with access
# to your private key can decrypt anything that was encryped with your public key.
#
# These steps only need to be performed one time when the script is first being 
# created. They are commented out intentionally, as they shouldn't need to be performed 
# every time the script is ran. 
# 
# Here's the a link to the documentation on this topic:
# https://pgpy.readthedocs.io/en/latest/examples.html

# # Load the extra things we need to define a new key
# from pgpy.constants import PubKeyAlgorithm, KeyFlags, HashAlgorithm, SymmetricKeyAlgorithm, CompressionAlgorithm
 
# # Gerate a new a primary key. For this example, we'll use RSA, but it could be DSA or ECDSA as well
# key = pgpy.PGPKey.new(PubKeyAlgorithm.RSAEncryptOrSign, 4096)

# # Define a new user 
# uid = pgpy.PGPUID.new('USER_NAME', comment='Generic user', email='YOUR_EMAIL')

# # Add the new user id to the key, and define all the key preferences.
# key.add_uid(uid, usage={KeyFlags.Sign, KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
#             hashes=[HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512, HashAlgorithm.SHA224],
#             ciphers=[SymmetricKeyAlgorithm.AES256, SymmetricKeyAlgorithm.AES192, SymmetricKeyAlgorithm.AES128],
#             compression=[CompressionAlgorithm.ZLIB, CompressionAlgorithm.BZ2, CompressionAlgorithm.ZIP, CompressionAlgorithm.Uncompressed]
#             , is_compressed = True)

# # Write the ASCII armored public key to a network location.
# text_file = open(path_public_key, 'w')
# text_file.write(str(key.pubkey))
# text_file.close()

# # Write the ASCII armored private key to a network location.
# text_file = open(path_private_key, 'w')
# text_file.write(str(key))
# text_file.close()

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~ [1.4] Define custom functions
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

def file_encrypt(path_original_file, path_encrypted_file, key_public):
    """
    A function that encrypts the content of a file at the given path and 
    creates an ecryped version file at the new location using the specified
    public key.
    """
    
    # Create a PGP file, compressed with ZIP DEFLATE by default unless otherwise specified
    pgp_file = pgpy.PGPMessage.new(path_original_file, file=True)
    
    # Encrypt the data with the public key
    encrypted_data = key_public.encrypt(pgp_file) 
    
    # Write the encryped data to the encrypted destination
    text_file = open(path_encrypted_file, 'w')
    text_file.write(str(encrypted_data))
    text_file.close()
   
def file_decrypt(path_encrypted_file, path_decrypted_file, key_private):
    """
    A function that decrypts the content of a file at path path and 
    creates a decrypted file at the new location using the given 
    private key.
    """

    # Load a previously encryped message from a file
    pgp_file = pgpy.PGPMessage.from_file(path_encrypted_file)
    
    # Decrypt the data with the given private key
    decrypted_data = key_private.decrypt(pgp_file).message
   
    # Read in the bytes of the decrypted data
    toread = io.BytesIO()
    toread.write(bytes(decrypted_data))  
    toread.seek(0)  # reset the pointer 
   
    # Write the data to the location
    with open(path_decrypted_file, 'wb') as f:
        shutil.copyfileobj(toread, f)
        f.close()
      
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~ [2.0] Load keys and decrypt files
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

# Load your pre-generated public key from the network
key_public, _ = pgpy.PGPKey.from_file(path_public_key)

# Load your pre-generated public key from the network
key_private, _ = pgpy.PGPKey.from_file(path_private_key)

# Find and process any encrypted files in the landing folder
for file in glob.glob(path_original_files + '\*.pgp'):
    
    # Get the path to the file we need to decrypt
    path_encrypted_file = str(file)
    
    # Extract the file name
    parts = path_encrypted_file.split('\\')
    str_file_name = parts[len(parts)-1]
    str_clean_file_name = str_file_name[:-4]
    
    # Extract the file exension
    str_extension = str_clean_file_name.split('.')
    str_extension = str_extension[len(str_extension) - 1]
    
    # Create the path to the new decryped file, dropping the ".pgp" extension
    path_decrypted_file = path_decrypted_files + '\\' + str_clean_file_name
    
    # Create the path to the place we'll store the encryped file
    path_archived_encrypted_file = path_encrypted_files + '\\' + str_file_name
    
    # Decrypt the file
    try:
        file_decrypt(path_encrypted_file, path_decrypted_file, key_private)
        
        # Move the encryped file to its new location
        shutil.move(path_encrypted_file, path_archived_encrypted_file)
    except:
        print('DECRYPTION ERROR!')
        print(f'Unable to decrypt {path_encrypted_file}')
    
# Just for reference, here's how you would call the function to encrypt a file:
# file_encrypt(path_original_file, path_encrypted_file, key_public)

1

使用仅导出的公钥文件进行签名,而无需使用密钥环。

使用PGPy 0.5.2(纯Python GPG RFC实现):

key_fpath = './recipient-PUB.gpg'
     
rsa_pub, _ = pgpy.PGPKey.from_file(rkey_fpath)
rkey = rsa_pub.subkeys.values()[0]                                                                                                     
     
text_message = pgpy.PGPMessage.new('my msg')
encrypted_message = rkey.encrypt(text_message)
print encrypted_message.__bytes__()

使用gpg 1.10.0(gpgme Python绑定-前身为PyME):

rkey_fpath = './recipient-PUB.gpg'
cg = gpg.Context()
rkey = list(cg.keylist(source = rkey_fpath))                                                                                                                
 
ciphertext, result, sign_result = cg.encrypt('my msg', recipients=rkey, sign=False, always_trust=True)
print ciphertext

在一个for循环中进行的简单基准测试表明,在我的系统上,对于这个简单操作,PGPy比gpgme Python绑定快了约3倍(请不要将此声明视为X比Y更快:我将邀请您在您的环境中进行测试)。


使用PGP,生成公钥/私钥的Python函数是什么? --> rkey_fpath - Kar
我尝试了您的逻辑,但遇到了错误 TypeError: 'odict_values' object is not subscriptable。 我已将我的公钥导出到 my-pubkey.asc - Kar

0
我只是想补充一下@Roee Anuar和他的pgpy想法。
# pip install pgpy
from pgpy import PGPKey, PGPMessage
from pgpy.packet.packets import LiteralData
from pgpy.constants import CompressionAlgorithm
from datetime import datetime, timezone
import json

pass_phrase = "some_password_anyway"

# simple string
message = PGPMessage.new("42 is quite a pleasant number")
enc_message = message.encrypt(pass_phrase)
print(json.dumps(str(enc_message)))

# file
PGPMessage.filename = "filename.txt"
file_message = PGPMessage.new("/tmp/123", file=True)
enc_message = file_message.encrypt(pass_phrase)
print(json.dumps(str(enc_message)))

这是如何加密字符串和文件的方法。 此外,我还实现了一种将数据加密为文件的方式:
# file from byte array and with custom name
msg = PGPMessage()
lit = LiteralData()
lit._contents = bytearray(msg.text_to_bytes("Some stuff in file"))
lit.filename = "any file name here even long ones are ok but do not exceed 192 bytes.txt"
lit.mtime = datetime.now(timezone.utc)
lit.format = 'b'
lit.update_hlen()

msg |= lit
msg._compression = CompressionAlgorithm.ZIP

enc_message = msg.encrypt(pass_phrase)
print(json.dumps(str(enc_message)))


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