Ejemplo n.º 1
0
    def __init__(self, homedir=None):
        # Ensure that there is a homedir for GNUPGHOME.
        if not homedir:
            homedir = '/tmp/p3store-gpg-{}'.format(os.getuid())
        self._homedir = homedir
        self.ensure_homedir()

        # Init a GPG context.
        self._gpgme = Context()
        self._gpgme.passphrase_cb = self._password_callback
Ejemplo n.º 2
0
Archivo: secret.py Proyecto: nima/site
def vault(sid):
    vault = os.path.join(os.environ['SITE_USER_ETC'], 'site.vault')
    if os.path.exists(vault):
        from io import BytesIO
        o = BytesIO()
        with open(vault, 'r') as ifH:
            from gpgme import Context
            ctx = Context()
            ctx.armor = True
            ctx.decrypt(ifH, o)
        return dict(
            (_.split(None, 1) for _ in o.getvalue().split('\n') if _)
        ).get(secret, '').strip()
Ejemplo n.º 3
0
def vault(sid):
    vault = os.path.join(os.environ['SIMBOL_USER_ETC'], 'simbol.vault')
    if os.path.exists(vault):
        from io import BytesIO
        o = BytesIO()
        with open(vault, 'r') as ifH:
            from gpgme import Context
            ctx = Context()
            ctx.armor = True
            ctx.decrypt(ifH, o)
        return dict(
            (_.split(None, 1) for _ in o.getvalue().split('\n') if _)
        ).get(secret, '').strip()
Ejemplo n.º 4
0
 def __init__(self):
     # Gnome keyring already takes care of the key access if you're running
     # X. If you cancel the GPG popup, you get this callback instead.
     self.password_cb = self.default_password_cb
     # Init a GPG context.
     self.context = Context()
     self.context.passphrase_cb = self._password_cb  # wrap the other cb
     # No ascii armor stuff. We'll juggle some base64 around ourselves.
     self.context.armor = False
Ejemplo n.º 5
0
    def __init__(self, homedir, keyfpr=None):
        gpgbin = path.join(homedir, 'gpg')
        ctx = self._context = Context()
        self._fpr = keyfpr
        self._key = None

        self.passphrase = None

        if not path.exists(homedir):
            makedirs(homedir, 0o700)

            open(path.join(homedir, 'gpg-agent.conf'),
                 'w').writelines(l + '\n' for l in ['default-cache-ttl 0'])

            open(gpgbin, 'w').writelines(
                l + '\n' for l in
                ['#!/bin/bash', 'exec gpg --pinentry-mode loopback "$@"'])

            chmod(gpgbin, 0o700)

        ctx.set_engine_info(PROTOCOL_OpenPGP, gpgbin, homedir)
        ctx.passphrase_cb = self._passphrase_cb
