kjo
kjo

Reputation: 35331

How to prevent passphrase-caching from within a gpgme-based Python script?

The following short Python script takes three command-line arguments: a passphrase, an input path, and an output path. Then it uses the passphrase to decrypt the contents of the input path, and puts the decrypted content in the output path.

from gpg import Context
import sys

pp = sys.argv[1]    # passphrase
enc = sys.argv[2]   # input file (assumed to be encrypted)
dec = sys.argv[3]   # output file

with open(enc, 'rb') as reader, open(dec, 'wb') as writer, Context() as ctx:

    try:

        ctx.decrypt(reader, sink=writer, passphrase=pp)

    except Exception as e:
        print(str(e), file=sys.stderr)

This decryption works fine, as long as the correct passphrase is provided, but it apparently results in the caching of such correct passphrase, so that any subsequent decryption attempts succeed irrespective of the passphrase one provides. (I give a fuller illustration of what I mean at the end of this post, together with version details.)

Clearly, there's some passphrase caching going on, but I don't really understand the details.

What I want to know is: how can I modify the Python script so that it disables the caching of passphrases? Note that I am not interested in how to disable passphrase caching outside of the script! I want the script to disable passphrase caching autonomously. Is that possible?


Here's a detailed example of what I alluded to above. The script ./demo.py is the one whose source I listed above. IMPORTANT: the code given below behaved as shown only when I executed it from the command line. If I put it in a file and execute it (or source it) as a script, then all decryptions with the wrong passphrase fail, irrespective of any prior successful decryptions with the correct passphrase.

# Prologue: preparation

# First, define some variables

% ORIGINAL=/tmp/original.txt
% ENCRYPTED=/tmp/encrypted.gpg
% DECRYPTED=/tmp/decrypted.txt
% PASSPHRASE=yowzayowzayowza

# Next, create a cleartext original:

% echo 'Cool story, bro!' > "$ORIGINAL"

# Next, encrypt the original using /usr/bin/gpg

% rm -f "$ENCRYPTED"
% /usr/bin/gpg --batch --symmetric --cipher-algo=AES256 --compress-algo=zlib --passphrase="$PASSPHRASE" --output="$ENCRYPTED" "$ORIGINAL"

# Confirm encryption

% od -c "$ENCRYPTED"
0000000 214  \r 004  \t 003 002 304 006 020   %   q 353 335 212 361 322
0000020   U 001   w 350 335   K 347 320 260 224 227 025 275 274 033   X
0000040 020 352 002 006 254 331 374 300 221 265 021 376 254   9   $   <
0000060 233 275 361 226 340 177 330   !   c 372 017   & 300 352   $   k
0000100 252 205 244 336 222   N 027 200   | 211 371   r   Z   ] 353   6
0000120 261 177   b 336 026 023 367 220 354 210 265 002   :   r 262 037
0000140 367   L   H 262 370    
0000146


# Now, the demonstration proper.

# Initially, decryption with the wrong passphrase fails:

% rm -f "$DECRYPTED"
% python ./demo.py "certainly the wrong $PASSPHRASE" "$ENCRYPTED" "$DECRYPTED"
gpgme_op_decrypt_verify: GPGME: Decryption failed


# Decryption with the right passphrase succeeds:

% rm -f "$DECRYPTED"
% python ./demo.py "$PASSPHRASE" "$ENCRYPTED" "$DECRYPTED"
% od -c "$DECRYPTED"
0000000   C   o   o   l       s   t   o   r   y   ,       b   r   o   !
0000020  \n
0000021


# After the first successful decryption with the right
# passphrase, decryption with the wrong passphrase always
# succeeds:

% rm -f "$DECRYPTED"
% python ./demo.py "certainly the wrong $PASSPHRASE" "$ENCRYPTED" "$DECRYPTED"
% od -c "$DECRYPTED"
0000000   C   o   o   l       s   t   o   r   y   ,       b   r   o   !
0000020  \n
0000021


# Some relevant version info

% python -c 'import gpg; print((gpg.version.versionstr, gpg.version.gpgme_versionstr))'
('1.10.0', '1.8.0')

% gpg --version
gpg (GnuPG) 2.1.18
libgcrypt 1.7.6-beta
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Home: /home/kj146/.gnupg
Supported algorithms:
Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,
        CAMELLIA128, CAMELLIA192, CAMELLIA256
Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2

% python --version
Python 3.5.3

% uname -ar
Linux parakeet 3.16.0-4-amd64 #1 SMP Debian 3.16.43-2 (2017-04-30) x86_64 GNU/Linux

Upvotes: 6

Views: 1409

Answers (4)

Malekai
Malekai

Reputation: 5031

Over on the GnuPG documentation, under chapter 9.6, there's a section called "List of all commands and options".

Which shows a --forget option that you can use to:

"Flush the passphrase for the given cache ID from the cache."

In the "GnuPG Made Easy" Reference Manual, under chapter 7.5 key management, there's a section called deleting keys which contains documentation for a function called gpgme_op_delete_ext which allows you to delete public keys.

You could also delete private keys using the GPGME_DELETE_ALLOW_SECRET flag, which according to documentation:

"If not set, only public keys are deleted. If set, secret keys are deleted as well, if that is supported."

NOTE: To skip user confirmation you could use the GPGME_DELETE_FORCE flag.

Good luck.

Upvotes: 0

imposeren
imposeren

Reputation: 4392

  • According to source: Context.decrypt can get passphrase using "pinentry", I think that by default context uses it (some kind of gpg-agent in your case)

  • depending on your desktop environment some "agent" may be used as a part of pinentry so it "remembers" passphrase.

  • I think that you should initialize context with pinentry_mode=gpg.constants.SOME_CONSTANT (maybe gpg.constants.PINENTRY_MODE_ERROR... I'm not sure: have no experience with gpgme, just investigated the docs and code) Modes: see the docs

  • Or you can stop/kill gpg-agent/kde-wallet/gnome-keyring: one of them is performing the "caching".

  • or add a line no-use-agent to ~/.gnupg/gpg.conf

  • Maybe calling ctx.set_ctx_flag("no-symkey-cache", "1") after initializing will solve your problem (see other answer)

Upvotes: 1

xilpex
xilpex

Reputation: 3237

There is no pure pythonic way to do it. The most pythonic you can go is setting the PYTHONDONTWRITEBYTECODE environment variable to 1. Here is a code to set the variable:

import os
os.environ['PYTHONDONTWRITEBYTECODE'] = 1

NOTE: This code will also disable caching on other python scripts

Upvotes: 0

Filip Dimitrovski
Filip Dimitrovski

Reputation: 1736

Digging in the C gpgme library (which is what the Python library you use is wrapping), there is:

https://www.gnupg.org/documentation/manuals/gpgme/Context-Flags.html#Context-Flags

"no-symkey-cache"
For OpenPGP disable the passphrase cache used for symmetrical en- and decryption.
This cache is based on the message specific salt value. Requires at least GnuPG
2.2.7 to have an effect.

I'm not sure how the context is interacting with the filesystem or a GPG agent, but your first attempt should be setting this flag to true.

Upvotes: 4

Related Questions