Skip to content

Commit

Permalink
init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
adsr committed Dec 9, 2017
0 parents commit 951c274
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Makefile
@@ -0,0 +1,8 @@
DESTDIR?=/usr/local/bin

all:

install: pwdrive.sh
install -v -m 755 pwdrive.sh $(DESTDIR)/pwdrive

.PHONY: all install
50 changes: 50 additions & 0 deletions README.md
@@ -0,0 +1,50 @@
# pwdrive

pwdrive is a GnuPG and Google Drive-based password vault written in Bash.
Passwords are stored as base64-encoded GnuPG-encrypted files on Google Drive.

### Requirements

In addition to Bash, the following programs need to be in `PATH`:

gpg curl grep mktemp mkdir cat base64

You also need a working GPG setup:

https://www.gnupg.org/gph/en/manual/c14.html

### Installing

To install to `/usr/local/bin`:

# make install

To install to a custom directory, supply `DESTDIR`, e.g.:

# DESTDIR=/usr/bin make install

### Usage

Usage:
pwdrive <command> [argv]

Commands:
ls List all entries
ls <str> List all entries containing str
set <entry> <pass> Set password for entry
set <entry> - Set password for entry from stdin
get <entry> Get password for entry
rm <entry> Remove entry
token Print an access token
help Show pwdrive usage

Environment:
PWDRIVE_ACCESS_TOKEN Use this access token instead of fetching one
PWDRIVE_HOME Home dir of pwdrive (~/.pwdrive)
PWDRIVE_GPG_ARGS Extra args for get/set (--no-options --default-recipient-self --quiet)

### Tip

In order to minimize dependencies, `grep -P` is used to extract JSON fields
from the Google Drive API. Naturally this is not ideal. If you stick to
ascii-only for `entry` params, things should work.
213 changes: 213 additions & 0 deletions pwdrive.sh
@@ -0,0 +1,213 @@
#!/bin/bash
set -o pipefail

pwdrive_desc='GnuPG+GDrive-based password vault'
pwdrive_url='https://github.com/adsr/pwdrive'
pwdrive_version='0.2'
pwdrive_cmd=$1
shift

pwdrive_main() {
_set_globals
_check_reqs
_maybe_init
case "$pwdrive_cmd" in
ls) _require_access_token && pwdrive_ls "$@";;
set) _require_access_token && pwdrive_set "$@";;
get) _require_access_token && pwdrive_get "$@";;
rm) _require_access_token && pwdrive_rm "$@";;
token) _require_access_token && pwdrive_token "$@";;
help) pwdrive_usage 0;;
*) pwdrive_usage 1;;
esac
}

pwdrive_usage() {
_print_header
echo "Usage:"
echo " pwdrive <command> [argv]"
echo
echo "Commands:"
echo " ls List all entries"
echo " ls <str> List all entries containing str"
echo " set <entry> <pass> Set password for entry"
echo " set <entry> - Set password for entry from stdin"
echo " get <entry> Get password for entry"
echo " rm <entry> Remove entry"
echo " token Print an access token"
echo " help Show pwdrive usage"
echo
echo "Environment:"
echo " PWDRIVE_ACCESS_TOKEN Use this access token instead of fetching one"
echo " PWDRIVE_HOME Home dir of pwdrive ($home_dir)"
echo " PWDRIVE_GPG_ARGS Extra args for get/set (${gpg_args:-<none>})"
exit $1
}

pwdrive_ls() {
query=''
[ -n "$1" ] && query="q=name contains '$1'"
response=$(curl -sf 'https://www.googleapis.com/drive/v3/files' \
-G -X GET \
-H "Authorization: Bearer $access_token" \
--data-urlencode "$query")
[ "$?" -eq 0 ] || _die "Query failed: www.googleapis.com/drive/v3/files (pwdrive_ls)"
echo $response | grep -Po '(?<="name": ").+?(?=")'
}

pwdrive_get() {
[ -n "$1" ] || _die "Expected entry param (pwdrive_get)"
_fetch_file_id_by_name "$1"
[ -n "$file_id" ] || _die "Nothing found for '$1' (pwdrive_get)"
response=$(curl -sf "https://www.googleapis.com/drive/v3/files/$file_id" \
-G -X GET \
-H "Authorization: Bearer $access_token" \
-d "alt=media")
[ "$?" -eq 0 ] || _die "Query failed: www.googleapis.com/drive/v3/files/$file_id (pwdrive_get)"
echo $response | base64 -w0 -d | gpg $gpg_args --decrypt
echo
}

pwdrive_set() {
name="$1"
[ -n "$name" ] || _die "Expected entry param (pwdrive_set)"
[ "$stdin_is_pipe" -eq 1 ] && read pass
[ -n "$pass" ] || pass="$2"
[ -n "$pass" ] || _die "Expected pass as param or stdin (pwdrive_set)"
_fetch_file_id_by_name "$name"
if [ -n "$file_id" ]; then
method='PATCH'
uri="/$file_id"
else
method='POST'
uri=''
fi
post_data=$(
mktemp /tmp/pwdrive.XXXXXX)
echo -en "--$boundary\r\n" >$post_data
echo -en "Content-Type: application/json; charset=UTF-8\r\n\r\n" >>$post_data
echo -en "{\"name\":\"$name\"}\r\n" >>$post_data
echo -en "--$boundary\r\n" >>$post_data
echo -en "Content-Type: application/octet-stream\r\n\r\n" >>$post_data
echo -n "$pass" | gpg $gpg_args --encrypt | base64 -w0 >>$post_data
echo -en "\r\n--$boundary--" >>$post_data
curl -sf "https://www.googleapis.com/upload/drive/v3/files$uri?uploadType=multipart" \
-X $method \
-H "Content-Type: multipart/related; boundary=$boundary" \
-H "Authorization: Bearer $access_token" \
--data-binary "@$post_data" >/dev/null
curlec="$?"
rm -f $post_data
[ "$curlec" -eq 0 ] || _die "Query failed: www.googleapis.com/upload/drive/v3/files (pwdrive_set)"
}

pwdrive_rm() {
_fetch_file_id_by_name "$1"
[ -n "$file_id" ] || _die "Nothing found for '$1' (pwdrive_rm)"
curl -sf "https://www.googleapis.com/drive/v3/files/$file_id" \
-X DELETE \
-H "Authorization: Bearer $access_token"
[ "$?" -eq 0 ] || _die "Query failed: www.googleapis.com/drive/v3/files/$file_id (pwdrive_rm)"
}

pwdrive_token() {
echo $access_token
}

_die() {
echo "$@" >&2
exit 1
}

_set_globals() {
home_dir="${PWDRIVE_HOME:-$HOME/.pwdrive}"
client_id='118479055818-svi4vafeo8g5ka4dbauopo1o4s9j7qra.apps.googleusercontent.com'
# TODO Figure out if Google offers a way to do OAuth without a secret
client_secret='86Ev0arsPbBlp6J5v9IZU4Rq'
scope='https://www.googleapis.com/auth/drive.appdata+https://www.googleapis.com/auth/drive.file'
redirect_uri='urn:ietf:wg:oauth:2.0:oob'
refresh_token_path="$home_dir/refresh_token"
boundary='925a89b43f3caff507db0a86d20a2428007f10b6'
gpg_args="${PWDRIVE_GPG_ARGS:---no-options --default-recipient-self --quiet}"
stdin_is_pipe=0; [ -t 0 ] || stdin_is_pipe=1
stdout_is_pipe=0; [ -t 1 ] || stdout_is_pipe=1
}

_check_reqs() {
for req in gpg curl grep mktemp mkdir cat base64; do
which $req &>/dev/null || _die "Expected $req in PATH (_check_reqs)"
done
}

_print_header() {
echo -en '\xe2\x94\x8f'
printf '\xe2\x94\x81%.0s' {1..40}
echo -e '\xe2\x94\x93'
printf '\xe2\x94\x83 %-38s \xe2\x94\x83\n' "pwdrive v${pwdrive_version}"
printf '\xe2\x94\x83 %-38s \xe2\x94\x83\n' ""
printf '\xe2\x94\x83 %-38s \xe2\x94\x83\n' "$pwdrive_desc"
printf '\xe2\x94\x83 %-38s \xe2\x94\x83\n' "$pwdrive_url"
echo -en '\xe2\x94\x97'
printf '\xe2\x94\x81%.0s' {1..40}
echo -e '\xe2\x94\x9b\n'
}

_maybe_init() {
[ -d "$home_dir" ] || mkdir -p $home_dir
}

_require_access_token() {
if [ -n "$PWDRIVE_ACCESS_TOKEN" ]; then
access_token="$PWDRIVE_ACCESS_TOKEN"
return
elif [ -s "$refresh_token_path" ]; then
refresh_token=$(cat $refresh_token_path)
else
_fetch_refresh_token
[ -n "$refresh_token" ] || _die "Failed to get refresh token (_require_access_token)"
echo -n "$refresh_token" >$refresh_token_path
fi
_fetch_access_token
[ -n "$access_token" ] || _die "Failed to get access token. Run 'rm -f $refresh_token_path' and try again. (_require_access_token)"
}

_fetch_refresh_token() {
local auth_url="https://accounts.google.com/o/oauth2/auth?client_id=$client_id&scope=$scope&redirect_uri=$redirect_uri&response_type=code"
echo 'Visit the following auth URL:'
echo
echo " $auth_url"
echo
echo 'Then paste in the auth code and press ENTER:'
read code
echo
[ -n "$code" ] || _die "Empty auth code (_fetch_refresh_token)"
response=$(curl -sf 'https://accounts.google.com/o/oauth2/token' \
-d "client_id=$client_id" \
-d "client_secret=$client_secret" \
-d "redirect_uri=$redirect_uri" \
-d 'grant_type=authorization_code' \
-d "code=$code")
[ "$?" -eq 0 ] || _die "Query failed: accounts.google.com/o/oauth2/token (_fetch_refresh_token)"
refresh_token=$(echo $response | grep -Po '(?<="refresh_token")\s*:\s*".+?(?=")' | grep -Po '(?<=").+')
}

_fetch_access_token() {
response=$(curl -s 'https://accounts.google.com/o/oauth2/token' \
-d "client_id=$client_id" \
-d "client_secret=$client_secret" \
-d 'grant_type=refresh_token' \
-d "refresh_token=$refresh_token")
[ "$?" -eq 0 ] || _die "Query failed: accounts.google.com/o/oauth2/token (_fetch_access_token)"
access_token=$(echo $response | grep -Po '(?<="access_token")\s*:\s*".+?(?=")' | grep -Po '(?<=").+')
}

_fetch_file_id_by_name() {
response=$(curl -sf 'https://www.googleapis.com/drive/v3/files' \
-G -X GET \
-H "Authorization: Bearer $access_token" \
--data-urlencode "q=name = '$1'")
[ "$?" -eq 0 ] || _die "Query failed: www.googleapis.com/drive/v3/files (_fetch_file_id_by_name)"
file_id=$(echo $response | grep -Po '(?<="id": ").+?(?=")')
}

pwdrive_main "$@"

0 comments on commit 951c274

Please sign in to comment.