Python/POpen/gpg:通过 stdin 或文件描述符提供密码和加密文本



我正在尝试通过 python 程序通过 POpen 远程控制 gpg。
我有一个包含加密数据的文件,我想解密、修改并写回磁盘重新加密。
目前,我将解密的信息存储在一个临时文件中(我在程序结束时shred)。然后我对该文件进行修改,然后使用函数重新加密它,该函数通过stdin管道传输密码。
其代码如下:

def encrypt(source, dest, passphrase, cipher=None):
  """Encrypts the source file.
  @param source Source file, that should be encrypted.
  @param dest Destination file.
  @param passphrase Passphrase to be used.
  @param cipher Cipher to use. If None or empty string gpg's default cipher is
  used.
  """
  phraseecho = Popen(("echo", passphrase), stdout=subprocess.PIPE)
  gpgargs = [
          "gpg",
          "-c",
          "--passphrase-fd", "0", # read passphrase from stdin
          "--output", dest,
          "--batch",
          "--force-mdc"]
  if not cipher is None and len(cipher) > 0:
      gpgargs.extend(("--cipher-algo", cipher))
  gpgargs.append(source)
  encrypter = Popen(
          gpgargs,
          stdin=phraseecho.stdout,
          stdout=subprocess.PIPE,
          stderr=subprocess.PIPE)
  stdout, stderr = encrypter.communicate()
  rc = encrypter.returncode
  if not rc == 0:
      raise RuntimeError(
              "Calling gpg failed with return code %d: %s" % (rc, stderr))

这非常有效,但我相当确定将潜在敏感的解密数据存储在临时文件中是一个相当大的安全漏洞。
因此,我想以某种方式重写我的加密/解密函数,使它们能够完全在内存中工作,而无需将敏感数据存储在磁盘上。
解密通过管道stdin传递密码短语并捕获解密数据的stdout来直接工作。

另一方面,加密让我发疯,因为我不能只是将密码短语和消息通过管道传输到"stdin"......至少

encrypter.stdin.write("%sn%s" % (passphrase, message))

没用。
我的下一个最佳猜测是提供某种内存中文件/管道/套接字的文件描述符或任何--passphrase-fd参数。问题是:我不知道是否有诸如内存中文件之类的东西,或者套接字是否适用,因为我从未使用过它们。

任何人都可以帮助或指出我更好的问题解决方案吗?
该解决方案不一定是可移植的 - 我完全可以使用仅Linux的方法。

提前感谢...

编辑:
非常感谢你们俩,拉尔斯和瑞兰。两种解决方案都完美运行!不幸的是,我只能接受一个

下面是我在 Obnam 中用来运行 gpg 的代码,也许它可以对你有所帮助。

