Skip to content

Instantly share code, notes, and snippets.

@tvhung83
Last active April 27, 2020 06:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tvhung83/9ff0d117460ecde8ead360a32e73b078 to your computer and use it in GitHub Desktop.
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
#! /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
@tvhung83
Copy link
Author

tvhung83 commented Apr 27, 2020

Usage

  • Prerequisite:
$ brew cask install 1password-cli
$ brew install jq expect
  • Install:
curl -L https://gist.github.com/tvhung83/9ff0d117460ecde8ead360a32e73b078/raw > /usr/local/bin/1pass
chmod a+x /usr/local/bin/1pass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment