Source code for crx_unpack.encrypted_dir

# *-* coding: utf-8 *-*
"""
The :py:class:`EncryptedTempDirectory` class is designed for when you need a
directory whose contents are encrypted by `eCryptfs <http://ecryptfs.org/>`_,
but you would also like to take advantage of the features of in-memory-only
directories, such as increased access speed and automatic deletion (e.g.\  when
you're :doc:`unpacking a CRX <crx_unpack>`).

To understand this class fully, please also read the documentation on the
:py:func:`~tempfile.TemporaryDirectory` class.
"""

from base64 import b64encode
from os import mkdir, remove
from os.path import basename, isdir, join, expanduser
from re import search
from subprocess import run, PIPE
from tempfile import TemporaryDirectory


[docs]class EncryptedTempDirectory(TemporaryDirectory): """Create and return an encrypted temporary directory. This behaves similarly to :class:`TemporaryDirectory`, except for the following: - It requires that an "upper directory" be specified, which will be the mount point used by eCryptfs to mount the encrypted directory to the filesystem. - It creates two files in `~/.ecryptfs` required to mount the directory (both of which are deleted when this object is): - `ALIAS.sig` - Contains the signatures for the FEK and FNEK encryption keys. - `ALIAS.conf` - Contains `fstab`-style information for which directory eCryptfs should mount and where. In the above notes, `ALIAS` (which is a term used in the eCryptfs documentation, see links below) will be the name of the created temp directory, accessible as the basename of ``self.name``. To use an :class:`EncryptedTempDirectory` object, it's best to use it with a ``with`` clause, like so: :: with EncryptedTempDirectory(upper_dir=upper) as lower: ... Better yet, use an instance of :class:`TemporaryDirectory` as the upper directory, like this: :: with TemporaryDirectory() as upper, \\ EncryptedTempDirectory(upper_dir=upper) as lower: ... .. note:: In the above example, both temporary directories are deleted as soon as the ``__exit__()`` method is called (triggered by the close of the ``with`` clause). So make sure that anything you need to do with these objects, you do before leaving the ``with`` clause. .. note:: This class depends on eCryptfs, so it will need to be installed on the system to work properly. Similarly, this class depends on the following Unix tools/devices: - ``head`` - ``ecryptfs-add-passphrase`` - ``mount`` - ``keyctl`` - ``/dev/urandom`` On Debian/Ubuntu-based systems, you can install these with: :: sudo apt-get install coreutils mount keyutils ecryptfs-utils For more information, see the following resources: - http://manpages.ubuntu.com/manpages/zesty/en/man1/mount.ecryptfs_private.1.html - http://manpages.ubuntu.com/manpages/zesty/en/man1/ecryptfs-add-passphrase.1.html - https://askubuntu.com/questions/574110/how-to-use-ecryptfs-with-a-random-directory/574425#574425 """ def __init__(self, *, upper_dir, **kwargs): """ :param str upper_dir: Path where the encrypted directory will be mounted, and where the unencrypted version of the files will be accessible. :param kwargs: Additional parameters to pass to the constructor of the :py:class:`~tempfile.TemporaryDirectory` class. """ self._upper_dir = upper_dir super().__init__(**kwargs) # Make the ~/.ecryptfs dir if it doesn't already exist ecrypt_dir = expanduser(join('~', '.ecryptfs')) if not isdir(ecrypt_dir): mkdir(ecrypt_dir, mode=0o700) # Prep dirs for eCryptfs self._alias = basename(self.name) self._conf_file = '{}.conf'.format(join(ecrypt_dir, self._alias)) with open(self._conf_file, 'w') as conf: conf.write('{} {} ecryptfs'.format(self.name, self._upper_dir)) # Create the signature file and add it to the keyring self._sig_file = '{}.sig'.format(join(ecrypt_dir, self._alias)) passwd = b64encode(run(['head', '-c 32', '/dev/urandom'], check=True, stdout=PIPE).stdout) sig = run(['ecryptfs-add-passphrase', '--fnek'], input=passwd, stdout=PIPE).stdout.decode('utf-8') with open(self._sig_file, 'w') as sig_out: for line in sig.split('\n'): # There should be two matches. We want both in the signature file. m = search(r'sig \[(.*?)\]', line) if m: sig_out.write(m.group(1)) sig_out.write('\n') def __enter__(self): # Mount the encrypted dir try: run(['mount.ecryptfs_private', self._alias], check=True) except: # No matter what the exception, we should try to remove the keys from the keyring keys = {} # Get a list of the keys so we can reference them later key_output = run(['keyctl', 'show'], stdout=PIPE).stdout.decode('utf-8') for key_line in key_output.split('\n'): m = search(r'^ *([0-9]*).* ([a-f0-9]*)$', key_line) if m: keys[m.group(2)] = m.group(1) # The contents of the sig file will help us determine which keys should be deleted with open(self._sig_file) as sig_file: for line in sig_file: line = line.strip() if not len(line): continue # Unlink the key run(['keyctl', 'unlink', keys[line]]) self.cleanup() # Do all the regular cleanup raise # Re-raise the original exception return super().__enter__() def __exit__(self, exc, value, tb): # Unmount the encrypted dir run(['umount.ecryptfs_private', self._alias], check=True) super().__exit__(exc, value, tb) def cleanup(self): # Remove all the working files for f in (self._conf_file, self._sig_file): remove(f) super().cleanup()