|
#!/usr/bin/env bash |
|
|
|
# pinentry-wsl-ps1 |
|
# |
|
# (c) 2018 Dale Phurrough |
|
# Licensed under the Mozilla Public License 2.0 |
|
# |
|
# Allows GnuPG to prompt and read passphrases by the pinentry standard |
|
# with a GUI when running within WSL (Windows Subsystem for Linux). |
|
# Works for all keys managed by gpg-agent (GPG, SSH, etc). |
|
# This is a drop-in GUI alternative to pinentry-curses, pinentry-gtk-2, etc. |
|
# https://www.gnupg.org/software/pinentry/index.html |
|
# |
|
# Setup: |
|
# 1. Save this script and set its permissions to be executable |
|
# 2. Configure gpg-agent to use this script for pinentry using |
|
# one of the following methods: |
|
# a) Set pinentry-program within ~/.gnupg/gpg-agent.conf |
|
# pinentry-program /mnt/c/repos/pinentry-wsl-ps1/pinentry-wsl-ps1.sh |
|
# b) Set the path to this script when you launch gpg-agent |
|
# gpg-agent --pinentry-program /mnt/c/repos/pinentry-wsl-ps1/pinentry-wsl-ps1.sh |
|
# 3. Optionally enable persistence of passwords. |
|
# Requires https://github.com/davotronic5000/PowerShell_Credential_Manager |
|
# Please follow instructions there to install from the Gallery or GitHub. |
|
# Note security perspectives like https://security.stackexchange.com/questions/119765/how-secure-is-the-windows-credential-manager |
|
# Possible values for PERSISTENCE are: "", "Session", "LocalMachine", or "Enterprise" |
|
# 4. Optionally disable toast notification of password retrieval from Credential Manager. |
|
# By default, this code notifies you with a toast notification every time gpg-agent |
|
# retrieves a password from the Windows Credential Manager. Gpg-agent caches passwords |
|
# by default (see gpg-agent settings like max-cache-ttl) so you may not see the notification |
|
# with every usage. |
|
# * Disable: edit the script, near the top, set NOTIFY to the value "0" |
|
# * Enable: edit the script, near the top, set NOTIFY to the value "1" |
|
PERSISTENCE="" |
|
NOTIFY="1" |
|
DEBUGLOG="" |
|
|
|
# Do not casually edit the below values |
|
VERSION="0.2.1" |
|
TIMEOUT="0" |
|
DESCRIPTION="Enter password for GPG key" |
|
PROMPT="Password:" |
|
TITLE="GPG Key Credentials" |
|
CACHEPREFIX="gpgcache:" |
|
CACHEUSER="" |
|
KEYINFO="" |
|
OKBUTTON="&OK" |
|
CANCELBUTTON="&Cancel" |
|
NOTOKBUTTON="&Do not do this" |
|
PINERROR="" |
|
EXTPASSCACHE="0" |
|
REPEATPASSWORD="0" |
|
REPEATDESCRIPTION="Confirm password for GPG key" |
|
REPEATERROR="Error: Passwords did not match." |
|
GRABKEYBOARD="0" |
|
|
|
# convert Assuan protocol error into an ERR number, e.g. echo -n $(( (5 << 24) | $1 )) |
|
assuan_result() { |
|
case $1 in |
|
0) |
|
echo -n "ERR 0 no error" |
|
;; |
|
62) |
|
echo -n "ERR 83886142 timeout" |
|
;; |
|
99) |
|
echo -n "ERR 83886179 cancelled" |
|
;; |
|
114) |
|
echo -n "ERR 83886194 not confirmed" |
|
;; |
|
174) |
|
echo -n "ERR 83886254 invalid option" |
|
;; |
|
257) |
|
echo -n "ERR 83886337 general error" |
|
;; |
|
261) |
|
echo -n "ERR 83886341 invalid value" |
|
;; |
|
275) |
|
echo -n "ERR 83886355 unknown command" |
|
;; |
|
esac |
|
} |
|
|
|
# GUI dialogs for passwords; text is dynamically set by gpg-agent via protocol |
|
getpassword() { |
|
if [ -n "$CACHEUSER" ]; then |
|
local creduser="$CACHEUSER" |
|
else |
|
if [ -n "$KEYINFO" ]; then |
|
local creduser="$KEYINFO" |
|
else |
|
local creduser="--not yet defined--" |
|
fi |
|
fi |
|
local cmd_prompt=$(cat <<-DLM |
|
\$cred = \$Host.ui.PromptForCredential("$TITLE", |
|
"$PINERROR$DESCRIPTION", |
|
"$creduser", |
|
"", |
|
"Generic", |
|
"None,ReadOnlyUserName") |
|
if (\$cred) { |
|
Write-Output \$cred.GetNetworkCredential().Password |
|
} |
|
DLM |
|
) |
|
local cmd_repeat=$(cat <<-DLM |
|
\$cred = \$Host.ui.PromptForCredential("$TITLE", |
|
"$REPEATDESCRIPTION", |
|
"$creduser", |
|
"", |
|
"Generic", |
|
"None,ReadOnlyUserName") |
|
if (\$cred) { |
|
Write-Output \$cred.GetNetworkCredential().Password |
|
} |
|
DLM |
|
) |
|
local cmd_lookup=$(cat <<-DLM |
|
\$cred = Get-StoredCredential -Target "$CACHEPREFIX$KEYINFO" -Type GENERIC |
|
if (\$cred) { |
|
Write-Output \$cred.GetNetworkCredential().Password |
|
} |
|
DLM |
|
) |
|
local cmd_store=$(cat <<-DLM |
|
\$pw = \$Input | Select-Object -First 1 |
|
\$securepw = ConvertTo-SecureString \$pw -AsPlainText -Force |
|
New-StoredCredential -Target "$CACHEPREFIX$KEYINFO" -Type GENERIC -UserName "$creduser" -SecurePassword \$securepw -Persist $PERSISTENCE | |
|
out-null |
|
DLM |
|
) |
|
# idea from http://thewindowscollege.com/display-toast-notifications-windows-10.html |
|
# alt1: https://gist.github.com/loge5/7ec41e2e2f0e0293fdcc5155499e9072 |
|
# alt2: https://gist.github.com/Windos/9aa6a684ac583e0d38a8fa68196bc2dc |
|
local cmd_toast=$(cat <<-DLM |
|
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") |
|
[reflection.assembly]::loadwithpartialname("System.Drawing") |
|
\$notify = new-object system.windows.forms.notifyicon |
|
\$notify.icon = [System.Drawing.SystemIcons]::Information |
|
\$notify.visible = \$true |
|
\$notify.showballoontip(10, "GPG pinentry-wsl-ps1", "GPG password retrieved from Windows Credential Manager", [system.windows.forms.tooltipicon]::Info) |
|
DLM |
|
) |
|
local credpassword |
|
local credpasswordrepeat |
|
local passwordfromcache=0 |
|
if [ -z "$PINERROR" ]; then |
|
if [ "$REPEATPASSWORD" == "0" ]; then |
|
if [ "$EXTPASSCACHE" == "1" ]; then |
|
if [ -n "$KEYINFO" ]; then |
|
credpassword="$(powershell.exe -nologo -noprofile -noninteractive -command "$cmd_lookup")" |
|
if [ -n "$credpassword" ]; then |
|
echo -e "S PASSWORD_FROM_CACHE\nD $credpassword\nOK" |
|
if [ "$NOTIFY" == "1" ]; then |
|
powershell.exe -nologo -noprofile -noninteractive -command "$cmd_toast" > /dev/null |
|
fi |
|
return |
|
fi |
|
fi |
|
fi |
|
fi |
|
fi |
|
PINERROR="" |
|
credpassword="$(powershell.exe -nologo -noprofile -noninteractive -command "$cmd_prompt")" |
|
if [ -n "$credpassword" ]; then |
|
if [ "$REPEATPASSWORD" == "1" ]; then |
|
credpasswordrepeat="$(powershell.exe -nologo -noprofile -noninteractive -command "$cmd_repeat")" |
|
if [ "$credpassword" == "$credpasswordrepeat" ]; then |
|
echo -e "S PIN_REPEATED\nD $credpassword\nOK" |
|
else |
|
message "$REPEATERROR" > /dev/null |
|
echo "$(assuan_result 114)" # unsure this is the correct error |
|
return |
|
fi |
|
else |
|
echo -e "D $credpassword\nOK" |
|
fi |
|
if [ "$EXTPASSCACHE" == "1" ]; then |
|
if [ -n "$KEYINFO" ]; then |
|
# avoid setting password on visible param |
|
# alt is to always save on the single or last-of-repeat dialog. And if the repeat fails, then immediately delete it from the cred store |
|
builtin echo -n "$credpassword" | powershell.exe -nologo -noprofile -noninteractive -command "$cmd_store" |
|
fi |
|
fi |
|
else |
|
echo "$(assuan_result 99)" |
|
fi |
|
} |
|
|
|
# remove password from persistent store |
|
removepassword() { |
|
if [ -z "$1" ]; then |
|
echo "$(assuan_result 261)" |
|
return |
|
fi |
|
local cmd_remove=$(cat <<-DLM |
|
try { |
|
Remove-StoredCredential -Target "$CACHEPREFIX$1" -Type GENERIC -ErrorAction Stop |
|
} |
|
catch { |
|
Write-Output "$(assuan_result 261)" |
|
return |
|
} |
|
Write-Output "OK" |
|
DLM |
|
) |
|
if [ "$EXTPASSCACHE" == "1" ]; then |
|
echo "$(powershell.exe -nologo -noprofile -noninteractive -command "$cmd_remove")" |
|
else |
|
echo "OK" |
|
fi |
|
} |
|
|
|
# GUI dialog box with simple message and one OK button |
|
message() { |
|
local desc |
|
if [ -n "$1" ]; then |
|
desc="$1" |
|
else |
|
desc="$DESCRIPTION" |
|
fi |
|
local cmd=$(cat <<-DLM |
|
\$wshShell = New-Object -ComObject WScript.Shell |
|
\$options = 0x0 + 0x40 + 0x2000 + 0x10000 # 1 + 16 + 8192 + 65536 |
|
\$result = \$wshShell.Popup("$desc", $TIMEOUT, "$TITLE", \$options) |
|
[System.Runtime.Interopservices.Marshal]::ReleaseComObject(\$wshShell) | Out-Null |
|
DLM |
|
) |
|
local result="$(powershell.exe -nologo -noprofile -noninteractive -command "$cmd")" #> /dev/null |
|
echo "OK" |
|
} |
|
|
|
# GUI dialog box with test and two buttons: OK, Cancel |
|
confirm() { |
|
PINERROR="" |
|
if [ "$1" == "--one-button" ]; then |
|
message |
|
return |
|
fi |
|
local cmd=$(cat <<-DLM |
|
\$wshShell = New-Object -ComObject WScript.Shell |
|
\$options = 0x1 + 0x30 + 0x2000 + 0x10000 # 1 + 16 + 8192 + 65536 |
|
\$result = \$wshShell.Popup("$DESCRIPTION", $TIMEOUT, "$TITLE", \$options) |
|
[System.Runtime.Interopservices.Marshal]::ReleaseComObject(\$wshShell) | Out-Null |
|
if (\$result) { |
|
switch(\$result) { |
|
1 { Write-Output "OK" } |
|
2 { Write-Output "$(assuan_result 99)" } |
|
default { Write-Output "$(assuan_result 114)" } |
|
} |
|
} |
|
else { |
|
Write-Output "$(assuan_result 114)" |
|
} |
|
DLM |
|
) |
|
local result="$(powershell.exe -nologo -noprofile -noninteractive -command "$cmd")" |
|
echo "$result" |
|
} |
|
|
|
# set a timeout value in seconds after which prompts/dialogs are automatically cancelled |
|
# limited functionality in current codebase |
|
# potential improvements at https://stackoverflow.com/questions/21176487/adding-a-timeout-to-batch-powershell |
|
settimeout() { |
|
TIMEOUT="$1" |
|
echo "OK" |
|
} |
|
|
|
# helper function for decoding strings from gpg-agent into Windows-compatible format |
|
decodegpgagentstr() { |
|
local decode="${1//%0A/%0D%0A}" # convert hex LF into hex Windows CRLF |
|
decode="${decode//%/\\x}" # convert hex encoding style |
|
decode="$(echo -en "$decode")" # decode hex |
|
echo -n "${decode//\"/\`\"}" # escape double quotes for powershell |
|
} |
|
|
|
# commonly used to set main text in GUI dialog boxes |
|
# also parses for key ids to display in GUI prompts |
|
setdescription() { |
|
DESCRIPTION="$(decodegpgagentstr "$1")" |
|
local searchfor='ID ([[:xdigit:]]{16})' # hack to search for first gpg key id in description |
|
if [[ "$1" =~ $searchfor ]]; then |
|
CACHEUSER="${BASH_REMATCH[1]}" |
|
fi |
|
local searchfor='(([[:xdigit:]][[:xdigit:]]:){15}[[:xdigit:]][[:xdigit:]])' # hack to search for ssh fingerprint in description |
|
if [[ "$1" =~ $searchfor ]]; then |
|
CACHEUSER="${BASH_REMATCH[1]}" |
|
fi |
|
echo "OK" |
|
} |
|
|
|
setprompt() { |
|
PROMPT="$1" |
|
echo "OK" |
|
} |
|
|
|
settitle() { |
|
TITLE="$1" |
|
echo "OK" |
|
} |
|
|
|
setpinerror() { |
|
PINERROR="$(decodegpgagentstr "** $1 **")"$'\r'$'\n' # decode and add CRLF to separate line |
|
echo "OK" |
|
} |
|
|
|
setkeyinfo() { |
|
if [ "$1" == "--clear" ]; then |
|
KEYINFO="" |
|
else |
|
KEYINFO="$1" |
|
fi |
|
echo "OK" |
|
} |
|
|
|
setrepeatpassword() { |
|
REPEATPASSWORD="1" |
|
REPEATDESCRIPTION="$(decodegpgagentstr "$1")" |
|
echo "OK" |
|
} |
|
|
|
setrepeaterror () { |
|
REPEATERROR="$(decodegpgagentstr "$1")" |
|
echo "OK" |
|
} |
|
|
|
setokbutton() { |
|
OKBUTTON="${1//_/&}" |
|
echo "OK" |
|
} |
|
|
|
setcancelbutton() { |
|
CANCELBUTTON="${1//_/&}" |
|
echo "OK" |
|
} |
|
|
|
setnotokbutton() { |
|
NOTOKBUTTON="${1//_/&}" |
|
echo "OK" |
|
} |
|
|
|
getinfo() { |
|
if [ "$1" == "version" ]; then |
|
echo -e "D $VERSION\nOK" |
|
elif [ "$1" == "pid" ]; then |
|
echo -e "D $BASHPID\nOK" |
|
else |
|
echo "$(assuan_result 275)" |
|
fi |
|
} |
|
|
|
# often called by gpg-agent to set default values |
|
setoption() { |
|
local key="$(echo "$1" | cut -d'=' -f1)" |
|
local value="$(echo "$1" | cut -d'=' -s -f2-)" |
|
case $key in |
|
allow-external-password-cache) |
|
if [ -n "$PERSISTENCE" ]; then |
|
EXTPASSCACHE=1 |
|
fi |
|
echo "OK" |
|
;; |
|
default-ok) |
|
setokbutton "$value" |
|
;; |
|
default-cancel) |
|
setcancelbutton "$value" |
|
;; |
|
default-notok) |
|
setnotokbutton "$value" |
|
;; |
|
default-prompt) |
|
setprompt "$value" |
|
;; |
|
grab) |
|
GRABKEYBOARD="1" |
|
echo "OK" |
|
;; |
|
no-grab) |
|
GRABKEYBOARD="0" |
|
echo "OK" |
|
;; |
|
*) |
|
echo "OK" |
|
;; |
|
esac |
|
} |
|
|
|
# check that we are running within WSL |
|
if ! cat /proc/sys/kernel/osrelease | grep -q -i Microsoft; then |
|
echo "$(assuan_result 257)" |
|
exit 1 |
|
fi |
|
|
|
# main loop to read stdin and respond |
|
echo "OK Your orders please" |
|
while IFS= read -r line; do |
|
if [ -n "$DEBUGLOG" ]; then |
|
echo "$line" >> "$DEBUGLOG" |
|
fi |
|
action="$(echo $line | cut -d' ' -f1)" |
|
args="$(echo $line | cut -d' ' -s -f2-)" |
|
case $action in |
|
BYE) |
|
echo "OK closing connection" |
|
exit 0 |
|
;; |
|
GETPIN) |
|
getpassword |
|
;; |
|
SETTIMEOUT) |
|
settimeout "$args" |
|
;; |
|
SETDESC) |
|
setdescription "$args" |
|
;; |
|
SETPROMPT) |
|
setprompt "$args" |
|
;; |
|
SETTITLE) |
|
settitle "$args" |
|
;; |
|
SETKEYINFO) |
|
setkeyinfo "$args" |
|
;; |
|
SETOK) |
|
setokbutton "$args" |
|
;; |
|
SETCANCEL) |
|
setcancelbutton "$args" |
|
;; |
|
SETNOTOK) |
|
setnotokbutton "$args" |
|
;; |
|
CONFIRM) |
|
confirm "$args" |
|
;; |
|
MESSAGE) |
|
message "$args" |
|
;; |
|
SETERROR) |
|
setpinerror "$args" |
|
;; |
|
GETINFO) |
|
getinfo "$args" |
|
;; |
|
OPTION) |
|
setoption "$args" |
|
;; |
|
SETREPEAT) |
|
setrepeatpassword "$args" |
|
;; |
|
SETREPEATERROR) |
|
setrepeaterror "$args" |
|
;; |
|
CLEARPASSPHRASE) |
|
removepassword "$args" |
|
;; |
|
RESET) |
|
echo "OK" |
|
;; |
|
*) |
|
echo "OK" |
|
;; |
|
esac |
|
done |
take a look at: https://vidu.sh/an/blog/browserpass-with-wsl