Last active
April 27, 2020 06:47
-
-
Save tvhung83/9ff0d117460ecde8ead360a32e73b078 to your computer and use it in GitHub Desktop.
Revised version of https://github.com/dcreemer/1pass which supports of template 102
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env bash | |
# | |
# 1pass -- a simple caching wrapper for the "op" 1Password CLI. | |
# | |
# Copyright (C) 2017 David Creemer, (twitter: @dcreemer) | |
# | |
# This program is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this program. If not, see <http://www.gnu.org/licenses/>. | |
# | |
set -e | |
set -o pipefail | |
VERSION="1.1.1" | |
if [ "$XDG_CONFIG_HOME" != "" ] && [ ! -d "${HOME}/.1pass" ]; then | |
op_dir="${XDG_CONFIG_HOME}/1pass" | |
else | |
op_dir=${HOME}/.1pass | |
fi | |
if [ "$XDG_CACHE_HOME" != "" ] && [ ! -d "${op_dir}/cache" ]; then | |
cache_dir="${XDG_CACHE_HOME}/1pass" | |
else | |
cache_dir=${op_dir}/cache | |
fi | |
# check for bare -V/version request first: | |
if [ $# -eq 1 ] && [ "$1" == "-V" ]; then | |
echo "${VERSION}" | |
exit 0 | |
fi | |
# test setup: | |
if [ ! -d "$op_dir" ] || [ ! -r "${op_dir}/config" ]; then | |
mkdir -p "$cache_dir" | |
cat > "${op_dir}/config" <<CONFIG | |
# configuration file for 1pass | |
# set to the ID of your GPG key | |
self_key="" | |
# set to the email address associated with your 1Password account | |
email="" | |
# set to your 1password domain (e.g. example.1password.com) | |
domain="" | |
CONFIG | |
chmod go-rw "${op_dir}/config" | |
echo "please config 1pass by editing ${op_dir}/config" | |
exit 1 | |
fi | |
if [ ! -d "$cache_dir" ]; then | |
mkdir -p "$cache_dir" | |
fi | |
# these are read from the config file: | |
email="" | |
self_key="" | |
domain="" | |
# old setting, still supported | |
subdomain="" | |
# shellcheck source=config.sample | |
source "${op_dir}/config" | |
master=${op_dir}/_master.gpg | |
secret=${op_dir}/_secret.gpg | |
# check settings: | |
if [ "$email" == "" ]; then | |
echo "please configure your 1Password email address in ${op_dir}/config" | |
exit 1 | |
fi | |
if [ "$self_key" == "" ]; then | |
echo "please configure your GPG key in ${op_dir}/config" | |
exit 1 | |
fi | |
if [ "$subdomain" == "" ] && [ "$domain" == "" ]; then | |
echo "please configure your 1Password domain in ${op_dir}/config, e.g. example.1password.com" | |
exit 1 | |
fi | |
domain=${domain:-${subdomain}.1password.com} | |
if [ ! -r "${master}" ]; then | |
echo "please put your master password into ${master}" | |
echo "ex: echo \"master-password\" | gpg -er $email > ${master}" | |
exit 1 | |
fi | |
if [ ! -r "${secret}" ]; then | |
echo "please put your ${domain} secret key into ${secret}" | |
echo "ex: echo \"A3-XXXXXX-XXXXXX-XXXXX-XXXXX-XXXXX-XXXXX\" | gpg -er $email > ${secret}" | |
exit 1 | |
fi | |
index=${cache_dir}/_index.gpg | |
session=${cache_dir}/_session.gpg | |
token="" | |
get_result="" | |
OPTIND=1 | |
refresh=0 | |
verbose=0 | |
print_output=0 | |
clip_time=30 | |
OP_SESSION_NAME=$(echo "$domain" | cut -f1 -d'.' | tr '-' '_') | |
usage() | |
{ | |
cat <<USAGE | |
usage: 1pass [-fhprv] [<Item> [<username|password|totp>]] | |
-f Forget GPG key from gpg-agent, and remove local session | |
-h Help | |
-p Print the 1pass output to stdout, rather than copying to the clipboard | |
-r Refresh all appropriate data from 1password.com, ignoring local cache | |
-v Verbose output | |
-V Print 1pass version and exit | |
With no arguments, prints a list of all Logins and Passwords in all 1Password vaults. | |
With a single argument, fetches the Item (Login, Password, or TOTP) matching the | |
given name, and copies the resulting password to the clipboard. | |
With two arguments, fetches the specified field (e.g.) "username" from the named | |
item, and copies the results to the clipboard. | |
USAGE | |
} | |
sanity_check() | |
{ | |
for cmd in "op" "jq" "gpg" "expect" | |
do | |
if [ $verbose -eq 1 ]; then | |
echo "checking for $cmd" | |
fi | |
if ! command -v "$cmd" > /dev/null; then | |
echo "Cannot find the '$cmd' command. Please make sure it is installed" | |
exit 1 | |
fi | |
done | |
} | |
signin() | |
{ | |
local pw | |
pw=$(gpg -d -q "$master") | |
local se | |
se=$(gpg -d -q "$secret") | |
if [ $verbose -eq 1 ]; then | |
echo "signing in to ${domain} $email" | |
fi | |
local script=" | |
spawn op signin ${domain} ${email} ${se} | |
expect \"${domain}:\" | |
send \"${pw}\n\" | |
expect { | |
\"Enter your six-digit authentication code:\" { | |
puts -nonewline stderr \"Enter your six-digit authentication code: \" | |
flush stderr | |
interact -o \"\r\" return | |
puts stderr \"\" | |
exp_continue | |
} | |
eof | |
} | |
" | |
local output0 | |
output0=$(expect -c "${script}") | |
local output | |
output=$(echo "${output0}" | grep "export" || echo -n "_fail_") | |
if [ "$output" == "_fail_" ]; then | |
echo "1pass failed to signin to ${domain}" | |
exit 1 | |
fi | |
# extract token from 'export OP_SESSION_domain="asdsad"' | |
local token | |
token=$(expr "${output}" : '.*="\(.*\)"') | |
echo -n "${token}" | gpg -qe --batch -r "$self_key" > "$session" | |
} | |
init_session() | |
{ | |
if [ "${token}" != "" ]; then | |
# already have token | |
return | |
fi | |
# test for stale session | |
if [ ! -r "$session" ] || [ ! "$(find "$session" -mmin -29)" ] || [ $refresh -eq 1 ]; then | |
signin | |
else | |
if [ $verbose -eq 1 ]; then | |
echo "using existing session token" | |
fi | |
fi | |
token=$(gpg -d -q "$session") | |
touch "$session" | |
} | |
forget_session() | |
{ | |
unset "$OP_SESSION_NAME" | |
rm -f "$session" | |
gpgconf --kill gpg-agent | |
echo "cleared local session" | |
} | |
# | |
# fetch the index of all items from the net, and cache | |
# | |
fetch_index() | |
{ | |
init_session | |
if [ $verbose -eq 1 ]; then | |
echo "fetching index of all items" | |
fi | |
local items | |
items=$(op list items --session="${token}" || echo -n "_fail_") | |
if [ "$items" == "_fail_" ]; then | |
echo "1pass: failed to fetch index of all items" | |
exit 1 | |
fi | |
# backup current index | |
if [ -r "$index" ]; then | |
cp -a "$index" "${index}.bak" | |
fi | |
echo -n "${items}" | gpg -qe --batch -r "$self_key" > "$index" | |
} | |
# | |
# fetch an item from the net by uuid and cache it locally | |
# | |
fetch_item() | |
{ | |
local uuid=$1 | |
init_session | |
if [ $verbose -eq 1 ]; then | |
echo "fetching item $uuid" | |
fi | |
local item | |
item=$(op get item "$uuid" --session="$token" || echo -n "_fail_") | |
if [ "$item" == "_fail_" ]; then | |
echo "1pass: failed to fetch item $uuid" | |
exit 1 | |
fi | |
echo -n "${item}" | gpg -qe --batch -r "$self_key" > "${cache_dir}/${uuid}.gpg" | |
} | |
# | |
# list the titles of all items in the index | |
# | |
list_items() | |
{ | |
if [ ! -r "$index" ] || [ $refresh -eq 1 ]; then | |
fetch_index | |
fi | |
gpg -qd "$index" | jq -r ".[].overview.title" | LC_ALL="C" bash -c 'sort -bf' | |
} | |
# | |
# ensure we have the local gpg encoded file of the item given by the uuid | |
# | |
ensure_item() | |
{ | |
local uuid=$1 | |
local file=${cache_dir}/${uuid}.gpg | |
if [ ! -r "$file" ] || [ $refresh -eq 1 ]; then | |
fetch_item "$uuid" | |
fi | |
} | |
# | |
# fetch a field from template 001 ("Login") | |
# | |
get_001() | |
{ | |
local uuid=$1 | |
local field=$2 | |
local q="" | |
if [ "$field" == "username" ] || [ "$field" == "password" ]; then | |
q=".details.fields[] | select(.designation==\"${field}\").value" | |
else | |
q=".details.sections[] | select(.fields).fields[] | select(.t==\"${field}\").v" | |
fi | |
ensure_item "$uuid" | |
get_result=$(gpg -qd "${cache_dir}/${uuid}.gpg" | jq -r "${q}" || echo -n "_fail_") | |
} | |
# | |
# fetch a field from template 005 ("Password") | |
# | |
get_005() | |
{ | |
local uuid=$1 | |
local field=$2 | |
local q="" | |
if [ "$field" == "password" ]; then | |
q=".details.${field}" | |
else | |
q=".details.sections[] | select(.fields).fields[] | select(.t==\"${field}\").v" | |
fi | |
ensure_item "$uuid" | |
get_result=$(gpg -qd "${cache_dir}/${uuid}.gpg" | jq -r "${q}" || echo -n "_fail_") | |
} | |
# | |
# fetch a field from template 102 ("Password") | |
# | |
get_102() | |
{ | |
local uuid=$1 | |
local field=$2 | |
local q=".details.sections[] | select(.fields).fields[] | select(.n==\"${field}\").v" | |
ensure_item "$uuid" | |
get_result=$(gpg -qd "${cache_dir}/${uuid}.gpg" | jq -r "${q}" || echo -n "_fail_") | |
} | |
# | |
# fetch a TOTP value for the given item | |
# | |
get_totp() | |
{ | |
# Make sure we have a current and valid session and then get the UUID | |
init_session | |
if [ ! -r "$index" ] || [ $refresh -eq 1 ]; then | |
fetch_index | |
fi | |
local uuid | |
uuid=$(gpg -qd "$index" | jq -r ".[] | select(.overview.title==\"${1}\").uuid") | |
# Fetch the TOTP | |
if [ $verbose -eq 1 ]; then | |
echo "fetching TOTP for $uuid" | |
fi | |
local totp | |
totp=$(op get totp "$uuid" --session="$token" || echo -n "_fail_") | |
if [ "$item" == "_fail_" ]; then | |
echo "1pass: failed to fetch TOTP for $uuid" | |
exit 1 | |
fi | |
if [ $? ]; then | |
get_result="${totp}" | |
output_result | |
fi | |
} | |
output_result() | |
{ | |
if [ $print_output -eq 1 ]; then | |
echo "${get_result}" | |
else | |
local os | |
os=$(uname) | |
if [ "$os" == "Darwin" ]; then | |
echo -n "${get_result}" | pbcopy | |
elif [ "$os" == "Linux" ] || [ "$os" == "FreeBSD" ]; then | |
echo -n "${get_result}" | xclip -selection clipboard | |
fi | |
# sleep and reset clipboard | |
local sleep_argv0 | |
sleep_argv0="1pass sleep for user $(id -u)" | |
pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5 | |
( | |
( exec -a "$sleep_argv0" sleep "$clip_time" ) | |
echo -n "CLEAR" | pbcopy | |
) 2>/dev/null & disown | |
fi | |
} | |
get_by_title() | |
{ | |
local title=$1 | |
local field=$2 | |
if [ ! -r "$index" ] || [ $refresh -eq 1 ]; then | |
fetch_index | |
fi | |
# read both uuid and templateUuid. Complicated call is so that we only call GPG once | |
q=".[] | select(.overview.title==\"${title}\").uuid + \":\" + select(.overview.title==\"${title}\").templateUuid" | |
IFS=':' read -r uuid tid <<< "$(gpg -qd "$index" | jq -r "${q}")" | |
if [ "$tid" != "" ]; then | |
"get_${tid}" "$uuid" "$field" | |
if [ $? ]; then | |
output_result | |
fi | |
fi | |
} | |
while getopts "f?h?p?r?v?:" opt; do | |
case $opt in | |
h) | |
usage | |
exit 0 | |
;; | |
f) | |
forget_session | |
exit 0 | |
;; | |
p) | |
print_output=1 | |
;; | |
r) | |
refresh=1 | |
;; | |
v) | |
verbose=1 | |
;; | |
\?) | |
echo "Invalid option: -$OPTARG" >&2 | |
;; | |
esac | |
done | |
shift $((OPTIND-1)) | |
sanity_check | |
if [ $# -eq 0 ]; then | |
list_items | |
elif [ $# -eq 1 ]; then | |
get_by_title "$1" password | |
elif [ $# -eq 2 ]; then | |
case "$2" in | |
totp ) get_totp "$1" ;; | |
* ) get_by_title "$1" "$2" ;; | |
esac | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage