#!/bin/bash

# Opt this host in to git over ssh with the YubiKey git keys, or back out.
#
#   yubikey-git enable     set it up; safe to re-run
#   yubikey-git disable    undo everything enable did
#   yubikey-git status     what is in place
#
# Then, with any one of the YubiKeys plugged in, one touch per fetch or push:
#
#   git clone git@git.soarez.net:sz/<name>.git
#
# For a machine that has this repo and nothing else -- one that is not trusted
# with a key of its own. Where a private layer is installed, its git config
# usually sends that URL elsewhere, and enabling this changes nothing there.
#
# What enable does, all of it undone by disable:
#
#   ~/.ssh/id_ed25519_sk_git*       copies of config/ssh/yubikey-git's keys, at
#                                   mode 600, which ssh insists on and git
#                                   cannot store. Each is a key's handle, not
#                                   its private half: see that directory's
#                                   config for why they are safe in this repo.
#   ~/.config/ssh/yubikey-git       a link to that directory: the ssh config
#                                   and git.soarez.net's pinned host key
#   ~/.config/ssh/enabled.d/yubikey-git.conf
#                                   a link to its config, which is what turns it
#                                   on: config/ssh/entry includes enabled.d/*.conf
#
# On macOS it also needs Homebrew's OpenSSH, and installs it if Homebrew is
# there: Apple's ssh cannot talk to a security key. git runs whichever ssh is
# first on PATH, and config/env puts Homebrew's ahead of /usr/bin.
#
# Runs under macOS's /bin/bash 3.2.

set -euo pipefail

name="yubikey-git"
# One per YubiKey, each its own key; any of them will do.
keys=(id_ed25519_sk_git_main_nfc id_ed25519_sk_git_backup_nfc id_ed25519_sk_git)
# The same path config/ssh/entry includes from, not $XDG_CONFIG_HOME.
cfg="${HOME}/.config/ssh"
dir_link="${cfg}/${name}"
conf_link="${cfg}/enabled.d/${name}.conf"

die() { >&2 echo "${name}: $*"; exit 1; }

# This script's real directory: ~/.bin/yubikey-git is a link into the repo, and
# macOS's readlink has no -f before 13.
resolve_repo() {
  local src="$0" dir
  while [[ -L "${src}" ]]; do
    dir="$(cd "$(dirname "${src}")" && pwd)"
    src="$(readlink "${src}")"
    [[ "${src}" == /* ]] || src="${dir}/${src}"
  done
  cd "$(dirname "${src}")/.." && pwd
}
src_dir="$(resolve_repo)/config/ssh/${name}"
[[ -f "${src_dir}/config" ]] || die "no ${src_dir}/config -- is this the dotfiles checkout?"

# A link we made points at exactly this target; anything else there is not ours.
ours() { [[ -L "$1" && "$(readlink "$1")" == "$2" ]]; }

need_fido_ssh() {
  local ssh_bin
  ssh_bin="$(command -v ssh || true)"
  if [[ "$(uname)" == Darwin ]]; then
    if [[ "${ssh_bin}" == /usr/bin/ssh || -z "${ssh_bin}" ]]; then
      if [[ ! -x /opt/homebrew/bin/ssh && ! -x /usr/local/bin/ssh ]]; then
        command -v brew > /dev/null || die "Apple's ssh cannot use a security key, and there is no Homebrew to install OpenSSH with"
        echo "installing Homebrew's OpenSSH: Apple's ssh cannot use a security key"
        brew install openssh
      fi
      ssh_bin="$(command -v ssh || true)"
      [[ "${ssh_bin}" != /usr/bin/ssh ]] ||
        die "Homebrew's ssh is installed but /usr/bin/ssh comes first on PATH; open a new shell and re-run"
    fi
  else
    ssh -Q key 2> /dev/null | grep -qx 'sk-ssh-ed25519@openssh.com' ||
      die "this ssh ($(ssh -V 2>&1)) has no security-key support"
  fi
}

enable() {
  local k key
  need_fido_ssh

  install -d -m 700 "${HOME}/.ssh"
  for k in "${keys[@]}"; do
    key="${HOME}/.ssh/${k}"
    if [[ -e "${key}" ]] && ! cmp -s "${key}" "${src_dir}/${k}"; then
      die "${key} exists and is a different key; move it aside first"
    fi
    install -m 600 "${src_dir}/${k}" "${key}"
    install -m 644 "${src_dir}/${k}.pub" "${key}.pub"
  done

  mkdir -p "${cfg}/enabled.d"
  if [[ -e "${dir_link}" || -L "${dir_link}" ]] && ! ours "${dir_link}" "${src_dir}"; then
    die "${dir_link} exists and is not this repo's; move it aside first"
  fi
  ln -sfn "${src_dir}" "${dir_link}"
  if [[ -e "${conf_link}" || -L "${conf_link}" ]] && ! ours "${conf_link}" "${dir_link}/config"; then
    die "${conf_link} exists and is not this repo's; move it aside first"
  fi
  ln -sfn "${dir_link}/config" "${conf_link}"

  # Fail now, not at the first push: a config ssh cannot parse breaks every ssh.
  if ! ssh -G git.soarez.net 2>&1 | grep -qix "identityfile ~/.ssh/id_ed25519_sk_git"; then
    ssh -G git.soarez.net > /dev/null || true
    disable > /dev/null
    die "ssh did not pick the config up, so it is undone; is ~/.ssh/config this repo's config/ssh/entry?"
  fi

  echo "${name}: enabled. With one of the YubiKeys plugged in:"
  echo "  git clone git@git.soarez.net:sz/<name>.git"
}

disable() {
  local k key
  ours "${conf_link}" "${dir_link}/config" && rm "${conf_link}"
  ours "${dir_link}" "${src_dir}" && rm "${dir_link}"
  for k in "${keys[@]}"; do
    key="${HOME}/.ssh/${k}"
    if [[ -f "${key}" ]] && cmp -s "${key}" "${src_dir}/${k}"; then
      rm -f "${key}" "${key}.pub"
    fi
  done
  echo "${name}: disabled"
}

status() {
  local on=y k key
  if ours "${conf_link}" "${dir_link}/config"; then echo "ssh config:  enabled (${conf_link})"; else echo "ssh config:  not enabled"; on=""; fi
  if ours "${dir_link}" "${src_dir}"; then echo "config dir:  ${dir_link}"; else echo "config dir:  missing"; on=""; fi
  for k in "${keys[@]}"; do
    key="${HOME}/.ssh/${k}"
    if [[ -f "${key}" ]] && cmp -s "${key}" "${src_dir}/${k}"; then echo "key handle:  ${key}"; else echo "key handle:  ${k} missing"; on=""; fi
  done
  echo "ssh:         $(command -v ssh) ($(ssh -V 2>&1))"
  [[ -n "${on}" ]]
}

case "${1:-}" in
  enable)  enable ;;
  disable) disable ;;
  status)  status ;;
  *) >&2 echo "usage: ${name} enable|disable|status"; exit 2 ;;
esac
