#!/bin/bash # pkce - Generate a PKCE code verifier and its corresponding S256 code challenge # # Usage: # pkce [-n|--newline] # pkce -h # # Options: # -n, --newline Separate verifier and challenge with a newline (default: tab) # -h, --help Show help message # # Reference: # https://datatracker.ietf.org/doc/html/rfc7636 _pkce() ( local SCRIPT_NAME; SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" case "${BASH_SOURCE[0]}" in /dev/*|/proc/*) SCRIPT_NAME="" ;; esac case "$SCRIPT_NAME" in ""|bash|sh|zsh|dash) SCRIPT_NAME="pkce" ;; esac _error() { echo "[ERR][$SCRIPT_NAME] $*" >&2; } # Converts base64 to base64-url (URL-safe version) _base64_to_base64url() { # Remove padding (=) and newlines, replace '+' with '-' and '/' with '_' tr -d '=\n' | tr '+/' '-_' } _show_help() { echo "NAME" echo " $SCRIPT_NAME - generate PKCE code verifier and challenge" echo "SYNOPSIS" echo " $SCRIPT_NAME [-n | --newline]" echo " $SCRIPT_NAME -h" echo "DESCRIPTION" echo " Generates a 128-character PKCE code verifier and its S256 code" echo " challenge per RFC 7636. By default the two values are separated" echo " by a tab; use -n to separate with a newline instead." echo "" echo " Reference: https://datatracker.ietf.org/doc/html/rfc7636" echo "OPTIONS" echo " -n, --newline Separate verifier and challenge with a newline" echo " instead of a tab" echo " -h, --help Show this help message" echo "EXAMPLES" echo " codes=\"\$($SCRIPT_NAME)\"" echo " verifier=\"\$(echo \"\$codes\" | cut -f1)\"" echo " challenge=\"\$(echo \"\$codes\" | cut -f2)\"" echo "" echo " { read -r verifier; read -r challenge; } < <($SCRIPT_NAME -n)" echo "EXIT STATUS" echo " 0 Success" echo " 2 Usage error (unknown option)" echo " 3 Dependency error (openssl missing)" echo "DEPENDENCIES" echo " openssl" } _expand_short_opts() { # $1 = string of short-opt letters that take a value (e.g. "nXHd"); "" for flag-only scripts # $2..$N = "$@" # Populates _EXPANDED; caller does: set -- "${_EXPANDED[@]}"; unset _EXPANDED local value_opts="$1"; shift _EXPANDED=() local passthru="" local arg local rest local c for arg in "$@"; do if [ -n "$passthru" ]; then _EXPANDED+=("$arg"); continue; fi case "$arg" in --) passthru=1; _EXPANDED+=("$arg") ;; --*|-|"") _EXPANDED+=("$arg") ;; -[a-zA-Z]?*) rest="${arg#-}" while [ -n "$rest" ]; do c="${rest%"${rest#?}"}"; rest="${rest#?}" _EXPANDED+=("-$c") case "$value_opts" in *"$c"*) [ -n "$rest" ] && _EXPANDED+=("$rest") rest="" ;; esac done ;; *) _EXPANDED+=("$arg") ;; esac done } # Default to tab-separated output; -n|--newline switches to newline local delimiter='\t' _expand_short_opts "" "$@" set -- "${_EXPANDED[@]}"; unset _EXPANDED while [ $# -gt 0 ]; do case "$1" in -h|--help) _show_help; return 0 ;; -n|--newline) delimiter='\n'; shift ;; -*) _error "Unknown argument '$1'. Run \`$SCRIPT_NAME -h\` for usage"; return 2 ;; *) _error "Unknown argument '$1'. Run \`$SCRIPT_NAME -h\` for usage"; return 2 ;; esac done # Fail fast if openssl is missing -- otherwise openssl-rand (below) yields # an empty verifier and the subsequent dgst call silently produces garbage if ! command -v openssl >/dev/null 2>&1; then _error "openssl is required" return 3 fi # RFC7636 specifies code verifier should be 43 to 128 random characters # 32 bytes in base64 is 43 characters (unpadded) # 96 bytes in base64 is 128 characters (unpadded) # https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 local code_verifier; code_verifier="$(openssl rand -base64 96 | _base64_to_base64url)" local code_challenge; code_challenge="$(printf %s "$code_verifier" | openssl dgst -binary -sha256 | openssl enc -base64 | _base64_to_base64url)" # Print code verifier and code challenge, separated by a tab or newline printf "%s$delimiter%s" "$code_verifier" "$code_challenge" ) _pkce "$@" __pkce_rc=$? unset -f _pkce if [ -n "${BASH_SOURCE[0]}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then eval "unset __pkce_rc; return $__pkce_rc" fi eval "unset __pkce_rc; exit $__pkce_rc"