--- /dev/null
+#!/usr/bin/env bash
+
+# Copyright (C) 2012 - 2014 Jason A. Donenfeld <
[email protected]>. All Rights Reserved.
+# This file is licensed under the GPLv2+. Please see COPYING for more information.
+
+umask "${PASSWORD_STORE_UMASK:-077}"
+set -o pipefail
+
+GPG_OPTS=( $PASSWORD_STORE_GPG_OPTS "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" )
+GPG="gpg"
+export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}"
+which gpg2 &>/dev/null && GPG="gpg2"
+[[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--use-agent" )
+
+PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
+X_SELECTION="${PASSWORD_STORE_X_SELECTION:-clipboard}"
+CLIP_TIME="${PASSWORD_STORE_CLIP_TIME:-45}"
+
+export GIT_DIR="${PASSWORD_STORE_GIT:-$PREFIX}/.git"
+export GIT_WORK_TREE="${PASSWORD_STORE_GIT:-$PREFIX}"
+
+#
+# BEGIN helper functions
+#
+
+git_add_file() {
+ [[ -d $GIT_DIR ]] || return
+ git add "$1" || return
+ [[ -n $(git status --porcelain "$1") ]] || return
+ git_commit "$2"
+}
+git_commit() {
+ local sign=""
+ [[ -d $GIT_DIR ]] || return
+ [[ $(git config --bool --get pass.signcommits) == "true" ]] && sign="-S"
+ git commit $sign -m "$1"
+}
+yesno() {
+ [[ -t 0 ]] || return 0
+ local response
+ read -r -p "$1 [y/N] " response
+ [[ $response == [yY] ]] || exit 1
+}
+die() {
+ echo "$@" >&2
+ exit 1
+}
+set_gpg_recipients() {
+ GPG_RECIPIENT_ARGS=( )
+ GPG_RECIPIENTS=( )
+
+ if [[ -n $PASSWORD_STORE_KEY ]]; then
+ for gpg_id in $PASSWORD_STORE_KEY; do
+ GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
+ GPG_RECIPIENTS+=( "$gpg_id" )
+ done
+ return
+ fi
+
+ local current="$PREFIX/$1"
+ while [[ $current != "$PREFIX" && ! -f $current/.gpg-id ]]; do
+ current="${current%/*}"
+ done
+ current="$current/.gpg-id"
+
+ if [[ ! -f $current ]]; then
+ cat >&2 <<-_EOF
+ Error: You must run:
+ $PROGRAM init your-gpg-id
+ before you may use the password store.
+
+ _EOF
+ cmd_usage
+ exit 1
+ fi
+
+ local gpg_id
+ while read -r gpg_id; do
+ GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
+ GPG_RECIPIENTS+=( "$gpg_id" )
+ done < "$current"
+}
+
+reencrypt_path() {
+ local prev_gpg_recipients="" gpg_keys="" current_keys="" index passfile
+ local groups="$($GPG $PASSWORD_STORE_GPG_OPTS --list-config --with-colons | grep "^cfg:group:.*")"
+ while read -r -d "" passfile; do
+ local passfile_dir="${passfile%/*}"
+ passfile_dir="${passfile_dir#$PREFIX}"
+ passfile_dir="${passfile_dir#/}"
+ local passfile_display="${passfile#$PREFIX/}"
+ passfile_display="${passfile_display%.gpg}"
+ local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
+
+ set_gpg_recipients "$passfile_dir"
+ if [[ $prev_gpg_recipients != "${GPG_RECIPIENTS[*]}" ]]; then
+ for index in "${!GPG_RECIPIENTS[@]}"; do
+ local group="$(sed -n "s/^cfg:group:$(sed 's/[\/&]/\\&/g' <<<"${GPG_RECIPIENTS[$index]}"):\\(.*\\)\$/\\1/p" <<<"$groups" | head -n 1)"
+ [[ -z $group ]] && continue
+ IFS=";" eval 'GPG_RECIPIENTS+=( $group )' # http://unix.stackexchange.com/a/92190
+ unset GPG_RECIPIENTS[$index]
+ done
+ gpg_keys="$($GPG $PASSWORD_STORE_GPG_OPTS --list-keys --with-colons "${GPG_RECIPIENTS[@]}" | sed -n 's/sub:[^:]*:[^:]*:[^:]*:\([^:]*\):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[a-zA-Z]*e[a-zA-Z]*:.*/\1/p' | LC_ALL=C sort -u)"
+ fi
+ current_keys="$($GPG $PASSWORD_STORE_GPG_OPTS -v --no-secmem-warning --no-permission-warning --list-only --keyid-format long "$passfile" 2>&1 | cut -d ' ' -f 5 | LC_ALL=C sort -u)"
+
+ if [[ $gpg_keys != "$current_keys" ]]; then
+ echo "$passfile_display: reencrypting to ${gpg_keys//$'\n'/ }"
+ $GPG -d "${GPG_OPTS[@]}" "$passfile" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}" &&
+ mv "$passfile_temp" "$passfile" || rm -f "$passfile_temp"
+ fi
+ prev_gpg_recipients="${GPG_RECIPIENTS[*]}"
+ done < <(find "$1" -iname '*.gpg' -print0)
+}
+check_sneaky_paths() {
+ local path
+ for path in "$@"; do
+ [[ $path =~ /\.\.$ || $path =~ ^\.\./ || $path =~ /\.\./ || $path =~ ^\.\.$ ]] && die "Error: You've attempted to pass a sneaky path to pass. Go home."
+ done
+}
+
+#
+# END helper functions
+#
+
+#
+# BEGIN platform definable
+#
+
+clip() {
+ # This base64 business is because bash cannot store binary data in a shell
+ # variable. Specifically, it cannot store nulls nor (non-trivally) store
+ # trailing new lines.
+ local sleep_argv0="password store sleep on display $DISPLAY"
+ pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5
+ local before="$(xclip -o -selection "$X_SELECTION" 2>/dev/null | base64)"
+ echo -n "$1" | xclip -selection "$X_SELECTION" || die "Error: Could not copy data to the clipboard"
+ (
+ ( exec -a "$sleep_argv0" sleep "$CLIP_TIME" )
+ local now="$(xclip -o -selection "$X_SELECTION" | base64)"
+ [[ $now != $(echo -n "$1" | base64) ]] && before="$now"
+
+ # It might be nice to programatically check to see if klipper exists,
+ # as well as checking for other common clipboard managers. But for now,
+ # this works fine -- if qdbus isn't there or if klipper isn't running,
+ # this essentially becomes a no-op.
+ #
+ # Clipboard managers frequently write their history out in plaintext,
+ # so we axe it here:
+ qdbus org.kde.klipper /klipper org.kde.klipper.klipper.clearClipboardHistory &>/dev/null
+
+ echo "$before" | base64 -d | xclip -selection "$X_SELECTION"
+ ) 2>/dev/null & disown
+ echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds."
+}
+tmpdir() {
+ [[ -n $SECURE_TMPDIR ]] && return
+ local warn=1
+ [[ $1 == "nowarn" ]] && warn=0
+ local template="$PROGRAM.XXXXXXXXXXXXX"
+ if [[ -d /dev/shm && -w /dev/shm && -x /dev/shm ]]; then
+ SECURE_TMPDIR="$(mktemp -d "/dev/shm/$template")"
+ remove_tmpfile() {
+ rm -rf "$SECURE_TMPDIR"
+ }
+ trap remove_tmpfile INT TERM EXIT
+ else
+ [[ $warn -eq 1 ]] && yesno "$(cat <<-_EOF
+ Your system does not have /dev/shm, which means that it may
+ be difficult to entirely erase the temporary non-encrypted
+ password file after editing.
+
+ Are you sure you would like to continue?
+ _EOF
+ )"
+ SECURE_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/$template")"
+ shred_tmpfile() {
+ find "$SECURE_TMPDIR" -type f -exec $SHRED {} +
+ rm -rf "$SECURE_TMPDIR"
+ }
+ trap shred_tmpfile INT TERM EXIT
+ fi
+
+}
+GETOPT="getopt"
+SHRED="shred -f -z"
+
+
+#
+# END platform definable
+#
+
+
+#
+# BEGIN subcommand functions
+#
+
+cmd_version() {
+ cat <<-_EOF
+ ============================================
+ = pass: the standard unix password manager =
+ = =
+ = v1.6.5 =
+ = =
+ = Jason A. Donenfeld =
+ = =
+ = http://www.passwordstore.org/ =
+ ============================================
+ _EOF
+}
+
+cmd_usage() {
+ cmd_version
+ echo
+ cat <<-_EOF
+ Usage:
+ $PROGRAM init [--path=subfolder,-p subfolder] gpg-id...
+ Initialize new password storage and use gpg-id for encryption.
+ Selectively reencrypt existing passwords using new gpg-id.
+ $PROGRAM [ls] [subfolder]
+ List passwords.
+ $PROGRAM find pass-names...
+ List passwords that match pass-names.
+ $PROGRAM [show] [--clip,-c] pass-name
+ Show existing password and optionally put it on the clipboard.
+ If put on the clipboard, it will be cleared in $CLIP_TIME seconds.
+ $PROGRAM grep search-string
+ Search for password files containing search-string when decrypted.
+ $PROGRAM insert [--echo,-e | --multiline,-m] [--force,-f] pass-name
+ Insert new password. Optionally, echo the password back to the console
+ during entry. Or, optionally, the entry may be multiline. Prompt before
+ overwriting existing password unless forced.
+ $PROGRAM edit pass-name
+ Insert a new password or edit an existing password using ${EDITOR:-vi}.
+ $PROGRAM generate [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name pass-length
+ Generate a new password of pass-length with optionally no symbols.
+ Optionally put it on the clipboard and clear board after $CLIP_TIME seconds.
+ Prompt before overwriting existing password unless forced.
+ Optionally replace only the first line of an existing file with a new password.
+ $PROGRAM rm [--recursive,-r] [--force,-f] pass-name
+ Remove existing password or directory, optionally forcefully.
+ $PROGRAM mv [--force,-f] old-path new-path
+ Renames or moves old-path to new-path, optionally forcefully, selectively reencrypting.
+ $PROGRAM cp [--force,-f] old-path new-path
+ Copies old-path to new-path, optionally forcefully, selectively reencrypting.
+ $PROGRAM git git-command-args...
+ If the password store is a git repository, execute a git command
+ specified by git-command-args.
+ $PROGRAM help
+ Show this text.
+ $PROGRAM version
+ Show version information.
+
+ More information may be found in the pass(1) man page.
+ _EOF
+}
+
+cmd_init() {
+ local opts id_path=""
+ opts="$($GETOPT -o p: -l path: -n "$PROGRAM" -- "$@")"
+ local err=$?
+ eval set -- "$opts"
+ while true; do case $1 in
+ -p|--path) id_path="$2"; shift 2 ;;
+ --) shift; break ;;
+ esac done
+
+ [[ $err -ne 0 || $# -lt 1 ]] && die "Usage: $PROGRAM $COMMAND [--path=subfolder,-p subfolder] gpg-id..."
+ [[ -n $id_path ]] && check_sneaky_paths "$id_path"
+ [[ -n $id_path && ! -d $PREFIX/$id_path && -e $PREFIX/$id_path ]] && die "Error: $PREFIX/$id_path exists but is not a directory."
+
+ local gpg_id="$PREFIX/$id_path/.gpg-id"
+
+ if [[ $# -eq 1 && -z $1 ]]; then
+ [[ ! -f "$gpg_id" ]] && die "Error: $gpg_id does not exist and so cannot be removed."
+ rm -v -f "$gpg_id" || exit 1
+ if [[ -d $GIT_DIR ]]; then
+ git rm -qr "$gpg_id"
+ git_commit "Deinitialize ${gpg_id}."
+ fi
+ rmdir -p "${gpg_id%/*}" 2>/dev/null
+ else
+ mkdir -v -p "$PREFIX/$id_path"
+ printf "%s\n" "$@" > "$gpg_id"
+ local id_print="$(printf "%s, " "$@")"
+ echo "Password store initialized for ${id_print%, }"
+ git_add_file "$gpg_id" "Set GPG id to ${id_print%, }."
+ fi
+
+ reencrypt_path "$PREFIX/$id_path"
+ git_add_file "$PREFIX/$id_path" "Reencrypt password store using new GPG id ${id_print%, }."
+}
+
+cmd_show() {
+ local opts clip=0
+ opts="$($GETOPT -o c -l clip -n "$PROGRAM" -- "$@")"
+ local err=$?
+ eval set -- "$opts"
+ while true; do case $1 in
+ -c|--clip) clip=1; shift ;;
+ --) shift; break ;;
+ esac done
+
+ [[ $err -ne 0 ]] && die "Usage: $PROGRAM $COMMAND [--clip,-c] [pass-name]"
+
+ local path="$1"
+ local passfile="$PREFIX/$path.gpg"
+ check_sneaky_paths "$path"
+ if [[ -f $passfile ]]; then
+ if [[ $clip -eq 0 ]]; then
+ $GPG -d "${GPG_OPTS[@]}" "$passfile" || exit $?
+ else
+ local pass="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | head -n 1)"
+ [[ -n $pass ]] || exit 1
+ clip "$pass" "$path"
+ fi
+ elif [[ -d $PREFIX/$path ]]; then
+ if [[ -z $path ]]; then
+ echo "Password Store"
+ else
+ echo "${path%\/}"
+ fi
+ tree -C -l --noreport "$PREFIX/$path" | tail -n +2 | sed 's/\.gpg\(\x1B\[[0-9]\+m\)\{0,1\}\( ->\|$\)/\1\2/g' # remove .gpg at end of line, but keep colors
+ elif [[ -z $path ]]; then
+ die "Error: password store is empty. Try \"pass init\"."
+ else
+ die "Error: $path is not in the password store."
+ fi
+}
+
+cmd_find() {
+ [[ -z "$@" ]] && die "Usage: $PROGRAM $COMMAND pass-names..."
+ IFS="," eval 'echo "Search Terms: $*"'
+ local terms="*$(printf '%s*|*' "$@")"
+ tree -C -l --noreport -P "${terms%|*}" --prune --matchdirs --ignore-case "$PREFIX" | tail -n +2 | sed 's/\.gpg\(\x1B\[[0-9]\+m\)\{0,1\}\( ->\|$\)/\1\2/g'
+}
+
+cmd_grep() {
+ [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND search-string"
+ local search="$1" passfile grepresults
+ while read -r -d "" passfile; do
+ grepresults="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | grep --color=always "$search")"
+ [ $? -ne 0 ] && continue
+ passfile="${passfile%.gpg}"
+ passfile="${passfile#$PREFIX/}"
+ local passfile_dir="${passfile%/*}/"
+ [[ $passfile_dir == "${passfile}/" ]] && passfile_dir=""
+ passfile="${passfile##*/}"
+ printf "\e[94m%s\e[1m%s\e[0m:\n" "$passfile_dir" "$passfile"
+ echo "$grepresults"
+ done < <(find -L "$PREFIX" -iname '*.gpg' -print0)
+}
+
+cmd_insert() {
+ local opts multiline=0 noecho=1 force=0
+ opts="$($GETOPT -o mef -l multiline,echo,force -n "$PROGRAM" -- "$@")"
+ local err=$?
+ eval set -- "$opts"
+ while true; do case $1 in
+ -m|--multiline) multiline=1; shift ;;
+ -e|--echo) noecho=0; shift ;;
+ -f|--force) force=1; shift ;;
+ --) shift; break ;;
+ esac done
+
+ [[ $err -ne 0 || ( $multiline -eq 1 && $noecho -eq 0 ) || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--echo,-e | --multiline,-m] [--force,-f] pass-name"
+ local path="$1"
+ local passfile="$PREFIX/$path.gpg"
+ check_sneaky_paths "$path"
+
+ [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
+
+ mkdir -p -v "$PREFIX/$(dirname "$path")"
+ set_gpg_recipients "$(dirname "$path")"
+
+ if [[ $multiline -eq 1 ]]; then
+ echo "Enter contents of $path and press Ctrl+D when finished:"
+ echo
+ $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}"
+ elif [[ $noecho -eq 1 ]]; then
+ local password password_again
+ while true; do
+ read -r -p "Enter password for $path: " -s password || exit 1
+ echo
+ read -r -p "Retype password for $path: " -s password_again || exit 1
+ echo
+ if [[ $password == "$password_again" ]]; then
+ $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password"
+ break
+ else
+ echo "Error: the entered passwords do not match."
+ fi
+ done
+ else
+ local password
+ read -r -p "Enter password for $path: " -e password
+ $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password"
+ fi
+ git_add_file "$passfile" "Add given password for $path to store."
+}
+
+cmd_edit() {
+ [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND pass-name"
+
+ local path="$1"
+ check_sneaky_paths "$path"
+ mkdir -p -v "$PREFIX/$(dirname "$path")"
+ set_gpg_recipients "$(dirname "$path")"
+ local passfile="$PREFIX/$path.gpg"
+
+ tmpdir #Defines $SECURE_TMPDIR
+ local tmp_file="$(mktemp -u "$SECURE_TMPDIR/XXXXX")-${path//\//-}.txt"
+
+
+ local action="Add"
+ if [[ -f $passfile ]]; then
+ $GPG -d -o "$tmp_file" "${GPG_OPTS[@]}" "$passfile" || exit 1
+ action="Edit"
+ fi
+ ${EDITOR:-vi} "$tmp_file"
+ [[ -f $tmp_file ]] || die "New password not saved."
+ $GPG -d -o - "${GPG_OPTS[@]}" "$passfile" 2>/dev/null | diff - "$tmp_file" &>/dev/null && die "Password unchanged."
+ while ! $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" "$tmp_file"; do
+ yesno "GPG encryption failed. Would you like to try again?"
+ done
+ git_add_file "$passfile" "$action password for $path using ${EDITOR:-vi}."
+}
+
+cmd_generate() {
+ local opts clip=0 force=0 symbols="-y" inplace=0
+ opts="$($GETOPT -o ncif -l no-symbols,clip,in-place,force -n "$PROGRAM" -- "$@")"
+ local err=$?
+ eval set -- "$opts"
+ while true; do case $1 in
+ -n|--no-symbols) symbols=""; shift ;;
+ -c|--clip) clip=1; shift ;;
+ -f|--force) force=1; shift ;;
+ -i|--in-place) inplace=1; shift ;;
+ --) shift; break ;;
+ esac done
+
+ [[ $err -ne 0 || $# -ne 2 || ( $force -eq 1 && $inplace -eq 1 ) ]] && die "Usage: $PROGRAM $COMMAND [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name pass-length"
+ local path="$1"
+ local length="$2"
+ check_sneaky_paths "$path"
+ [[ ! $length =~ ^[0-9]+$ ]] && die "Error: pass-length \"$length\" must be a number."
+ mkdir -p -v "$PREFIX/$(dirname "$path")"
+ set_gpg_recipients "$(dirname "$path")"
+ local passfile="$PREFIX/$path.gpg"
+
+ [[ $inplace -eq 0 && $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
+
+ local pass="$(pwgen -s $symbols $length 1)"
+ [[ -n $pass ]] || exit 1
+ if [[ $inplace -eq 0 ]]; then
+ $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$pass"
+ else
+ local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
+ if $GPG -d "${GPG_OPTS[@]}" "$passfile" | sed $'1c \\\n'"$(sed 's/[\/&]/\\&/g' <<<"$pass")"$'\n' | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}"; then
+ mv "$passfile_temp" "$passfile"
+ else
+ rm -f "$passfile_temp"
+ die "Could not reencrypt new password."
+ fi
+ fi
+ local verb="Add"
+ [[ $inplace -eq 1 ]] && verb="Replace"
+ git_add_file "$passfile" "$verb generated password for ${path}."
+
+ if [[ $clip -eq 0 ]]; then
+ printf "\e[1m\e[37mThe generated password for \e[4m%s\e[24m is:\e[0m\n\e[1m\e[93m%s\e[0m\n" "$path" "$pass"
+ else
+ clip "$pass" "$path"
+ fi
+}
+
+cmd_delete() {
+ local opts recursive="" force=0
+ opts="$($GETOPT -o rf -l recursive,force -n "$PROGRAM" -- "$@")"
+ local err=$?
+ eval set -- "$opts"
+ while true; do case $1 in
+ -r|--recursive) recursive="-r"; shift ;;
+ -f|--force) force=1; shift ;;
+ --) shift; break ;;
+ esac done
+ [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--recursive,-r] [--force,-f] pass-name"
+ local path="$1"
+ check_sneaky_paths "$path"
+
+ local passfile="$PREFIX/${path%/}"
+ if [[ ! -d $passfile ]]; then
+ passfile="$PREFIX/$path.gpg"
+ [[ ! -f $passfile ]] && die "Error: $path is not in the password store."
+ fi
+
+ [[ $force -eq 1 ]] || yesno "Are you sure you would like to delete $path?"
+
+ rm $recursive -f -v "$passfile"
+ if [[ -d $GIT_DIR && ! -e $passfile ]]; then
+ git rm -qr "$passfile"
+ git_commit "Remove $path from store."
+ fi
+ rmdir -p "${passfile%/*}" 2>/dev/null
+}
+
+cmd_copy_move() {
+ local opts move=1 force=0
+ [[ $1 == "copy" ]] && move=0
+ shift
+ opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")"
+ local err=$?
+ eval set -- "$opts"
+ while true; do case $1 in
+ -f|--force) force=1; shift ;;
+ --) shift; break ;;
+ esac done
+ [[ $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND [--force,-f] old-path new-path"
+ check_sneaky_paths "$@"
+ local old_path="$PREFIX/${1%/}"
+ local new_path="$PREFIX/$2"
+ local old_dir="$old_path"
+
+ if [[ ! -d $old_path ]]; then
+ old_dir="${old_path%/*}"
+ old_path="${old_path}.gpg"
+ [[ ! -f $old_path ]] && die "Error: $1 is not in the password store."
+ fi
+
+ mkdir -p -v "${new_path%/*}"
+ [[ -d $old_path || -d $new_path || $new_path =~ /$ ]] || new_path="${new_path}.gpg"
+
+ local interactive="-i"
+ [[ ! -t 0 || $force -eq 1 ]] && interactive="-f"
+
+ if [[ $move -eq 1 ]]; then
+ mv $interactive -v "$old_path" "$new_path" || exit 1
+ [[ -e "$new_path" ]] && reencrypt_path "$new_path"
+
+ if [[ -d $GIT_DIR && ! -e $old_path ]]; then
+ git rm -qr "$old_path"
+ git_add_file "$new_path" "Rename ${1} to ${2}."
+ fi
+ rmdir -p "$old_dir" 2>/dev/null
+ else
+ cp $interactive -r -v "$old_path" "$new_path" || exit 1
+ [[ -e "$new_path" ]] && reencrypt_path "$new_path"
+ git_add_file "$new_path" "Copy ${1} to ${2}."
+ fi
+}
+
+cmd_git() {
+ if [[ $1 == "init" ]]; then
+ git "$@" || exit 1
+ git_add_file "$PREFIX" "Add current contents of password store."
+
+ echo '*.gpg diff=gpg' > "$PREFIX/.gitattributes"
+ git_add_file .gitattributes "Configure git repository for gpg file diff."
+ git config --local diff.gpg.binary true
+ git config --local diff.gpg.textconv "$GPG -d ${GPG_OPTS[*]}"
+ elif [[ -d $GIT_DIR ]]; then
+ tmpdir nowarn #Defines $SECURE_TMPDIR. We don't warn, because at most, this only copies encrypted files.
+ export TMPDIR="$SECURE_TMPDIR"
+ git "$@"
+ else
+ die "Error: the password store is not a git repository. Try \"$PROGRAM git init\"."
+ fi
+}
+
+#
+# END subcommand functions
+#
+
+PROGRAM="${0##*/}"
+COMMAND="$1"
+
+case "$1" in
+ init) shift; cmd_init "$@" ;;
+ help|--help) shift; cmd_usage "$@" ;;
+ version|--version) shift; cmd_version "$@" ;;
+ show|ls|list) shift; cmd_show "$@" ;;
+ find|search) shift; cmd_find "$@" ;;
+ grep) shift; cmd_grep "$@" ;;
+ insert|add) shift; cmd_insert "$@" ;;
+ edit) shift; cmd_edit "$@" ;;
+ generate) shift; cmd_generate "$@" ;;
+ delete|rm|remove) shift; cmd_delete "$@" ;;
+ rename|mv) shift; cmd_copy_move "move" "$@" ;;
+ copy|cp) shift; cmd_copy_move "copy" "$@" ;;
+ git) shift; cmd_git "$@" ;;
+ *) COMMAND="show"; cmd_show "$@" ;;
+esac
+exit 0
--- /dev/null
+.TH PASS 1 "2014 March 18" ZX2C4 "Password Store"
+
+.SH NAME
+pass - stores, retrieves, generates, and synchronizes passwords securely
+
+.SH SYNOPSIS
+.B pass
+[
+.I COMMAND
+] [
+.I OPTIONS
+]... [
+.I ARGS
+]...
+
+.SH DESCRIPTION
+
+.B pass
+is a very simple password store that keeps passwords inside
+.BR gpg2 (1)
+encrypted files inside a simple directory tree residing at
+.IR ~/.password-store .
+The
+.B pass
+utility provides a series of commands for manipulating the password store,
+allowing the user to add, remove, edit, synchronize, generate, and manipulate
+passwords.
+
+If no COMMAND is specified, COMMAND defaults to either
+.B show
+or
+.BR ls ,
+depending on the type of specifier in ARGS. Otherwise COMMAND must be one of
+the valid commands listed below.
+
+Several of the commands below rely on or provide additional functionality if
+the password store directory is also a git repository. If the password store
+directory is a git repository, all password store modification commands will
+cause a corresponding git commit. See the \fIEXTENDED GIT EXAMPLE\fP section
+for a detailed description using \fBinit\fP and
+.BR git (1).
+
+The \fBinit\fP command must be run before other commands in order to initialize
+the password store with the correct gpg key id. Passwords are encrypting using
+the gpg key set with \fBinit\fP.
+
+There is a corresponding bash completion script for use with tab completing
+password names in
+.BR bash (1).
+
+.SH COMMANDS
+
+.TP
+\fBinit\fP [ \fI--path=sub-folder\fP, \fI-p sub-folder\fP ] \fIgpg-id...\fP
+Initialize new password storage and use
+.I gpg-id
+for encryption. Multiple gpg-ids may be specified, in order to encrypt each
+password with multiple ids. This command must be run first before a password
+store can be used. If the specified \fIgpg-id\fP is different from the key
+used in any existing files, these files will be reencrypted to use the new id.
+Note that use of
+.BR gpg-agent (1)
+is recommended so that the batch decryption does not require as much user
+intervention. If \fI--path\fP or \fI-p\fP is specified, along with an argument,
+a specific gpg-id or set of gpg-ids is assigned for that specific sub folder of
+the password store. If only one \fIgpg-id\fP is given, and it is an empty string,
+then the current \fI.gpg-id\fP file for the specified \fIsub-folder\fP (or root if
+unspecified) is removed.
+.TP
+\fBls\fP \fIsubfolder\fP
+List names of passwords inside the tree at
+.I subfolder
+by using the
+.BR tree (1)
+program. This command is alternatively named \fBlist\fP.
+.TP
+\fBgrep\fP \fIsearch-string\fP
+Searches inside each decrypted password file for \fIsearch-string\fP, and displays line
+containing matched string along with filename. Uses
+.BR grep (1)
+for matching. Make use of the \fIGREP_OPTIONS\fP environment variable to set particular
+options.
+.TP
+\fBfind\fP \fIpass-names\fP...
+List names of passwords inside the tree that match \fIpass-names\fP by using the
+.BR tree (1)
+program. This command is alternatively named \fBsearch\fP.
+.TP
+\fBshow\fP [ \fI--clip\fP, \fI-c\fP ] \fIpass-name\fP
+Decrypt and print a password named \fIpass-name\fP. If \fI--clip\fP or \fI-c\fP
+is specified, do not print the password but instead copy the first line to the
+clipboard using
+.BR xclip (1)
+and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) seconds.
+.TP
+\fBinsert\fP [ \fI--echo\fP, \fI-e\fP | \fI--multiline\fP, \fI-m\fP ] [ \fI--force\fP, \fI-f\fP ] \fIpass-name\fP
+Insert a new password into the password store called \fIpass-name\fP. This will
+read the new password from standard in. If \fI--echo\fP or \fI-e\fP is \fInot\fP specified,
+disable keyboard echo when the password is entered and confirm the password by asking
+for it twice. If \fI--multiline\fP or \fI-m\fP is specified, lines will be read until
+EOF or Ctrl+D is reached. Otherwise, only a single line from standard in is read. Prompt
+before overwriting an existing password, unless \fI--force\fP or \fI-f\fP is specified. This
+command is alternatively named \fBadd\fP.
+.TP
+\fBedit\fP \fIpass-name\fP
+Insert a new password or edit an existing password using the default text editor specified
+by the environment variable \fIEDITOR\fP or using
+.BR vi (1)
+as a fallback. This mode makes use of temporary files for editing, but care is taken to
+ensure that temporary files are created in \fI/dev/shm\fP in order to avoid writing to
+difficult-to-erase disk sectors. If \fI/dev/shm\fP is not accessible, fallback to
+the ordinary \fITMPDIR\fP location, and print a warning.
+.TP
+\fBgenerate\fP [ \fI--no-symbols\fP, \fI-n\fP ] [ \fI--clip\fP, \fI-c\fP ] [ \fI--in-place\fP, \fI-i\fP | \fI--force\fP, \fI-f\fP ] \fIpass-name pass-length\fP
+Generate a new password using
+.BR pwgen (1)
+of length \fIpass-length\fP and insert into \fIpass-name\fP. If \fI--no-symbols\fP or \fI-n\fP
+is specified, do not use any non-alphanumeric characters in the generated password.
+If \fI--clip\fP or \fI-c\fP is specified, do not print the password but instead copy
+it to the clipboard using
+.BR xclip (1)
+and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) seconds.
+Prompt before overwriting an existing password,
+unless \fI--force\fP or \fI-f\fP is specified. If \fI--in-place\fP or \fI-i\fP is
+specified, do not interactively prompt, and only replace the first line of the password
+file with the new generated password, keeping the remainder of the file intact.
+.TP
+\fBrm\fP [ \fI--recursive\fP, \fI-r\fP ] [ \fI--force\fP, \fI-f\fP ] \fIpass-name\fP
+Remove the password named \fIpass-name\fP from the password store. This command is
+alternatively named \fBremove\fP or \fBdelete\fP. If \fI--recursive\fP or \fI-r\fP
+is specified, delete pass-name recursively if it is a directory. If \fI--force\fP
+or \fI-f\fP is specified, do not interactively prompt before removal.
+.TP
+\fBmv\fP [ \fI--force\fP, \fI-f\fP ] \fIold-path\fP \fInew-path\fP
+Renames the password or directory named \fIold-path\fP to \fInew-path\fP. This
+command is alternatively named \fBrename\fP. If \fI--force\fP is specified,
+silently overwrite \fInew-path\fP if it exists. If \fInew-path\fP ends in a
+trailing \fI/\fP, it is always treated as a directory. Passwords are selectively
+reencrypted to the corresponding keys of their new destination.
+.TP
+\fBcp\fP [ \fI--force\fP, \fI-f\fP ] \fIold-path\fP \fInew-path\fP
+Copies the password or directory named \fIold-path\fP to \fInew-path\fP. This
+command is alternatively named \fBcopy\fP. If \fI--force\fP is specified,
+silently overwrite \fInew-path\fP if it exists. If \fInew-path\fP ends in a
+trailing \fI/\fP, it is always treated as a directory. Passwords are selectively
+reencrypted to the corresponding keys of their new destination.
+.TP
+\fBgit\fP \fIgit-command-args\fP...
+If the password store is a git repository, pass \fIgit-command-args\fP as arguments to
+.BR git (1)
+using the password store as the git repository. If \fIgit-command-args\fP is \fBinit\fP,
+in addition to initializing the git repository, add the current contents of the password
+store to the repository in an initial commit. If the git config key \fIpass.signcommits\fP
+is set to \fItrue\fP, then all commits will be signed using \fIuser.signingkey\fP or the
+default git signing key. This config key may be turned on using:
+.B `pass git config --bool --add pass.signcommits true`
+.TP
+\fBhelp\fP
+Show usage message.
+.TP
+\fBversion\fP
+Show version information.
+
+.SH SIMPLE EXAMPLES
+
+.TP
+Initialize password store
+.br
+mkdir: created directory \[u2018]/home/zx2c4/.password-store\[u2019]
+.br
+.TP
+List existing passwords in store
+.B zx2c4@laptop ~ $ pass
+.br
+Password Store
+.br
+\[u251C]\[u2500]\[u2500] Business
+.br
+\[u2502] \[u251C]\[u2500]\[u2500] some-silly-business-site.com
+.br
+\[u2502] \[u2514]\[u2500]\[u2500] another-business-site.net
+.br
+\[u251C]\[u2500]\[u2500] Email
+.br
+\[u2502] \[u251C]\[u2500]\[u2500] donenfeld.com
+.br
+\[u2502] \[u2514]\[u2500]\[u2500] zx2c4.com
+.br
+\[u2514]\[u2500]\[u2500] France
+.br
+ \[u251C]\[u2500]\[u2500] bank
+.br
+ \[u251C]\[u2500]\[u2500] freebox
+.br
+ \[u2514]\[u2500]\[u2500] mobilephone
+.br
+
+.br
+Alternatively, "\fBpass ls\fP".
+.TP
+Find existing passwords in store that match .com
+.B zx2c4@laptop ~ $ pass find .com
+.br
+Search Terms: .com
+.br
+\[u251C]\[u2500]\[u2500] Business
+.br
+\[u2502] \[u251C]\[u2500]\[u2500] some-silly-business-site.com
+.br
+\[u2514]\[u2500]\[u2500] Email
+.br
+ \[u251C]\[u2500]\[u2500] donenfeld.com
+.br
+ \[u2514]\[u2500]\[u2500] zx2c4.com
+.br
+
+.br
+Alternatively, "\fBpass search .com\fP".
+.TP
+Show existing password
+.B zx2c4@laptop ~ $ pass Email/zx2c4.com
+.br
+sup3rh4x3rizmynam3
+.TP
+Copy existing password to clipboard
+.B zx2c4@laptop ~ $ pass -c Email/zx2c4.com
+.br
+.TP
+Add password to store
+.B zx2c4@laptop ~ $ pass insert Business/cheese-whiz-factory
+.br
+Enter password for Business/cheese-whiz-factory: omg so much cheese what am i gonna do
+.TP
+Add multiline password to store
+.B zx2c4@laptop ~ $ pass insert -m Business/cheese-whiz-factory
+.br
+Enter contents of Business/cheese-whiz-factory and press Ctrl+D when finished:
+.br
+
+.br
+Hey this is my
+.br
+awesome
+.br
+multi
+.br
+line
+.br
+passworrrrrrrrd.
+.br
+^D
+.TP
+Generate new password
+.B zx2c4@laptop ~ $ pass generate Email/jasondonenfeld.com 15
+.br
+The generated password to Email/jasondonenfeld.com is:
+.br
+$(-QF&Q=IN2nFBx
+.TP
+Generate new alphanumeric password
+.B zx2c4@laptop ~ $ pass generate -n Email/jasondonenfeld.com 12
+.br
+The generated password to Email/jasondonenfeld.com is:
+.br
+YqFsMkBeO6di
+.TP
+Generate new password and copy it to the clipboard
+.B zx2c4@laptop ~ $ pass generate -c Email/jasondonenfeld.com 19
+.br
+Copied Email/jasondonenfeld.com to clipboard. Will clear in 45 seconds.
+.TP
+Remove password from store
+.B zx2c4@laptop ~ $ pass remove Business/cheese-whiz-factory
+.br
+rm: remove regular file \[u2018]/home/zx2c4/.password-store/Business/cheese-whiz-factory.gpg\[u2019]? y
+.br
+removed \[u2018]/home/zx2c4/.password-store/Business/cheese-whiz-factory.gpg\[u2019]
+
+.SH EXTENDED GIT EXAMPLE
+Here, we initialize new password store, create a git repository, and then manipulate and sync passwords. Make note of the arguments to the first call of \fBpass git push\fP; consult
+.BR git-push (1)
+for more information.
+
+.br
+mkdir: created directory \[u2018]/home/zx2c4/.password-store\[u2019]
+.br
+
+.B zx2c4@laptop ~ $ pass git init
+.br
+Initialized empty Git repository in /home/zx2c4/.password-store/.git/
+.br
+[master (root-commit) 998c8fd] Added current contents of password store.
+.br
+ 1 file changed, 1 insertion(+)
+.br
+ create mode 100644 .gpg-id
+
+.B zx2c4@laptop ~ $ pass git remote add origin kexec.com:pass-store
+
+.br
+mkdir: created directory \[u2018]/home/zx2c4/.password-store/Amazon\[u2019]
+.br
+.br
+1 file changed, 0 insertions(+), 0 deletions(-)
+.br
+.br
+.br
+<5m,_BrZY`antNDxKN<0A
+
+.B zx2c4@laptop ~ $ pass git push -u --all
+.br
+Counting objects: 4, done.
+.br
+Delta compression using up to 2 threads.
+.br
+Compressing objects: 100% (3/3), done.
+.br
+Writing objects: 100% (4/4), 921 bytes, done.
+.br
+Total 4 (delta 0), reused 0 (delta 0)
+.br
+To kexec.com:pass-store
+.br
+* [new branch] master -> master
+.br
+Branch master set up to track remote branch master from origin.
+
+.br
+.br
+.br
+1 file changed, 0 insertions(+), 0 deletions(-)
+.br
+
+.br
+rm: remove regular file \[u2018]/home/zx2c4/.password-store/Amazon/
[email protected]\[u2019]? y
+.br
+.br
+.br
+.br
+1 file changed, 0 insertions(+), 0 deletions(-)
+.br
+
+.B zx2c4@laptop ~ $ pass git push
+.br
+Counting objects: 9, done.
+.br
+Delta compression using up to 2 threads.
+.br
+Compressing objects: 100% (5/5), done.
+.br
+Writing objects: 100% (7/7), 1.25 KiB, done.
+.br
+Total 7 (delta 0), reused 0 (delta 0)
+.br
+To kexec.com:pass-store
+
+.SH FILES
+
+.TP
+.B ~/.password-store
+The default password storage directory.
+.TP
+.B ~/.password-store/.gpg-id
+Contains the default gpg key identification used for encryption and decryption.
+Multiple gpg keys may be specified in this file, one per line. If this file
+exists in any sub directories, passwords inside those sub directories are
+encrypted using those keys. This should be set using the \fBinit\fP command.
+
+.SH ENVIRONMENT VARIABLES
+
+.TP
+.I PASSWORD_STORE_DIR
+Overrides the default password storage directory.
+.TP
+.I PASSWORD_STORE_KEY
+Overrides the default gpg key identification set by \fBinit\fP. Keys must not
+contain spaces and thus use of the hexidecimal key signature is recommended.
+Multiple keys may be specified separated by spaces.
+.TP
+.I PASSWORD_STORE_GIT
+Overrides the default root of the git repository, which is helpful if
+\fIPASSWORD_STORE_DIR\fP is temporarily set to a sub-directory of the default
+password store.
+.TP
+.I PASSWORD_STORE_GPG_OPTS
+Additional options to be passed to all invocations of GPG.
+.TP
+.I PASSWORD_STORE_X_SELECTION
+Overrides the selection passed to \fBxclip\fP, by default \fIclipboard\fP. See
+.BR xclip (1)
+for more info.
+.TP
+.I PASSWORD_STORE_CLIP_TIME
+Specifies the number of seconds to wait before restoring the clipboard, by default
+\fI45\fP seconds.
+.TP
+.I PASSWORD_STORE_UMASK
+Sets the umask of all files modified by pass, by default \fI077\fP.
+.TP
+.I EDITOR
+The location of the text editor used by \fBedit\fP.
+.SH SEE ALSO
+.BR gpg2 (1),
+.BR pwgen (1),
+.BR git (1),
+.BR xclip (1).
+
+.SH AUTHOR
+.B pass
+was written by
+Jason A. Donenfeld
+.ME .
+For updates and more information, a project page is available on the
+.UR http://\:www.passwordstore.org/
+World Wide Web
+.UE .
+
+.SH COPYING
+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 2
+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, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.