|
#!/usr/bin/env zsh |
|
set -euo pipefail |
|
|
|
umask 077 |
|
|
|
step_no=1 |
|
work_dir="" |
|
|
|
cleanup() { |
|
if [[ -n "${work_dir:-}" && -d "$work_dir" ]]; then |
|
rm -rf "$work_dir" |
|
work_dir="" |
|
fi |
|
} |
|
|
|
abort_script() { |
|
local exit_code="${1:-130}" |
|
|
|
print -u2 -- "" |
|
print -u2 -- "Interrupted; exiting." |
|
trap - EXIT INT TERM |
|
cleanup |
|
exit "$exit_code" |
|
} |
|
|
|
trap cleanup EXIT |
|
trap 'abort_script 130' INT |
|
trap 'abort_script 143' TERM |
|
|
|
fail() { |
|
print -u2 -- "ERROR: $*" |
|
exit 1 |
|
} |
|
|
|
step() { |
|
print |
|
print -- "== Step ${step_no}: $*" |
|
(( step_no++ )) |
|
} |
|
|
|
prompt_input() { |
|
local prompt="$1" |
|
|
|
REPLY="" |
|
if [[ -t 0 && -t 1 ]]; then |
|
vared -p "$prompt" -c REPLY || abort_script 130 |
|
else |
|
print -rn -- "$prompt" |
|
IFS= read -r REPLY || fail "No input received for prompt: $prompt" |
|
fi |
|
} |
|
|
|
ask_yes_no() { |
|
local prompt="$1" |
|
local default="${2:-n}" |
|
local reply suffix |
|
|
|
if [[ "$default" == "y" ]]; then |
|
suffix="[Y/n]" |
|
else |
|
suffix="[y/N]" |
|
fi |
|
|
|
while true; do |
|
prompt_input "${prompt} ${suffix} " |
|
reply="${REPLY}" |
|
reply="${reply:l}" |
|
|
|
if [[ -z "$reply" ]]; then |
|
[[ "$default" == "y" ]] && return 0 || return 1 |
|
fi |
|
|
|
case "$reply" in |
|
y|yes) return 0 ;; |
|
n|no) return 1 ;; |
|
*) print -- "Please answer yes or no." ;; |
|
esac |
|
done |
|
} |
|
|
|
require_cmd() { |
|
command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1" |
|
} |
|
|
|
collect_secret_fprs() { |
|
gpg --with-colons --fingerprint --list-secret-keys 2>/dev/null | |
|
awk -F: '/^sec/ { want=1; next } want && /^fpr:/ { print $10; want=0 }' |
|
} |
|
|
|
contains_value() { |
|
local needle="$1" |
|
shift |
|
local item |
|
for item in "$@"; do |
|
[[ "$item" == "$needle" ]] && return 0 |
|
done |
|
return 1 |
|
} |
|
|
|
gpg_with_passphrase() { |
|
print -r -- "$passphrase" | |
|
gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 "$@" |
|
} |
|
|
|
op_ref() { |
|
print -r -- "op://${op_vault}/${op_item}/${op_field}" |
|
} |
|
|
|
generate_1password_passphrase() { |
|
local generated |
|
|
|
generated="$( |
|
op item create --category=password --title "${op_item} generated passphrase preview" \ |
|
--vault "$op_vault" --generate-password='letters,digits,symbols,64' \ |
|
--dry-run --reveal --format json | |
|
jq -r '.fields[] | select(.id == "password") | .value // empty' |
|
)" || return 1 |
|
|
|
[[ -n "$generated" ]] || return 1 |
|
print -r -- "$generated" |
|
} |
|
|
|
create_1password_api_credential_item() { |
|
local credential="$1" |
|
|
|
print -rn -- "$credential" | |
|
jq -Rs --arg title "$op_item" '{ |
|
title: $title, |
|
category: "API_CREDENTIAL", |
|
fields: [ |
|
{ |
|
id: "credential", |
|
type: "CONCEALED", |
|
label: "credential", |
|
value: . |
|
}, |
|
{ |
|
id: "notesPlain", |
|
type: "STRING", |
|
purpose: "NOTES", |
|
label: "notesPlain", |
|
value: "Created by gpg-setup.sh for a GPG key passphrase." |
|
} |
|
] |
|
}' | |
|
op item create --vault "$op_vault" --tags "gpg,gpg-key" - >/dev/null |
|
} |
|
|
|
date_add_iso() { |
|
local amount="$1" |
|
local unit="$2" |
|
local mac_unit="$2" |
|
local gnu_unit result |
|
|
|
case "$unit" in |
|
d) gnu_unit="days" ;; |
|
w) gnu_unit="weeks" ;; |
|
m) gnu_unit="months" ;; |
|
y) gnu_unit="years" ;; |
|
*) return 1 ;; |
|
esac |
|
|
|
if result="$(date "-v+${amount}${mac_unit}" +%F 2>/dev/null)"; then |
|
print -r -- "$result" |
|
return 0 |
|
fi |
|
|
|
if result="$(date -d "+${amount} ${gnu_unit}" +%F 2>/dev/null)"; then |
|
print -r -- "$result" |
|
return 0 |
|
fi |
|
|
|
return 1 |
|
} |
|
|
|
gpg_expiry_to_iso() { |
|
local value="$1" |
|
local amount unit |
|
|
|
if [[ "$value" == "0" ]]; then |
|
return 1 |
|
fi |
|
|
|
if [[ "$value" =~ "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" ]]; then |
|
print -r -- "$value" |
|
return 0 |
|
fi |
|
|
|
if [[ "$value" =~ "^([0-9]+)([dwmy])$" ]]; then |
|
amount="$match[1]" |
|
unit="$match[2]" |
|
date_add_iso "$amount" "$unit" |
|
return $? |
|
fi |
|
|
|
return 1 |
|
} |
|
|
|
update_1password_expiry() { |
|
local expires="$1" |
|
local valid_from="$2" |
|
|
|
op item edit "$op_item" --vault "$op_vault" \ |
|
"validFrom[date]=$valid_from" \ |
|
"expires[date]=$expires" >/dev/null || |
|
fail "Could not set 1Password expiry on '$op_item'." |
|
print -- "Set native 1Password expiry to $expires." |
|
} |
|
|
|
write_pinentry_script() { |
|
local pinentry_path="$1" |
|
local config_path="$2" |
|
local vault="$3" |
|
local item="$4" |
|
local field="$5" |
|
|
|
mkdir -p "${pinentry_path:h}" "${config_path:h}" |
|
chmod 700 "${pinentry_path:h}" "${config_path:h}" |
|
|
|
{ |
|
print -r -- "# Written by gpg-setup.sh. Contains no secret, only the 1Password item location." |
|
print -r -- "OP_GPG_VAULT=${(qq)vault}" |
|
print -r -- "OP_GPG_ITEM=${(qq)item}" |
|
print -r -- "OP_GPG_FIELD=${(qq)field}" |
|
} > "$config_path" |
|
chmod 600 "$config_path" |
|
|
|
cat > "$pinentry_path" <<'PINENTRY' |
|
#!/usr/bin/env zsh |
|
set -u |
|
|
|
CONFIG_FILE="${PINENTRY_1PASSWORD_CONFIG:-$HOME/.config/gpg-1password-pinentry/config.zsh}" |
|
LOG_FILE="${PINENTRY_1PASSWORD_LOG:-/dev/null}" |
|
|
|
log_line() { |
|
print -r -- "$*" >> "$LOG_FILE" |
|
} |
|
|
|
assuan_escape() { |
|
local value="$1" |
|
value="${value//\%/%25}" |
|
value="${value//$'\r'/%0D}" |
|
value="${value//$'\n'/%0A}" |
|
print -r -- "$value" |
|
} |
|
|
|
read_passphrase() { |
|
[[ -r "$CONFIG_FILE" ]] && source "$CONFIG_FILE" |
|
|
|
local vault="${OP_GPG_VAULT:?OP_GPG_VAULT is not set}" |
|
local item="${OP_GPG_ITEM:?OP_GPG_ITEM is not set}" |
|
local field="${OP_GPG_FIELD:-credential}" |
|
|
|
op read "op://${vault}/${item}/${field}" |
|
} |
|
|
|
print -r -- "OK" |
|
|
|
while IFS= read -r line; do |
|
cmd="${line%% *}" |
|
log_line "$(date '+%Y-%m-%d %H:%M:%S') ${line}" |
|
|
|
case "$cmd" in |
|
GETPIN) |
|
if secret="$(read_passphrase 2>>"$LOG_FILE")"; then |
|
print -r -- "D $(assuan_escape "$secret")" |
|
print -r -- "OK" |
|
else |
|
print -r -- "ERR 83886179 1Password lookup failed" |
|
fi |
|
;; |
|
BYE) |
|
print -r -- "OK" |
|
exit 0 |
|
;; |
|
\#*|OPTION|SETDESC|SETPROMPT|SETTITLE|SETKEYINFO|SETOK|SETCANCEL|SETNOTOK|SETERROR) |
|
print -r -- "OK" |
|
;; |
|
CONFIRM) |
|
print -r -- "OK" |
|
;; |
|
*) |
|
print -r -- "OK" |
|
;; |
|
esac |
|
done |
|
PINENTRY |
|
|
|
chmod 700 "$pinentry_path" |
|
} |
|
|
|
configure_gpg_agent() { |
|
local agent_conf="$1" |
|
local pinentry_path="$2" |
|
local backup tmp |
|
|
|
mkdir -p "${agent_conf:h}" |
|
chmod 700 "${agent_conf:h}" |
|
|
|
if [[ -f "$agent_conf" ]]; then |
|
backup="${agent_conf}.backup.$(date +%Y%m%d-%H%M%S)" |
|
cp -p "$agent_conf" "$backup" |
|
print -- "Backed up existing gpg-agent.conf to: $backup" |
|
fi |
|
|
|
tmp="${agent_conf}.tmp.$$" |
|
if [[ -f "$agent_conf" ]]; then |
|
awk ' |
|
/^[[:space:]]*pinentry-program([[:space:]]|$)/ { next } |
|
/^[[:space:]]*allow-loopback-pinentry([[:space:]]|$)/ { next } |
|
{ print } |
|
' "$agent_conf" > "$tmp" |
|
else |
|
: > "$tmp" |
|
fi |
|
|
|
{ |
|
print -r -- "" |
|
print -r -- "# Added by gpg-setup.sh on $(date -u '+%Y-%m-%dT%H:%M:%SZ')" |
|
print -r -- "pinentry-program $pinentry_path" |
|
print -r -- "allow-loopback-pinentry" |
|
} >> "$tmp" |
|
|
|
mv "$tmp" "$agent_conf" |
|
chmod 600 "$agent_conf" |
|
} |
|
|
|
restart_gpg_agent() { |
|
gpg-connect-agent killagent /bye >/dev/null 2>&1 || true |
|
gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1 || true |
|
} |
|
|
|
step "Check required commands" |
|
require_cmd gpg |
|
require_cmd gpgconf |
|
require_cmd gpg-connect-agent |
|
require_cmd op |
|
require_cmd awk |
|
require_cmd date |
|
require_cmd jq |
|
print -- "Found required commands." |
|
|
|
step "Choose the 1Password item that holds the GPG passphrase" |
|
default_vault="${OP_GPG_VAULT:-Employee}" |
|
default_item="${OP_GPG_ITEM:-gpg_key}" |
|
op_field="credential" |
|
|
|
prompt_input "1Password vault [${default_vault}]: " |
|
op_vault="${REPLY}" |
|
op_vault="${op_vault:-$default_vault}" |
|
|
|
prompt_input "1Password item [${default_item}]: " |
|
op_item="${REPLY}" |
|
op_item="${op_item:-$default_item}" |
|
|
|
print -- "Passphrase source: $(op_ref)" |
|
print -- "The passphrase is stored as an API Credential and used without printing it." |
|
ask_yes_no "Verify access to this 1Password item now?" "y" || fail "Stopped before verifying 1Password access." |
|
|
|
if existing_category="$(op item get "$op_item" --vault "$op_vault" --format json 2>/dev/null | jq -r '.category // empty')"; then |
|
[[ "$existing_category" == "API_CREDENTIAL" ]] || |
|
fail "Item '$op_item' in vault '$op_vault' is a ${existing_category:-non-API-Credential} item. This script keeps the passphrase in the 'credential' field of an API Credential item so 1Password can track the key's expiry. Choose a different item name or convert the existing one." |
|
else |
|
print -- "Could not find item '$op_item' in 1Password vault '$op_vault'." |
|
print -- "It will be created as an API Credential item so 1Password can track the key's expiry." |
|
if ask_yes_no "Create '$op_item' now with a 1Password-generated 64-character credential?" "y"; then |
|
generated_passphrase="$(generate_1password_passphrase)" || |
|
fail "Could not generate a 1Password passphrase preview." |
|
create_1password_api_credential_item "$generated_passphrase" || |
|
fail "Could not create 1Password item '$op_item' in vault '$op_vault'." |
|
unset generated_passphrase |
|
print -- "Created $(op_ref) with a 1Password-generated credential." |
|
else |
|
fail "Stopped because the 1Password item does not exist." |
|
fi |
|
fi |
|
|
|
passphrase="$(op read "$(op_ref)")" || |
|
fail "Could not verify access to $(op_ref). Unlock/sign in to 1Password CLI and retry." |
|
|
|
[[ -n "$passphrase" ]] || fail "The 1Password field is empty." |
|
[[ "$passphrase" != *$'\n'* ]] || fail "The passphrase contains a newline; this script expects a single-line passphrase." |
|
print -- "1Password secret read successfully. The passphrase was not printed." |
|
|
|
step "Review existing GPG secret keys" |
|
gpg_home="$(gpgconf --list-dirs homedir)" |
|
print -- "GPG home: $gpg_home" |
|
|
|
if gpg --list-secret-keys --keyid-format LONG; then |
|
existing_fprs=(${(f)"$(collect_secret_fprs)"}) |
|
else |
|
existing_fprs=() |
|
fi |
|
|
|
if (( ${#existing_fprs[@]} == 0 )); then |
|
print -- "No existing secret keys found." |
|
else |
|
print |
|
print -- "The script can delete every secret key listed above, plus its matching public key." |
|
print -- "This is destructive. Only continue if these are the forgotten-passphrase keys you want to replace." |
|
if ask_yes_no "Delete all listed secret/public keys?" "n"; then |
|
prompt_input "Type DELETE-ALL-GPG-KEYS to confirm deletion: " |
|
delete_confirm="${REPLY}" |
|
[[ "$delete_confirm" == "DELETE-ALL-GPG-KEYS" ]] || fail "Deletion confirmation did not match; stopped." |
|
|
|
for fpr in "${existing_fprs[@]}"; do |
|
print -- "Deleting $fpr" |
|
gpg --batch --yes --delete-secret-and-public-key "$fpr" |
|
done |
|
else |
|
print -- "Skipped key deletion." |
|
fi |
|
fi |
|
|
|
step "Choose new GPG key details" |
|
if command -v git >/dev/null 2>&1; then |
|
default_name="$(git config --global user.name 2>/dev/null || true)" |
|
default_email="$(git config --global user.email 2>/dev/null || true)" |
|
else |
|
default_name="" |
|
default_email="" |
|
fi |
|
default_expiry="1y" |
|
|
|
prompt_input "Name for the new key${default_name:+ [$default_name]}: " |
|
real_name="${REPLY}" |
|
real_name="${real_name:-$default_name}" |
|
[[ -n "$real_name" ]] || fail "Name is required." |
|
|
|
prompt_input "Email for the new key${default_email:+ [$default_email]}: " |
|
email="${REPLY}" |
|
email="${email:-$default_email}" |
|
[[ -n "$email" ]] || fail "Email is required." |
|
|
|
prompt_input "Optional comment, usually blank: " |
|
comment="${REPLY}" |
|
|
|
prompt_input "Expiry, for example 1y, 2y, 0 for never [${default_expiry}]: " |
|
expiry="${REPLY}" |
|
expiry="${expiry:-$default_expiry}" |
|
onepassword_valid_from="$(date +%F)" |
|
onepassword_expiry_date="" |
|
if onepassword_expiry_date="$(gpg_expiry_to_iso "$expiry")"; then |
|
: |
|
else |
|
onepassword_expiry_date="" |
|
fi |
|
|
|
print -- "Key type:" |
|
print -- " 1) ed25519 primary key + cv25519 encryption subkey (recommended)" |
|
print -- " 2) rsa4096 primary key + rsa4096 encryption subkey" |
|
prompt_input "Choose key type [1]: " |
|
key_choice="${REPLY}" |
|
key_choice="${key_choice:-1}" |
|
case "$key_choice" in |
|
1) key_type="ed25519" ;; |
|
2) key_type="rsa4096" ;; |
|
*) fail "Unknown key type choice: $key_choice" ;; |
|
esac |
|
|
|
uid="$real_name" |
|
[[ -n "$comment" ]] && uid+=" ($comment)" |
|
uid+=" <$email>" |
|
|
|
print |
|
print -- "New key summary:" |
|
print -- " User ID: $uid" |
|
print -- " Expiry: $expiry" |
|
if [[ -n "$onepassword_expiry_date" ]]; then |
|
print -- " 1Password expiry: $onepassword_expiry_date" |
|
else |
|
print -- " 1Password expiry: not set for '$expiry'" |
|
fi |
|
print -- " Type: $key_type" |
|
ask_yes_no "Generate this new GPG key using the 1Password passphrase?" "y" || fail "Stopped before key generation." |
|
|
|
step "Generate new GPG key" |
|
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/gpg-setup.XXXXXX")" |
|
chmod 700 "$work_dir" |
|
keygen_log="$work_dir/keygen.log" |
|
subkey_log="$work_dir/subkey.log" |
|
|
|
before_fprs=(${(f)"$(collect_secret_fprs)"}) |
|
|
|
if [[ "$key_type" == "rsa4096" ]]; then |
|
primary_algo="rsa4096" |
|
subkey_algo="rsa4096" |
|
else |
|
primary_algo="ed25519" |
|
subkey_algo="cv25519" |
|
fi |
|
|
|
if ! gpg_with_passphrase --status-fd=1 --quick-generate-key "$uid" "$primary_algo" sign "$expiry" > "$keygen_log" 2>&1; then |
|
print -u2 -- "GPG key generation failed. Output:" |
|
sed 's/^/ /' "$keygen_log" >&2 |
|
fail "Stopped after failed key generation." |
|
fi |
|
|
|
new_fpr="$(awk '/^\[GNUPG:\] KEY_CREATED/ { print $4; exit }' "$keygen_log")" |
|
if [[ -z "$new_fpr" ]]; then |
|
after_fprs=(${(f)"$(collect_secret_fprs)"}) |
|
new_fprs=() |
|
for fpr in "${after_fprs[@]}"; do |
|
if ! contains_value "$fpr" "${before_fprs[@]}"; then |
|
new_fprs+=("$fpr") |
|
fi |
|
done |
|
(( ${#new_fprs[@]} == 1 )) || fail "Could not determine the new key fingerprint automatically." |
|
new_fpr="$new_fprs[1]" |
|
fi |
|
|
|
if ! gpg_with_passphrase --quick-add-key "$new_fpr" "$subkey_algo" encr "$expiry" > "$subkey_log" 2>&1; then |
|
print -u2 -- "GPG encryption subkey generation failed. Output:" |
|
sed 's/^/ /' "$subkey_log" >&2 |
|
fail "Stopped after failed subkey generation. The primary key was created: $new_fpr" |
|
fi |
|
|
|
key_id="${new_fpr[-16,-1]}" |
|
short_fpr="${new_fpr[-12,-1]}" |
|
print -- "Created new key:" |
|
print -- " Fingerprint: $new_fpr" |
|
print -- " Key ID: $key_id" |
|
|
|
step "Set 1Password expiry reminder" |
|
if [[ -n "$onepassword_expiry_date" ]]; then |
|
update_1password_expiry "$onepassword_expiry_date" "$onepassword_valid_from" |
|
else |
|
print -- "Skipped 1Password expiry reminder because '$expiry' does not map to a reminder date." |
|
fi |
|
|
|
step "Export public key, private key, and revocation certificate" |
|
public_file="$work_dir/${short_fpr}-public.asc" |
|
private_file="$work_dir/${short_fpr}-private.asc" |
|
revocation_source="${gpg_home}/openpgp-revocs.d/${new_fpr}.rev" |
|
revocation_file="$work_dir/${short_fpr}-revocation-cert.rev" |
|
|
|
gpg --armor --export "$new_fpr" > "$public_file" |
|
gpg_with_passphrase --armor --export-secret-keys "$new_fpr" > "$private_file" |
|
|
|
[[ -r "$revocation_source" ]] || |
|
fail "Could not find the auto-generated revocation certificate at $revocation_source" |
|
cp "$revocation_source" "$revocation_file" |
|
chmod 600 "$public_file" "$private_file" "$revocation_file" |
|
print -- "Exports prepared in a temporary private directory. They will be removed from disk when the script exits." |
|
|
|
step "Store exported artifacts in 1Password" |
|
timestamp="$(date +%Y%m%d-%H%M%S)" |
|
doc_prefix="${op_item} ${short_fpr} ${timestamp}" |
|
public_title="${doc_prefix} public key" |
|
private_title="${doc_prefix} private key" |
|
revocation_title="${doc_prefix} revocation certificate" |
|
|
|
print -- "The following 1Password document items will be created in vault '$op_vault':" |
|
print -- " $public_title" |
|
print -- " $private_title" |
|
print -- " $revocation_title" |
|
ask_yes_no "Create these 1Password documents now?" "y" || fail "Stopped before storing artifacts in 1Password." |
|
|
|
op document create "$public_file" --vault "$op_vault" --title "$public_title" --tags "gpg,gpg-key" >/dev/null |
|
op document create "$private_file" --vault "$op_vault" --title "$private_title" --tags "gpg,gpg-key,secret" >/dev/null |
|
op document create "$revocation_file" --vault "$op_vault" --title "$revocation_title" --tags "gpg,gpg-key,revocation" >/dev/null |
|
|
|
print -- "Created 1Password document items." |
|
|
|
if ask_yes_no "Add the new key metadata to op://${op_vault}/${op_item}?" "y"; then |
|
op item edit "$op_item" --vault "$op_vault" \ |
|
"GPG.fingerprint=$new_fpr" \ |
|
"GPG.key_id=$key_id" \ |
|
"GPG.user_id=$uid" \ |
|
"GPG.public_key_document=$public_title" \ |
|
"GPG.private_key_document=$private_title" \ |
|
"GPG.revocation_certificate_document=$revocation_title" >/dev/null |
|
print -- "Updated metadata on op://${op_vault}/${op_item}." |
|
fi |
|
|
|
step "Install and configure 1Password pinentry for gpg-agent" |
|
pinentry_path="${PINENTRY_1PASSWORD_PATH:-$HOME/.local/bin/pinentry-1password.sh}" |
|
pinentry_config="${PINENTRY_1PASSWORD_CONFIG:-$HOME/.config/gpg-1password-pinentry/config.zsh}" |
|
agent_conf="${gpg_home}/gpg-agent.conf" |
|
|
|
print -- "Pinentry script: $pinentry_path" |
|
print -- "Pinentry config: $pinentry_config" |
|
print -- "gpg-agent config: $agent_conf" |
|
ask_yes_no "Install the pinentry wrapper and update gpg-agent.conf?" "y" || fail "Stopped before pinentry setup." |
|
|
|
write_pinentry_script "$pinentry_path" "$pinentry_config" "$op_vault" "$op_item" "$op_field" |
|
configure_gpg_agent "$agent_conf" "$pinentry_path" |
|
restart_gpg_agent |
|
print -- "gpg-agent restarted." |
|
|
|
step "Optional signing test" |
|
print -- "This test kills/restarts gpg-agent and signs a short message without loopback mode." |
|
print -- "If 1Password is locked, you may need to approve the 1Password CLI prompt." |
|
if ask_yes_no "Run the signing test now?" "y"; then |
|
restart_gpg_agent |
|
if print -r -- "gpg-setup signing test $(date -u '+%Y-%m-%dT%H:%M:%SZ')" | |
|
gpg --armor --local-user "$new_fpr" --clearsign >/dev/null; then |
|
print -- "Signing test passed." |
|
else |
|
fail "Signing test failed. Check $agent_conf and try: gpg-connect-agent killagent /bye" |
|
fi |
|
fi |
|
|
|
step "Done" |
|
print -- "New GPG key fingerprint: $new_fpr" |
|
print -- "Public/private/revocation artifacts were stored in 1Password vault '$op_vault'." |
|
print -- "The passphrase remains in op://${op_vault}/${op_item}/${op_field}." |
|
print -- "The pinentry wrapper is installed at: $pinentry_path" |
|
print -- "To show the public key later, run:" |
|
print -- " gpg --armor --export $new_fpr" |