#!/bin/bash
#
# Bash library and executable that manage id compliant with foopgp: 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


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_pgpid_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 ;;
			--*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_pgpid_completion "$(basename "$BASH_SOURCE")" "$BASH_SOURCE"
	return 0
fi

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

### Constants ###

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

declare -r BL_PGPID_CO_REGEX="e[_-][0-9]{2}\.[0-9]{2}[_-][01][0-9]{2}\.[0-9]{2}"
# Best way to NOT manage LC_COLLATE(="C.UTF-8") - cf. https://unix.stackexchange.com/questions/645054/how-do-bash-and-zsh-handle-collation-in-patterns-and-regexes
declare -r BL_PGPID_U4H_REGEX="[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0-9_-]{22}"
declare -r BL_PGPID_U4_REGEX="${BL_PGPID_U4H_REGEX}${BL_PGPID_CO_REGEX}"							# Birth of "human" (must have a name, should have a soul)
declare -r BL_PGPID_TIME_REGEX="[01-][0-9]{11}\.[0-9]{3}"											# From 'Thu Feb 15 14:22:42 LMT -1199' to 'Wed Nov 16 10:46:39 CET 5138'
declare -r BL_PGPID_U5_REGEX="${BL_PGPID_TIME_REGEX}${BL_PGPID_CO_REGEX}"							# Apparition of anything (with or without any ghost in the shell)
declare -r BL_PGPID_DATE_REGEX="(18|19|20)[0-9]{2}(0[1-9]|1[0,1,2])(0[1-9]|[12][0-9]|3[01])"
declare -r BL_PGPID_N2_REGEX="${BL_PGPID_DATE_REGEX}${BL_PGPID_CO_REGEX}"							# Deprecated

declare -r BL_PGPID_XUID_MIN=$((1<<18))
declare -r BL_PGPID_XUID_MAX=$(((1<<31)-2))

declare -Ar BL_PGPID_COORDINATES=(
[ABW]="e_12.52-069.98 Aruba"
[AFG]="e_33.84_066.00 Afghanistan"
[AGO]="e-12.29_017.54 Angola"
[AIA]="e_18.22-063.06 Anguilla"
[ALB]="e_41.14_020.05 Albania"
[ALA]="e_60.21_019.95 Aland Islands"
[AND]="e_42.54_001.56 Andorra"
[ARE]="e_23.91_054.30 United Arab Emirates"
[ARG]="e-35.38-065.18 Argentina"
[ARM]="e_40.29_044.93 Armenia"
[ASM]="e-14.30-170.72 American Samoa"
[ATA]="e-80.51_019.92 Antarctica"
[ATF]="e-49.25_069.23 French Southern and Antarctic Lands"
[ATG]="e_17.28-061.79 Antigua and Barbuda"
[AUS]="e-25.73_134.49 Australia"
[AUT]="e_47.59_014.13 Austria"
[AZE]="e_40.29_047.55 Azerbaijan"
[BDI]="e-03.36_029.88 Burundi"
[BEL]="e_50.64_004.64 Belgium"
[BEN]="e_09.64_002.33 Benin"
[BFA]="e_12.27-001.75 Burkina Faso"
[BGD]="e_23.87_090.24 Bangladesh"
[BGR]="e_42.77_025.22 Bulgaria"
[BHR]="e_26.04_050.54 Bahrain"
[CAN]="e_61.36-098.31 Canada"
[BHS]="e_24.29-076.63 Bahamas"
[BIH]="e_44.17_017.77 Bosnia and Herzegovina"
[BLM]="e_17.90-062.84 Saint-Barthélemy"
[BLR]="e_53.53_028.03 Belarus"
[BLZ]="e_17.20-088.71 Belize"
[BMU]="e_32.31-064.75 Bermuda"
[BOL]="e-16.71-064.69 Bolivia"
[BRA]="e-10.79-053.10 Brazil"
[BRB]="e_13.18-059.56 Barbados"
[BRN]="e_04.52_114.72 Brunei Darussalam"
[BTN]="e_27.41_090.40 Bhutan"
[BWA]="e-22.18_023.80 Botswana"
[CAF]="e_06.57_020.47 Central African Republic"
[CHE]="e_46.80_008.21 Switzerland"
[CHL]="e-37.73-071.38 Chile"
[CHN]="e_36.56_103.82 China"
[CIV]="e_07.63-005.57 Côte d'Ivoire"
[CMR]="e_05.69_012.74 Cameroon"
[COD]="e-02.88_023.64 Democratic Republic of the Congo"
[COG]="e-00.84_015.22 Republic of Congo"
[COK]="e-21.22-159.79 Cook Islands"
[COL]="e_03.91-073.08 Colombia"
[COM]="e-11.88_043.68 Comoros"
[CPV]="e_15.96-023.96 Cape Verde"
[CRI]="e_09.98-084.19 Costa Rica"
[CUB]="e_21.62-079.02 Cuba"
[CUW]="e_12.20-068.97 Curaçao"
[CYM]="e_19.43-080.91 Cayman Islands"
[CYP]="e_34.92_033.01 Cyprus"
[CZE]="e_49.73_015.31 Czech Republic"
[DEU]="e_51.11_010.39 Germany"
[DJI]="e_11.75_042.56 Djibouti"
[DMA]="e_15.44-061.36 Dominica"
[DNK]="e_55.98_010.03 Denmark"
[DOM]="e_18.89-070.51 Dominican Republic"
[DZA]="e_28.16_002.62 Algeria"
[ECU]="e-01.42-078.75 Ecuador"
[EGY]="e_26.50_029.86 Egypt"
[ERI]="e_15.36_038.85 Eritrea"
[ESP]="e_40.24-003.65 Spain"
[EST]="e_58.67_025.54 Estonia"
[ETH]="e_08.62_039.60 Ethiopia"
[FIN]="e_64.50_026.27 Finland"
[FJI]="e-17.43_165.45 Fiji"
[FRA]="e_42.17-002.76 France"
[FRO]="e_62.05-006.88 Faeroe Islands"
[FSM]="e_07.45_153.24 Federated States of Micronesia"
[GAB]="e-00.59_011.79 Gabon"
[GBR]="e_54.12-002.87 United Kingdom"
[GEO]="e_42.17_043.51 Georgia"
[GGY]="e_49.47-002.57 Guernsey"
[GHA]="e_07.95-001.22 Ghana"
[GIN]="e_10.44-010.94 Guinea"
[GMB]="e_13.45-015.40 The Gambia"
[GNB]="e_12.05-014.95 Guinea-Bissau"
[GNQ]="e_01.71_010.34 Equatorial Guinea"
[GRC]="e_39.07_022.96 Greece"
[GRD]="e_12.12-061.68 Grenada"
[GRL]="e_74.71-041.34 Greenland"
[GTM]="e_15.69-090.36 Guatemala"
[GUM]="e_13.44_144.77 Guam"
[GUY]="e_04.79-058.98 Guyana"
[HKG]="e_22.40_114.11 Hong Kong"
[HMD]="e-53.09_073.52 Heard I. and McDonald Islands"
[HND]="e_14.83-086.62 Honduras"
[HRV]="e_45.08_016.40 Croatia"
[HTI]="e_18.94-072.69 Haiti"
[HUN]="e_47.16_019.40 Hungary"
[IDN]="e-02.22_117.24 Indonesia"
[IMN]="e_54.22-004.54 Isle of Man"
[IND]="e_22.89_079.61 India"
[IRL]="e_53.18-008.14 Ireland"
[IRN]="e_32.58_054.27 Iran"
[IRQ]="e_33.04_043.74 Iraq"
[ISL]="e_65.00-018.57 Iceland"
[ISR]="e_31.46_035.00 Israel"
[ITA]="e_42.80_012.07 Italy"
[JAM]="e_18.16-077.31 Jamaica"
[JEY]="e_49.22-002.13 Jersey"
[JOR]="e_31.25_036.77 Jordan"
[JPN]="e_37.59_138.03 Japan"
[KAZ]="e_48.16_067.29 Kazakhstan"
[KEN]="e_00.60_037.80 Kenya"
[KGZ]="e_41.46_074.54 Kyrgyzstan"
[KHM]="e_12.72_104.91 Cambodia"
[KIR]="e_00.86-045.61 Kiribati"
[KNA]="e_17.26-062.69 Saint Kitts and Nevis"
[KOR]="e_36.39_127.84 Republic of Korea"
[KWT]="e_29.33_047.59 Kuwait"
[LAO]="e_18.50_103.74 Lao PDR"
[LBN]="e_33.92_035.88 Lebanon"
[LBR]="e_06.45-009.32 Liberia"
[LBY]="e_27.03_018.01 Libya"
[LCA]="e_13.89-060.97 Saint Lucia"
[LIE]="e_47.14_009.54 Liechtenstein"
[LKA]="e_07.61_080.70 Sri Lanka"
[LSO]="e-29.58_028.23 Lesotho"
[LTU]="e_55.33_023.89 Lithuania"
[LUX]="e_49.77_006.07 Luxembourg"
[LVA]="e_56.85_024.91 Latvia"
[MAC]="e_22.22_113.51 Macao"
[MAF]="e_18.09-063.06 Saint-Martin"
[MAR]="e_29.84-008.46 Morocco"
[MCO]="e_43.75_007.41 Monaco"
[MDA]="e_47.19_028.46 Moldova"
[MDG]="e-19.37_046.70 Madagascar"
[MDV]="e_03.73_073.46 Maldives"
[MEX]="e_23.95-102.52 Mexico"
[MHL]="e_07.00_170.34 Marshall Islands"
[MKD]="e_41.60_021.68 Macedonia"
[MLI]="e_17.35-003.54 Mali"
[MLT]="e_35.92_014.41 Malta"
[MMR]="e_21.19_096.49 Myanmar"
[MNE]="e_42.79_019.24 Montenegro"
[MNG]="e_46.83_103.05 Mongolia"
[MNP]="e_15.83_145.62 Northern Mariana Islands"
[MOZ]="e-17.27_035.53 Mozambique"
[MRT]="e_20.26-010.35 Mauritania"
[MSR]="e_16.74-062.19 Montserrat"
[MUS]="e-20.28_057.57 Mauritius"
[MWI]="e-13.22_034.29 Malawi"
[MYS]="e_03.79_109.70 Malaysia"
[NAM]="e-22.13_017.21 Namibia"
[NCL]="e-21.30_165.68 New Caledonia"
[NER]="e_17.42_009.39 Niger"
[NFK]="e-29.05_167.95 Norfolk Island"
[NGA]="e_09.59_008.09 Nigeria"
[NIC]="e_12.85-085.03 Nicaragua"
[NIU]="e-19.05-169.87 Niue"
[NLD]="e_52.10_005.28 Netherlands"
[NOR]="e_68.75_015.35 Norway"
[NPL]="e_28.25_083.92 Nepal"
[NRU]="e-00.52_166.93 Nauru"
[NZL]="e-41.81_171.48 New Zealand"
[OMN]="e_20.61_056.09 Oman"
[PAK]="e_29.95_069.34 Pakistan"
[PAN]="e_08.52-080.12 Panama"
[PCN]="e-24.37-128.32 Pitcairn Islands"
[PER]="e-09.15-074.38 Peru"
[PHL]="e_11.78_122.88 Philippines"
[PLW]="e_07.29_134.41 Palau"
[PNG]="e-06.46_145.21 Papua New Guinea"
[POL]="e_52.13_019.39 Poland"
[PRI]="e_18.23-066.47 Puerto Rico"
[PRK]="e_40.15_127.19 Dem. Rep. Korea"
[PRT]="e_39.60-008.50 Portugal"
[PRY]="e-23.23-058.40 Paraguay"
[PSE]="e_31.92_035.20 Palestine"
[PYF]="e-14.72-144.90 French Polynesia"
[QAT]="e_25.31_051.18 Qatar"
[ROU]="e_45.85_024.97 Romania"
[RUS]="e_61.98_096.69 Russian Federation"
[RWA]="e-01.99_029.92 Rwanda"
[SAU]="e_24.12_044.54 Saudi Arabia"
[SDN]="e_15.99_029.94 Sudan"
[SSD]="e_07.31_030.25 South Sudan"
[SEN]="e_14.37-014.47 Senegal"
[SGP]="e_01.36_103.82 Singapore"
[SGS]="e-54.46-036.43 South Georgia and South Sandwich Islands"
[SHN]="e-12.40-009.55 Saint Helena"
[SLB]="e-08.92_159.63 Solomon Islands"
[SLE]="e_08.56-011.79 Sierra Leone"
[SLV]="e_13.74-088.87 El Salvador"
[SMR]="e_43.94_012.46 San Marino"
[SOM]="e_04.75_045.71 Somalia"
[SPM]="e_46.92-056.30 Saint Pierre and Miquelon"
[SRB]="e_44.22_020.79 Serbia"
[STP]="e_00.44_006.72 Sao Tome and Principe"
[SUR]="e_04.13-055.91 Suriname"
[SVK]="e_48.71_019.48 Slovakia"
[SVN]="e_46.12_014.80 Slovenia"
[SWE]="e_62.78_016.75 Sweden"
[SWZ]="e-26.56_031.48 Swaziland"
[SXM]="e_18.05-063.06 Sint Maarten"
[SYC]="e-04.66_055.48 Seychelles"
[SYR]="e_35.03_038.51 Syria"
[TCA]="e_21.83-071.97 Turks and Caicos Islands"
[TCD]="e_15.33_018.64 Chad"
[TGO]="e_08.53_000.96 Togo"
[THA]="e_15.12_101.00 Thailand"
[TJK]="e_38.53_071.01 Tajikistan"
[TKM]="e_39.12_059.37 Turkmenistan"
[TLS]="e-08.83_125.84 Timor-Leste"
[TON]="e-20.43-174.81 Tonga"
[TTO]="e_10.46-061.27 Trinidad and Tobago"
[TUN]="e_34.12_009.55 Tunisia"
[TUR]="e_39.06_035.17 Turkey"
[TZA]="e-06.28_034.81 Tanzania"
[UGA]="e_01.27_032.37 Uganda"
[UKR]="e_49.00_031.38 Ukraine"
[URY]="e-32.80-056.02 Uruguay"
[USA]="e_45.68-112.46 United States"
[UZB]="e_41.76_063.14 Uzbekistan"
[VAT]="e_41.90_012.43 Vatican"
[VCT]="e_13.22-061.20 Saint Vincent and the Grenadines"
[VEN]="e_07.12-066.18 Venezuela"
[VGB]="e_18.53-064.47 British Virgin Islands"
[VIR]="e_17.96-064.80 United States Virgin Islands"
[VNM]="e_16.65_106.30 Vietnam"
[VUT]="e-16.23_167.69 Vanuatu"
[WLF]="e-13.89-177.35 Wallis and Futuna Islands"
[WSM]="e-13.75-172.16 Samoa"
[YEM]="e_15.91_047.59 Yemen"
[ZAF]="e-29.00_025.08 South Africa"
[ZMB]="e-13.46_027.77 Zambia"
[ZWE]="e-19.00_029.85 Zimbabwe"
)

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