Ejemplo n.º 6
0
class Gpgmex(object):
    """
    Gpgme context object. Holds convenience functions.

    If you're running a password caching agent, you'll have the
    GPG_AGENT_INFO environment variable set. Unset it to disable the
    agent.
    """
    def __init__(self, homedir=None):
        # Ensure that there is a homedir for GNUPGHOME.
        if not homedir:
            homedir = '/tmp/p3store-gpg-{}'.format(os.getuid())
        self._homedir = homedir
        self.ensure_homedir()

        # Init a GPG context.
        self._gpgme = Context()
        self._gpgme.passphrase_cb = self._password_callback

    def destroy_homedir(self):
        # Clean up our act. This should be called from tests, but is not
        # necessary when using it normally. There is nothing wrong with
        # a bit of cache.
        rmtree(self._homedir)
        self._homedir = None
        self._gpgme = None

    def ensure_homedir(self):
        orig_umask = os.umask(0o077)
        try:
            os.makedirs(self._homedir)
        except FileExistsError:  # OSError.errno=17
            info = os.stat(self._homedir)
            if S_IMODE(info.st_mode) != 0o700:
                raise
        finally:
            os.umask(orig_umask)

    def get_key_by_id(self, key_id):
        """
        Get a GpgmexKey wrapped key. Pass key_id as uppercase radix16.
        """
        # "If [PARAM2] is 1, only private keys will be returned."
        with self._environment():
            gpgme_key = self._gpgme.get_key(key_id, 0)
        return GpgmexKey(gpgme_key)

    def import_ascii_key(self, key):
        keyfile = BytesIO(key.encode('utf-8'))
        with self._environment():
            self._gpgme.armor = True
            res = self._gpgme.import_(keyfile)

        if (res.imported + res.unchanged) != 1:
            raise ValueError(
                'import failed (imported={}, unchanged={}); '
                'homedir issues with {}?'.format(
                    res.imported, res.unchanged, self._homedir))

    def import_binary_key(self, key):
        raise NotImplementedError()

    def encrypt(self, infile, outfile, public_keys):
        """
        Encrypt infile to outfile, using the supplied public keys. The
        GPG internals will take the encryption subkey from the supplied
        keys.

        Note that for p3store purposes, we'll probably encrypt to a
        single user, then take the first PGP blob, decrypt the
        symmetric key and re-encrypt it for several users using
        python-pgp: that way we won't need to import foreign public
        keys into the user's GNUPGHOME.
        """
        assert hasattr(infile, 'read')
        assert hasattr(outfile, 'write')
        assert public_keys

        with self._environment():
            # No ascii armor stuff. We'll juggle some base64 around
            # ourselves.
            self._gpgme.armor = False
            self._gpgme.encrypt(
                [i.gpgme_key for i in public_keys], 1, infile, outfile)

        # length = output.tell()
        outfile.seek(0)

    def decrypt(self, infile, outfile):
        """
        Decrypt infile to outfile. We let the GPG internal handle the
        private key match.
        """
        assert hasattr(infile, 'read')
        assert hasattr(outfile, 'write')

        try:
            with self._environment():
                # No ascii armor stuff. We'll juggle some base64 around
                # ourselves.
                self._gpgme.armor = False
                self._gpgme.decrypt(infile, outfile)
        except GpgmeError as e:
            # If you press ^C during passphrase input.
            #   gpgme.GpgmeError: (7, 58, u'No data')
            # If the decryption failed (badly encrypted, secret key missing)
            #   gpgme.GpgmeError: (7, 152, u'Decryption failed')
            # If the password callback raised an error.
            #   gpgme.GpgmeError: (7, 32779, u'Bad file descriptor')
            if e.args[0] == 7:
                if e.args[1] == 11:
                    raise ValueError('Bad password')
                if e.args[1] == 152:
                    raise ValueError('Bad private key')
            raise

        # length = output.tell()
        outfile.seek(0)

    def password_callback(self, key, prev_was_bad):
        """
        Really basic password callback that doesn't do any caching.

        Gnome keyring already takes care of the key access if you're
        running the GPG agent. If you cancel the GPG popup, you get
        this callback instead. Subclass if you want special behaviour.
        """
        return getpass('Enter passphrase for {}: '.format(key))

    def _password_callback(self, uid_hint, passphrase_info, prev_was_bad, fd):
        """
        The password callback is called from the gpgme context if the
        GPG_AGENT is unavailable and a password is required.

        This method wraps the password callback.
        """
        # The argument uid_hint might contain a string that gives an
        # indication for which user ID the passphrase is required. If
        # this is not available, or not applicable (in the case of
        # symmetric encryption, for example), uid_hint will be NULL.
        #
        #     E41BEA77E2F8AB82 Walter (Example) <*****@*****.**>
        #     ^-- encryptkey_id  ^-- name ^-- comment  ^-- email
        #
        # The argument passphrase_info, if not NULL, will give further
        # information about the context in which the passphrase is
        # required. This information is engine and operation specific.
        #
        #    E41BEA77E2F8AB82 AF386C4BFA33BF5B 1 0
        #    ^-- encryptkey_id  ^-- mainkey_id
        #
        # If this is the repeated attempt to get the passphrase,
        # because previous attempts failed, then prev_was_bad is 1,
        # otherwise it will be 0.

        if uid_hint:
            encryptkey_id = uid_hint.split(' ', 1)[0]
            key = self.get_key_by_id(encryptkey_id)
        prev_was_bad = bool(prev_was_bad)

        # Get the password from the callback.
        try:
            password = self.password_callback(key, prev_was_bad)
            assert isinstance(password, str)
            password = password.encode('utf-8')
        except KeyboardInterrupt:
            # For some reason, we return 'No data' next..
            pass
        except Exception:
            import traceback
            traceback.print_exc()
        else:
            # Writing empty passwords too, because the close below is so
            # drastic that we wouldn't get a second try.
            os.write(fd, password + b'\n')
            return 0

        # > The user must write the passphrase, followed by a newline
        # > character, to the file descriptor fd. If the user returns 0
        # > indicating success, the user must at least write a newline
        # > character before returning from the callback.
        # >
        # > If an error occurs, return the corresponding gpgme_error_t value.
        # > You can use the error code GPG_ERR_CANCELED to abort the operation.
        # > Otherwise, return 0.
        #
        # But that doesn't work. We must always write a newline, or the thing
        # hangs. Returning 0 or ERR_CANCELED doesn't seem to make any
        # difference. So, instead, we close() the fd. That will make for a
        # quicker abort: 'Bad file descriptor'
        os.close(fd)
        return ERR_CANCELED

    def _environment(self):
        """
        Temporarily set alter environment for GnuPG/gpgme.
        """
        return _EnvironmentContext(self._homedir)
