feat: pass for WSL

This commit is contained in:
Klesh Wong 2021-08-03 11:51:50 +08:00
parent e9e5ea9d67
commit 8ae722f037
4 changed files with 493 additions and 2 deletions

470
bin/pinentry-wsl-ps1.sh Executable file
View File

@ -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

3
bin/wsl-win-path.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
cmd.exe /c 'echo '$1 2>/dev/null | awk '{sub("C:", "/mnt/c"); gsub("\\\\","/"); gsub("\r$", ""); print}'

5
env.sh
View File

@ -8,6 +8,7 @@ XDG_CONFIG_HOME=${XDG_CONFIG_HOME-"$HOME/.config"}
PREFIX=/usr/local PREFIX=/usr/local
PDIR=$(dirname "${DIR-$0}") PDIR=$(dirname "${DIR-$0}")
GITHUB_PROXY=${GITHUB_PROXY-$HTTPS_PROXY} GITHUB_PROXY=${GITHUB_PROXY-$HTTPS_PROXY}
WSL=$(grep -i Microsoft /proc/sys/kernel/osrelease)
in_china() { in_china() {
! [ -f /tmp/myip_full ] && curl -s myip.ipip.net > /tmp/myip_full ! [ -f /tmp/myip_full ] && curl -s myip.ipip.net > /tmp/myip_full
@ -94,6 +95,10 @@ pm_update() {
echo "$CHECKSUM" > "$TSFILE" 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 sudo mkdir -p $PREFIX

View File

@ -11,10 +11,18 @@ fi
# install pass # install pass
case "$PM" in case "$PM" in
apt) apt)
sudo apt install -y pass pinentry-gtk2 sudo apt install -y pass
GNUPG=$HOME/.gnupg GNUPG=$HOME/.gnupg
GPG_AGENT_CONF=$GNUPG/gpg-agent.conf GPG_AGENT_CONF=$GNUPG/gpg-agent.conf
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) PINENTRY=$(command -v pinentry-gtk-2)
fi
SETTING="pinentry-program $PINENTRY" SETTING="pinentry-program $PINENTRY"
mkdir -p "$GNUPG" mkdir -p "$GNUPG"
if ! grep -Fq "$SETTING" "$GPG_AGENT_CONF"; then if ! grep -Fq "$SETTING" "$GPG_AGENT_CONF"; then
@ -49,3 +57,8 @@ cd -
# [PasswordStore - sync / ui](https://f-droid.org/packages/dev.msfjarvis.aps/) # [PasswordStore - sync / ui](https://f-droid.org/packages/dev.msfjarvis.aps/)
# configuration # configuration
if [ -n "$WSL" ]; then
echo "Please update path in *-host.json file located at C:\Program Files\Browserpass to"
echo $BROWSERPASS_NATIVE
fi