def _gpg_pipe(args, data, passphrase):
    '''Pipe things through gpg.
    With the right args, this can be either an encryption or a decryption
    operation.
    For safety, we give the passphrase to gpg via a file descriptor.
    The argument list is modified to include the relevant options for that.
    The data is fed to gpg via a temporary file, readable only by
    the owner, to avoid congested pipes.
    '''
    # Open pipe for passphrase, and write it there. If passphrase is
    # very long (more than 4 KiB by default), this might block. A better
    # implementation would be to have a loop around select(2) to do pipe
    # I/O when it can be done without blocking. Patches most welcome.
    keypipe = os.pipe()
    os.write(keypipe[1], passphrase + 'n')
    os.close(keypipe[1])
    # Actually run gpg.
    argv = ['gpg', '--passphrase-fd', str(keypipe[0]), '-q', '--batch'] + args
    tracing.trace('argv=%s', repr(argv))
    p = subprocess.Popen(argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    out, err = p.communicate(data)
    os.close(keypipe[0])
    # Return output data, or deal with errors.
    if p.returncode: # pragma: no cover
        raise obnamlib.Error(err)
    return out

def encrypt_symmetric(cleartext, key):
    '''Encrypt data with symmetric encryption.'''
    return _gpg_pipe(['-c'], cleartext, key)

def decrypt_symmetric(encrypted, key):
    '''Decrypt encrypted data with symmetric encryption.'''
    return _gpg_pipe(['-d'], encrypted, key)

Chris:由于你有一个使用 os.pipe 的简单示例,感谢 Lars,我将提供 Pyrite(我的 gpg 的 GTK 前端)的功能,希望更多的代码示例更好。由于 gui 方面,我的用例比你的要复杂一些——我实际上使用字典进行输入和输出,我有代码以 stdin 作为输入启动 gpg,以及使用文件作为输入启动它的代码,以及其他复杂情况。

该警告说,我像您一样从列表中的gpg命令行开始;但是,我没有使用--passphrase-fd 0,而是通过os.pipe()创建自定义文件描述符,以便在加载Popen()实例之前发送密码短语,该实例对输入数据具有stdin=subprocess.PIPE。以下是黄铁矿crypt_interface模块的一些相关(修改)摘录。

#!/usr/bin/env python
# Adapted excerpts from Pyrite <http://github.com/ryran/pyrite>
from subprocess import Popen, PIPE, check_output
...
 # I/O dictionary obj
 self.io = dict(
    stdin='',   # Stores input text for subprocess
    stdout='',  # Stores stdout stream from subprocess
    stderr=0,   # Stores tuple of r/w file descriptors for stderr stream
    gstatus=0,  # Stores tuple of r/w file descriptors for gpg-status stream
    infile=0,   # Input filename for subprocess
    outfile=0)  # Output filename for subprocess
...
cmd = ['gpg']
fd_pwd_R, fd_pwd_W = os.pipe()
os.write(fd_pwd_W, passwd)
os.close(fd_pwd_W)
cmd.append('--passphrase-fd')
cmd.append(str(fd_pwd_R))
...
# If working direct with files, setup our Popen instance with no stdin
if self.io['infile']:
    self.childprocess = Popen(cmd, stdout=PIPE, stderr=self.io['stderr'][3])
# Otherwise, only difference for Popen is we need the stdin pipe
else:
    self.childprocess = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=self.io['stderr'][4])
# Time to communicate! Save output for later
self.io['stdout'] = self.childprocess.communicate(input=self.io['stdin'])[0]
# Clear stdin from our dictionary asap, in case it's huge
self.io['stdin'] = ''
# Close os file descriptors
if fd_pwd_R:
    os.close(fd_pwd_R)
time.sleep(0.1)  # Sleep a bit to ensure everything gets read
os.close(self.io['stderr'][5])
if self.io['gstatus']:
    os.close(self.io['gstatus'][6])
...

调用所有等到self.childprocess对象具有returncode属性的函数,并假设返回代码已0并且输入是文本(而不是文件),然后它从该字典中读取 gpg 的 stdout 并将其打印到屏幕上。

很乐意回答问题或尝试从我有限的经验中提供帮助。可以通过以下链接找到我的联系信息。

编辑:你可能还会发现a4crypt很有启发性,因为它是一个更简单的gpg前端 - 这是我为了学习python而开始的项目,后来在我"完成"(如果有这样的事情)黄铁矿后被封存。

来自未来。

来自 Python 文档:

在 3.4 版更改: 新的文件描述符现在是不可继承的。

这意味着此线程中的解决方案不再有效,因为子进程(在本例中为 gpg)将无法访问 os.pipe() 创建的文件描述符。有两种方法可以规避此行为。

通过使用os.set_inheritable()

os.set_inheritable(fd,可继承)

设置指定文件描述符的"可继承"标志。

或者通过传递pass_fdsPopen()

pass_fds 是可选的文件描述符序列,用于在父级和子级之间保持打开状态。提供任何pass_fds力close_fds为真。(仅 POSIX)

在 3.2 版更改: 添加了 pass_fds 参数。

所以现在你必须使用 Python 3 来做到这一点:

#!/usr/bin/env python3
# coding: utf-8
import os
import subprocess
read_fd, write_fd = os.pipe()
os.write(write_fd, b"MyS3cretK3y")
os.close(write_fd)
try:
    out = subprocess.check_output(
        ["/usr/bin/gpg", "--some-other-parameter", "--passphrase-fd", str(read_fd)],
        input=b"Data to encrypt/decrypt",
        pass_fds=(read_fd,)
    )
except subprocess.CalledProcessError as cpe:
    out = cpe.stdout
print("child stdout:", out.decode())
os.close(read_fd)

我在这个例子中使用了check_output(),但当然你可以使用任何包装Popen()的函数

最新更新