### Others Globals ###

[[ "${BL_PGPID_KEYSERVERS[*]}" ]] || BL_PGPID_KEYSERVERS=(
"hkps://keys.foopgp.org"
"hkps://pgp.id:11371"
)

if ((BL_PGPID_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_PGPID_chelpmsg="
"$"Generate and manage OpenPGP ID, an OpenPGP configuration providing universal and decentralized civil status to secure your digital life (emails, git, ssh, avatar, sso, etc.).""
"$"It combines the power of OpenPGP (RFC 9580) with those of others open international standards: POSIX, ICAO 9303, ISO/IEC 7816, many others RFC.""

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 --

### internal functions ###

_bl_pgpid_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_PGPID_usage" "$BL_PGPID_chelpmsg" "$BL_PGPID_shelpmsg" ; return 1 ;;
			-V|--version) printf "%s %s\n" "$BL_PGPID_NAME" "$BL_PGPID_VERSION" ; return 1 ;;
			--) shift ; break ;;
			-*) printf -- "$BL_PGPID_NAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$BASH_SOURCE" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done
	BL_PGPID_NOPTIONS=$((npp-$#))
	if [[ "$frontend" ]] ; then
		source "$(dirname "$BASH_SOURCE")"/bl-interactive --frontend "$frontend" --
	fi
}

_bl_icao9303_mrz_checkdigit() {
	local helpmsg="Usage: $FUNCNAME STRING [EXPECTED_RESULT]
If there is no second arg: output calculated check digit from [0-9A-Z<]* string
else return non-zero if EXPECTED_RESULT differs from calculated check digit.
"
	for ((;$#;)) ; do
		case "$1" in
			-h|--h*) echo "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) echo -e "$FUNCNAME: Error: Unrecognized option/parameters $1\n$helpmsg" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local char sum=0 weight=(7 3 1)
	for ((i=0;i<${#1};i++)) ; do
		char=${1:$i:1}
		case $char in
			[0-9]) ((sum+=char*${weight[$((i%3))]})) ;;
			[A-Z]) ((sum+=($(printf "%d" "'$char'")-55)*${weight[$((i%3))]})) ;;
		esac
	done

	if ! [[ "$2" ]] ; then
		echo $((sum%10))
	else
		[[ "$2" == $((sum%10)) ]]
		return $?
	fi
}

_bl_icao9303_mrz_analyse() {
	local helpmsg="Usage: $FUNCNAME [Options] AANAME [MRZ]
Analyse a Machine Readable Zone and fill the associative array named AANAME
If MRZ is not passed as argument, it will we read from STDIN.

OPTIONS:
  -u, --uncheck                Change some MRZ errors to warnings (size and digitchecks)
  -d, --declare                Output content of AANAME as a 'declare -A ...' statement
"

	local errlvl="Error" outdec
	for ((;$#;)) ; do
		case "$1" in
			-u|--uncheck) errlvl="Warning" ;;
			-d|--declare) outdec="." ;;
			-h|--h*) echo "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) echo -e "$FUNCNAME: Error: Unrecognized option/parameters $1\n$helpmsg" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local -n doc=${1:?}
	shift
	local mrz
	[[ "$*" ]] && mrz=$* || mrz=$(< /dev/stdin)
	mrz=${mrz//[$'\t\r\n ']}

	local ch ret=0
	if [[ ${mrz:0:1} != P ]] ; then
		# Only ICAO-9303 (ISO/IEC-7501-1) passport are yet supported.
		printf "$FUNCNAME: Error: "$"Unsupported MRZ"" '%s...'.\n"  "${mrz:0:1}" >&2
		return 1
	fi
	if [[ ${#mrz} != 88 ]] ; then
		printf "$FUNCNAME: %s: "$"Invalid MRZ lenght:"" %d/88.\n" "$errlvl" "${#mrz}" >&2
		[[ "$errlvl" == "Warning" ]] || ret=1
	fi

	doc=(
		[type]=${mrz:0:2}
		[country]=${mrz:2:3}
		[all_names]=${mrz:5:39}
		[number]=${mrz:44:9}
		[check_number]=${mrz:53:1}
		[nationality]=${mrz:54:3}
		[date_of_birth]=${mrz:57:6}
		[check_date_of_birth]=${mrz:63:1}
		[sex]=${mrz:64:1}
		[expiration_date]=${mrz:65:6}
		[check_expiration_date]=${mrz:71:1}
		[personal_number]=${mrz:72:14}
		[check_personal_number]=${mrz:86:1}
		[composite]="${mrz:44:10}${mrz:57:7}${mrz:65:20}"
		[check_composite]=${mrz:87:1}
	)


	for ch in number date_of_birth expiration_date personal_number composite ; do
		doc[checked_$ch]=$(_bl_icao9303_mrz_checkdigit "${doc[$ch]}")
		doc[valid_$ch]=$( [[ ${doc[checked_$ch]} == ${doc[check_$ch]} ]] && echo true || echo false )
		if ! ${doc[valid_$ch]} ; then
			printf "$FUNCNAME: %s: %s checksum -> %s. "$"Should be"" %s.'\n" "$errlvl" "$ch" "${doc[check_$ch]}" "${doc[checked_$ch]}" >&2
			[[ "$errlvl" == "Warning" ]] || ret=1
		fi
	done
	[[ -z "$outdec" ]] || echo "${doc[@]@A}"

	return $ret
}

_bl_pgp_get_seckeyid() {
	local helpmsg="Usage: $FUNCNAME [NAME|U4|U5|EMAIL]
Call 'gpg --with-colon' and ouput exportable secret keys (all if no argument), one by line:
KeyID     Creation_Date    Public_Key_Algorithm

OPTIONS:
  -H, --homedir GNUPGHOME   GnuPG home directory - Environment variable: GNUPGHOME, default: '~/.gnupg'.
  -e, --exportable          Ouput only exportable secret keys (ie. present localy, excluding those on security token)
"
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local filtertest=true
	for ((;$#;)) ; do
		case "$1" in
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-e|--exportable) filtertest='[[ "${record[TockenSN]}" == \+ ]]' ;;
			-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

	# See: https://www.rfc-editor.org/rfc/rfc9580#name-public-key-algorithms
	local -a pka=([1]="RSA" "RSA_Encrypt-Only" "RSA_Sign-Only" [16]="Elgamal_Encrypt-Only" "DSA" "ECDH" "ECDSA" "Elgamal" "Diffie-Hellman" "EdDSALegacy" "AEDH" "AEDSA" "X25519" "X448" "Ed25519" "Ed448")
	local pkaindex
	local -A record

	# See: https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob_plain;f=doc/DETAILS
	gpg --homedir "$gpghome" --list-secret-keys --with-colons $1 | grep "^sec:" | while	IFS=':' read record[Type] record[Validity] record[KeyLenght] record[PubKeyAlgo] record[KeyID] record[CreationDate] record[ExpirationDate] record[Field8] record[Ownertrust] record[UserID] record[SignatureClass] record[KeyCapabilities] record[Field13] record[Flags] record[TockenSN] record[HashAlgo] record[CurveName] record[ComplianceFlags] record[LastUpdate] record[Origin] record[Comment] record[rfu] ; do
		if eval "$filtertest" ; then
			if ! pkaindex=$((${record[PubKeyAlgo]})) ; then
				printf "$FUNCNAME: Error: "$"Unexpected field 4 (Public Key Algorithm)"".\n" >&2
				return 1
			fi
			echo "${record[KeyID]}  $(date --iso-8601=hours --date "@${record[CreationDate]}")  ${pka[${pkaindex}]:-unkown}"
		fi
	done
	return ${PIPESTATUS[0]}
}

_bl_pgp_choose_seckeyid() {
	local qtxt=$"Select KeyID (your OpenPGP certificate)"
	local helpmsg="Usage: $FUNCNAME [NAME|U4|U5|EMAIL]
Output keyid choosen from available (main) secrets keys.
If there is only one available, output its keyid whithout asking.
Argument NAME|U4|U5|EMAIL allow to make an (almost totaly useless) input filter.

OPTIONS:
  -H, --homedir GNUPGHOME   GnuPG home directory (default: ~/.gnupg). Environment variable: \$GNUPGHOME
  -t, --text QUESTION       Text to introduce selection's radiolist (default: '$qtxt')
  -e, --exportable          Only exportable secret keys (ie. present localy, excluding those on security token)
"
	local keyid gpghome=${GNUPGHOME:-~/.gnupg} getargs outfpr=0
	for ((;$#;)) ; do
		case "$1" in
			-F|--fingerprint) outfpr=1 ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-t|--text)
				shift ; qtxt=$1 ;;
			-e|--exportable) getargs+="$1 " ;;
			-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 -a seckeyids
	mapfile -t seckeyids < <(_bl_pgp_get_seckeyid --homedir "$gpghome" $getargs $1)
	((${#seckeyids[@]}>0)) || { printf "$FUNCNAME: Error: "$"No editable OpenPGP certificate (in %s)"".\n" "$gpghome" >&2 ; return 1 ; }
	if ((${#seckeyids[@]}==1)) ; then
		keyid=${seckeyids[0]}
		printf "$FUNCNAME: Notice: "$"Choosing the only one editable OpenPGP certificate (%s)"".\n" "$keyid" >&2
	else
		keyid="$(bl_radiolist --output-value --num-per-line 1 --text "$qtxt" "${seckeyids[@]}")"
	fi
	echo "${keyid%% *}"
}

### public functions / program actions ###

bl_pgpid_get() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... NAME|U4|U5|EMAIL"
	local maxfromks=100
	local keyserv keyservs=("${BL_PGPID_KEYSERVERS[@]}")
	local helpmsg="
"$"Output fingerprints, emails and OpenPGP IDentifier of certificates matching NAME|U4|U5|EMAIL.""
"$"May also get or refresh certificates from keyservers.""

OPTIONS:
  -F, --fingerprint           "$"Output only fingerprints.""
  -E, --email                 "$"Output only emails.""
  -s, --secret                "$"Search through local secret keys instead of public certificate.""
  -m, --errexit-g=1           "$"Return an error if there is more than one (1) entry."" - "$"You may replace '1' by an other number.""
  -M, --max-from-keyserv MAX  "$"Set maximum matching certificates to receive from keyservers. "$"Default: ""$maxfromks
  -T, --ownertrust THRESHOLD  "$"Only output fingerprint with ownertrust greater or equal to THRESHOLD."" (Not implemented)
  -H, --homedir GNUPGHOME     "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME
  -K, --keyservers KEYSERVERS "$"If non-empty, search and refresh certificates from this keyservers. "$"Default: ""
$(printf -- "%80s\n" ${keyservs[@]})
"
	local onlyfpr=0 onlymbox=0
	local gpgaction="--list-keys"
	local errexit=$(getconf INT_MAX)
	local gpghome=${GNUPGHOME:-~/.gnupg}
	for ((;$#;)) ; do
		case "$1" in
			-F|--fpr|--fingerprint)
				onlyfpr=1 onlymbox=0 ;;
			-E|--mbox|--email)
				onlyfpr=0 onlymbox=1 ;;
			-s|--secret)
				gpgaction="--list-secret-keys"
				keyservs=() ;;
			-m|--errexit|--errexit-g)
				errexit=1 ;;
			--errexit*=*)
				errexit=${1#*=}
				[[ "$errexit" =~ ^[1-9][0-9]*$ ]] || { printf "$FUNCNAME: Error: "$"Option %s expect a number"".\n" "--errexit-g=" >&2 ; return 2 ; }
				;;
			-M|--max-from-keyserv)
				[[ "$2" =~ ^[0-9]+$ ]] || { printf "$FUNCNAME: Error: "$"Option %s expect a number"".\n" "--max-from-keyserv" >&2 ; return 2 ; }
				(($2 <= 100)) || printf "$FUNCNAME: Warning: "$"%s exceeds usual key server limit %s"".\n" "$2" "(100)" >&2
				shift ; maxfromks=$1 ;;
			-T|--ownertrust)
				printf "$FUNCNAME: Error: "$"Sorry, option %s not implemented yet.""\n" "$1" >&2 ; return 2 ;;
			-K|--keyservers)
				shift ; keyservs=($1) ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-v|--verify) verify=1 ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_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

	[[ "$1" ]] || { printf "$usage\n" >&2 ; return 2 ; }
	local user=$1

	if ((maxfromks>0)) ; then
		local -a ksfprs
		local scheme serv port path ksurl
		for keyserv in ${keyservs[@]} ; do
			[[ "$keyserv" =~ // ]] || keyserv="hkps://$keyserv"
			[[ "${keyserv,,}" =~ ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*) ]] || { printf "$FUNCNAME: Warning: "$"Non RFC3986 URI, skipping  %s.""\n" "'$keyserv'" ; continue ; }
			scheme=${BASH_REMATCH[2]}
			serv=${BASH_REMATCH[4]}
			path=${BASH_REMATCH[5]}
			[[ $scheme == hkp ]] || [[ $scheme == hkps ]] ||{ printf "$FUNCNAME: Warning: "$"Non hkp or hkps scheme, skipping  %s.""\n" "'$keyserv'" ; continue ; }
			if [[ "${serv##*:}" =~ ^[0-9]+$ ]] ; then
				port=${BASH_REMATCH[0]}
				serv=${serv%:*}
			else
				[[ "${scheme:3:1}" == s ]] && port=443 || port=11371
			fi
			ksurl="http${scheme:3:1}://${serv}:${port}${path}/pks/lookup"
			ksfprs=($(curl --no-progress-meter --location --get --data-urlencode 'op=index' --data-urlencode "search=$user" --data-urlencode "options=mr" "$ksurl" | sed -n 's,^pub:\([[:xdigit:]]*\):.*,\1,p'))
			if [[ -z "${ksfprs[@]}" ]] ; then
				printf "$FUNCNAME: Notice: "$"No certificate for %s on %s.""\n" "'$user'" "'$keyserv'" >&2
				continue
			elif ((${#ksfprs[@]} <= maxfromks )) ; then
				curl --no-progress-meter --location --get --data-urlencode 'op=get' --data-urlencode "search=$user" --data-urlencode "options=mr" "$ksurl" | gpg --homedir "$gpghome" --import-options import-clean --import
			else
				local i
				for ((i=0;i<maxfromks;i++)) ; do
					curl --no-progress-meter --location --get --data-urlencode 'op=get' --data-urlencode "search=0x${ksfprs[i]}" --data-urlencode "options=mr" "$ksurl" | gpg --homedir "$gpghome" --import
				done
			fi
		done
	fi

	local -a fprmboxs fprs
	readarray -t fprmboxs < <(gpg --homedir "$gpghome" --list-options show-only-fpr-mbox $gpgaction "$user")
	fprs=($(printf -- "%s\n" "${fprmboxs[@]%% *}" | sort -u))
	(( ${#fprs[@]} > 0 )) || { printf "$FUNCNAME: Error: "$"No certificate for user"" '%s'.\n" "$user" >&2 ; return 141 ;}
	if ((onlyfpr)) ; then
		printf -- "%s\n" "${fprs[@]}"
	elif ((onlymbox)) ; then
		printf -- "%s\n" "${fprmboxs[@]#* }"
	else
		local cl cfpr prevfpr id
		for cl in "${fprmboxs[@]}" ; do
			cfpr=${cl%% *}
			if [[ "$cfpr" != $prevfpr ]] ; then
				id=" $(gpg --homedir "$gpghome" --with-colons $gpgaction "0x$cfpr" | sed --silent --regexp-extended "s,^uid:.*4=?(${BL_PGPID_U4_REGEX}).*,u4\1,p ; s,^uid:.*5=?(${BL_PGPID_U5_REGEX}).*,u5\1,p" | sort -u)"
				if [[ "$id" == *$'\n'* ]] ; then
					printf "$FUNCNAME: Warning: "$"Certificate %s contain more than one OpenPGP ID string.""\n" "0x$cfpr" >&2
					id=""
				fi
			fi
			printf -- "%-80s${id}\n" "$cl"
			prevfpr=$cfpr
		done
	fi

	if ((${#fprs[@]} != 1 )) ; then
		(( ${#fprs[@]} <= errexit )) && local errlvl="Notice" || local errlvl="Error"
		printf "$FUNCNAME: $errlvl: "$"%s certificates for %s." "${#fprs[@]}" "'$user'" >&2
		(( ${#fprs[@]} >= maxfromks )) && printf " "$"Some public certificates may be missing.""\n" >&2 || printf "\n" >&2
	fi
	(( ${#fprs[@]} <= errexit )) || return 7
}

bl_pgpid_mrz_to_u4() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [MRZ]..."
	local helpmsg="
Calculate and output a OpenPGP ID u4, from the Machine Readable Zone of an icao9303 passport.
If MRZ is not passed as argument, it will we read from STDIN.

OPTIONS:
  -d, --birth-date YYYY-MM-DD  "$"Birth date, expected format : Year-Month-Day (for people born 100+ years ago).""
  -u, --uncheck                "$"Change some MRZ errors to warnings (only if you really know what you do !).""

WARNING: mrz data may be irrelevant to generate u4 string. eg:
         * Surname or given names may be incomplete (cut bc exceed mrz size)
         * Surname or given names may differs from those given at birth (marriage, gender change, etc.)
         * Surname or given names transliteration may have change over time
         * Year of birth date is only written with 2 digits (and people may live longer than 100 years)
         * Humans may have done error on birth date, surname or given names.
By the way, there are ~80% chance that an icao9303 MRZ allow to generate a correct u4 string.
People have to check, and fix when NOK.
"

	local gdate uncheck
	for ((;$#;)) ; do
		case "$1" in
			-d|--birth-d*) shift ; gdate=$(sed ' s,[^0-9],,g ' <<<"$1") ;; # keep only [0-9] from given input
			-u|--uncheck) uncheck="--uncheck" ;;
			-h|--h*) echo "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) echo -e "$name: Error:" $"Unrecognized option/parameters"" $1\n$helpmsg" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local -A mrza
	if ! _bl_icao9303_mrz_analyse $uncheck mrza "$@" ; then
		echo "$FUNCNAME: Error:" $"Invalid Machine Readable Zone (ICAO9303)" >&2
		return 1
	fi

	local allnames bdate countrycode coordinates

	if ! allnames=$(LC_COLLATE="C.UTF-8" grep -o "[A-Z]\{1,32\}<<[A-Z]\{1,32\}<[A-Z]\{0,32\}<" <<<"${mrza[all_names]}" ) ; then
		echo "$FUNCNAME: Error:" $"No valid and complete surname and given names extracted from" "${mrza[all_names]}" >&2
		return 1
	fi

	countrycode="${mrza[country]}"
	coordinates="${BL_PGPID_COORDINATES[$countrycode]: 0:14}"
	if ! [[ "$coordinates" =~ ^${BL_PGPID_CO_REGEX}$ ]] ; then
		printf "$FUNCNAME: Error: "$"Invalid coordinates (%s) for country '%s'.""\n" "$coordinates" "$countrycode" >&2
		return 1
	fi

	if [[ "$gdate" ]] ; then
		((${#gdate}==8)) || printf "$FUNCNAME: Notice: "$"Invalid lenght for birth date '%s'.""\n" "$gdate" >&2
	else
		gdate="${mrza[date_of_birth]}"
	fi
	if ! bdate=$(date --date "${gdate}" +"%Y-%m-%d" ) ; then
		printf "$FUNCNAME: Error: "$"Can't reformat birthdate '%s'.""\n" "$gdate" >&2
		return 1
	fi

	echo "$(printf "${allnames}${bdate}" | md5sum | xxd -r -p | basenc --base64url | sed 's/==$//')$coordinates"
}

bl_pgpid_gen_u4() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [MRZ]..."
	local helpmsg="
"$"Generate OpenPGP ID u4 string"". "$"Missing input will be asked interactively.""
"$"You may pass the Machine Readable Zone of an international passport as argument, but there are ~20% chances that generated *u4* is incorrect.""

OPTIONS:
  -s, --surname SURNAME            "$"Surname/family name at birth"".
  -g, --given-names GIVEN_NAMES    "$"Given names at birth, separated by space ' ' or comma ',' or hyphen '-'.""
  -d, --birth-date YYYY-MM-DD      "$"Birth date, expected format : Year-Month-Day"".
  -c, --birth-country COUNTRY_CODE "$"3 letters country code of birth place: GBR, NGA, FRA, ...""
  -v, --verify                     "$"Ask for verification"".
"
	local sname gname translit bdate bcountry errlvl="Error"
	for ((;$#;)) ; do
		case "$1" in
			-s|--s*)
				shift ; sname=$1 ;;
			-g|--g*)
				shift ; gname=$1 ;;
			-d|--birth-d*)
				shift ; bdate=$1 ;; # $bdate is always be passed as an argument to bl_pgpid_mrz_to_u4()
			-c|--birth-c*)
				shift ; bcountry=$1 ;;
			-v|--verify) errlvl="Warning" ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_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 a Machine Readable Zone is passed as arguments
	if [[ "$1" ]] ; then
		local -A mrza=()
		if ! _bl_icao9303_mrz_analyse mrza "$@" ; then
			printf "$FUNCNAME: %s: "$"Invalid Machine Readable Zone"" (ICAO9303)\n" "$errlvl" >&2
			# If not --verify, errexit.
			[[ "$errlvl" == Warning ]] || return 1
		fi
		[[ "$sname" ]] || sname=$(tr '<' ' ' <<<"${mrza[all_names]%%<<*}")
		[[ "$gname" ]] || gname=$(sed ' s,<*$,,g ; s,<, , ' <<<"${mrza[all_names]#*<<}")
		[[ "$bdate" ]] || bdate=$(date --iso-8601 --date "${mrza[date_of_birth]}")
		[[ "$bcountry" ]] || bcountry="${mrza[country]}"
	fi

	[[ "$sname" ]] || sname=$(bl_input $"Birth surname (family name)")
	[[ "$gname" ]] || gname=$(bl_input $"Birth names (all given names)")
	[[ "$bdate" ]] || bdate=$(bl_input --iso-8601 $"Birth date (YYYY-MM-DD)")
	[[ "$bcountry" ]] || bcountry=$(bl_input --default "FRA" $"Birth country (3 letter code)")

	# --verify
	while [[ "$errlvl" == Warning ]] ; do
		! bl_yesno --default=no --text \
	"
	"$"Surname at birth:""     ${sname}
	"$"Given names at birth:"" ${gname}
	"$"Date of birth:""        ${bdate}
	"$"Country of birth:""     ${bcountry^^}
	" \
			$"Is that correct" || break

		sname=$(bl_input --default "${sname^^}" $"Birth surname (family name)")
		gname=$(bl_input --default "${gname}" $"Birth names (all given names)")
		bdate=$(bl_input --iso-8601 --default "${bdate}" $"Birth date (YYYY-MM-DD)")
		bcountry=$(bl_input --default "${bcountry^^}" $"Birth country (3 letter code)")
	done

	sname=$(sed 's,[ ;,<-]\+,<,g'<<<"$sname" ) # replace and squeeze some word separators
	translit=$(iconv -f utf-8 -t ascii//TRANSLIT <<<"$sname") # transliterate (according to iconv), may differ from Passport transliterations.
	if [[ "$translit" != "$sname" ]] ; then
		printf "$FUNCNAME: Warning: "$"'%s' has been transliterated to '%s'. "$"It may be WRONG !""\n" "$sname" "$translit" >&2
		sname="$translit"
	fi
	sname="${sname^^}" # uppercase

	gname=$(sed 's,[ ;,<-]\+,<,g'<<<"$gname" )
	translit=$(iconv -f utf-8 -t ascii//TRANSLIT <<<"$gname")
	if [[ "$translit" != "$gname" ]] ; then
		printf "$FUNCNAME: Warning: "$"'%s' has been transliterated to '%s'. "$"It may be WRONG !""\n" "$gname" "$translit" >&2
		gname="$translit"
	fi
	gname="${gname^^}"

	if ! fakemrz="P<${bcountry^^}$(LC_COLLATE="C.UTF-8" grep -o "[A-Z]\{1,32\}<<[A-Z]\{1,32\}<[A-Z]\{0,32\}<" <<<"${sname}<<$gname<<" )" ; then
		echo "$FUNCNAME: Error: "$"Only [A-Z] characters allowed in names." >&2
		return 1
	fi
	fakemrz+="000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"

	bl_pgpid_mrz_to_u4 --uncheck --birth-date "$bdate" "${fakemrz: 0:88}"
}

bl_pgpid_gen_uid() {
#  Since linux kernel 2.6, user ID (uid) are coded on 32 bit, so it should range from 0 (root) to 2^32-1 = 4294967295
#  On older unix systems it was coded on 16 bit (0 to 65535) :
#  - 0: root
#  - under 1000: "system" users
#  - 1000: Main "human" user (also often: first administrator)
#  - above 1000: other "human" users
#  - up to 65534, which is use by convention by the user 'nobody'.
#
#  tests:
#  - $ LANG=C.UTF-8 sudo useradd -u 4294967295 test
#    useradd: invalid user ID '4294967295'
#    Return value $?: 3 (error)
#  - $ LANG=C.UTF-8 sudo useradd -u 4294967294 test
#    useradd warning: test's uid -2 outside of the UID_MIN 1000 and UID_MAX 60000 range.
#    Return value $?: 0 (OK)
#
#   If we let 4294967294, 4294967293 and id under 2^18 (< 262144), for some futur usage/convention,
#   then we can use range 262144 to 4294967292, to get an almost unique Unix User ID from unique OpenPGP ID string (u4,...)
#
#   See: https://www.debian.org/doc/debian-policy/ch-opersys.html#users-and-groups
#
#   Which make 4294705148 slots, ~= about half of the actual human population (~8 billions).
#   But we encounter many problems when ( uid >= 2^31 ), probably because many software manipulate uid as signed instead of unsigned.
#   Then we have had to decrease BL_PGPID_XUID_MAX to 2^31-2=2147483646
#
#   Constants: BL_PGPID_XUID_MIN and BL_PGPID_XUID_MAX

	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [U4]"
	local helpmsg="
"$"Generate a 32bit Unix User ID, from 2^18 to (2^31)-2"" ([$BL_PGPID_XUID_MIN,$BL_PGPID_XUID_MAX]).
"$"If no argument is given, it will ask for surname, names and date of birth.""

OPTIONS:
  -f, --free-input            "$"Accept any input, not only valid PGPID U4 string.""
  -F, --from FILE             "$"Get input from first line of FILE (eg: fifo, tmpfs, /dev/stdin ...)""
"
	local freeinput=0 str_in u4h num
	for ((;$#;)) ; do
		case "$1" in
			-f|--free-input) freeinput=1 ;;
			-F|--from)
				shift ; str_in=$( sed 1q < "$1" ) || return 1 ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_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 "$str_in" ]] ; then
		if [[ "$1" ]] ; then
			# input is passed as arguments
			str_in="$*"
		else
			# No input at all (then --free-input is non-sense)
			str_in="$(bl_pgpid_gen_u4 --birth-country FRA)==" || return $?
			freeinput=0
		fi
	elif [[ "$1" ]] ; then
			printf "$FUNCNAME: Warning: "$"Option %s overide given argument '%s'""...\n" "--from" "${1: 0:44}" >&2
	fi

	if ((freeinput)) ; then
		u4h="$(echo "$str_in" | md5sum | xxd -r -p | basenc --base64url)"
		echo "$u4h"
	elif [[ "$str_in" =~ (${BL_PGPID_U4H_REGEX})${BL_PGPID_CO_REGEX} ]] ; then
		u4h="${BASH_REMATCH[1]}=="
	else
		printf "$FUNCNAME: Error: "$"No u4 string (hash) found in '%s'""...\n" "${str_in: 0:50}" >&2
		return 2
	fi

	num=$(bl_shrink_num --input-encoding base64url $((BL_PGPID_XUID_MAX - BL_PGPID_XUID_MIN)) <<<"$u4h") && echo $((num + BL_PGPID_XUID_MIN))
}

bl_pgpid_gen_key() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... EMAIL"
	local keyserv="${BL_PGPID_KEYSERVERS[0]}"
	local helpmsg="
"$"Generate an OpenPGP key pair (public and secret) according to OpenPGP ID standards.""
"$"Missing input will be asked interactively.""
"$"Output 3 lines for each fingerprints:""
* "$"main key"" (Sign Certify)
* "$"decryption key"" (Encrypt)
* "$"authentication key"" (Auth)

OPTIONS:
  -N, --name PSEUDONYM             "$"Common name or pseudonym. "$"Default: "$"first part of email"".
  -c, --comment U4|U5              "$"First part of comment field MUST be a OpenPGP ID string."$" See: ""\$BL_PGPID_U4_REGEX \$BL_PGPID_U5_REGEX.
  -C, --extra-comment COMMENT      "$"Comment (second part of comment field, free to use).""
  -p, --passphrase PASSPHRASE      "$"Passphrase to (symetric) encrypt secret part of OpenPGP key. CAN'T BE EMPTY (at this stage).""
  -P, --passfrom FILE              "$"Get passphrase from first line of FILE (eg: fifo, tmpfs, /dev/stdin ...)""
  -e, --expiration YEARS           "$"Number of years before certificate expiration. "$"Default: ""11 -> $(date -I -d "+11 years"))
  -k, --keyserver KEYSERVER        "$"Prefered OpenPGP certificate server. "$"Default: ""$keyserv
  -H, --homedir GNUPGHOME          "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME
"
	# TODO: (maybe) sanitize --name or --extra-comment inputs (removing '\' '(' ')' ... ) ?
	local email passphrase pseudo pgpidu pgpidugiven extracomment
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local expire=11 verify=""
	for ((;$#;)) ; do
		case "$1" in
			-n|-N|--name*)
				shift ; pseudo=$1 ;;
			-c|--comment)
				shift ; pgpidu=$1 ; pgpidugiven="." ;;
			-C|--extracomment|--extra-comment)
				shift ; extracomment=$1 ;;
			-p|--passphrase)
				shift ; passphrase=$1 ;;
			-P|--passfrom|--pass-from)
				shift ; passphrase=$( sed 1q < "$1" ) || return 1 ;;
			-e|--expir*)
				shift ; expire=$1 ;;
			-k|--keyserver)
				shift ; keyserv=$1 ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-v|--verify) verify=1 ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_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 ! [[ "$1" =~ ^${BL_INTERACTIVE_EMAIL_REGEX}$ ]] ; then
		printf "$usage\n" >&2
		return 2
	fi
	email="$1"
	shift

#TODO: implement --verify to check input and ask when incoherent or missing.
#TODO (also): check gpg version, because we use some recent feature, and compatibility may change (tested with gpg (GnuPG) 2.2.27 libgcrypt 1.8.8)

	if [[ "$pgpidu" =~ ${BL_PGPID_U4_REGEX} ]] ; then
		pgpidu="u4=${BASH_REMATCH[0]}"
	elif [[ "$pgpidu" =~ ${BL_PGPID_U5_REGEX} ]] ; then
		pgpidu="u5=${BASH_REMATCH[0]}"
	elif [[ "$pgpidugiven" ]] ; then
		printf "%s: Error: No OpenPGP ID string in given '%s'.\n" "$FUNCNAME" "$pgpidu" >&2
		return 2
	else
		pgpidu="u4=$(bl_pgpid_gen_u4 --verify)"
	fi

	[[ "$pseudo" ]] || pseudo=${email%%@*}

	[[ "$passphrase" ]] || passphrase=$(bl_new_password "Passphrase")
	[[ "$passphrase" ]] || { printf "%s: Error: Passphrase can't be empty at this stage.\n" "$FUNCNAME" >&2 ; return 2 ;}

	cat <<-EOF | gpg --homedir "$gpghome" --batch --generate-key --allow-freeform-uid || return $?
		%echo Generating OpenPGP key for ${email}
		Key-Type: eddsa
		Key-Curve: Ed25519
		Key-Usage: cert sign
		Subkey-Type: ecdh
		Subkey-Curve: Curve25519
		Subkey-Usage: encrypt
		Name-Real: ${pseudo}
		Name-Comment: ${pgpidu} ${extracomment}
		Name-Email: ${email}
		Expire-Date: $(date -I -d "+$expire years")
		Passphrase: ${passphrase}
		Keyserver: ${keyserv}
		# Do a commit here, so that we can later print 'done' :-)
		%commit
	EOF

	# get first 2 fprs
	local fprs
	fprs=($(gpg --homedir "$gpghome" --list-secret-keys --with-colons "$email" | sed -n 's,^fpr:.*:\([[:xdigit:]]\{40\}\):,\1,p')) || return $?

	# Generate and add auth key...
	gpg --homedir "$gpghome" --batch --passphrase "${passphrase}" --pinentry-mode loopback --quick-add-key ${fprs[0]} ed25519 auth ${expire}y || return $?

	# output all (3) fingerprints, first fingerprint should be the one of the main key
	gpg --homedir "$gpghome" --list-secret-keys --with-colons "$email" | sed -n 's,^fpr:.*:\([[:xdigit:]]\{40\}\):,\1,p'
}

bl_pgpid_avatar() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local tmpdir="/tmp/$FUNCNAME.$USER"
	local usage=$"Usage:"" $name [OPTIONS]... [NAME|KEYID|U4|U5]"
	local keyserv keyservs=("${BL_PGPID_KEYSERVERS[@]}")
	local helpmsg="
"$"Extract or add image inside OpenPGP certificate.""
"$"Missing NAME|KEYID|U4|U5 => Guess it using security token, or interactively ask KEYID of secret key.""
"$"New IMAGE should be 180x180 pixels, or it will be resized.""
"$"Output path(s) of extracted image(s).""

OPTIONS:
  -E, --extract-all           "$"Extract also all revoked or expired existing images from OpenPGP certificate.""
  -A, --addfrom IMAGE         "$"Resize and add new IMAGE inside OpenPGP certificate (revoking any previous image).""
  -R, --revoke                "$"Just revoke all existing images inside OpenPGP certificate.""
  -W, --workdir DIRECTORY     "$"Working directory. Will contain previous and new resized images. "$"Default: ""$tmpdir .
  -H, --homedir GNUPGHOME     "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME .
  -K, --keyservers KEYSERVERS "$"If non-empty, send updated certificate to this keyservers. "$"Default: ""
$(printf -- "%80s\n" ${keyservs[@]})
"
	local image revoke
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local listopts="show-photos"
	for ((;$#;)) ; do
		case "$1" in
			-E|--extract-all)
				listopts+=",show-unusable-uids" ;;
			-A|--add|--addfrom|--add-from)
				shift ; image=${1:?} ;&
			-R|--revoke)
				revoke="." ;;
			-W|--tmpdir|--workdir)
				shift ; tmpdir=${1:?} ;;
			-k|--keyserver)
				printf -- "$FUNCNAME: Warning: "$"Deprecated option"" '--keyserver' - "$"Please use %s instead.""\n" "'--keyservers'" >&2
				;&
			-K|--keyservers)
				shift ; keyservs=($1) ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_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 user fprs

	if [[ "$1" ]] ; then
		user=$1
	else
		local -A cardinfo
		bl_pgpid_token_check --homedir "$gpghome" --no-fetch --quiet --aaname cardinfo || true
		[[ "${cardinfo[pgpid_Skeyfpr]}" ]] && user=${cardinfo[pgpid_Skeyfpr]} || user=$(_bl_pgp_choose_seckeyid --homedir "$gpghome")
	fi

	mkdir -p "$tmpdir" || return $?

	# Backup existing photos and get fingerprints
	echo -n > "$tmpdir/imglist" || return $?
	fprs=($(gpg --homedir "$gpghome" --list-key --list-options "$listopts" --photo-viewer "img=\$(md5sum %i | head -c 32).%t && cp %i ${tmpdir@Q}/\$img && echo ${tmpdir@Q}/\$img >> ${tmpdir@Q}/imglist" "$user" | sed -nE 's,^ *([0-9ABCDEF]{40})$,\1,p' | sort -u ))
	(( ${#fprs[@]} > 0 )) || { printf "$FUNCNAME: Error: "$"No certificate for user '%s'.""\n" "'$user'" >&2 ; return 1 ;}

	if [[ "$image" ]] ; then
		local -a size
		# copy image to read it once if it was a fifo or /dev/stdin ...
		cat "$image" > "$tmpdir/origin.image" || { printf "%s: Error: Can't retrieve image file %s.\n" "$FUNCNAME" "${image@Q}" >&2 ; return 1 ;}
		size=($(gm identify -format "%w %h" "$tmpdir/origin.image")) || { printf "$FUNCNAME: Error: "$"Unable to identify image"" ${image@Q}.\n" >&2 ; return 1 ;}
		if ((size[0]==180 && size[1]==180)) ; then
			# Image has already the expected size, don't alter it.
			mv "$tmpdir/origin.image" "$tmpdir/new.jpg" || return $?
		else
			# Resize
			gm convert -geometry "180^" -gravity center -extent 180 -strip "$tmpdir/origin.image" jpeg:"$tmpdir/new.jpg" || { printf "$FUNCNAME: Error: "$"Unable to resize image"" ${image@Q}.\n" >&2 ; return 1 ;}
			rm "$tmpdir/origin.image" || return $?
		fi
	fi

	local ret=0
	if [[ "$revoke" ]] ; then
		(( ${#fprs[@]} < 2 )) || { printf "$FUNCNAME: Error: "$"User '%s' match many (%d) certificates"" (%s).\n" "$user" ${#fprs[@]} "${fprs[*]}" >&2 ; return 1 ;}

		local commands
		# Revoke non-revoked photos (in first keyring only))
		commands=$(gpg --homedir "$gpghome" --with-colons --list-secret-key "${fprs[0]}" | awk '/^pub:/ { if (i>0)  exit } /^(uid|uat):/ { i+=1 }  /^uat:[^r]/ { printf "uid "i"\nrevuid\ny\n4\n\ny\nuid "i"\n" } END { if (i) printf "save\n" }')
		[[ "$commands" ]] || { printf "$FUNCNAME: Error: "$"No editable certificate %s (in %s).""\n" "'$user'" "$gpghome" >&2 ; return 1 ; }

		if [[ "$commands" =~ ^uid ]] ; then
			printf "$FUNCNAME: Info: "$"Revoking previous image(s) inside %s certificate...""\n" "$user" >&2
			gpg --homedir "$gpghome" --batch --command-fd 0 --edit-key "${fprs[0]}" 2> >(grep "image of size" | sort -u >&2) <<<"$commands" || { printf "%s: Error: Can't revoke - good PIN ?.\n" "$FUNCNAME" >&2 ; return 1 ;}
		fi

		if [[ "$image" ]] ; then
			commands=$(printf "addphoto\n${tmpdir}/new.jpg\ny\nsave\n")
			printf "$FUNCNAME: Info: "$"Adding %s into %s certificate...""\n" "$image" "$user" >&2
			gpg --homedir "$gpghome" --batch --command-fd 0 --edit-key "${fprs[0]}" 2> >(grep "image of size" | sort -u >&2 ) <<<"$commands" || { printf "%s: Error: Can't %s - good PIN ?.\n" "$FUNCNAME" "addphoto" >&2 ; return 1 ;}
		fi

		# Send new certificate to keyservers
		for keyserv in ${keyservs[@]} ; do
			gpg --homedir "$gpghome" --keyserver "${keyserv}" --send-keys "${fprs[0]}" || ret=$?
		done
	fi

	if [[ -s "$tmpdir/imglist" ]] ; then
		cat "$tmpdir/imglist"
	else
		printf "%s: Notice: No image extracted from %s certificate...\n" "$FUNCNAME" "$user" >&2
	fi

	return $ret
}

bl_pgpid_email() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [NAME|KEYID|U4|U5]"
	local keyserv keyservs=("${BL_PGPID_KEYSERVERS[@]}")
	local helpmsg="
"$"Display and add or revoke emails inside OpenPGP certificate.""
"$"Missing NAME|KEYID|U4|U5 => Guess it using security token, or interactively ask KEYID of secret key.""
"$"Output usable emails (non-revoked and non-expired).""

OPTIONS:
  -R, --revoke EMAIL          "$"Revoke existing EMAIL (may be used more than once).""
  -A, --add EMAIL             "$"Add EMAIL. Enable --name and --extra-comment options.""
  -N, --name PSEUDONYM        "$"Common name or pseudonym beside new EMAIL. "$"Default: "$"first part of email.""
  -C, --extra-comment COMMENT "$"(Free to use part of) comment beside new EMAIL.""
  -v, --verbose               "$"Increase verbosity.""
  -H, --homedir GNUPGHOME     "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME .
  -K, --keyservers KEYSERVERS "$"If non-empty, send updated certificate to this keyservers. "$"Default: ""
$(printf -- "%80s\n" ${keyservs[@]})
"
	# TODO: (maybe) sanitize --name or --extra-comment inputs (removing '\' '(' ')' ... ) ?
	local toadd torev pseudo extracomment
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local gpgfilter=false
	for ((;$#;)) ; do
		case "$1" in
			-R|--rev|--revoke)
				[[ "$2" =~ ^${BL_INTERACTIVE_EMAIL_REGEX}$ ]] || { printf "%s: Error: Invalid email %s.\n" "$FUNCNAME" "'$2'" >&2 ; return 2 ;}
				shift ; torev+=("$1") ;;
			-A|--add)
				[[ "$2" =~ ^${BL_INTERACTIVE_EMAIL_REGEX}$ ]] || { printf "%s: Error: Invalid email %s.\n" "$FUNCNAME" "'$2'" >&2 ; return 2 ;}
				shift ; toadd+=("$1") ;;
			-N|--name)
				shift ; pseudo=$1 ;;
			-C|--extracomment|--extra-comment)
				shift ; extracomment=" $1" ;;
			-k|--keyserver)
				printf -- "$FUNCNAME: Warning: "$"Deprecated option"" '--keyserver' - "$"Please use %s instead.""\n" "'--keyservers'" >&2
				;&
			-K|--keyservers)
				shift ; keyservs=($1) ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-v|--verbose) gpgfilter="cat" ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_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 user fprs

	if [[ "$1" ]] ; then
		user=$1
	else
		local -A cardinfo
		bl_pgpid_token_check --homedir "$gpghome" --no-fetch --quiet --aaname cardinfo || true
		[[ "${cardinfo[pgpid_Skeyfpr]}" ]] && user=${cardinfo[pgpid_Skeyfpr]} || user=$(_bl_pgp_choose_seckeyid --homedir "$gpghome")
	fi

	# Check that $user match only one certificate
	local fprs
	fprs=($(bl_pgpid_get --fpr --errexit-g=1 --homedir "$gpghome" --keyservers "" "$user")) || return $?
	user=${fprs[0]}

	if [[ "$torev" || "$toadd" ]] ; then
		local uids email commands
		# Get all uid/uat (from first keyring only)
		uids=$(gpg --homedir "$gpghome" --with-colons --list-secret-key "$user" | awk '/^pub:/ { if (i>0)  exit } /^(uid|uat):/ { i+=1 ; print }')
		[[ "$uids" ]] || { printf "$FUNCNAME: Error: "$"No editable certificate %s (in %s).""\n" "$user" "$gpghome" >&2 ; return 1 ; }
	fi

	if [[ "$torev" ]] ; then
		for email in "${torev[@]}" ; do
			commands=$(awk "/^uid:[^r].*<$email>/ "'{ printf "uid "NR"\nrevuid\ny\n4\n\ny\nsave\n" }' <<<"$uids")
			[[ "$commands" ]] || { printf "$FUNCNAME: Error: "$"No revokable %s inside certificate %s (in %s).""\n" "<$email>" "$user" "$gpghome" >&2 ; return 1 ; }

			printf "$FUNCNAME: Notice: "$"Revoking %s inside %s certificate...""\n" "<$email>" "$user" >&2
			gpg --homedir "$gpghome" --batch --command-fd 0 --edit-key "$user" 2> >( $gpgfilter || grep "^\[" | sort -u | sort -t'(' --key=2n ) <<<"$commands" || { echo "$FUNCNAME: Error: "$"Can't revoke - good PIN ?." >&2 ; return 1 ;}
		done
	fi

	if [[ "$toadd" ]] ; then
		local ids

		ids=($(sed --silent --regexp-extended "s,.*4.?(${BL_PGPID_U4_REGEX}).*,u4=\1,p ; s,.*5.?(${BL_PGPID_U5_REGEX}).*,u5=\1,p" <<<"$uids" | sort -u))
		(( ${#ids[@]} > 0 )) || { printf "$FUNCNAME: Error: "$"No OpenPGP ID string (u4|u5) in %s certificate.""\n" "$user" >&2 ; return 1 ;}
		(( ${#ids[@]} < 2 )) || { echo "$FUNCNAME: Error:" $"supernumerary OpenPGP ID string" "(${ids[*]})" >&2 ; return 1 ;}

		# Add emails
		for email in "${toadd[@]}" ; do
			[[ "$pseudo" ]] || pseudo=${email%%@*}
			commands=$(printf "adduid\n$pseudo\n$email\n${ids[0]}$extracomment\nsave\n")
			printf "%s: Notice: Adding %s into %s certificate...\n" "$FUNCNAME" "'$pseudo (${ids[0]}$extracomment) <$email>'" "$user" >&2
			gpg --homedir "$gpghome" --batch --command-fd 0 --edit-key "$user" 2> >( $gpgfilter || grep "^ *[A-Z«]" ) <<<"$commands" || { printf "%s: Error: Can't %s - good PIN ?.\n" "$FUNCNAME" "adduid" >&2 ; return 1 ;}
		done
	fi

	local ret=0
	# Send updated certificate to keyservers
	if [[ "$torev" || "$toadd" ]] ; then
		for keyserv in ${keyservs[@]} ; do
			gpg --homedir "$gpghome" --keyserver "${keyserv}" --send-keys "$user" || ret=$?
		done
	fi

	gpg --homedir "$gpghome" --list-options show-only-fpr-mbox --list-key "$user"  | cut -d ' ' -f 2
	return $ret
}

bl_pgpid_token_check()
{
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]..."
	local helpmsg="
"$"Check if security token is correctly configured for OpenPGP ID ; may output informations.""
"$"Will try to import cleaned certificate indicated in 'URL of public key' field.""

OPTIONS:
  -f, --no-fetch          "$"Don't try to fetch public certificate (from URL indicated in token metadata).""
  -q, --quiet             "$"Don't errput 'Info' or 'Notice' messages.""
  -p, --cert-fpr          "$"Output certificate fingerprint (Certification key fpr).""
  -i, --info              "$"Output content of the associative array containing relevant metadata (may be evaluted with eval).""
  -A, --aaname VARNAME    "$"If VARNAME is a reacheable associative array: fill it (whithout cleaning known fields)""
                            "$"else: Output 'declare -A VARNAME=...'""
  -H, --homedir GNUPGHOME "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME

"$"Return value:""
-   "$"0 if no error and security token is correctly configured for OpenPGP ID.""
- "$"100 + number of missing OpenPGP ID data fields.""
- "$"then 107 if all required data are missing (OpenPGP card is probably empty).""
- "$"Other non-zero on other errors.""
"
	local varname fetch=1 info=0 quiet=0 isaa=0 noCfpr=1
	local gpghome=${GNUPGHOME:-~/.gnupg}
	for ((;$#;)) ; do
		case "$1" in
			-f|--no-fetch) fetch=0 ;;
			-p|--cert-fpr) noCfpr=0 ;;
			-i|--info) info=1 ;;
			-q|--quiet) quiet=1 ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-A|--aaname)
				shift
				varname=${1:?}
				if [[ "$(eval echo \${$varname@a})" =~ A ]] ; then
					local -n aaname=$varname
					isaa=1
				fi
				;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_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

	((! $#)) || printf "$FUNCNAME: Warning: "$"Ignoring extra args '%s'...""\n" "$1" >&2

	[[ "${aaname@a}" ]] || local -A aaname

	# Create gpghome if missing (workaround for Gnupg's bug : sometime ~/.gnupg is created (-K or -k), sometime not (--card-status))
	[[ -d "$gpghome" ]] || ( mkdir -p "$gpghome" && chmod go-rwx "$gpghome" ) || return $?

	local cardstatus
	cardstatus=$(LANG=C.UTF-8 gpg --homedir "$gpghome" --card-status) || return $?

	# Try Import/Refresh Key from URL metadata (as email may be in public certificate only)
	aaname[pgpid_certurl]=$(sed -n ' /^URL/ { s,[^:]*: ,,p ; q }' <<<"$cardstatus")
	[[ "${aaname[pgpid_certurl]}" != "[not set]" ]] || aaname[pgpid_certurl]=""
	if [[ "${aaname[pgpid_certurl]}" ]] && ((fetch)) ; then
		((quiet)) || printf "$FUNCNAME: Notice: "$"Getting pubkey from %s ...""\n" "${aaname[pgpid_certurl]}" >&2
		if curl --no-progress-meter "${aaname[pgpid_certurl]}" | gpg --homedir "$gpghome" --import-options keep-ownertrust,import-clean --import ; then
			cardstatus=$(LANG=C.UTF-8 gpg --homedir "$gpghome" --card-status) || return $?
		fi
	fi

	local line emails ids ttype tversion manufacturer sn

	while read line ; do
		if [[ "$line" =~ ^Application\ ID ]] ; then
			aaname[token_ID]="${line#*: }"
		elif [[ "$line" =~ ^Application\ type ]] ; then
			ttype="${line#*: }"
		elif [[ "$line" =~ ^Version ]] ; then
			tversion="${line#*: }"
		elif [[ "$line" =~ ^Manufacturer ]] ; then
			manufacturer="${line#*: }"
		elif [[ "$line" =~ ^Serial\ number ]] ; then
			sn="${line#*: }"
		elif [[ "$line" =~ ^Name\ of\ cardholder ]] ; then
			aaname[pgpid_name]="${line#*: }"
			[[ "${aaname[pgpid_name]}" != "[not set]" ]] || aaname[pgpid_name]=""
			if [[ "${aaname[pgpid_name]}" =~ ^${BL_INTERACTIVE_EMAIL_REGEX}$ ]] ; then
				# Only first part of email should be relevant
				aaname[pgpid_name]=${BASH_REMATCH[1]}
			else
				# remove first eventual email
				aaname[pgpid_name]=$(sed --regexp-extended "s,${BL_INTERACTIVE_EMAIL_REGEX},, ; s, *$,, ; s,^ *,," <<<"${aaname[pgpid_name]}")
			fi
		elif [[ "$line" =~ ^Signature\ key ]] ; then
			aaname[pgpid_Skeyfpr]=$(sed --regexp-extended --silent ' { s, ,,g ; s,.*([ABCDEF0-9]{40}).*,\1,p } ' <<<"${line}")
		elif [[ "$line" =~ ^Encryption\ key ]] ; then
			aaname[pgpid_Ekeyfpr]=$(sed --regexp-extended --silent ' { s, ,,g ; s,.*([ABCDEF0-9]{40}).*,\1,p } ' <<<"${line}")
		elif [[ "$line" =~ ^Authentication\ key ]] ; then
			aaname[pgpid_Akeyfpr]=$(sed --regexp-extended --silent ' { s, ,,g ; s,.*([ABCDEF0-9]{40}).*,\1,p } ' <<<"${line}")
		fi

		if [[ "$line" =~ ${BL_INTERACTIVE_EMAIL_REGEX} ]] ; then
			grep --quiet "\<${BASH_REMATCH[0]}\>" <<<"${emails[@]}" || emails+=("${BASH_REMATCH[0]}")
		fi
	done <<<"${cardstatus}"

	# Signature key on token may differs from main key, aka Certification key.
	aaname[pgpid_Ckeyfpr]=$(gpg --homedir "$gpghome" --list-options show-only-fpr-mbox --list-key "${aaname[pgpid_Skeyfpr]:- ${aaname[pgpid_Ekeyfpr]:- ${aaname[pgpid_Akeyfpr]}}}" | grep -o -m1 "\<[0-9ABCDEF]\{40\}\>") || echo "$FUNCNAME: Warning:" $"No known certification key. Please share or get certificate (keyserver, by email, ...)" >&2
	((noCfpr)) || echo "${aaname[pgpid_Ckeyfpr]}"

	aaname[pgpid_email]="${emails[0]}"
	(( ${#emails[@]} < 2 )) || ((quiet)) || echo "$FUNCNAME: Notice:" $"supernumerary emails, skipping" "${emails[@]:1} ..." >&2

	# Should be in 'Login data' field, but who knows ? (yet...) 
	ids=($(sed --silent --regexp-extended "s,.*4.?(${BL_PGPID_U4_REGEX}).*,u4\1,p ; s,.*5.?(${BL_PGPID_U5_REGEX}).*,u5\1,p" <<<"${cardstatus}" | sort -u))
	(( ${#ids[@]} < 2 )) || { echo "$FUNCNAME: Error:" $"supernumerary OpenPGP ID string" "(${ids[*]})" >&2 ; return 1 ;}
	aaname[pgpid_id]="${ids[0]}"

	aaname[token_AVersion]="${ttype} ${tversion}"
	aaname[token_MSN]="${manufacturer} ${sn}"

	local key ret=0
	if ((info)) ; then
		echo >&2
		for key in "${!aaname[@]}" ; do
			echo "${key}=${aaname[$key]@Q}"
		done
		echo >&2
	fi

	if ((!isaa)) && [[ "$varname" ]] ; then
	   declare -p aaname | sed "s, aaname=, $varname=,"
	fi

	for key in pgpid_name pgpid_id pgpid_email pgpid_Skeyfpr pgpid_Ekeyfpr pgpid_Akeyfpr pgpid_certurl ; do
		if [[ -z "${aaname[$key]}" ]] ; then
			((ret = ret ? ret+1 : 101))
			((quiet)) || printf "$FUNCNAME: Notice: "$"Invalid OpenPGP ID token, missing:"" '${key}'.\n" >&2
		fi
	done

	! ((ret)) || return $ret

	((quiet)) || printf "$FUNCNAME: Info: "$"Valid OpenPGP ID token"", pgpid_Ckeyfpr='${aaname[pgpid_Ckeyfpr]}', pgpid_id='${aaname[pgpid_id]}'.\n" >&2
	return 0
}

bl_pgpid_certify() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage=$"Usage:"" $name [OPTIONS]... [TARGET_U4|TARGET_U5] [TARGET_KEYFPR]"
	local keyserv keyservs=("${BL_PGPID_KEYSERVERS[@]}")

	local helpmsg="
"$"Certify somebody else, identified by its OpenPGP ID TARGET_U4.""
"$"Missing input will be asked interactively."" E.g.: "$"Civil status to calculate TARGET_U4.""

"$"Certification means : I know this other certificate belongs to this real person.""
"$"This implies verifying the civil status and the public key fingerprint of the TARGET.""
"$"This allows you to expand and strengthen your web of trust and those of your close ones.""
"$"This is a commitment: the more you certify, the more you increase your reputation, but if you do it wrong, you will ruin your credibility.""

OPTIONS:
  -u, --use-privkey NAME|KEYID "$"Select private key to use. "$"Default: "$"Guess it from connected token, else ask.""
  -R, --revoke                 "$"Revoke your previous certifications on someone else's certificate.""
  -o, --ownertrust VALUE       "$"What level of trust do you assign to the target to correctly certify others"" {undefined,marginal,full,ultimate,never} - "$"Default: ""marginal.
  -G, --vouch                  "$"Take legal responsability for any actions by the target. "$"Same as"" '--vouch-for ALL'. (Not implemented yet)
  -g, --vouch-for DOMAIN       "$"Assume some legal responsibilities on the target."" (Not implemented yet)
  -l, --local                  "$"« Non-exportable » certification. Pretty useless, except for testing.""
  -f, --force-sign-key         "$"Allow to unrevoke or to change vouching.""
  -H, --homedir GNUPGHOME      "$"GnuPG home directory"" - "$"Environnement variable: ""GNUPGHOME, "$"default: ""'~/.gnupg'.
  -K, --keyservers KEYSERVERS  "$"If non-empty, receive and send updated certificate from and to this keyservers. "$"Default: ""
$(printf -- "%80s\n" ${keyservs[@]})

"$"Return value:""
-   0 "$"No error""
-   2 "$"Input/Usage error""
- 140 "$"Interactivity error""
- 141 "$"No certificate for user""
- 142 "$"Self-certification is not innovative! ;-)""
- 143 "$"Key fingerprint does not match.""
- "$"Other non-zero on other errors.""
"
	local revoke=0 keyid vouchfor signcmd="sign" oforce ownertrust
	local gpghome=${GNUPGHOME:-~/.gnupg}
	for ((;$#;)) ; do
		case "$1" in
			-u|--use-privkey)
				shift ; keyid=$1 ;;
			-R|--revoke)
				revoke=1 ;;
			-o|--ownertrust)
				[[ "$2" =~ ^(undefined|marginal|full|ultimate|never)$ ]] || { printf -- "$FUNCNAME: Error: "$"Unknown ownertrust value '%s'"".\n" "$2" >&2 ; return 2 ;}
				shift ; ownertrust=$1 ;;
			-G|--vouch)
				{ echo "$FUNCNAME: Error: "$"Not implemented yet." >&2 ; return 2 ;}
				vouchfor="ALL" ;;
			-g|--vouch-for)
				[[ "$2" == ALL ]] || { printf -- "$FUNCNAME: Error:" $"Only %s is yet a known domain for vouching"".\n" "'ALL'" >&2 ; return 2 ;}
				{ echo "$FUNCNAME: Error: "$"Not implemented yet." >&2 ; return 2 ;}
				shift ; vouchfor=${1:?} ;;
			-l|--local)
				signcmd="lsign" ;;
			-f|--force-sign-key|--force)
				oforce="--force-sign-key" ;;
			-k|--keyserver)
				printf -- "$FUNCNAME: Warning: "$"Deprecated option"" '--keyserver' - "$"Please use %s instead.""\n" "'--keyservers'" >&2
				;&
			-K|--keyservers)
				shift ; keyservs=($1) ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_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 targetid targetfpr keyfpr
	for ((;$#;)) ; do
		if [[ "$1" =~ (${BL_PGPID_U4_REGEX}|${BL_PGPID_U5_REGEX}) ]] ; then
			targetid=${BASH_REMATCH[0]}
		elif [[ "${1^^}" =~ ^[ABCDEF0-9]{40}$ ]] ; then
			targetfpr=${BASH_REMATCH[0]}
		else
			printf "$FUNCNAME: Error: "$"Unrecognized argument"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2
			return 2
		fi
		shift
	done

	if [[ -z "$keyid" ]] ; then
		local -A cardinfo
		bl_pgpid_token_check --homedir "$gpghome" --no-fetch --quiet --aaname cardinfo || true
		[[ "${cardinfo[pgpid_Ckeyfpr]}" ]] && keyid=${cardinfo[pgpid_Ckeyfpr]} || keyid=$(_bl_pgp_choose_seckeyid --homedir "$gpghome" --text $"Private key to use ? (KeyID of your Certification Key)")
	fi

	# Get fpr for private key to use
	keyfpr=$(gpg --homedir "$gpghome" --list-options show-only-fpr-mbox --list-secret-keys "$keyid" | cut -d ' '  -f 1 | sort -u)

	[[ "$keyfpr" =~ ^[0-9ABCDEF]{40}$ ]] || { printf -- "$FUNCNAME: Error: "$"Invalid or ambiguous private key to use '%s'.""\n" "$keyid" >&2 ; return 2 ;}

	[[ "$targetid" ]] || targetid=$(bl_pgpid_gen_u4)

	# We have to focus on BL_PGPID_U4H_REGEX because BL_PGPID_CO_REGEX part of udid4 is fuzzy (and may be used to distinguish different persons with same BL_PGPID_U4H_REGEX). And we have to include ${BASH_REMATCH[2]::1} because search feature on key server use to search for whole words.
	! [[ "$targetid" =~ (${BL_PGPID_U4H_REGEX})(${BL_PGPID_CO_REGEX}) ]] || targetid="${BASH_REMATCH[1]}${BASH_REMATCH[2]::1}"

	# get fingerprints for certificate to certify
	local fprs ifpr=0 uids
	fprs=($(bl_pgpid_get --fpr --homedir "$gpghome" --keyservers "${keyservs[*]}" "$targetid")) || return 141
	if (( ${#fprs[@]} >= 2 )) ; then
		printf "$FUNCNAME: Info: "$"User '%s' match many (%d) certificates"".\n" "$targetid" "${#fprs[@]}" >&2
		[[ "$targetfpr" ]] || ifpr=$(bl_radiolist --output-index --text $"Please select key fingerprint" -- "${fprs[@]/#????????????????????????/...}") || return 140
	fi

	if [[ -z "$targetfpr" ]] ; then
		local rep txt=$"Please verify and complete target's fingerprint:"
		local question=".... .... $(sed -E 's,(....),\1 ,g' <<<${fprs[$ifpr]:8:32})"
		for ((;;)) ; do
			rep=$(bl_input --default "$rep" --text "$txt" "$question") || return 140
			targetfpr=$(sed "s,[^ABCDEF0-9],,g" <<<${rep^^} )${fprs[$ifpr]:8:32}
			[[ ${#targetfpr} != 40 ]] || break
			txt=$"Enter the first 8 hexadecimal characters of target's key fingerprint:"
		done
	fi

	[[ "$targetfpr" != "$keyfpr" ]] || { echo "$FUNCNAME: Warning: "$"Self-certification is not innovative! ;-)" >&2 ; return 142 ;}
	grep -q "$targetfpr" <<<"${fprs[@]}"|| { echo "$FUNCNAME: Error: "$"Key fingerprint does not match." >&2 ; return 143 ;}

	# Get full uids containing $targetid (cf. /usr/share/doc/gnupg/DETAILS.gz ; from first keyring only)
	readarray -t uids < <(gpg --homedir "$gpghome" --with-colons --list-key "$targetfpr" | awk -F: '/^pub:/ { if (i>0)  exit } /^uid:.*'"$targetid"'/ { i+=1 ; print $10 }')
	[[ "$uids" ]] || { echo "$FUNCNAME: Crit: "$"No uid. Certificate is probably corrupted." >&2 ; return 1 ;}

	if ((revoke)) ; then
		[[ -z "$ownertrust" ]] || printf -- "$FUNCNAME: Info: "$"%s is non-sense, then ignored, when revoking.""\n" "--ownertrust" >&2
		[[ -z "$oforce" ]] || printf -- "$FUNCNAME: Info: "$"%s is non-sense, then ignored, when revoking.""\n" "--force-sign-key" >&2
		[[ -z "$vouchfor" ]] || printf -- "$FUNCNAME: Info: "$"%s is non-sense, then ignored, when revoking.""\n" "--vouch-for" >&2
		gpg --homedir "$gpghome" --quick-revoke-sig "$targetfpr" "$keyfpr" "${uids[@]/#/=}" || return $?
		printf "$FUNCNAME: Notice: "$"The sub-layer (%s) return no error, but it may have found nothing to revoke.""\n" "gpg --quick-revoke-sig" >&2
	else
		gpg --homedir "$gpghome" --local-user "$keyfpr" $oforce --quick-$signcmd-key "$targetfpr" "${uids[@]/#/=}" || return $?
		printf "$FUNCNAME: Notice: "$"Successfully sign %d '%s' inside certificate %s.""\n" ${#uids[@]} "$targetid" "$targetfpr" >&2
		gpg --homedir "$gpghome" --quick-set-ownertrust "$targetfpr" "${ownertrust:-marginal}" || return $?
	fi

	local ret=0
	for keyserv in ${keyservs[@]} ; do
		gpg --homedir "$gpghome" --keyserver "$keyserv" --send-keys "$targetfpr" || ret=$?
	done
	return $ret
}

### Init ###

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

ACTIONS:
$(for f in "${BL_PGPID_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_PGPID_FUNCTIONS:9} --help

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

Functions:
$(for f in "${BL_PGPID_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_PGPID_RETVAL=0
_bl_pgpid_parseoptions "$@" || _BL_PGPID_RETVAL=$?

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

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

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

