4

I'm trying to remote control gpg through a python program via POpen.
I have a file that contains encrypted data which I want to decrypt, modify and write back to disk re-encrypted.
Currently I am storing the decrypted information in a temporary file (which I shred when the program ends). Then I perform my modifications to that file and then re-encrypt it using a function, which pipes the passphrase through stdin.
The code for this is as follows:

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))

This works perfectly well, but I'm fairly sure that storing potentionally sensitive, decrypted data in a temporary file is a rather big security flaw.
So I want to rewrite my encryption/decryption functions in a way, that enables them to work completely in memory without storing sensitive data on disk.
Decryption works straight forward by also piping the passphrase via stdin and capturing stdout for the decrypted data.

Encryption on the other hand drives me mad, since I can't just pipe the passphrase AND the message to `stdin'...at least

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

didn't work.
My next best guess is to supply the file-descriptor of some kind of in-memory file/pipe/socket or whatever as --passphrase-fd argument. The thing is: I don't know if there even is a thing such as in-memory files or if sockets would apply, since I never used them.

Can anybody help out or point me to a better solution for my problem?
The solution does not have to be portable - I'm totally fine with Linux only approaches.

Thanks in advance...

Edit:
Thanks a lot to both of you, Lars and ryran. Both solutions work perfectly! Unfortunately I can only accept one

Chris
  • 1,613
  • 1
  • 18
  • 27

3 Answers3

2

Below is the code I use in Obnam to run gpg, perhaps it can be of some assistance to you.

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)
2

Chris: Since you have a simple-ish example of using os.pipe thanks to Lars, I'll offer what Pyrite (my GTK frontend for gpg) does as well in the hope that more code examples are better. My use case is a little more complicated than yours due to the gui aspect -- I actually use a dictionary for input and output, and I have code to launch gpg with stdin as input and code that launches it with files as input, among other complications.

That warning said, I start with the gpg commandline in a list just like you do; however, instead of using --passphrase-fd 0, I create a custom file descriptor via os.pipe() to send the passphrase before loading the Popen() instance, which has stdin=subprocess.PIPE for the input data. Following are some relevant (modified) excerpts from pyrite's crypt_interface module.

#!/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])
...

The function that calls all that waits until the self.childprocess object has a returncode attribute and assuming the returncode was 0 and that the input was text (and not a file), it then reads gpg's stdout from that dictionary and prints it to the screen.

Happy to answer questions or try to help from my limited experience. Can find my contact info by following links.

Edit: You might also find a4crypt instructive as it is a much simpler frontend for gpg -- it was the project I started in order to learn python, and later mothballed after I "completed" (if there is such a thing) pyrite.

rsaw
  • 3,315
  • 2
  • 28
  • 30
0

Coming from the future here.

From Python documentation :

Changed in version 3.4: The new file descriptors are now non-inheritable.

Which means solutions in this thread don't work anymore because the child process (gpg in this case) won't have access to the file descriptor created by os.pipe(). There are two ways to circumvent this behavior.

By using os.set_inheritable() :

os.set_inheritable(fd, inheritable)

Set the “inheritable” flag of the specified file descriptor.

Or by passing pass_fds to Popen() :

pass_fds is an optional sequence of file descriptors to keep open between the parent and child. Providing any pass_fds forces close_fds to be True. (POSIX only)

Changed in version 3.2: The pass_fds parameter was added.

So now you must do it like this using 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)

I used check_output() in this example but of course you can use any function that wraps Popen()

ShellCode
  • 1,072
  • 8
  • 17