diff --git a/bin/pinentry-wsl-ps1.sh b/bin/pinentry-wsl-ps1.sh new file mode 100755 index 0000000..2f0e447 --- /dev/null +++ b/bin/pinentry-wsl-ps1.sh @@ -0,0 +1,470 @@ +#!/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 diff --git a/bin/wsl-win-path.sh b/bin/wsl-win-path.sh new file mode 100755 index 0000000..b586572 --- /dev/null +++ b/bin/wsl-win-path.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +cmd.exe /c 'echo '$1 2>/dev/null | awk '{sub("C:", "/mnt/c"); gsub("\\\\","/"); gsub("\r$", ""); print}' diff --git a/env.sh b/env.sh index f7e2b5c..de7ed05 100755 --- a/env.sh +++ b/env.sh @@ -8,6 +8,7 @@ XDG_CONFIG_HOME=${XDG_CONFIG_HOME-"$HOME/.config"} PREFIX=/usr/local PDIR=$(dirname "${DIR-$0}") GITHUB_PROXY=${GITHUB_PROXY-$HTTPS_PROXY} +WSL=$(grep -i Microsoft /proc/sys/kernel/osrelease) in_china() { ! [ -f /tmp/myip_full ] && curl -s myip.ipip.net > /tmp/myip_full @@ -94,6 +95,10 @@ pm_update() { echo "$CHECKSUM" > "$TSFILE" } +win_env_path() { + cmd.exe /c 'echo '$1 2>/dev/null | awk '{sub("C:", "/mnt/c"); gsub("\\\\","/"); print}' +} + sudo mkdir -p $PREFIX diff --git a/gui/pass.sh b/gui/pass.sh index 165655c..69e11b0 100755 --- a/gui/pass.sh +++ b/gui/pass.sh @@ -11,10 +11,18 @@ fi # install pass case "$PM" in apt) - sudo apt install -y pass pinentry-gtk2 + sudo apt install -y pass GNUPG=$HOME/.gnupg GPG_AGENT_CONF=$GNUPG/gpg-agent.conf - PINENTRY=$(command -v pinentry-gtk-2) + if [ -n "$WSL" ]; then + PINENTRY=$PDIR/bin/pinentry-wsl-ps1.sh + BROWSERPASS_NATIVE= "$(wsl-win-path.sh %USERPROFILE%)/browser-wsl.bat" + echo "@echo off\r\nbash -c 'browserpass'" \ + > "$BROWSERPASS_NATIVE" + else + sudo apt install -y pinentry-gtk2 + PINENTRY=$(command -v pinentry-gtk-2) + fi SETTING="pinentry-program $PINENTRY" mkdir -p "$GNUPG" if ! grep -Fq "$SETTING" "$GPG_AGENT_CONF"; then @@ -49,3 +57,8 @@ cd - # [PasswordStore - sync / ui](https://f-droid.org/packages/dev.msfjarvis.aps/) # configuration + +if [ -n "$WSL" ]; then + echo "Please update path in *-host.json file located at C:\Program Files\Browserpass to" + echo $BROWSERPASS_NATIVE +fi