#!/usr/bin/env bash

########################################################################
# Globals, Includes
########################################################################

BASE64="openssl base64 -e -A"
SCRIPTS_DIR="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd -P)"
BASE_DIR="$(dirname "${SCRIPTS_DIR}")"
source "${SCRIPTS_DIR}/colors.sh"

########################################################################
# Colors
########################################################################

SECTION=${LIGHT_BLUE}
SUCCESS=${LIGHT_GREEN}
ERROR=${RED}
INFO=${PURPLE}

########################################################################
# Functions
########################################################################

# Is this macOS?
function is_macos() {
  uname -a | grep -i darwin >/dev/null
}

# Print to stderr.
function error() {
  local msg=$1
  echo -e "${ERROR}$msg${NC}" 1>&2
}

# Prints the installable belonging to an `executable` ($1).
# If $1 is not in known_installables_map, it returns $1.
function map_installable() {

    local installable=$1

    # Mapping of executables to installable packages in the form:
    # "<EXECUTABLE>:<INSTALLABLE>".
    known_installables_map=(
        "cargo-set-version:cargo-edit"
        "p11tool:gnutls"
    )

    for mapping in "${known_installables_map[@]}"; do
        local key="${mapping%%:*}"
        local value="${mapping##*:}"

        if [[ "$key" == "$installable" ]]; then
            installable=("$value")
        fi
    done

    echo "${installable[@]}"
}

