Pinentry: Difference between revisions
| (11 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
== pinentry == | |||
== peak inside pinentry == | |||
https://velvetcache.org/2023/03/26/a-peek-inside-pinentry/ | |||
== assuan protocol == | |||
https://www.gnupg.org/documentation/manuals/assuan/index.html | |||
https://www.gnupg.org/software/libassuan/index.html | |||
https://velvetcache.org/2023/03/26/a-peek-inside-pinentry/ | |||
https://docs.jade.fyi/gnu/gnupg.html | |||
== sample 1 == | |||
echo -e 'SETTITLE YUBI\nSETDESC YUBI\nSETPROMPT YUBI\nSETERROR YUBI\nGETPIN\n' | /usr/bin/pinentry | grep '^D' | echo -e 'SETTITLE YUBI\nSETDESC YUBI\nSETPROMPT YUBI\nSETERROR YUBI\nGETPIN\n' | /usr/bin/pinentry | grep '^D' | ||
| Line 394: | Line 412: | ||
unset password | unset password | ||
</pre> | |||
== No cached passwords == | |||
gpg-agent caches passphrases by cache ID | |||
Option 1: Use a unique cache ID every time | |||
<pre> | |||
CACHE_ID="myprompt-$$-$(date +%s)" | |||
gpg-connect-agent <<EOF | |||
GET_PASSPHRASE $CACHE_ID X X "Enter Password" | |||
/bye | |||
EOF | |||
</pre> | |||
Option 2: Clear the cache after use | |||
<pre> | |||
gpg-connect-agent <<EOF | |||
CLEAR_PASSPHRASE mytest | |||
/bye | |||
EOF | |||
</pre> | |||
Option 3: Disable caching globally | |||
~/.gnupg/gpg-agent.conf | |||
<pre> | |||
default-cache-ttl 0 | |||
max-cache-ttl 0 | |||
</pre> | |||
or very short values: | |||
<pre> | |||
default-cache-ttl 1 | |||
max-cache-ttl 1 | |||
</pre> | |||
gpgconf --kill gpg-agent | |||
-- | |||
Current cache settings: | |||
gpgconf --list-options gpg-agent | grep cache | |||
# or | |||
gpg-connect-agent 'GETINFO gpgconf_name' /bye | |||
<pre> | |||
default-cache-ttl:24:0:expire cached PINs after N seconds:3:3:N:600:: | |||
default-cache-ttl-ssh:24:1:expire SSH keys after N seconds:3:3:N:1800:: | |||
max-cache-ttl:24:2:set maximum PIN cache lifetime to N seconds:3:3:N:7200:: | |||
max-cache-ttl-ssh:24:2:set maximum SSH key lifetime to N seconds:3:3:N:7200:: | |||
ignore-cache-for-signing:8:0:do not use the PIN cache when signing:0:0:::: | |||
no-allow-external-cache:8:0:disallow the use of an external password cache:0:0:::: | |||
</pre> | |||
-- test cache -- | |||
<pre> | |||
export GPG_TTY=$(tty) | |||
gpg-connect-agent updatestartuptty /bye >/dev/null | |||
# First prompt | |||
gpg-connect-agent <<EOF | |||
GET_PASSPHRASE mytest X X "Password" | |||
/bye | |||
EOF | |||
# Second prompt using same cache id | |||
gpg-connect-agent <<EOF | |||
GET_PASSPHRASE mytest X X "Password" | |||
/bye | |||
EOF | |||
</pre> | |||
If cache is enabled it won't prompt for second. | |||
-- | |||
sample-no-cache.sh | |||
<pre> | |||
#!/bin/bash | |||
export GPG_TTY=$(tty) | |||
gpg-connect-agent updatestartuptty /bye >/dev/null | |||
CACHE_ID="prompt-$$-$(date +%s%N)" | |||
gpg-connect-agent <<EOF | |||
GET_PASSPHRASE $CACHE_ID X X "Password Required" | |||
/bye | |||
EOF | |||
</pre> | |||
--- | |||
<pre> | |||
#!/bin/bash | |||
export GPG_TTY=$(tty) | |||
gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1 | |||
CACHE_ID="pw-$$-$(date +%s%N)" | |||
response=$( | |||
gpg-connect-agent <<EOF | |||
GET_PASSPHRASE $CACHE_ID X X "Enter Password" | |||
/bye | |||
EOF | |||
) | |||
echo "$response" | |||
</pre> | |||
== example 11 == | |||
<pre> | |||
#!/bin/bash | |||
coproc PINENTRY { pinentry-curses; } | |||
echo "SETTITLE Login" >&"${PINENTRY[1]}" | |||
echo "SETPROMPT Password:" >&"${PINENTRY[1]}" | |||
echo "GETPIN" >&"${PINENTRY[1]}" | |||
while read -r line <&"${PINENTRY[0]}"; do | |||
echo "$line" | |||
if [[ "$line" =~ ^D\ (.*) ]]; then | |||
password="${BASH_REMATCH[1]}" | |||
break | |||
fi | |||
done | |||
echo "Password: $password" | |||
</pre> | |||
== example 12 == | |||
this one works | |||
<pre> | |||
#!/bin/bash | |||
export GPG_TTY=$(tty) | |||
response=$( | |||
gpg-connect-agent \ | |||
'GET_PASSPHRASE myprompt7 Password X Password\ Please' \ | |||
/bye 2>/dev/null | |||
) | |||
password=$(awk '/^D /{print substr($0,3)}' <<< "$response") | |||
echo "Password length: ${#password}" | |||
</pre> | |||
== exmaple 13 == | |||
<pre> | |||
#!/bin/bash | |||
export GPG_TTY=$(tty) | |||
gpg-connect-agent updatestartuptty /bye >/dev/null | |||
response=$( | |||
gpg-connect-agent \ | |||
'GET_PASSPHRASE mycache X X "Enter Password"' \ | |||
/bye | |||
) | |||
password=$(sed -n 's/^D //p' <<< "$response") | |||
echo "Password entered." | |||
echo "Length: ${#password}" | |||
</pre> | |||
== example 14 == | |||
<pre> | |||
#!/bin/bash | |||
coproc PIN { pinentry-curses; } | |||
printf 'SETTITLE Login\n' >&"${PIN[1]}" | |||
printf 'SETPROMPT Password:\n' >&"${PIN[1]}" | |||
printf 'GETPIN\n' >&"${PIN[1]}" | |||
while IFS= read -r line <&"${PIN[0]}"; do | |||
case "$line" in | |||
D\ *) | |||
password="${line#D }" | |||
break | |||
;; | |||
esac | |||
done | |||
echo "Password length: ${#password}" | |||
</pre> | |||
== example 15 == | |||
This one worked!! | |||
<pre> | |||
TTY=$(tty) | |||
{ | |||
echo "OPTION ttyname=$TTY" | |||
echo "SETTITLE Login" | |||
echo "SETPROMPT Password:" | |||
echo "GETPIN" | |||
} | pinentry-curses | |||
</pre> | |||
-- fix no lc_ctype -- | |||
pinentry-curses: no LC_CTYPE known - assuming UTF-8 | |||
<pre> | |||
{ | |||
echo "OPTION lc-ctype=en_US.UTF-8" | |||
echo "BYE" | |||
} | pinentry-curses | |||
</pre> | |||
OPTION lc-ctype=en_US.UTF-8 | |||
<pre> | |||
TTY=$(tty) | |||
{ | |||
echo "OPTION ttyname=$TTY" | |||
echo "OPTION lc-ctype=$LC_CTYPE" | |||
echo "SETTITLE Login" | |||
echo "SETPROMPT Password:" | |||
echo "GETPIN" | |||
} | pinentry-curses | |||
</pre> | |||
--- | |||
<pre> | |||
TTY=$(tty) | |||
response=$( | |||
{ | |||
echo "OPTION ttyname=$TTY" | |||
echo "OPTION lc-ctype=$LC_CTYPE" | |||
echo "SETTITLE Login" | |||
echo "SETPROMPT Password:" | |||
echo "GETPIN" | |||
} | pinentry-curses | |||
) | |||
# Extract the entered PIN/password | |||
pin=$(echo "$response" | awk '/^D / {print substr($0,3)}') | |||
echo $pin | |||
</pre> | |||
-- robust version -- | |||
<pre> | |||
#!/bin/bash | |||
{ | |||
echo "OPTION ttyname=$(tty)" | |||
echo "OPTION lc-ctype=${LC_CTYPE:-${LANG:-en_US.UTF-8}}" | |||
echo "SETTITLE Login" | |||
echo "SETPROMPT Password:" | |||
echo "GETPIN" | |||
} | pinentry-curses | |||
</pre> | |||
For completeness should also include: | |||
<pre> | |||
echo "OPTION lc-messages=${LC_MESSAGES:-$LANG}" | |||
</pre> | </pre> | ||
== keywords == | == keywords == | ||
Latest revision as of 03:14, 27 August 2026
pinentry
peak inside pinentry
https://velvetcache.org/2023/03/26/a-peek-inside-pinentry/
assuan protocol
https://www.gnupg.org/documentation/manuals/assuan/index.html
https://www.gnupg.org/software/libassuan/index.html
https://velvetcache.org/2023/03/26/a-peek-inside-pinentry/
https://docs.jade.fyi/gnu/gnupg.html
sample 1
echo -e 'SETTITLE YUBI\nSETDESC YUBI\nSETPROMPT YUBI\nSETERROR YUBI\nGETPIN\n' | /usr/bin/pinentry | grep '^D'
sample-pinentry.sh ::
#!/bin/bash
# Ensure pinentry is installed
command -v pinentry >/dev/null 2>&1 || {
echo "ERROR: pinentry not found"
exit 1
}
# Talk to pinentry using its text protocol
response=$(
{
echo "SETTITLE Credential Prompt"
echo "SETPROMPT Passphrase:"
echo "SETDESC Please enter your passphrase."
echo "GETPIN"
} | pinentry
)
# Extract the entered PIN/password
pin=$(echo "$response" | awk '/^D / {print substr($0,3)}')
if [[ -n "$pin" ]]; then
echo "Passphrase was entered."
echo "Length: ${#pin}"
# Do something with $pin here
else
echo "No passphrase entered or dialog cancelled."
fi
secure-sample-pinentry.sh ::
#!/bin/bash
pin=$(
{
echo "SETTITLE Secure Prompt"
echo "SETPROMPT Password:"
echo "GETPIN"
} | pinentry | sed -n 's/^D //p'
)
if [[ -z "$pin" ]]; then
echo "Cancelled"
exit 1
fi
# Use the secret
echo "Received secret"
# Clear the variable when done
unset pin
Info
When the user enters a value, pinentry returns lines similar to:
OK Pleased to meet you D MySecretPassword OK
The line beginning with D contains the entered secret, which is why the script extracts it.
gpg-agent version
Example 1: Ask for a passphrase via gpg-agent
#!/bin/bash
# Establish a connection to gpg-agent
GPG_CONNECT=$(gpg-connect-agent /bye 2>/dev/null)
if [[ $? -ne 0 ]]; then
echo "gpg-agent is not running"
exit 1
fi
# Request a passphrase
response=$(
gpg-connect-agent <<EOF
GET_PASSPHRASE --data mycacheid X X "Enter your password"
/bye
EOF
)
password=$(echo "$response" | sed -n 's/^D //p')
if [[ -z "$password" ]]; then
echo "User cancelled"
exit 1
fi
echo "Password received"
unset password
This caches the password, so if you call it again, it will have it stored.
--
Example 2: Trigger the user's configured pinentry
This example lets gpg-agent display the configured Pinentry GUI:
#!/bin/bash
result=$(
gpg-connect-agent <<'EOF'
GET_PASSPHRASE myapp-login X X "Please enter your password"
/bye
EOF
)
secret=$(echo "$result" | sed -n 's/^D //p')
if [[ -n "$secret" ]]; then
echo "Success"
else
echo "Cancelled"
fi
--
Example 3: Cache the secret
A nice feature of gpg-agent is passphrase caching:
#!/bin/bash CACHE_ID="my-company-tool" secret=$( gpg-connect-agent <<EOF | GET_PASSPHRASE $CACHE_ID X X "Authentication Required" /bye EOF sed -n 's/^D //p' ) echo "Secret obtained"
This one didn't seem to work for me?
--- Setup --
export GPG_TTY=$(tty) gpg-connect-agent updatestartuptty /bye
Reload agent:
gpg-connect-agent reloadagent /bye
Configure ~/.gnupg/gpg-agent.conf
pinentry-program /usr/bin/pinentry-tty pinentry-program /usr/bin/pinentry-curses
--- manually test --
gpg-connect-agent <<EOF GET_PASSPHRASE mytest X X "Enter Password" EOF
Direct Pin Entry Example
#!/bin/bash
output=$(
{
echo "SETTITLE Login"
echo "SETPROMPT Password:"
echo "SETDESC Please enter your password."
echo "GETPIN"
} | pinentry
)
secret=$(sed -n 's/^D //p' <<< "$output")
if [[ -z "$secret" ]]; then
echo "Cancelled"
exit 1
fi
echo "Got password (${#secret} chars)"
Normal Usage
echo "hello" > test.txt gpg -c test.txt
will trigger:
gpg
-> gpg-agent
-> pinentry
reusable password prompt via gpg-agent
gpg-preset-passphrase # or gpg-agent --daemon
Quick and dirty
{
echo "SETTITLE Login"
echo "SETPROMPT Password:"
echo "GETPIN"
} | pinentry
#!/bin/bash
output=$(
{
echo "SETTITLE Test"
echo "SETPROMPT Password:"
echo "GETPIN"
} | pinentry
)
echo "----- RAW OUTPUT -----"
printf '%s\n' "$output"
echo "----------------------"
if "D secret", then:
secret=$(printf '%s\n' "$output" | sed -n 's/^D //p')
If it contains percent-encoding: "D abc%20def", then you will need to decode before use
Another Try
export GPG_TTY=$(tty) gpg-connect-agent updatestartuptty /bye gpg-connect-agent <<EOF GET_PASSPHRASE mytest X X "Enter Password" EOF
Decode with:
xxd -r -p
{
gpg-connect-agent <<EOF
GET_PASSPHRASE mytest X X "Enter Password"
EOF
} | xxd -r -p
# echo "pinentry-program /usr/bin/pinentry-tty" >> ~/.gnupg/gpg-agent.conf echo "pinentry-program /usr/bin/pinentry-gtk" >> ~/.gnupg/gpg-agent.conf gpgconf --kill gpg-agent
---
~/.gnupg/gpg-agent.conf
# pinentry-tty renders cleanly in the Copilot CLI terminal (no curses/mouse junk) pinentry-program /usr/bin/pinentry-tty # Allow non-interactive passphrase paths allow-loopback-pinentry allow-preset-passphrase # Keep the passphrase cached for a full workday so signing is silent after one unlock default-cache-ttl 34200 max-cache-ttl 34200
another try
Fix #3: Use a dedicated TTY
A trick for scripts is to communicate over stdin/stdout while pinentry accesses /dev/tty:
#!/bin/bash
exec 3<> /dev/tty
output=$(
{
echo "SETTITLE Login"
echo "SETPROMPT Password:"
echo "GETPIN"
} | pinentry 2>&1
)
echo "$output"
---
gpg-connect-agent <<EOF GET_PASSPHRASE mytest X X "Test" EOF
---
{
echo "GETPIN"
} | pinentry
---
export GPG_TTY=$(tty) pinentry-curses <<EOF SETTITLE Login SETPROMPT Password: GETPIN EOF
decode version
#!/bin/bash
response=$(
gpg-connect-agent <<EOF
GET_PASSPHRASE mytest X X "Enter Password"
EOF
)
hex=$(awk '/^OK / {print $2}' <<< "$response")
password=$(printf '%s' "$hex" | xxd -r -p)
echo "Password: $password"
reliable option gpg-agent
#!/bin/bash
# Ensure gpg-agent knows which terminal to use
export GPG_TTY=$(tty)
gpg-connect-agent updatestartuptty /bye >/dev/null
response=$(
gpg-connect-agent <<'EOF'
GET_PASSPHRASE mytest X X "Enter Password"
/bye
EOF
)
echo "Raw response:"
echo "$response"
# Your system appears to return:
# OK <hex-encoded-password>
hex=$(echo "$response" | awk '/^OK / {print $2}')
if [[ -z "$hex" ]]; then
echo "Cancelled or no password entered"
exit 1
fi
password=$(printf '%s' "$hex" | xxd -r -p)
echo "Password length: ${#password}"
# Use password here
# ...
unset password
No cached passwords
gpg-agent caches passphrases by cache ID
Option 1: Use a unique cache ID every time
CACHE_ID="myprompt-$$-$(date +%s)" gpg-connect-agent <<EOF GET_PASSPHRASE $CACHE_ID X X "Enter Password" /bye EOF
Option 2: Clear the cache after use
gpg-connect-agent <<EOF CLEAR_PASSPHRASE mytest /bye EOF
Option 3: Disable caching globally
~/.gnupg/gpg-agent.conf
default-cache-ttl 0 max-cache-ttl 0
or very short values:
default-cache-ttl 1 max-cache-ttl 1
gpgconf --kill gpg-agent
--
Current cache settings:
gpgconf --list-options gpg-agent | grep cache # or gpg-connect-agent 'GETINFO gpgconf_name' /bye
default-cache-ttl:24:0:expire cached PINs after N seconds:3:3:N:600:: default-cache-ttl-ssh:24:1:expire SSH keys after N seconds:3:3:N:1800:: max-cache-ttl:24:2:set maximum PIN cache lifetime to N seconds:3:3:N:7200:: max-cache-ttl-ssh:24:2:set maximum SSH key lifetime to N seconds:3:3:N:7200:: ignore-cache-for-signing:8:0:do not use the PIN cache when signing:0:0:::: no-allow-external-cache:8:0:disallow the use of an external password cache:0:0::::
-- test cache --
export GPG_TTY=$(tty) gpg-connect-agent updatestartuptty /bye >/dev/null # First prompt gpg-connect-agent <<EOF GET_PASSPHRASE mytest X X "Password" /bye EOF # Second prompt using same cache id gpg-connect-agent <<EOF GET_PASSPHRASE mytest X X "Password" /bye EOF
If cache is enabled it won't prompt for second.
--
sample-no-cache.sh
#!/bin/bash export GPG_TTY=$(tty) gpg-connect-agent updatestartuptty /bye >/dev/null CACHE_ID="prompt-$$-$(date +%s%N)" gpg-connect-agent <<EOF GET_PASSPHRASE $CACHE_ID X X "Password Required" /bye EOF
---
#!/bin/bash export GPG_TTY=$(tty) gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1 CACHE_ID="pw-$$-$(date +%s%N)" response=$( gpg-connect-agent <<EOF GET_PASSPHRASE $CACHE_ID X X "Enter Password" /bye EOF ) echo "$response"
example 11
#!/bin/bash
coproc PINENTRY { pinentry-curses; }
echo "SETTITLE Login" >&"${PINENTRY[1]}"
echo "SETPROMPT Password:" >&"${PINENTRY[1]}"
echo "GETPIN" >&"${PINENTRY[1]}"
while read -r line <&"${PINENTRY[0]}"; do
echo "$line"
if [[ "$line" =~ ^D\ (.*) ]]; then
password="${BASH_REMATCH[1]}"
break
fi
done
echo "Password: $password"
example 12
this one works
#!/bin/bash
export GPG_TTY=$(tty)
response=$(
gpg-connect-agent \
'GET_PASSPHRASE myprompt7 Password X Password\ Please' \
/bye 2>/dev/null
)
password=$(awk '/^D /{print substr($0,3)}' <<< "$response")
echo "Password length: ${#password}"
exmaple 13
#!/bin/bash
export GPG_TTY=$(tty)
gpg-connect-agent updatestartuptty /bye >/dev/null
response=$(
gpg-connect-agent \
'GET_PASSPHRASE mycache X X "Enter Password"' \
/bye
)
password=$(sed -n 's/^D //p' <<< "$response")
echo "Password entered."
echo "Length: ${#password}"
example 14
#!/bin/bash
coproc PIN { pinentry-curses; }
printf 'SETTITLE Login\n' >&"${PIN[1]}"
printf 'SETPROMPT Password:\n' >&"${PIN[1]}"
printf 'GETPIN\n' >&"${PIN[1]}"
while IFS= read -r line <&"${PIN[0]}"; do
case "$line" in
D\ *)
password="${line#D }"
break
;;
esac
done
echo "Password length: ${#password}"
example 15
This one worked!!
TTY=$(tty)
{
echo "OPTION ttyname=$TTY"
echo "SETTITLE Login"
echo "SETPROMPT Password:"
echo "GETPIN"
} | pinentry-curses
-- fix no lc_ctype --
pinentry-curses: no LC_CTYPE known - assuming UTF-8
{
echo "OPTION lc-ctype=en_US.UTF-8"
echo "BYE"
} | pinentry-curses
OPTION lc-ctype=en_US.UTF-8
TTY=$(tty)
{
echo "OPTION ttyname=$TTY"
echo "OPTION lc-ctype=$LC_CTYPE"
echo "SETTITLE Login"
echo "SETPROMPT Password:"
echo "GETPIN"
} | pinentry-curses
---
TTY=$(tty)
response=$(
{
echo "OPTION ttyname=$TTY"
echo "OPTION lc-ctype=$LC_CTYPE"
echo "SETTITLE Login"
echo "SETPROMPT Password:"
echo "GETPIN"
} | pinentry-curses
)
# Extract the entered PIN/password
pin=$(echo "$response" | awk '/^D / {print substr($0,3)}')
echo $pin
-- robust version --
#!/bin/bash
{
echo "OPTION ttyname=$(tty)"
echo "OPTION lc-ctype=${LC_CTYPE:-${LANG:-en_US.UTF-8}}"
echo "SETTITLE Login"
echo "SETPROMPT Password:"
echo "GETPIN"
} | pinentry-curses
For completeness should also include:
echo "OPTION lc-messages=${LC_MESSAGES:-$LANG}"