I recently struggled with this and (after endless stumbling around gpg docs and qa sites) came across this 2015 thread that mentions the answer:
Maybe what i'm missing is how the "cache_id" is selected for the
symmetric passphrase, both at creation time and at re-use time. can you
summarize that? I dug around in the code a bit but didn't sort out how
it's being done.
If I remember correctly (and the logic hasn't changed), it is a
randomly chosen 8-octet salt value:
http://tools.ietf.org/html/rfc4880#section-3.7.1.2
...
The S2K salt is used as cache id.
You can get the salt by listing packets for your encrypted file:
$ echo | gpg --list-packets ~/myencryptedfile 2>&1 | grep -o '[0-9A-F]\{16\}'
0123456789ABCDEF
- leading
echo
forces Inappropriate ioctl for device
error, bypassing pinentry prompt
2>&1
hide stderr
grep -o '[0-9A-F]\{16\}'
extract salt from command output
Now we have the 8-octet hex-encoded salt value.
Unfortunately that didn't work directly for me. So after more stumbling around the gpg codebase, I came across this snippet:
memset (s2k_cacheidbuf, 0, sizeof s2k_cacheidbuf);
*s2k_cacheidbuf = 'S';
bin2hex (s2k->salt, 8, s2k_cacheidbuf + 1);
s2k_cacheid = s2k_cacheidbuf;
The crux is the "S" prepended on the second line. I couldn't find this mentioned anywhere else.
So all together now. Encrypt the file:
$ gpg --symmetric --passphrase-fd 3 --batch --output ~/myencryptedfile <<EOF 3<<EOF3
> myencryptedcontent
> EOF
> mypassphrase
> EOF3
--passphrase-fd 3
: supply passphrase over file descriptor 3
--batch
: required with --passphrase-fd
- I prefer to supply passphrase via
--passphrase-fd 3
and data to be encrypted over stdin (<<EOF
and 3<<EOF3
) rather than write it to a file initially for cat
, or echoing it into stdin (which makes me fearful of process list snooping). Docs: here-documents (EOF syntax).
If you don't care about portability outside of bash here-strings are much more palatable:
gpg --symmetric ... ~/myencryptedfile <<<myencryptedcontent 3<<<mypassphrase
Get the 8-octet hex salt value:
$ echo | gpg --list-packets ~/myencryptedfile 2>&1 | grep -o '[0-9A-F]\{16\}'
0123456789ABCDEF
Make sure passphrase reset is allowed by gpg-agent:
$ cat ~/.gnupg/gpg-agent.conf
allow-preset-passphrase
$ gpg-connect-agent reloadagent /bye # restart the agent after changing config
Preset the passphrase:
$ /usr/local/Cellar/gnupg/2.2.17/libexec/gpg-preset-passphrase --preset S0123456789ABCDEF <<EOF
> mypassphrase
> EOF
- "S" prefixed onto the salt, so the resulting gpg-agent cacheid is
--preset S0123456789ABCDEF
- passphrase over stdin via heredoc (EOF) to avoid it showing up in the process list
- If you get
ERR 67108924 Not supported <GPG Agent> - no --allow-preset-passphrase
, you need to add allow-preset-passphrase
to ~/.gnupg/gpg-agent.conf
and restart the agent (see above)
- The location of
gpg-preset-passphrase
may be different on your system.
And finally, decrypt the file!
$ gpg --decrypt --pinentry-mode loopback ./myencryptedfile
gpg: AES encrypted data
gpg: encrypted with 1 passphrase
myencryptedcontent
Notes:
- I've found locating
gpg-preset-passphrase
in my scripts annoying since it isn't installed anywhere on the system path (at least not in my installation). However, gpg-connect-agent
is, and can be used for the same purpose with an additional step. You just need to hex-encode the passphrase before using it (tr -d '\n'
because EOF adds a newline):
$ (tr -d '\n' | hexdump -v -e '/1 "%02X"' && echo) <<EOF
> mypassphrase
> EOF
6D7970617373706872617365
$ gpg-connect-agent <<EOF
> preset_passphrase S0123456789ABCDEF -1 6D7970617373706872617365
> EOF
OK
# now decryption should work as above
- I'm running gnupg version 2.2.17 on OSX, installed via homebrew
$ gpg --version
gpg (GnuPG) 2.2.17
libgcrypt 1.8.5
How were you caching the passphrase initially? – Xen2050 – 2019-04-22T07:42:43.627
gpg-agent does it when it has a file name. – Jason Livesay – 2019-04-22T18:08:34.197