# Check if required executables are available on the path.
function have() {
    local missing=()
    for executable in "$@"; do
        which "$executable" &>/dev/null || missing+=($(map_installable "$executable"))
    done
    if [ ${#missing[@]} -eq 0 ]; then
        return 0
    else
        error "Missing required tool(s) to run this: ${missing[*]}"
        exit 1
    fi
}

function base64_url_encode() {
    ${BASE64} | tr '/+' '_-' | tr -d '=\n';
}

function detect_softhsm() {
  # shellcheck disable=SC2206
  local locations=("/usr/local/lib" ${NIX_PROFILES:-} ${nativeBuildInputs:-} "${HOMEBREW_PREFIX:+${HOMEBREW_PREFIX}/lib}" "/usr/lib" "opt/softhsm2")

  for location in "${locations[@]}"; do
      local library_path
      if [ -d "$location" ]; then
        library_path=$(find -L "$location" -maxdepth 3 -name "libsofthsm2.so" -or -name "libsofthsm2.dylib" | head -n 1)
        if [ -n "$library_path" ]; then
            echo "$library_path"
            return
        fi
      fi
  done
}

function check_openssl() {
  if ! openssl version | grep "OpenSSL" > /dev/null
  then
    error "Please install an actual, real OpenSSL version"
    exit 1
  fi
}

# Execute envsubst on TEMPLATE and write the result to TARGET
#
# $1 - TEMPLATE: Template file
# $2 - TARGET: Target file
function render_template {
    echo -e "Generating ${CYAN}$2${NC} from template ${CYAN}$1${NC}"
    envsubst < "$1" > "$2"
}

# Generate n random bytes.
#
# $1 n: how many random bytes to generate
function random_bytes {
    dd if=/dev/urandom bs="$1" count=1 2>/dev/null
}

# Generate or re-use by linking, a key and certificate to use as root CA.
#
# $1 - Target directory
# $2 - Common name
#
# If USE_SINGLE_CA is "1", and USE_SINGLE_CA_PATH is set to some file
# system path, the function will check the location of USE_SINGLE_CA_PATH
# and if a CA exists there, it will link to that instead of generate a
# new CA. This allows us to switch between single and multiple CA usage
# transparently. Single CA mode is convenient when doing API traffic
# inspection where the proxy used uses the same CA to generate in-between
# certificates to record traffic.
function generate_or_reuse_root_ca {

    # If single ca is wanted, and exists already, and target dir
    # is not equal to single ca path, link single ca to target:
    if [[ ${USE_SINGLE_CA} == 1 && -f ${USE_SINGLE_CA_PATH}/ca.crt.pem && "$1" != "${USE_SINGLE_CA_PATH}" ]]; then
        echo -e "${INFO}Single CA exists, re-using by linking files to $1 ${NC}"
        mkdir -p "$1"
        ln -sf "${USE_SINGLE_CA_PATH}"/ca.{key,crt}.{pem,der} "$1/"

    # If target ca files already exist:
    elif [[ -f ${1}/ca.crt.pem ]]; then
        echo -e "${INFO}CA files in $1 already exist, not (re-)generating${NC}"

    # Else just create:
    else
        echo -e "${INFO}Generating CA $2 in ${1}${NC}"
        mkdir -p "$1"
        openssl req -subj "/C=NL/CN=$2" -nodes -x509 -sha256 -days 1825 -newkey rsa:2048 -keyout "$1/ca.key.pem" -out "$1/ca.crt.pem" > /dev/null
        openssl pkcs8 -topk8 -inform PEM -outform DER -in "$1/ca.key.pem" -out "$1/ca.key.der" -nocrypt
        openssl x509 -in "$1/ca.crt.pem" -outform DER -out "$1/ca.crt.der"
    fi
}

# Generate a key and certificate to use for local TLS.
# The generated certificate will have the following SAN names:
#
# [alt_names]
# IP.1    = 10.0.2.2 # special IP address for android emulator
# DNS.1   = localhost
#
# $1 - TARGET: Target directory
# $2 - NAME: Key name, used as file name without extension
# $3 - CA PUBLIC KEY: CA certificate signing public key
# $4 - CA PRIVATE KEY: CA certificate signing private key
function generate_ssl_key_pair_with_san {
    echo -e "${INFO}Generating SSL private key and CSR${NC}"
    openssl req \
            -newkey rsa:2048 \
            -subj "/C=US/CN=localhost" \
            -nodes \
            -sha256 \
            -keyout "$1/$2.key" \
            -out "$1/$2.csr" \
            -config "${DEVENV}/openssl-san.cfg" > /dev/null

    echo -e "${INFO}Generating SSL CERT from CSR${NC}"
    openssl x509 \
            -req \
            -sha256 \
            -in "$1/$2.csr" \
            -days 500 \
            -CA "$3" \
            -CAkey "$4" \
            -out "$1/$2.crt" \
            -extensions req_ext \
            -extfile "${DEVENV}/openssl-san.cfg" > /dev/null

    echo -e "${INFO}Exporting SSL public key${NC}"
    openssl rsa \
            -in "$1/$2.key" \
            -pubout > "$1/$2.pub"

    echo -e "${INFO}Converting SSL CERT to DER${NC}"
    openssl x509 -in "$1/$2.crt" \
                -outform der -out "$1/$2.crt.der"

    echo -e "${INFO}Converting SSL private key to DER${NC}"
    openssl pkcs8 -topk8 -inform PEM -outform DER \
            -in "$1/$2.key" -out "$1/$2.key.der" -nocrypt
}

# $1 name of the key
function generate_ec_key {
    echo -e "${INFO}Generating EC private key${NC}"
    openssl ecparam \
            -genkey \
            -name prime256v1 \
            -noout \
            -out "${TARGET_DIR}/wallet_provider/$1.ec.key" > /dev/null
    echo -e "${INFO}Generating private key from EC private key${NC}"
    openssl ec -in "${TARGET_DIR}/wallet_provider/$1.ec.key" -pubout -out "${TARGET_DIR}/wallet_provider/$1.pub.pem"
    openssl pkey -in "${TARGET_DIR}/wallet_provider/$1.pub.pem" -pubin -outform der -out "${TARGET_DIR}/wallet_provider/$1.pub.der"
}

# $1 name of the key
function private_key_to_pem {
    openssl pkcs8 \
            -topk8 \
            -nocrypt \
            -in "${TARGET_DIR}/wallet_provider/$1.ec.key" \
            -out "${TARGET_DIR}/wallet_provider/$1.pem" > /dev/null
}

# Generate a private EC key and return the PEM body
#
# $1 name of the key
function generate_wp_signing_key {
    generate_ec_key "$1"

    private_key_to_pem "$1"
}

# Generate a private EC key and a self-signed certificate
#
# $1 name of the key
# $2 common name to use in the certificate
function generate_wp_self_signed_certificate {
    generate_ec_key "$1"

    openssl req \
            -new \
            -subj "/CN=$2" \
            -days 1825 \
            -x509 \
            -sha256 \
            -outform der \
            -key "${TARGET_DIR}/wallet_provider/$1.ec.key" \
            -out "${TARGET_DIR}/wallet_provider/$1.crt" > /dev/null

    private_key_to_pem "$1"
}

# Generate a random key (32 bytes)
#
# random_data may contain the null byte (00) or newline (0a). `softhsm2-util --aes` uses fgets to read
# the key. It stops reading when encountering a null byte or newline. Therefore these are stripped out.
#
# $1 name of the key
function generate_wp_random_key {
    echo -e "${INFO}Generating random wallet provider key${NC}"
    # Replace '00' and '0a' by fixed values 'X' and 'Y'
    random_bytes 32 | LC_ALL=C tr '\000\n' "XY" > "${TARGET_DIR}/wallet_provider/$1.key"
}

# Generate a random key (64 bytes)
function generate_ws_random_key {
    echo -e "${INFO}Generating random wallet server key${NC}"
    random_bytes 64 > "${TARGET_DIR}/demo_relying_party/$1.key"
}

# Generate an EC root CA for issuer
function generate_issuer_root_ca {
    echo -e "${INFO}Generating Issuer CA key pair${NC}"
    cargo run --manifest-path "${BASE_DIR}"/wallet_core/Cargo.toml --bin wallet_ca ca \
        --common-name "ca.issuer.example.com" \
        --file-prefix "${TARGET_DIR}/ca.issuer" \
        --force
    openssl x509 -in "${TARGET_DIR}/ca.issuer.crt.pem" -outform der -out "${TARGET_DIR}/ca.issuer.crt.der"
}

# Generate an EC root CA for reader
function generate_reader_root_ca {
    echo -e "${INFO}Generating Reader CA key pair${NC}"
    cargo run --manifest-path "${BASE_DIR}"/wallet_core/Cargo.toml --bin wallet_ca ca \
        --common-name "ca.reader.example.com" \
        --file-prefix "${TARGET_DIR}/ca.reader" \
        --force
    openssl x509 -in "${TARGET_DIR}/ca.reader.crt.pem" -outform der -out "${TARGET_DIR}/ca.reader.crt.der"
}

# Generate an EC key pair for the pid_issuer
function generate_pid_issuer_key_pair {
    echo -e "${INFO}Generating PID Issuer key pair in HSM${NC}"

    # Generate EC key pair in the HSM
    p11tool \
        --provider "${HSM_LIBRARY_PATH}" \
        --login \
        --set-pin "${HSM_USER_PIN}" \
        --label "pid_issuer_key" \
        --generate-ecc \
        --curve secp256r1

    # Export the public key as PEM
    p11tool \
        --provider "${HSM_LIBRARY_PATH}" \
        --login \
        --set-pin "${HSM_USER_PIN}" \
        --export-pubkey "$(p11tool --login --set-pin 12345678 --provider="${HSM_LIBRARY_PATH}" --list-all --only-urls | grep "pid_issuer_key" | grep public)" \
        --label "pid_issuer_key" \
        --outfile "${TARGET_DIR}/pid_issuer/issuer.pub.pem"

    # Generate a certificate for the public key including issuer authentication
    cargo run --manifest-path "${BASE_DIR}"/wallet_core/Cargo.toml \
        --bin wallet_ca issuer-cert \
        --public-key-file "${TARGET_DIR}/pid_issuer/issuer.pub.pem" \
        --ca-key-file "${TARGET_DIR}/ca.issuer.key.pem" \
        --ca-crt-file "${TARGET_DIR}/ca.issuer.crt.pem" \
        --common-name "cert.issuer.example.com" \
        --issuer-auth-file "${DEVENV}/rvig_issuer_auth.json" \
        --file-prefix "${TARGET_DIR}/pid_issuer/issuer" \
        --force

    # Convert the PEM certificate to DER format
    openssl x509 \
            -in "${TARGET_DIR}/pid_issuer/issuer.crt.pem" \
            -inform PEM \
            -outform DER \
            -out "${TARGET_DIR}/pid_issuer/issuer.crt.der"
}

# Generate an EC key pairs for the demo_issuer
#
# $1 - ISSUER_NAME: Name of the Issuer
function generate_demo_issuer_key_pairs {
    cargo run --manifest-path "${BASE_DIR}"/wallet_core/Cargo.toml \
        --bin wallet_ca reader \
        --ca-key-file "${TARGET_DIR}/ca.reader.key.pem" \
        --ca-crt-file "${TARGET_DIR}/ca.reader.crt.pem" \
        --common-name "$1.example.com" \
        --reader-auth-file "${DEVENV}/$1_reader_auth.json" \
        --file-prefix "${TARGET_DIR}/demo_issuer/$1.reader" \
        --force

    cargo run --manifest-path "${BASE_DIR}"/wallet_core/Cargo.toml \
        --bin wallet_ca issuer \
        --ca-key-file "${TARGET_DIR}/ca.issuer.key.pem" \
        --ca-crt-file "${TARGET_DIR}/ca.issuer.crt.pem" \
        --common-name "$1.example.com" \
        --issuer-auth-file "${DEVENV}/$1_issuer_auth.json" \
        --file-prefix "${TARGET_DIR}/demo_issuer/$1.issuer" \
        --force

    openssl x509 -in "${TARGET_DIR}/demo_issuer/$1.reader.crt.pem" \
        -outform der -out "${TARGET_DIR}/demo_issuer/$1.reader.crt.der"
    openssl x509 -in "${TARGET_DIR}/demo_issuer/$1.issuer.crt.pem" \
        -outform der -out "${TARGET_DIR}/demo_issuer/$1.issuer.crt.der"

    openssl pkcs8 -topk8 -inform PEM -outform DER \
        -in "${TARGET_DIR}/demo_issuer/$1.reader.key.pem" \
        -out "${TARGET_DIR}/demo_issuer/$1.reader.key.der" -nocrypt
    openssl pkcs8 -topk8 -inform PEM -outform DER \
        -in "${TARGET_DIR}/demo_issuer/$1.issuer.key.pem" \
        -out "${TARGET_DIR}/demo_issuer/$1.issuer.key.der" -nocrypt
}

# Generate an EC key pair for the demo_relying_party
#
# $1 - RELYING_PARTY_NAME: Name of the Relying Party
function generate_demo_relying_party_key_pair {
    cargo run --manifest-path "${BASE_DIR}"/wallet_core/Cargo.toml \
        --bin wallet_ca reader \
        --ca-key-file "${TARGET_DIR}/ca.reader.key.pem" \
        --ca-crt-file "${TARGET_DIR}/ca.reader.crt.pem" \
        --common-name "$1.example.com" \
        --reader-auth-file "${DEVENV}/$1_reader_auth.json" \
        --file-prefix "${TARGET_DIR}/demo_relying_party/$1" \
        --force

    openssl x509 -in "${TARGET_DIR}/demo_relying_party/$1.crt.pem" \
        -outform der -out "${TARGET_DIR}/demo_relying_party/$1.crt.der"

    openssl pkcs8 -topk8 -inform PEM -outform DER \
        -in "${TARGET_DIR}/demo_relying_party/$1.key.pem" -out "${TARGET_DIR}/demo_relying_party/$1.key.der" -nocrypt
}

# Generate an EC key pair for the demo_relying_party in the HSM.
# The label of the key is "${READER_NAME}_key"
#
# $1 - READER_NAME: Name of the Relying Party
# $2 - path where the certificate will be written to
function generate_relying_party_hsm_key_pair {
    # Generate EC key pair in the HSM
    p11tool \
        --provider "${HSM_LIBRARY_PATH}" \
        --login \
        --set-pin "${HSM_USER_PIN}" \
        --label "$1_key" \
        --generate-ecc \
        --curve secp256r1

    # Export the public key as PEM
    p11tool \
        --provider "${HSM_LIBRARY_PATH}" \
        --login \
        --set-pin "${HSM_USER_PIN}" \
        --export-pubkey "$(p11tool --login --set-pin 12345678 --provider="${HSM_LIBRARY_PATH}" --list-all --only-urls | grep "$1_key" | grep public)" \
        --label "$1_key" \
        --outfile "${TARGET_DIR}/$2/$1.pub.pem"

    # Generate a certificate for the public key including reader authentication
    cargo run --manifest-path "${BASE_DIR}"/wallet_core/Cargo.toml \
          --bin wallet_ca reader-cert \
          --public-key-file "${TARGET_DIR}/$2/$1.pub.pem" \
          --ca-key-file "${TARGET_DIR}/ca.reader.key.pem" \
          --ca-crt-file "${TARGET_DIR}/ca.reader.crt.pem" \
          --common-name "$1.example.com" \
          --reader-auth-file "${DEVENV}/$1_reader_auth.json" \
          --file-prefix "${TARGET_DIR}/$2/$1" \
          --force

    # Convert the PEM certificate to DER format
    openssl x509 \
            -in "${TARGET_DIR}/$2/$1.crt.pem" \
            -inform PEM \
            -outform DER \
            -out "${TARGET_DIR}/$2/$1.crt.der"
}

function encrypt_gba_v_responses {
    mkdir -p "${GBA_HC_CONVERTER_DIR}/resources/encrypted-gba-v-responses"
    for file in "${GBA_HC_CONVERTER_DIR}"/resources/gba-v-responses/*; do
        if [ -f "$file" ]; then
            cargo run --manifest-path "${BASE_DIR}"/wallet_core/Cargo.toml \
                --bin gba_encrypt -- \
                --basename "$(basename "$file" .xml)" \
                --output "${GBA_HC_CONVERTER_DIR}/resources/encrypted-gba-v-responses" \
                "$file"
        fi
    done
}