Ejemplo n.º 7
0
class GPGCrypt(object):
    """
    Wrapper around GPGME GPG encrypting/decrypting.
    """
    def __init__(self):
        # Gnome keyring already takes care of the key access if you're running
        # X. If you cancel the GPG popup, you get this callback instead.
        self.password_cb = self.default_password_cb
        # Init a GPG context.
        self.context = Context()
        self.context.passphrase_cb = self._password_cb  # wrap the other cb
        # No ascii armor stuff. We'll juggle some base64 around ourselves.
        self.context.armor = False

    def set_password_cb(self, password_cb):
        # Set password callback. The callback must return a (password,
        # allow_caching) tuple.
        self.password_cb = password_cb

    def generate_key(self, **kwargs):
        # <GnupgKeyParms format="internal">
        # (Sub)Key-Type: RSA
        # (Sub)Key-Length: 2048 (or 4096)
        # Name+Comment+Email "Alex Boonstra (TEST) <*****@*****.**>"
        # Expire-Date: None (or in a couple of years)
        # Passphrase: xxx
        # |
        # v
        # self.context.op_genkey(parms.encode('utf-8'), None, None)
        # result = self.context.op_genkey(parms.encode('utf-8'), None, None)
        msg = ('This takes too long. Generate they keys yourself. Look in '
               'docs/examples for an example')
        raise NotImplementedError(msg)

    def get_key(self, **kwargs):
        """
        Get the appropriate key to work with.

        Takes either an id= or an email= argument.

        TODO: if the key is not found, get it from server?
        """
        if (len(kwargs) != 1 or
            any(i not in ('id', 'email') for i in kwargs.keys())):
            raise TypeError('get_key takes either id= or email=')

        # Lookup by id.
        if 'id' in kwargs:
            # "If [PARAM2] is 1, only private keys will be returned."
            key = self.context.get_key(kwargs['id'], 0)
            # Make sure we have an e-mail to go by.
            email = unicode(key.uids[0].email.lower())

        # Lookup by email. E-mail returned by gpgme is in unicode. (Even
        # though it probably won't contain any non-ascii.)
        else:
            email = unicode(kwargs['email']).lower()
            found = []

            for key in self.context.keylist():
                for uid in key.uids:
                    if uid.email.lower() == email:
                        found.append(key)
                        if len(found) > 1:
                            msg = 'Cannot cope with more than one recipient'
                            raise NotImplementedError(msg)
            if not found:
                return None

            key = found[0]

        # Do checks on the key.
        if key.expired:
            raise CryptBadPubKey('%s key is expired' % (email,))
        elif key.invalid or key.revoked or key.disabled:
            raise CryptBadPubKey('%s key is invalid/revoked/diabled' %
                                 (email,))
        elif not key.can_encrypt:  # also set to false if e.g. expired
            raise CryptBadPubKey('%s key is unusable' % (email,))

        # Find right subkey and check if expiry is nigh.
        for subkey in key.subkeys:
            if not (subkey.expired or subkey.invalid or subkey.revoked or
                    subkey.disabled or not subkey.can_encrypt):
                break
        if subkey.expires and float(subkey.expires - time()) / 86400.0 < 200:
            # Send out a warning that this key is about to expires. I'm not
            # sure what the implications of expired keys are, but let's prepare
            # for the worst and warn the user at an early stage.
            print >>stderr, ('WARNING: (sub)key %s for %s will expire in '
                             '%.1f days' % (subkey.keyid, key.uids[0].email,
                             float(subkey.expires - time()) / 86400.0))

        # Ok. All is good.
        return key

    def import_key(self, key):
        to_import = BytesIO(str(key))  # ensure bytestring!
        res = self.context.import_(to_import)
        if (res.imported + res.unchanged) != 1:
            raise CryptError('import failed (imported=%d, unchanged=%d). '
                             'Try setting os.environ["GNUPGHOME"] to an '
                             '*existing* writable path in settings; '
                             'e.g. "/tmp"' % (res.imported, res.unchanged))

        try:
            key_id = get_pubkey_id_from_ascii(key)
            key = self.get_key(id=unicode(key_id))
        except Exception, e:
            raise CryptError('GPG quick hack failed; no key found', e)

        return key