isislovecruft / python-gnupg

A modified version of python-gnupg, including security patches, extensive documentation, and extra features.
Other
426 stars 171 forks source link

Unable to import private key with passpharse #284

Open pajuscz opened 2 years ago

pajuscz commented 2 years ago

I came across this while looking into this closed issue: https://github.com/isislovecruft/python-gnupg/issues/194

I wasn't able to import private key with passpharse using the same method. What happens is that public key is imported, but not the secret key - I cannot decrypt data after I do the import.

While looking into the code I don't think it is possible to import a key using passpharse, because the passpharse is set to False by default.

The _handle_io: def _handle_io(self, args, file, result, passphrase=False, binary=False):

called from import_keys(self, key_data) is called in this fashion: self._handle_io(['--import'], data, result, binary=True) I don't see a way to pass passphase there.

When I 'hard-code' the passphasee into the self._handle_io(['--import'], data, result, passphrase='secret', binary=True) It works as expected. I think there should be a posibility to pass passpharse as an argument into GPG.import_keys()

pajuscz commented 2 years ago

The key I am trying to import is:

-----BEGIN PGP PRIVATE KEY BLOCK-----

lQdGBGJy/wcBEAC6IvcYBmhTnFZNi+qYMTwPf2pixNx6IWUQZaGlbEBStuSucWZ2
MuAVDeL1+ahOUanResjfLgk0ivZ5ZykfDR4wrW9gqoahLFFu2Xb2S6eyIaba2Phm
Z+0xOLOdtambrxZDuN+/NCZoXE2EpZcVyeR4tiN1GrjNqF7jMwYBL8tjPGc1gLu+

When I do gpg --import import.key I am prompted for passpharse,

When I use GPG.import_keys() I don't know how to pass the passpharse and as a result I am getting:


'1' (4560127240) = {str} 'Invalid Certificate'
'0' (4563316368) = {str} 'No specific reason given'
'3' (4561059072) = {str} 'Certificate Chain too long'
'2' (4559698000) = {str} 'Issuer Certificate missing'
'4' (4561059112) = {str} 'Error storing certificate'

'17' (4570749568) = {str} 'Contains private key'
'16' (4570747448) = {str} 'Contains private key'
'1' (4560127240) = {str} 'Entirely new key'
'0' (4563316368) = {str} 'Not actually changed'
'2' (4559698000) = {str} 'New user IDs'
'4' (4561059112) = {str} 'New signatures'
'8' (4563315368) = {str} 'New subkeys'

u'gpg: no valid OpenPGP data found.
[GNUPG:] NODATA 1
gpg: Total number processed: 0
[GNUPG:] IMPORT_RES 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
'
pajuscz commented 2 years ago

The actual stacktrace with exception is here:

Traceback (most recent call last):
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 801, in __bootstrap_inner
    self.run()
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 754, in run
    self.__target(*self.__args, **self.__kwargs)
  File "lib/python2.7/site-packages/pretty_bad_protocol/_meta.py", line 670, in _read_response
    result._handle_status(keyword, value)
  File "/lib/python2.7/site-packages/pretty_bad_protocol/_parsers.py", line 1304, in _handle_status
    raise ValueError("Unknown status message: %r" % key)
ValueError: Unknown status message: u'USERID_HINT'
Exception in thread Thread-47:
Traceback (most recent call last):
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 801, in __bootstrap_inner
    self.run()
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 754, in run
    self.__target(*self.__args, **self.__kwargs)
  File "/lib/python2.7/site-packages/pretty_bad_protocol/_meta.py", line 670, in _read_response
    result._handle_status(keyword, value)
  File "lib/python2.7/site-packages/pretty_bad_protocol/_parsers.py", line 1304, in _handle_status
    raise ValueError("Unknown status message: %r" % key)
ValueError: Unknown status message: u'USERID_HINT'
pajuscz commented 2 years ago

I am using options=['--yes', '--pinentry-mode loopback'] when initializing the GPG object if that helps.

If I don't use it - there is no error, nor exception but the secret key is not imported - only the public key.

erickeniuk commented 1 year ago

Hi @pajuscz , I ran into this issue as well. You can simply pass passphrase to the _handle_io() as it is an already an available parameter when self._handle_io() is called within decrypt_files()

Changes made within gnupg.py:

def import_keys(self, key_data, passphrase=False):

and

self._handle_io(['--import'], data, result, passphrase, binary=True)

I tested these changes on my machine and I had a few unknown statuses which I had to patch as well within _parsers.py but after that I had no issues importing keys with passphrases. The other solution would be likely to import keys with no passphrase.

    def import_keys(self, key_data, passphrase=False):
    """
    Import the key_data into our keyring.

    >>> import shutil
    >>> shutil.rmtree("doctests")
    >>> gpg = gnupg.GPG(homedir="doctests")
    >>> inpt = gpg.gen_key_input()
    >>> key1 = gpg.gen_key(inpt)
    >>> print1 = str(key1.fingerprint)
    >>> pubkey1 = gpg.export_keys(print1)
    >>> seckey1 = gpg.export_keys(print1,secret=True)
    >>> key2 = gpg.gen_key(inpt)
    >>> print2 = key2.fingerprint
    >>> seckeys = gpg.list_keys(secret=True)
    >>> pubkeys = gpg.list_keys()
    >>> assert print1 in seckeys.fingerprints
    >>> assert print1 in pubkeys.fingerprints
    >>> str(gpg.delete_keys(print1))
    'Must delete secret key first'
    >>> str(gpg.delete_keys(print1,secret=True))
    'ok'
    >>> str(gpg.delete_keys(print1))
    'ok'
    >>> pubkeys = gpg.list_keys()
    >>> assert not print1 in pubkeys.fingerprints
    >>> result = gpg.import_keys(pubkey1)
    >>> pubkeys = gpg.list_keys()
    >>> seckeys = gpg.list_keys(secret=True)
    >>> assert not print1 in seckeys.fingerprints
    >>> assert print1 in pubkeys.fingerprints
    >>> result = gpg.import_keys(seckey1)
    >>> assert result
    >>> seckeys = gpg.list_keys(secret=True)
    >>> assert print1 in seckeys.fingerprints
    """
    ## xxx need way to validate that key_data is actually a valid GPG key
    ##     it might be possible to use --list-packets and parse the output

    result = self._result_map['import'](self)
    log.info('Importing: %r', key_data[:256])
    data = _make_binary_stream(key_data, self._encoding)
    self._handle_io(['--import'], data, result, passphrase, binary=True)
    data.close()
    return result