aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md41
-rw-r--r--pinentry-wsl-ps1.sh433
3 files changed, 474 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
index 940794e..42fa0b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ bld/
# Visual Studio 2015 cache/options directory
.vs/
+.vscode/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
diff --git a/README.md b/README.md
index e7ea4be..9421256 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,41 @@
# pinentry-wsl-ps1
-pinentry GUI for Windows WSL (useful for GPG)
+
+GUI for GPG within Windows WSL for passwords, pins, etc.
+Optional persistence of passwords into Windows Credential Manager
+
+(c) 2018 Dale Phurrough
+Licensed under the Mozilla Public License 2.0
+
+## Features
+
+* Allows GnuPG to prompt and read passphrases by the pinentry protocol
+with a GUI when running within WSL (Windows Subsystem for Linux)
+* Works for all keys managed by gpg-agent (GPG, SSH, etc)
+* Drop-in replacement GUI to pinentry-curses, pinentry-gtk-2, etc.
+
+## Setup
+
+1. Save the `pinentry-wsl-ps1.sh` script and set its permissions to be executable
+2. Configure gpg-agent to use this script for pinentry using
+ one of the following methods
+ * Set pinentry-program within ~/.gnupg/gpg-agent.conf to the script's path, e.g.
+ `pinentry-program /mnt/c/repos/pinentry-wsl-ps1/pinentry-wsl-ps1.sh`
+ * ... or, set the path to this script when you launch gpg-agent, e.g.
+ `gpg-agent --pinentry-program /mnt/c/repos/pinentry-wsl-ps1/pinentry-wsl-ps1.sh`
+3. Optionally enable persistence of passwords.
+ 1. Follow instructions https://github.com/davotronic5000/PowerShell_Credential_Manager
+ to install the needed module from the Powershell Gallery or GitHub.
+ 2. Note security perspectives like https://security.stackexchange.com/questions/119765/how-secure-is-the-windows-credential-manager
+ 3. Edit the script and set `PERSISTENCE` to one of the values:
+ * `""` no persistence
+ * `"Session"` persists the password only for the current Windows login session
+ * `"LocalMachine"` persists the password for the current Windows login on the local Windows computer
+ * `"Enterprise"` persists the password for the current Windows login and requests Windows Credential Manager to synchronize it across Windows computers for that same Windows login
+
+## References
+
+* https://www.gnupg.org/software/pinentry/index.html
+* https://www.gnupg.org/documentation/manuals/gnupg/Agent-Options.html
+* https://github.com/GPGTools/pinentry/blob/master/doc/pinentry.texi
+* https://gist.github.com/mdeguzis/05d1f284f931223624834788da045c65
+* https://github.com/GPGTools/pinentry/blob/master/pinentry/pinentry.c
diff --git a/pinentry-wsl-ps1.sh b/pinentry-wsl-ps1.sh
new file mode 100644
index 0000000..0a7ab00
--- /dev/null
+++ b/pinentry-wsl-ps1.sh
@@ -0,0 +1,433 @@
+#!/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"
+PERSISTENCE=""
+
+# Do not casually edit the below values
+VERSION="0.1.0"
+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."
+
+# 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
+ local creduser="$KEYINFO"
+ 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
+ )
+ 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"
+ 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]}"
+ echo "OK"
+ return
+ fi
+ local searchfor='(([[:xdigit:]][[:xdigit:]]:){15}[[:xdigit:]][[:xdigit:]])' # hack to search for ssh fingerprint in description
+ if [[ "$1" =~ $searchfor ]]; then
+ CACHEUSER="${BASH_REMATCH[1]}"
+ echo "OK"
+ return
+ fi
+}
+
+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"
+ ;;
+ *)
+ 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
+ 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