Skip to content

Instantly share code, notes, and snippets.

@augustohp
Created February 28, 2023 19:01
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 augustohp/a7b44d55b3ec70c2e4ea017548d9c837 to your computer and use it in GitHub Desktop.
Save augustohp/a7b44d55b3ec70c2e4ea017548d9c837 to your computer and use it in GitHub Desktop.
#!/bin/bash
# vim: noet ft=sh sw=4 ts=2:
#
# Unattended GPG key generation script.
# Author: Augusto Pascutti <augusto.hp+oss@gmail.com>
set -e
set -o pipefail
APP_NAME=$(basename $0)
APP_VERSION="1.0.0"
APP_AUTHOR="augusto.hp+oss@gmail.com"
OPTION_AUTHORS_FILE="authors-backup.txt"
OPTION_TEMPLATE_FILE="unattended-gen-key.template.txt"
OPTION_TEMPLATE_DIR="templates"
OPTION_KEYS_DIR="keys"
OPTION_AVOID_DELETION="true"
# Utilities -------------------------------------------------------------------
# Usage: echo "Loren Ipsum" | indent
function indent()
{
sed 's/^/ /'
}
# Usage: cat <file> | remove_blank_chars
remove_blank_chars()
{
sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'
}
# Usage: validate_password <password>
validate_password()
{
local password="$1"
[[ -z "$password" ]] && { echo "Error: password is required! " >&2; exit 2; }
# Checks if password has "\" and "#" chars
if [[ "$password" =~ [\\#\`\'\"\&] ]]
then
echo "Error: ${email} password cannot contain: \#\`'\"& " >&2
exit 2
fi
}
# Usage: years_in_future <years>
years_in_future()
{
local years="$1"
[[ -z "$years" ]] && { echo "Error: years is required! " >&2; exit 2; }
date -v+"$years"y +%Y-%m-%d
}
# Usage: gpg_list_keys <email>
gpg_list_keys()
{
local email="$1"
[[ -z "$email" ]] && { echo "Error: email is required! " >&2; exit 2; }
gpg --quiet --list-keys | \
grep -B 1 "$email" | \
grep -v "$email" | \
grep -v '^--$' | \
remove_blank_chars
}
# Usage: gpg_has_key <email>
gpg_has_key()
{
local email="$1"
[[ -n "$(gpg_list_keys "$email")" ]] && return 0 || return 1
}
# Usage: gpg_delete_keys <email>
gpg_delete_keys()
{
local email="$1"
[[ -z "$email" ]] && { echo "Error: email is required! " >&2; exit 2; }
echo "Listing all keys for $email ..."
for key_id in $(gpg_list_keys "$email")
do
echo "Deleting secret and public keys for $key_id ..." | indent
gpg --quiet --batch --yes --delete-secret-and-public-key "$key_id" 2>&1 | indent
done
}
# Usage: gpg_export_private_keys <email> <password> <output_dir>
gpg_export_private_keys()
{
local email="$1"
local password="$2"
local output_dir="$3"
[[ -d "$output_dir" ]] || { echo "Error: $output_dir does not exist! " >&2; exit 2; }
for key_id in $(gpg_list_keys "$email" | head -n 1)
do
gpg --batch --yes --export-secret-keys --pinentry-mode loopback --passphrase "${password}" "$key_id" > "$output_dir/$email.$key_id.private.key"
done
}
# Usage: generate_key <name> <email> <password>
generate_key()
{
local name="$1"
local email="$2"
local password="$3"
expiration="$(years_in_future 2)"
[[ -z "$name" ]] && { echo "Error: name is required! " >&2; exit 2; }
[[ -z "$email" ]] && { echo "Error: email is required! " >&2; exit 2; }
[[ -z "$password" ]] && { echo "Error: password is required! " >&2; exit 2; }
validate_password "$password"
current_template=$(mktemp -t "${APP_NAME}.XXXXXX") || { echo "Error: Could not create temporary file! " >&2; exit 2; }
cat > "$current_template" <<-EOT
%echo Creating private key...
Key-Type: EDDSA
Key-Curve: ed25519
Subkey-Type: ECDH
Subkey-Curve: cv25519
Name-Real: %NAME%
Name-Comment: Chave para acesso ao repositório de segredos.
Name-Email: %EMAIL%
Expire-Date: %EXPIRE_DATE%
Passphrase: %PASSWORD%
# Do a commit here, so that we can later print "done" :-)
%commit
%echo done
EOT
echo "🔑 ${person_name}"
if [ "$OPTION_AVOID_DELETION" == "false" ]
then
if gpg_has_key "$email"
then
echo "Key already exists. Removing..." | indent
gpg_delete_keys "$email" | indent | indent
fi
fi
sed -i '' "s#%NAME%#${name}#" "$current_template"
sed -i '' "s#%EMAIL%#${email}#" "$current_template"
sed -i '' "s#%PASSWORD%#${password}#" "$current_template"
sed -i '' "s#%EXPIRE_DATE%#${expiration}#" "$current_template"
gpg --quiet --batch --generate-key "${current_template}" 2>&1 | indent
gpg_export_private_keys "${email}" "${password}" "${OPTION_KEYS_DIR}" | indent
if [ "$OPTION_AVOID_DELETION" == "false" ]
then
echo "Removing key because it is not necessary anymore..." | indent
gpg_delete_keys "$email" | indent | indent
fi
}
# Usage: generate_keys <authors_file>
generate_keys()
{
local authors_file="$1"
[[ -z "$authors_file" ]] && { echo "Error: authors file is required! " >&2; exit 2; }
[[ -f "$authors_file" ]] || { echo "Error: $authors_file does not exist! " >&2; exit 2; }
while IFS=$'\t' read -r person_name person_email person_password
do
generate_key "$person_name" "$person_email" "$person_password"
done < "$authors_file"
}
# Usage remove_keys <author_file>
remove_keys()
{
local authors_file="$1"
[[ -z "$authors_file" ]] && { echo "Error: authors file is required! " >&2; exit 2; }
[[ -f "$authors_file" ]] || { echo "Error: $authors_file does not exist! " >&2; exit 2; }
while IFS=$'\t' read -r person_name person_email person_password
do
echo "🗑 ${person_name}"
if gpg_has_key "$person_email"
then
gpg_delete_keys "$person_email" | indent
fi
done < "$authors_file"
}
# Usage: display_help
display_help()
{
cat <<-EOT
Usage: $APP_NAME [options] generate-keys <authors file path>
$APP_NAME [options] generate-key <name> <email> <password>
$APP_NAME [options] remove-keys <authors file path>
$APP_NAME [options] remove-keys <email>
$APP_NAME -h | --help
$APP_NAME -v | --version
This script helps creating GPG keys given a list of authors. It is usefull
when you need to generate multiple keys for a team, for example.
Options
--debug Displays all commands being executed (set -x).
-h --help Show this screen.
-v --version Show version.
-t --template-dir <template dir path> Path to the template dir. [default: $OPTION_TEMPLATE_DIR]
-k --keys-dir <keys dir path> Path to the keys dir. [default: $OPTION_KEYS_DIR]
--delete-keys Delete keys after generating them. [default: $OPTION_DELETE_KEYS]
Authors file format (TSV)
One author per line, with columns separated by TAB, with the columns: name,
email, password.
Bugs and suggestions can be sent to $APP_AUTHOR.
EOT
}
# Sanity checks ---------------------------------------------------------------
[[ -f "$OPTION_AUTHORS_FILE" ]] || { echo "Error: $OPTION_AUTHORS_FILE does not exist! " >&2; exit 2; }
[[ -f "$OPTION_TEMPLATE_FILE" ]] || { echo "Error: $OPTION_TEMPLATE_FILE does not exist! " >&2; exit 2; }
[[ -d "$OPTION_TEMPLATE_DIR" ]] || { echo "Error: $OPTION_TEMPLATE_DIR does not exist! " >&2; exit 2; }
[[ -d "$OPTION_KEYS_DIR" ]] || { echo "Error: $OPTION_KEYS_DIR does not exist! " >&2; exit 2; }
for dependency in sed grep gpg
do
which "$dependency" > /dev/null || { echo "Error: $dependency is not installed! " >&2; exit 2; }
done
# Main ------------------------------------------------------------------------
if [ $# -eq 0 ]
then
echo "Error: No arguments provided! Try '$APP_NAME --help'" >&2
exit 2
fi
# Parsing command options
while [ $# -gt 0 ]
do
case "$1" in
-h|--help)
display_help
exit 0
;;
-v|--version)
echo "$APP_NAME $APP_VERSION"
exit 0
;;
--debug)
set -x
;;
-t|--template-dir)
OPTION_TEMPLATE_DIR="$2"
shift
;;
-k|--keys-dir)
OPTION_KEYS_DIR="$2"
shift
;;
generate-keys|gen-keys)
OPTION_AUTHORS_FILE="$2"
shift
generate_keys "${OPTION_AUTHORS_FILE}"
exit 0
;;
generate-key|gen-key)
OPTION_NAME="$2"
OPTION_EMAIL="$3"
OPTION_PASSWORD="$4"
shift 3
generate_key "$OPTION_NAME" "$OPTION_EMAIL" "$OPTION_PASSWORD"
exit 0
;;
remove-keys)
OPTION_AUTHORS_FILE="$2"
shift
remove_keys "${OPTION_AUTHORS_FILE}"
exit 0
;;
remove-key)
OPTION_EMAIL="$2"
shift
gpg_delete_keys "$OPTION_EMAIL"
exit 0
;;
--)
shift
break
;;
*)
echo "Error: Invalid option $1! " >&2
exit 2
;;
esac
shift
done
# Usage: echo " [ultimate] Diego Rabatone (Chave para acesso ao repositório de segredos.) <diego.rabatone@kobold.com.br>" | filter_email
filter_email()
{
sed -E 's/.*<(.*)>.*/\1/'
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment