#!/bin/bash
#
# Bash library and executable that backup or restore OpenPGP ID using printed
# QR codes and Shamir's secret sharing, configure Yubikeys or Nitrokeys for OpenPGP ID.
#
# Copyright © 2025-2026 Jean-Jacques Brucker <jjbrucker@foopgp.org>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
# shellcheck disable=SC2013 # To read lines rather than words
# shellcheck disable=SC2034 # variable appears unused
# shellcheck disable=SC2046 # Quote this to prevent word splitting
# shellcheck disable=SC2086 # Double quote warning
# shellcheck disable=SC2128 # expanding array without index warning

##TODO: check gpg version, because compatibility may change (tested with gpg (GnuPG) 2.4.7 libgcrypt 1.11.0)

if [[ "$1" == --bash-completion ]] ; then
	BL_tmp_a=$("${BASH_SOURCE[0]}" --help | sed -nE 's:^ {2,4}([a-z0-9_]+\>).*:\1:p')
	BL_tmp_o=$(for a in $BL_tmp_a ; do
		echo "[$a]=\"$(eval echo $("$BASH_SOURCE" $a --help | sed -n 's,^  ... \(--[a-z_-]\+[^ ]*\).*,\1,p' | sed 's_<_{_ ; s_|_,_g ; s_>_}_ '))\""
	done)

	eval '_bl_qrkey_completion()
	{
		local cur coptions="--help --version"
		local a actions="'$BL_tmp_a'"
		local -A aoptions=('$BL_tmp_o')

		COMPREPLY=()
		cur=${COMP_WORDS[COMP_CWORD]}

		case ${COMP_WORDS[COMP_CWORD-1]} in
			--help|--version)
				# should exit without executing any actions, so do not complete anything
				return 0 ;;
			--frontend)
				COMPREPLY=( $(compgen -W "whiptail dialog zenity NONE" -- $cur ) )
				return 0 ;;
			--camera)
				COMPREPLY=( $(compgen -W "$(ls /dev/v4l/by-id/*)" -- $cur ) )
				return 0 ;;
			--*from)
				compopt -o plusdirs
				COMPREPLY=( $(compgen -A file -- $cur) )
				return 0 ;;
			--homedir)
				compopt -o plusdirs
				COMPREPLY=( $(compgen -A directory -- $cur) )
				return 0 ;;
		esac
		case $cur in
			-*)
				if a=$(grep -o "\<\('${BL_tmp_a//$'\n'/\\|}'\)\>" <<< "${COMP_WORDS[@]}") ; then
					COMPREPLY=( $(compgen -W "$coptions ${aoptions[$a]}" -- $cur ) )
					return 0
				fi
				COMPREPLY=( $(compgen -W "$coptions --frontend" -- $cur ) )
				return 0 ;;
		esac
		# complete actions if none in ${COMP_WORDS[@]}
		grep -q "\<\('${BL_tmp_a//$'\n'/\\|}'\)\>" <<< "${COMP_WORDS[@]}" || COMPREPLY=( $(compgen -W "$actions" -- $cur ) )
		return 0
	}'
	unset BL_tmp_a BL_tmp_o
	complete -F _bl_qrkey_completion "$(basename "$BASH_SOURCE")" "$BASH_SOURCE"
	return 0
fi

# If sourcing while _bl_qrkey_parseoptions is already set, execute the function and return without reloading rest of file.
if [[ "$BASH_SOURCE" != "$0" ]] && [[ "$(type -t _bl_qrkey_parseoptions)" == function ]] ; then
	_bl_qrkey_parseoptions "$@"
	return $?
fi

### Constants ###

BL_QRKEY_NAME="$(basename "$(readlink -f "$BASH_SOURCE")" )"
BL_QRKEY_VERSION="0.3.5-1"
BL_QRKEY_FUNCTIONS=( $(sed -n 's,^\(bl_[^( ]*\) *().*,\1,p' "$BASH_SOURCE") )
readonly BL_QRKEY_NAME BL_QRKEY_VERSION BL_QRKEY_FUNCTIONS

[[ "$BASH_SOURCE" == "$0" ]] && declare -r BL_QRKEY_isprogram=1 || declare -r BL_QRKEY_isprogram=0

### Others Globals ###

if ((BL_QRKEY_isprogram)) ; then
	TEXTDOMAIN="bashlibs"
	TEXTDOMAINDIR="$(dirname "$(readlink -f "$BASH_SOURCE")" )/../share/locale"
	# Require Bash 5.2. - https://www.kurokatta.org/grumble/2023/11/bash-translated-strings#fnref3
	shopt -s noexpand_translation
fi

BL_QRKEY_chelpmsg="
"$"Backup or restore OpenPGP ID using printed QR codes and Shamir's secret sharing. Configure Yubikeys or Nitrokeys for OpenPGP ID.""
"$"Also facilitate changing passphrase protecting OpenPGP keys, or (PIN or Admin) codes of OpenPGP security tokens (YubiKey, ...).""

"$"Note: OpenPGP ID is a specific OpenPGP configuration managed by bl-pgpid. Using bl-qrkey with other OpenPGP configuration should cause unexpected issues.""

MAIN OPTIONS:
  -f, --frontend PROGRAM   "$"Select a frontend program"" {NONE,whiptail,dialog,zenity} - "$"Environment variable: ""BL_INTERACTIVE_FRONTEND"

### external functions ###

source "$(dirname "$BASH_SOURCE")"/bl-interactive --
source "$(dirname "$BASH_SOURCE")"/bl-security --
source "$(dirname "$BASH_SOURCE")"/bl-pgpid --

### internal functions ###

_bl_qrkey_parseoptions() {
	local npp=$# frontend
	for ((;$#;)) ; do
		case "$1" in
			-f|--frontend) shift; frontend="$1";; # Will be passed to bl-interactive
			-h|--help) printf "%s\n%s%s" "$BL_QRKEY_usage" "$BL_QRKEY_chelpmsg" "$BL_QRKEY_shelpmsg" ; return 1 ;;
			-V|--version) printf "%s %s\n" "$BL_QRKEY_NAME" "$BL_QRKEY_VERSION" ; return 1 ;;
			--) shift ; break ;;
			-*) printf -- "$BL_QRKEY_NAME: Error: "$"Unrecognized option"" '%s'\n\n"$"Try '%s --help' for more information"".\n" "$1" "$BASH_SOURCE" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done
	BL_QRKEY_NOPTIONS=$((npp-$#))
	if [[ "$frontend" ]] ; then
		source "$(dirname "$BASH_SOURCE")"/bl-interactive --frontend "$frontend" --
	fi
}

_bl_iso7816_sw_analyse() {
	local helpmsg="Usage: $FUNCNAME SW

"$"Status word (SW) is four hexa digits, as defined by ISO 7816-4.""
"$"Return: - 0 if SW means Success (SW == 0x9000)""
        - "$"194 (OxC2) if only 2 remaining attempt.""
        - "$"193 (OxC1) if only 1 remaining attempt.""
        - "$"192 (OxC0) if code is blocked.""
        - "$"other non-zero (usually SW1) with an error message in other cases.""
"
	for ((;$#;)) ; do
		case "$1" in
			-h|--h*) echo "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_QRKEY_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$FUNCNAME" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local sw sw1 sw2

	# Sanitize input (upper case + only keep hexa digits)
	sw=${*^^}
	sw=${sw//[^0-9ABCDEF]/}

	[[ "$sw" =~ ^[0-9ABCDEF]{4}$ ]] || { printf -- "$FUNCNAME: Error: "$"Invalid arguments"" '%s'\n"$"Try '%s --help' for more information"".\n" "$*" "$FUNCNAME" >&2 ; return 2 ; }

	sw1=${sw:0:2}
	sw2=${sw:2:2}

	# Take care of order: precise case before fuzzy ones (whith '?'), then most common first.
	case "$sw" in
		9000)
			return 0 ;;
		00??)
			printf "%s: Error: Unknown Status Word (%s).\n" "$FUNCNAME" "$sw" >&2
			return $((16#90)) ;;
		6300)
			printf "%s: Error: Verification/authentication failed (%s) - no retry info.\n" "$FUNCNAME" "$sw" >&2
			return $((16#$sw1)) ;;
		63??)
			# Typical usage: 63Cx where low nibble of SW2 indicates remaining retries (x).
			printf "%s: Error: Verification/authentication failed (%s) - remaining retries: %d .\n" "$FUNCNAME" "$sw" "$((16#${sw2:1:1}))" >&2
			return $((16#C${sw2:1:1})) ;;
		6C??)
			# 6C XX : wrong Le, card indicates correct Le = XX
			printf "%s: Error: Wrong expected length (%s) - correct lenght: %d .\n" "$FUNCNAME" "$sw" "$((16#sw2))" >&2
			return $((16#$sw1)) ;;
		6700)
			printf "%s: Error: Wrong length (67 00). Incorrect Lc and/or Le.\n" "$FUNCNAME" >&2
			return $((16#$sw1)) ;;
		6982)
			printf "%s: Error: Security status not satisfied (69 82). Likely need prior VERIFY or secure messaging.\n" "$FUNCNAME" >&2
			return $((16#$sw1)) ;;
		6983)
			printf "%s: Error: Authentication method blocked (69 83). PIN/method probably blocked; follow reset/unblock procedure.\n" "$FUNCNAME" >&2
			return $((16#$sw1)) ;;
		6985)
			printf "%s: Error: Conditions of use not satisfied (69 85). Operation not allowed in current state.\n" "$FUNCNAME" >&2
			return $((16#$sw1)) ;;
		6A82)
			printf "%s: Error: File / application not found (6A 82). Check AID or object reference (P1/P2).\n" "$FUNCNAME" >&2
			return $((16#$sw1)) ;;
		6B00)
			printf "%s: Error: Wrong parameters P1-P2 (6B 00). Verify P1/P2 are correct for this INS.\n" "$FUNCNAME" >&2
			return $((16#$sw1)) ;;
		6D00)
			printf "%s: Error: INS not supported (6D 00). The card does not implement this instruction.\n" "$FUNCNAME" >&2
			return $((16#$sw1)) ;;
		6E00)
			printf "%s: Error: CLA not supported (6E 00). The class byte is not acceptable.\n" "$FUNCNAME" >&2
			return $((16#$sw1)) ;;
		6F00)
			printf "%s: Error: No precise diagnosis / internal error (6F 00). Card refused without a detailed reason.\n" "$FUNCNAME" >&2
			return $((16#$sw1)) ;;
		61??)
			# 61 XX : response available, XX bytes remain
			printf "%s: Error: More data available (%s) - bytes to retrieve: %d .\n" "$FUNCNAME" "$sw" "$((16#sw2))" >&2
			return $((16#$sw1)) ;;
			# Note: to get full response : gpg-connect-agent "SCD APDU 00 C0 00 00 $sw2" /bye
		62??|64??)
			printf "%s: Error: Warning/non-fatal condition (%s). Check returned data or card documentation for details.\n" "$FUNCNAME" "$sw" >&2
			return $((16#$sw1)) ;;
		*)
			printf "%s: Error: Unhandled or unknown Status Word (%s).\n" "$FUNCNAME" "$sw" >&2
			return $((16#$sw1)) ;;
	esac
	return 255
}

### public functions / program actions ###

bl_qrkey_change_passphrase() {
	local name
	((BL_QRKEY_isprogram)) && name="$BL_QRKEY_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [KEY ID|FPR|EMAIL|NAME]"

	local helpmsg="
"$"Change GnuPG passphrase protecting secret parts of an OpenPGP key.""
"$"Missing input will be asked interactively.""

OPTIONS:
  -H, --homedir GNUPGHOME        "$"GnuPG home directory"" - "$"Environment variable: ""GNUPGHOME, "$"default: ""'~/.gnupg'.
  -p, --passphrase PASSPHRASE    "$"Current passphrase protecting secret parts of OpenPGP key"" (empty \"\" for none)
  -P, --passfrom FILE            "$"Get passphrase from first line of FILE"" (eg: fifo, tmpfs, /dev/stdin ...)
  -n, --newpassphrase PASSPHRASE "$"New passphrase to protect secret parts of OpenPGP key"" (empty \"\" for none)
  -N, --newpassfrom FILE         "$"Get new passphrase from the first line of FILE"" (eg: fifo, tmpfs, /dev/stdin ...)
"
	local passphrase passgiven newpass newgiven
	local gpghome=${GNUPGHOME:-~/.gnupg}
	for ((;$#;)) ; do
		case "$1" in
			-p|--passphrase)
				shift ; passgiven=true ; passphrase=$1 ;;
			-P|--passfrom|--pass-from)
				shift ; passgiven=true ; passphrase=$( sed 1q < "$1" ) || return 1 ;;
			-n|--newpassphrase)
				shift ; newgiven=true ; newpass=$1 ;;
			-N|--newpassfrom|--newpass-from)
				shift ; newgiven=true ; newpass=$( sed 1q < "$1" ) || return 1 ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_QRKEY_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	[[ -z "$1" ]] || keyid="$1"
	if [[ -z "$keyid" ]] ; then
		keyid=$(_bl_pgp_choose_seckeyid --homedir "$gpghome" --exportable --text $"For which OpenPGP secret key you want to change passphrase") || return $?
	else
		# Test input
		gpg --homedir "$gpghome" --list-secret-keys "$keyid" > /dev/null || { printf "%s: Error: No secret for '%s' (in %s).\n" "$FUNCNAME" "$keyid" "$gpghome" >&2 ; return 1 ; }
	fi

	[[ "$passgiven" ]] || passphrase=$(bl_input --password --text $"(if none, leave empty)" $"Current passphrase")

	# Test Current passphrase
	echo "$passphrase" | gpg --homedir "$gpghome" --command-fd 0 --batch --pinentry-mode loopback --dry-run --change-passphrase "$keyid" 2> >( grep -v "Operation cancelled" ) || return $?

	[[ "$newgiven" ]] || newpass=$(bl_new_password $"New passphrase")

	if [[ "$newpass" == "$passphrase" ]] ; then
		echo "$FUNCNAME: Notice: "$"Passphrase unchanged." >&2
		return 0
	fi

	if [[ "$passphrase" ]] ; then
		cmd="${passphrase}\n${newpass}\n${newpass}\n${newpass}\n"
	else
		cmd="${newpass}\n${newpass}\n${newpass}\n"
	fi

	printf "${cmd}" | gpg --homedir "$gpghome" --command-fd 0 --batch --pinentry-mode loopback --change-passphrase "${keyid}" || return $?
	echo "$FUNCNAME: Notice: "$"Passphrase successfully changed." >&2
	return 0
}

# gpg (GnuPG) 2.4.7 --batch can't do it, then we send direct APDU through gpg-connect-agent, see:
#   - https://foopgp.org/blog/2014-08-21-openpgp-card-une-application-cryptographique-pour-carte-a-puce/#communiquer-avec-scdaemon-et-la-carte
#   - https://g10code.com/docs/openpgp-card-2.0.pdf
# Note: existence of a (new in 2025) tool: 'oct' - https://codeberg.org/openpgp-card/openpgp-card-tools
bl_qrkey_change_token_code() {
	local name
	((BL_QRKEY_isprogram)) && name="$BL_QRKEY_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]..."

	local helpmsg="
"$"Check and change PIN (or Admin) code protecting use of a security token (OpenPGP smartcard).""
"$"Missing input will be asked interactively.""
"$"Return:"" - "$"194 (OxC2) if only 2 remaining attempt.""
        - "$"193 (OxC1) if only 1 remaining attempt.""
        - "$"192 (OxC0) if code is blocked.""
        - "$"other non-zero value on other errors.""

OPTIONS:
  -H, --homedir GNUPGHOME   "$"GnuPG home directory"" - "$"Environment variable: ""GNUPGHOME, "$"default: ""'~/.gnupg'.
  -p, --code CURRENTCODE    "$"Current PIN (or Admin) code protecting use of security token"" (empty \"\" for none)
  -P, --codefrom FILE       "$"Get current PIN (or Admin) code from first line of FILE"" (eg: fifo, tmpfs, /dev/stdin ...)
  -n, --newcode NEWCODE     "$"New PIN (or Admin) code to protect use of security token""
  -N, --newcodefrom FILE    "$"Get new PIN (or Admin) code from the first line of FILE"" (eg: fifo, tmpfs ...)
  -C, --onlycheck           "$"Only check current PIN (or Admin) code, don't change it""
  -A, --admin               "$"Change (or check) Admin code instead of PIN code""
  -U, --unblock             "$"Unblock PIN. Need Admin code to be passed to --code, and new PIN code to be passed to --newcode""
"
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local ccode ccodegiven newcode newgiven onlycheck unblock
	local ctype="PIN" xPW="81" lPW="6"
	for ((;$#;)) ; do
		case "$1" in
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-p|--code)
				shift ; ccodegiven=true ; ccode=$1 ;;
			-P|--codefrom|--code-from)
				shift ; ccodegiven=true ; ccode=$( head -n 1 < "$1" ) || return 1 ;;
			-n|--newcode)
				shift ; newgiven=true ; newcode=$1 ;;
			-N|--newcodefrom|--newcode-from)
				shift ; newgiven=true ; newcode=$( head -n 1 < "$1" ) || return 1 ;;
			-C|--onlycheck|--only-check)
				onlycheck="." ;;
			-U|--unblock)
				unblock="unblock" ;&
			-A|--admin)
				ctype="Admin" xPW="83" lPW="8" ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_QRKEY_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local out SW datalen ccodehex newhex

	# Select OpenPGP card application
	mapfile -t out < <(gpg-connect-agent --homedir "$gpghome" --hex "SCD APDU 00 A4 04 00 06 D2 76 00 01 24 01" "/bye")
	if [[ "${out[*]}" =~ ^D\[[0-9ABCDEF]+\]\ *([0-9ABCDEF]{2})\ *([0-9ABCDEF]{2})\ {4,}.*OK$ ]] ; then
		SW="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
		_bl_iso7816_sw_analyse "$SW" || return $?
	else
		printf "%s: Error: %s ...\n" "$FUNCNAME" "${out[0]}" >&2
		return 1
	fi

	[[ "$ccodegiven" ]] || ccode=$(bl_input --password --text "(usually: $lPW digits)" "Current $ctype code")

	[[ "$ccode" =~ ^[0-9]{$lPW}$ ]] || printf "$FUNCNAME: Warning: "$"Given %s code doesn't match %d digits.""\n" "$ctype" "$lPW" >&2

	datalen=${#ccode}
	ccodehex=$(printf "$ccode" | xxd -p)

	# Verify given code (wrong code decrease retry counter, good code reset it to its maximum)
	mapfile -t out < <(gpg-connect-agent --homedir "$gpghome" --hex "SCD APDU 00 20 00 $xPW $(printf "%02X" $datalen) $ccodehex" "/bye")
	if [[ "${out[*]}" =~ ^D\[[0-9ABCDEF]+\]\ *([0-9ABCDEF]{2})\ *([0-9ABCDEF]{2})\ {4,}.*OK$ ]] ; then
		SW="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
		_bl_iso7816_sw_analyse "$SW" || return $?
	else
		printf "%s: Error: %s ...\n" "$FUNCNAME" "${out[0]}" >&2
		return 1
	fi

	[[ -z "$onlycheck" ]] || { printf "$FUNCNAME: Notice: "$"%s code successfully verified.""\n" "$ctype" >&2 ; return 0 ;}

	[[ -z "$unblock" ]] || { ctype="PIN" ; xPW="81" ; lPW="6" ;}

	[[ "$newgiven" ]] || newcode=$(bl_new_password --text "($lPW digits)" "New $ctype code")

	[[ "$newcode" != "$ccode" ]] || { printf "$FUNCNAME: Notice: "$"%s code unchanged.""\n" "$ctype" >&2 ; return 0 ;}

	[[ "$newcode" =~ ^[0-9]{$lPW}$ ]] || { printf "$FUNCNAME: Error: "$"Given %s code doesn't match %d digits.""\n" "$ctype" "$lPW" >&2 ; return 2 ;}

	newhex=$(printf "$newcode" | xxd -p)

	if [[ "$unblock" ]] ; then
		# Unblock PIN
		mapfile -t out < <(gpg-connect-agent --homedir "$gpghome" --hex "SCD APDU 00 2C 02 $xPW $(printf "%02X" $lPW) $newhex" "/bye")
	else
		# Change code
		((datalen+=${lPW}))
		mapfile -t out < <(gpg-connect-agent --homedir "$gpghome" --hex "SCD APDU 00 24 00 $xPW $(printf "%02X" $datalen) $ccodehex $newhex" "/bye")
	fi
	if [[ "${out[*]}" =~ ^D\[[0-9ABCDEF]+\]\ *([0-9ABCDEF]{2})\ *([0-9ABCDEF]{2})\ {4,}.*OK$ ]] ; then
		SW="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
		_bl_iso7816_sw_analyse "$SW" || return $?
	else
		printf "%s: Error: %s ...\n" "$FUNCNAME" "${out[0]}" >&2
		return 1
	fi

	[[ "$unblock" ]]\
		&& printf "$FUNCNAME: Notice: "$"%s code successfully unblocked.""\n" "$ctype" >&2\
		|| printf "$FUNCNAME: Notice: "$"%s code successfully changed.""\n" "$ctype" >&2
	return 0
}

# bl_qrkey_printqr is inspired from https://gist.github.com/joostrijneveld/59ab61faa21910c8434c
#
# Comparison between 1) "--export-secret-key" and 2) "--export-secret-key | paperkey --output-type raw":
# First export also certificate, with pubkeys and uid. Then exported data are bigger, need bigger QRcodes, but import is easier.
# Second export only secrets (of asymmetric keys). Then data are smaller, use smaller QRcodes, but import is more complex as it need to get certificate somewhere
# If we are using curve 25519, we spare so much information, then we are not forced to use paperkey
#
bl_qrkey_print() (
	local name
	((BL_QRKEY_isprogram)) && name="$BL_QRKEY_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [KEY ID|FPR]"

# QRKEY QR VERSIONS:
# * 1: simple split (splits num == threshold num) with only numbers as extra symmetric encryption (deprecated)
# * 2: simple split with a complex extra symmetric encryption (deprecated)
# * 3: Shamir's secret sharing with only number as extra symmetric encryption (deprecated)
# * 4: simple split (splits num == threshold num). In this case, all secret protection relies on the passphrase.
# * 5: Shamir's secret sharing (splits num > threshold num)
#
	local splitn=5 thresn=3 qrversion=5
	local helpmsg="
"$"Export and print OpenPGP secrets on multiple QRcode using Shamir's secret sharing.""
"$"Missing input will be asked interactively.""

OPTIONS:
  -p, --passphrase PASSPHRASE    "$"Passphrase to access secret parts of OpenPGP key""
  -P, --passfrom FILE            "$"Get passphrase from first line of FILE"" (eg: fifo, tmpfs, /dev/stdin ...)
  -t, --printer PRINTER          "$"Name of printer to use""
  -w, --with-passphrase          "$"Also print passphrase beside QR codes (INCREASE UX, DECREASE SECURITY).""
  -W, --workdir DIRECTORY        "$"Use given working directory instead of a temporary directory (don't forget to shred its content).""
  -S, --split NUM                "$"Number of shares to be generated"" - "$"Default: ""$splitn
  -T, --threshold NUM            "$"Number of shares necessary to reconstruct the secret"" - "$"Default: ""$thresn
  -H, --homedir GNUPGHOME        "$"GnuPG home directory"" - "$"Environment variable: ""GNUPGHOME, "$"default: ""'~/.gnupg'.

"$"Note: Split number should be greater than threshold number.""
      "$"If they are equal, a simple split is used instead of Shamir's secret sharing,""
      "$"and all secret protection relies on the passphrase.""
      "$"In other terms: if (split_NUM == threshold_NUM), then no passphrase or printing passphrase is VERY UNSECURE.""
"
	local passphrase passgiven printer tmpdir withpass
	local gpghome=${GNUPGHOME:-~/.gnupg}
	for ((;$#;)) ; do
		case "$1" in
			-p|--passphrase)
				shift ; passgiven=true ; passphrase=$1 ;;
			-P|--passfrom|--pass-from)
				shift ; passgiven=true ; passphrase=$( sed 1q < "$1" ) || return 1 ;;
			-t|--printer)
				shift ; printer=$1 ;;
			-w|--with-passphrase)
				withpass=true ;;
			-D|--tmpdir|--tempdir|-W|--workdir)
				shift
				[[ -d "$1" ]] || { printf "%s: Error: nonexistent or unattainable directory (%s)\n" "$FUNCNAME" "$1" >&2 ; return 2 ; }
				tmpdir=$1 ;;
			-S|--split)
				shift ; splitn=$(($1)) ;;
			-T|--thres*)
				shift ; thresn=$(($1)) ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_QRKEY_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	if [[ -z "$tmpdir" ]] ; then
		tmpdir=$(mktemp --directory -t "$FUNCNAME".XXXXXX) || return $?
		trap "{ bl_shred_path --force --remove \"$tmpdir\" ; }" EXIT
	fi

	if [[ "$(find "$tmpdir" -name "SECRET*" -print -quit)" ]] ; then
		printf "%s: Error: Working directory is unclean (containing SECRET*).\n" "$FUNCNAME" >&2
		printf "%s: Suggestion: $ bl-security shred_path --remove '%s'\n" "$FUNCNAME" "$tmpdir" >&2
		return 2
	fi
	if ! rm -- $(mktemp --tmpdir="$tmpdir" testw.XXX) ; then
		printf "%s: Error: Can't write in working directory (%s).\n" "$FUNCNAME" "$tmpdir" >&2
		return 2
	fi

	if ((splitn<3)) ; then
		printf "%s: Error: splits number (%d) can't be lower than 3.\n" "$FUNCNAME" "$splitn" >&2
		return 2
	fi

	if ((splitn<thresn)) ; then
		printf "%s: Warning: threshold can't be greater than number of split, reducing threshold to splits number (%d).\n" "$FUNCNAME" "$splitn" >&2
		thresn=$split
	fi

	# If number of shares equal number of threshold, don't use shamir's secret sharing
	((splitn!=thresn)) || qrversion=4

	local keyfpr secstatus

	if [[ "$1" ]] ; then
		keyfpr="$1"
	else
		keyfpr=$(_bl_pgp_choose_seckeyid --homedir "$gpghome" --exportable --text "Select KeyID of secrets to print (on ${splitn} fragments)") || return $?
	fi

	# Check and get metadata
	secstatus=$(gpg --quiet --homedir "$gpghome" --list-secret-keys "$keyfpr") || { printf "$FUNCNAME: Error: "$"No secret for '%s' (in %s)"".\n" "$keyfpr" "$gpghome" >&2 ; return 1 ; }

	if [[ -z "$printer" ]] ; then
		local -a printers
		printers=($(LANG= lpstat -p | sed -n 's,^printer \([^ ]*\).*,\1,p'))
		[[ "${printers[0]}" ]] || { printf "%s: Error: No printer detected.\n" "$FUNCNAME" >&2 ; return 1 ; }
		printer="$(bl_radiolist --output-value --default-value "$(lpstat -d | sed 's,.* ,,')" --num-per-line 1 --text "Where to print the ${splitn} secret fragments ?" "${printers[@]}")"
	fi

	# Replace eventual keyID (short) by its keyFPR (longest)
	keyfpr=$(grep -o -m1 "\<[0-9ABCDEF]\{40\}\>" <<<"${secstatus}") || { printf "$FUNCNAME: Error: "$"Can't get fingerprint (maybe due to incompatible gpg's version)"".\n" "$keyfpr" "$gpghome" >&2 ; return 1 ; }

	[[ "$passgiven" ]] || passphrase=$(bl_input --password --text "(protecting your secret key)" "Passphrase")

	# TODO: Remove Photos and extras uid from exported data (containing not only secrets, but also public data)

	echo "${passphrase}" | gpg --homedir "$gpghome" --batch --passphrase-fd 0 --pinentry-mode loopback --export-options export-minimal,export-clean --export-secret-key "${keyfpr}" > "$tmpdir/priv.gpg" || return $?

	if (( qrversion==1 || qrversion==3 )) ; then
		gpg --batch --symmetric --passphrase "${passphrase//[^0-9]/}" --pinentry-mode loopback < "$tmpdir/priv.gpg" > "$tmpdir/s.gpg" || return $?
	elif (( qrversion==4 || qrversion==5 )) ; then
		mv "$tmpdir/priv.gpg" "$tmpdir/s.gpg"
	else
		printf "$FUNCNAME: Error: "$"Unexpected QR Version (%s).""\n" "$qrversion" >&2
		return 1
	fi
	
	local f

	if ((qrversion==1)) || ((qrversion==2)) || ((qrversion==4)) ; then
		# simple split
		basenc --base64url --wrap 0 < "$tmpdir/s.gpg" > "$tmpdir/s.gpg.b64url"
		split "$tmpdir/s.gpg.b64url" -d -n "$splitn" "$tmpdir/SECRET-"
	elif ((qrversion==3)) || ((qrversion==5)) ; then
		gfsplit -n ${thresn} -m ${splitn} "$tmpdir/s.gpg" "$tmpdir/SECRET"
		cd "$tmpdir/"
		for f in "SECRET."* ; do
			basenc --base64url --wrap 0 < "$f" > "${f/./-}"
		done
		cd -
	else
		printf "$FUNCNAME: Error: "$"Unexpected QR Version (%s).""\n" "$qrversion" >&2
		return 1
	fi

	printf "$FUNCNAME: Notice: "$"Printing secret splitted into %s fragments on printer %s"" ...\n" "$splitn" "$printer" >&2
	i=0
	for f in "$tmpdir/SECRET-"* ; do

		#TODO: manage stdouts and stderrs on next lines
		if ((qrversion==3)) || ((qrversion==5)); then
			# 7 chars QR Code Header : one magic char '~', one version char, one char for threshold number, one char for index of split part, 3 chars for the share number (cf man gfcombine)
			( printf "~$((qrversion))$((thresn-1))$i"${f##*-} ; cat "$f" ) | qrencode --level=L --dpi=50 --output "$f.png"
		else
			# 4 chars QR Code Header : one magic char '~', one version char, one char for split number, one char for index of split part.
			( printf "~$((qrversion))$((thresn-1))$i" ; cat "$f" ) | qrencode --level=M --dpi=50 --output "$f.png"
		fi
		((++i)) # Not i++ because i start from 0 and set -e ;-)
		cat <<-EOF | pandoc --from markdown --to pdf -fmarkdown-implicit_figures | pdfcrop --quiet --margins "4" - "$f.pdf" >&2
			\pagenumbering{gobble}

			*$(hostname) - $(date --iso-8601)*

			*${FUNCNAME} ${BL_QRKEY_VERSION} - QR version: ${qrversion}*

			## PGPID SECRET (/${thresn}) - FRAGMENT ${i}/${splitn}

			$(gpg --homedir "$gpghome" --with-colons --list-keys "$keyfpr" | awk -F: '$1=="uid" { print $10"\n" }')
			\`\`\`
			    ${keyfpr: 0:4} ${keyfpr: 4:4} ${keyfpr: 8:4} ${keyfpr: 12:4} ${keyfpr: 16:4} ${keyfpr: 20:4} ${keyfpr: 24:4} ${keyfpr: 28:4} ${keyfpr: 32:4} ${keyfpr: 36:4}
			\`\`\`
			![qrcode $i]($f.png)

			$( [[ -z "$withpass" ]] || echo "Passphrase: ${passphrase}" )
		EOF

		lpr -# 1 -P "${printer}" "${f}.pdf" >&2
	done
)

bl_qrkey_scan() (
	local name
	((BL_QRKEY_isprogram)) && name="$BL_QRKEY_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [IMAGES]..."
	local helpmsg="
"$"Reconstitute OpenPGP secrets from QRcodes scanned from IMAGES or webcam.""
"$"Missing input will be asked interactively.""
"$"Output OpenPGP certification key fingerprint.""

OPTIONS:
  -c, --camera V4LDEVICE        "$"Video device to scan for QR code"" (/dev/v4l/by-id/...)
  -W, --workdir DIRECTORY       "$"Use given working directory instead of a temporary directory (don't forget to shred its content).""
  -p, --passphrase PASSPHRASE   "$"This extra passphrase is related to QR VERSIONS:""
                                  * "$"1 to 3: Only numbers, use to be a date: YYYYMMDD.""
                                  * "$"4 to 5: No need for extra passphrase (in addition to the ones defined in RFC9580).""
  -P, --passfrom FILE           "$"Get extra passphrase from first line of FILE"" (eg: fifo, tmpfs, /dev/stdin ...)
  -H, --homedir GNUPGHOME       "$"GnuPG home directory"" - "$"Environment variable: ""GNUPGHOME, "$"default: ""'~/.gnupg'.
"

	local -a passphrase
	local tmpdir gpghome=${GNUPGHOME:-~/.gnupg}
	for ((;$#;)) ; do
		case "$1" in
			-p|--passphrase)
				shift ; passphrase=$1 ;;
			-P|--passfrom|--pass-from)
				shift ; passphrase=$( sed 1q < "$1" ) || return 1 ;;
			-D|--tmpdir|--tempdir|-W|--workdir)
				shift
				[[ -d "$1" ]] || { printf "%s: Error: nonexistent or unattainable directory (%s)\n" "$FUNCNAME" "$1" >&2 ; return 2 ; }
				tmpdir=$1 ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_QRKEY_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	if [[ -z "$tmpdir" ]] ; then
		tmpdir=$(mktemp --directory -t "$FUNCNAME".XXXXXX) || return $?
		trap "{ bl_shred_path --force --remove \"$tmpdir\" ; }" EXIT
	fi
	if [[ "$(find "$tmpdir" -name "SECRET*" -print -quit)" ]] ; then
		printf "%s: Error: Working directory is unclean (containing SECRET*).\n" "$FUNCNAME" >&2
		printf "%s: Suggestion: $ bl-security shred_path --remove '%s'\n" "$FUNCNAME" "$tmpdir" >&2
		return 2
	fi
	if ! echo -n > "$tmpdir/qrcontent" ; then
		printf "%s: Error: Can't write in working directory (%s).\n" "$FUNCNAME" "$tmpdir" >&2
		return 2
	fi

	local file imgfile
	for file in "$@" ; do
		if file "$file" | grep --quiet "PDF document" ; then
			imgfile="$tmpdir/imgfile.png"
			if ! convert pdf:"$file" "${imgfile}" ; then
				printf "%s: Error: Can't convert pdf %s -> %s.\n" "$FUNCNAME" "$file" "$imgfile" >&2
				return 1
			fi
		else
			imgfile="$file"
		fi

# Expected QRdata begin with a version number "~[1-9]" followed by the maximum number of part-1 (default: 4-1=3) then by the index of the part (O to 9).
		if zbarimg --quiet -Sdisable -Sqrcode.enable "$imgfile" | tr -d '\n' | sed 's,QR-Code:,\n,g' | grep "^~[1-9]" >> "$tmpdir/qrcontent" ; then
			printf "%s: Info: QR code(s) with expected data succefully extracted from '%s'.\n" "$FUNCNAME" "$file" >&2
		else
			printf "%s: Error: No QR code with expected data in '%s'.\n" "$FUNCNAME" "$file" >&2
			return 1
		fi
	done

	local part qrversion max
	local -a parts
	while read -r part ; do
		if [[ "${part:1:1}" != "${qrversion:=${part:1:1}}" ]] ; then
			printf "%s: Crit: QR codes doesn't share the same version (%s)\n" "$FUNCNAME" "$qrversion != ${part:1:1}" >&2
			return 3
		fi
		if [[ "${part:2:1}" != "${max:=${part:2:1}}" ]] ; then
			printf "%s: Crit: QR codes doesn't share the same division (%s)\n" "$FUNCNAME" "$max != ${part:2:1}" >&2
			return 3
		fi
		parts[${part:3:1}]="${part:4}"
	done <"$tmpdir/qrcontent"

	local -a v4ldevices i j
	for ((i=0;i<=${max:-3};i++)) ; do
		((${#parts[@]} <= ${max:-3})) || break
		[[ -z "${parts[$i]}" ]] || continue

		if [[ -z "$v4ldevice" ]] ; then
			if ! v4ldevices=($(ls /dev/v4l/by-id/*index0)) ; then
				printf "%s: Crit: No video input (eg. webcam) detected\n" "$FUNCNAME" >&2
				return 3
			elif [[ ${#v4ldevices[@]} == 1 ]] ; then
				v4ldevice=${v4ldevices[0]}
			else
				v4ldevice=$(bl_radiolist --output-value --num-per-line 1 --default -1 --text "Missing part $((i+1)), device to scan qrcode ?" "${v4ldevices[@]}")
			fi
		fi
		v4ldevice="$(readlink -f "${v4ldevice}")"

		j=0
		while ! part="$(zbarcam -Sdisable -Sqrcode.enable --oneshot --prescale=640x480 "$v4ldevice" | tr -d '\n' | sed 's,QR-Code:,\n,g' | grep "^~[1-9]")" ; do
			printf "%s: Notice: No QR code with expected data detected...\n" "$FUNCNAME" >&2
			((++j < 3)) || { printf "%s: Error: Giving up %s.\n" "$FUNCNAME" "'zbarcam (...) $v4ldevice'" >&2 ; return 1 ;}
		done

		if [[ "${part:1:1}" != "${qrversion:=${part:1:1}}" ]] ; then
			printf "%s: Crit: QR codes doesn't share the same version (%s)\n" "$FUNCNAME" "$qrversion != ${part:1:1}" >&2
			return 3
		fi
		if [[ "${part:2:1}" != "${max:=${part:2:1}}" ]] ; then
			printf "%s: Crit: QR codes doesn't share the same division (%s)\n" "$FUNCNAME" "$max != ${part:2:1}" >&2
			return 3
		fi
		i=${part:3:1}
		parts[$i]="${part:4}"
		printf "%s: Notice: Scanning part %s -> OK\n" "$FUNCNAME" "$((i+1)) / $((max+1))" >&2
		i=-1
	done

# If we are using paperkey, we have to retrieve the PGPcert (use to need network) and use something like:
# ... base64 -d | paperkey --pubring "$tmpdir/PGPcert.gpg" 2> >(bl_log crit) | gpg --homedir "$gpghome" --command-fd 0 --batch --pinentry-mode loopback --passphrase "${passphrase[*]}" --import 2> >(bl_log notice)

	for ((i=1;i<=3;i++)) ; do

		if [[ "$qrversion" == "0" ]] ; then
			printf "%s: Crit: Invalid qrcode version (%s)\n" "$FUNCNAME" "$qrversion" >&2
			return 3
		elif [[ "$qrversion" == "1" ]] || [[ "$qrversion" == "2" ]] || [[ "$qrversion" == "3" ]] ; then
			birth_date=$(bl_input --default "${birth_date}" "Birth date (YYYY-mm-dd)")
			passphrase[0]="$(sed 's/[^0-9]//g' <<<"${birth_date}" )"
		elif [[ "$qrversion" != "4" ]] && [[ "$qrversion" != "5" ]] ; then
			printf "%s: Crit: Unsupported qrcode version (%s)\n" "$FUNCNAME" "$qrversion" >&2
			return 3
		fi

		if [[ "$qrversion" == "1" ]] || [[ "$qrversion" == "2" ]] || [[ "$qrversion" == "4" ]] ; then
			# Simple split
			if [[ "$qrversion" == "2" ]] ; then
				for ((j=1;j<=max+1;j++)) ; do
					passphrase[$j]="$(bl_input --default "${passphrase[$j]}" "Secret word $j/$((max+1))")"
				done
			fi
			printf "%s" ${parts[@]} | basenc --decode --base64url > "$tmpdir/SECRET"
		elif [[ "$qrversion" == "3" ]] || [[ "$qrversion" == "5" ]] ; then
			# Shamir Secret Sharing
			for j in ${!parts[@]} ; do
				printf "%s" ${parts[$j]:3} | basenc --decode --base64url > "$tmpdir/SECRET.${parts[$j]:0:3}"
			done
			gfcombine "$tmpdir/SECRET."*
		fi

		if [[ "$qrversion" == "1" ]] || [[ "$qrversion" == "2" ]] || [[ "$qrversion" == "3" ]] ; then
			if gpg --homedir "$gpghome" --batch --pinentry-mode loopback --passphrase-fd 0 --decrypt "$tmpdir/SECRET" <<<"${passphrase[*]}" > "$tmpdir/s.gpg" ; then
				break
			fi
		elif [[ "$qrversion" == "4" ]] || [[ "$qrversion" == "5" ]] ; then
			mv "$tmpdir/SECRET" "$tmpdir/s.gpg"
			break
		fi
	done

	gpg --homedir "$gpghome" --batch --pinentry-mode loopback --passphrase-fd 0 --import "$tmpdir/s.gpg" <<<"${passphrase[*]}" || return $?
	[[ -z "${passphrase[*]}" ]] || echo "${passphrase[*]}" > "$tmpdir/passphrase"

	# Output certification key fpr
	gpg --list-packets "$tmpdir/s.gpg" | sed -n --regexp-extended  '/issuer.*[0-9ABCDEF]{40}/ { s,.*([0-9ABCDEF]{40}).*,\1,p ; q }'
	return ${PIPESTATUS[0]}
)

bl_qrkey_totoken() {
	local name
	((BL_QRKEY_isprogram)) && name="$BL_QRKEY_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local hkpsserv=${BL_PGPID_KEYSERVERS[0]##*//}
	local lang="${LANG:: 2}"
	local usage="Usage: $name [OPTIONS]... [KEY ID|FPR]"
	local helpmsg="
"$"Move OpenPGP secrets to security token (OpenPGP smartcard).""
"$"Missing input will be asked interactively.""
"$"Security token (OpenPGP smartcard) must be connected.""
"$"Output ASCII armored OpenPGP certificate, PIN code and admin code.""

OPTIONS:
  -H, --homedir GNUPGHOME      "$"GnuPG home directory"" - "$"Environment variable: ""GNUPGHOME, "$"default: ""'~/.gnupg'.
  -p, --passphrase PASSPHRASE  "$"Passphrase to access secret parts of OpenPGP key""
  -P, --passfrom FILE          "$"Get passphrase from first line of FILE"" (eg: fifo, tmpfs, /dev/stdin ...)
  -U, --certurl URL            "$"URL to retrieve your OpenPGP certificate"" (default: https://{KEYSERVER}/pks/lookup?op=get&search=0x{KEYFPR} )
  -L, --lang LANG              "$"Security token (OpenPGP smartcard) prefered language"" (default: $lang)
  -N, --name NAME              "$"Customize name of security token's cardholder""
  -k, --keyserver KEYSERVER    "$"Send OpenPGP certificate to this public keys server. Also modify default URL"" (see --certurl, default: $hkpsserv)
  -K, --pubkey FILE            "$"Also write armored OpenPGP certificate to given FILE""
  -v, --verbose                "$"Increase verbosity""
      --force                  "$"Don't ask before resetting unempty security token (OpenPGP smartcard)""
"

	local passphrase certurl username force=0 pubkeyfile
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local gpgfilter="grep ^gpg"
	for ((;$#;)) ; do
		case "$1" in
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-p|--passphrase)
				shift ; passphrase=$1 ;;
			-P|--passfrom|--pass-from)
				shift ; passphrase=$( sed 1q < "$1" ) || return 1 ;;
			-k|--keyserver)
				shift ; hkpsserv=$1 ;;
			-U|--certurl)
				shift ; certurl=$1 ;;
			-L|--lang)
				shift ; lang=$(sed -nE ' s,.*([A-Za-z]{2}).*,\L\1,p ' <<<"${1:: 2}") ;;
			-N|--name)
				shift ; username="${1//[^0-9a-zA-Z ._-]/}" ;;
			-K|--pubkey)
				shift ; pubkeyfile="$1" ;;
			-v|--verbose) gpgfilter="cat" ;;
			--no-send) echo "$FUNCNAME: Warning:" $"Deprecated option" "$1" >&2 ;;
			--force) force=1 ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_QRKEY_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	[[ "$lang" =~ ^[abcdefghijklmnopqrstuvwxyz]{2}$ ]] || { printf "$FUNCNAME: Warning: "$"Only 2 lower case ASCII letters can define prefered language, fallback to: ""'en'.\n" "$FUNCNAME" >&2 ; lang=en ;}

	local keyid secstatus tcheck_rvalue=0
	local -a emails ids
	local -A tcheck_info

	if [[ "$1" ]] ; then
		keyid="$1"
	else
		keyid=$(_bl_pgp_choose_seckeyid --homedir "$gpghome" --exportable --text $"Select KeyID of OpenPGP secrets to move on security token") || return $?
	fi

	# Check and get metadata
	secstatus=$(gpg --quiet --homedir "$gpghome" --list-secret-keys "$keyid") || { printf "$FUNCNAME: Error: "$"No secret for '%s' (in %s)"".\n" "$keyid" "$gpghome" >&2 ; return 1 ; }

	# Replace eventual keyID (short) by its keyFPR (longest)
	keyid=$(grep -o -m1 "\<[0-9ABCDEF]\{40\}\>" <<<"${secstatus}") || { printf "$FUNCNAME: Error: "$"Can't get fingerprint (maybe due to incompatible gpg's version)"".\n" "$keyid" "$gpghome" >&2 ; return 1 ; }

	# If no URL to get certificate, set one, assuming HKPS protocol.
	[[ "$certurl" ]] || certurl="https://${hkpsserv}/pks/lookup?op=get&search=0x${keyid}"

	# Write possibly given $pubkeyfile
	[[ -z "$pubkeyfile" ]] || gpg --homedir "$gpghome" --export --armor "$keyid" > "$pubkeyfile" || return $?

	emails=($(sed --silent --regexp-extended "s,^uid.*[<[:space:]](${BL_INTERACTIVE_EMAIL_REGEX}).*,\1,p" <<<"${secstatus}"))
	[[ "$emails" ]] || { printf "$FUNCNAME: Error: "$"No email for OpenPGP key '%s'"".\n" "$keyid" >&2 ; return 1 ; }

	# Don't error if there is no pgp_id
	ids=($(sed --silent --regexp-extended "s,.*4.?(${BL_PGPID_U4_REGEX}).*,u4\1,p ; s,.*5.?(${BL_PGPID_U5_REGEX}).*,u5\1,p" <<<"${secstatus}" | sort -u))
	(( ${#ids[@]} < 2 )) || { echo "$FUNCNAME: Error:" $"supernumerary pgpid string" "(${ids[*]})" >&2 ; return 1 ;}

	# Check and errout card-status
	bl_pgpid_token_check --homedir "$gpghome" --no-fetch --quiet --aaname tcheck_info || tcheck_rvalue=$?

	[[ "${tcheck_info[token_MSN]}" ]] || return $tcheck_rvalue

	echo "$FUNCNAME: Notice:" $"Detected OpenPGP security token:" "${tcheck_info[token_MSN]} -" $"Cardholder:" "${tcheck_info[pgpid_name]} <${tcheck_info[pgpid_email]}>" >&2

	# If we fail testing no passphrase, there is one. Then we have to remove it, or we can't exec gpg's keytocard
	if ! gpg --homedir "$gpghome" --batch --pinentry-mode loopback --passphrase "" --dry-run --change-passphrase "$keyid" 2> /dev/null ; then
		[[ "$passphrase" ]] || passphrase=$(bl_input --password --text "(To put OpenPGP secrets on security token)" "Passphrase")
		i=0
		while ! bl_qrkey_change_passphrase --homedir "$gpghome" --passfrom /dev/stdin --newpassphrase "" "$keyid" <<<"$passphrase" >&2 ; do
				((++i <= 3 )) || return 2
				passphrase=$(bl_input --password --text "(To put secret keys on security token)" "Passphrase")
		done
	fi

	((tcheck_rvalue==107)) || ((force)) || bl_yesno --text "${tcheck_info[token_MSN]} " \
		$"Factory reset OpenPGP security token" $"(This will wipe all the OpenPGP data stored there!)" || exit 42

	printf "%s: Notice: Resetting OpenPGP security token...\n" "$FUNCNAME" >&2

	echo -e "admin\nfactory-reset\ny\nyes\nquit" | \
	gpg --homedir "$gpghome" --command-fd 0 --batch --card-edit 2> >( $gpgfilter ) || { printf "Factory Reset OpenPGP card FAIL ($?)\n" >&2 ; return 1 ;}

	printf "%s: Notice: Configuring OpenPGP security token...\n" "$FUNCNAME" >&2

	echo -e "admin\nlogin\n${ids[0]:-${emails[0]}}\nurl\n${certurl}\nlang\n$lang\nname\n${emails[0]}\n${username}\n" | \
	gpg --homedir "$gpghome" --command-fd 0 --batch --pinentry-mode loopback --passphrase "12345678" --card-edit 2> >( $gpgfilter ) || { printf "Setting OpenPGP card holder FAIL ($?)\n" >&2 ; return 1 ;}

	echo -e "admin\nkey-attr\n2\n1\n2\n1\n2\n1\nquit" | \
	gpg --homedir "$gpghome" --command-fd 0 --batch --pinentry-mode loopback --passphrase "12345678" --card-edit 2> >( $gpgfilter ) || { printf "Setting OpenPGP card keys attributes FAIL ($?)\n" >&2 ; return 1 ;}

#TODO WKS address calculated from email

	printf "%s: Notice: Moving OpenPGP secrets to security token...\n" "$FUNCNAME" >&2

	echo -e "keytocard\ny\n1\nkey 1\nkeytocard\n2\nkey 1\nkey 2\nkeytocard\n3\nsave" | \
	gpg --homedir "$gpghome" --command-fd 0 --batch --pinentry-mode loopback --passphrase "12345678" --edit-key "${keyid}" 2> >( $gpgfilter ) || { printf "Moving OpenPGP keys to card FAIL ($?)\n" >&2 ; return 1 ;}

	# Change default PIN and Admin code
	local pincode admincode
	pincode=$(printf "%06d" $(bl_urandom 1000000))
	admincode=$(printf "%08d" $(bl_urandom 100000000))
	bl_qrkey_change_token_code --homedir "$gpghome" --code 123456 --newcodefrom <(cat <<<"$pincode") || { printf "Changing PIN code FAIL ($?)\n" >&2 ; return 1 ;}
	bl_qrkey_change_token_code --homedir "$gpghome" --admin --code 12345678 --newcodefrom <(cat <<<"$admincode") || { printf "Changing Admin code FAIL ($?)\n" >&2 ; return 1 ;}

	echo
	gpg --homedir "$gpghome" --export --armor "$keyid" || return $?
	if ! timeout --verbose 8 gpg --homedir "$gpghome" --keyserver "${hkpsserv}" --send-keys "$keyid" ; then
		printf "%s: Warning: Please publish this PGP PUBLIC KEY on the Internet (eg: on a keyserver)\n" "$FUNCNAME" >&2
	fi
	echo
	echo "ADMIN_CODE=$admincode"
	echo "PIN_CODE=$pincode"
	printf "%s: Notice: You should note these code, or change them using: \"bl-qrkey change_token_code\" or \"gpg --change-pin\"\n" "$FUNCNAME" >&2

	return 0
}

### Init ###

if ((BL_QRKEY_isprogram)) ; then
	BL_QRKEY_usage="Usage: $BASH_SOURCE [MAIN_OPTIONS]... ACTION [OPTIONS]... [ARGUMENTS]..."
	BL_QRKEY_shelpmsg="
  -h, --help               "$"Show help and exit.""
  -V, --version            "$"Show version and exit.""

ACTIONS:
$(for f in "${BL_QRKEY_FUNCTIONS[@]}" ; do printf "   %-19s%s\n" "${f:9}" "$($f --help | sed -n '/^$/{n;p;q}')" ; done)

All actions support a --help option, eg:
$ $BASH_SOURCE ${BL_QRKEY_FUNCTIONS:9} --help

$BL_QRKEY_NAME is also bash library, see:
$ source $BASH_SOURCE --help
"
else
	BL_QRKEY_usage="Usage: source $BASH_SOURCE [MAIN_OPTIONS]..."
	BL_QRKEY_shelpmsg="
      --bash-completion    set completion for $BASH_SOURCE program and return (without loading anything else)

Functions:
$(for f in "${BL_QRKEY_FUNCTIONS[@]}" ; do printf "   %-19s%s\n" "$f" "$($f --help | sed -n '/^$/{n;p;q}')" ; done)

"$"Reminder: when used as a library, all functions calls share the same environment variables, i.e. the same global options.""
"
fi

# Parse Options
_BL_QRKEY_RETVAL=0
_bl_qrkey_parseoptions "$@" || _BL_QRKEY_RETVAL=$?

# Do nothing else if sourced
[[ "$BASH_SOURCE" == "$0" ]] || return $_BL_QRKEY_RETVAL

[[ "$_BL_QRKEY_RETVAL" != "1"  ]] || exit 0
[[ "$_BL_QRKEY_RETVAL" < 2 ]] || exit $_BL_QRKEY_RETVAL

### Run ###
set -e
shift $BL_QRKEY_NOPTIONS
if function=$(grep -o "\<bl_qrkey_$1\>" <<< "${BL_QRKEY_FUNCTIONS[@]}") ; then
	shift
	$function "$@"
	exit $?
else
	printf "$BL_QRKEY_NAME: Error: "$"Unrecognized action"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$BASH_SOURCE" >&2
	exit 2
fi

