#!/bin/bash
#
# cc-password-check — central authority for password/passphrase strength
# and policy validation. See SPECIFICATION.md and SECURITY.md.

# Defeat an inherited xtrace (bash -x, or SHELLOPTS=xtrace in the
# environment) before it can echo the password to stderr — SECURITY.md §2.3.
set +x

set -Eeuo pipefail

# ============================================================
# 1. Safety setup
# ============================================================

: "${LC_ALL:=C.UTF-8}"
export LC_ALL

# disable core dumps
ulimit -c 0 2> /dev/null || true

PROGNAME=${0##*/}

MAX_PASSWORD_LEN=128
# the validated password, keep it secret!!
password=""
# the collected validation errors
VIOLATIONS=()

cleanup() {
  unset password
}
trap cleanup EXIT

on_error() {
  trap - ERR
  printf 'Error: internal error.\n' >&2
  exit 3
}
trap on_error ERR

# ============================================================
# 2. Rules
# ============================================================
# Pure checks against the global $password: no I/O, no logging, no
# subshells. On failure each appends a static, policy-derived message to
# $VIOLATIONS and returns 1; on success returns 0. See SECURITY.md §2.

rule_min_length() {
  local min=$1
  if ((${#password} < min)); then
    VIOLATIONS+=("Password must be at least ${min} characters long.")
    return 1
  fi
  return 0
}

rule_max_length() {
  local max=$1
  if ((${#password} > max)); then
    VIOLATIONS+=("Password must be at most ${max} characters long.")
    return 1
  fi
  return 0
}

rule_require_symbol() {
  if [[ $password != *[[:punct:]]* ]]; then
    VIOLATIONS+=("Password must contain at least one punctuation/symbol character.")
    return 1
  fi
  return 0
}

rule_require_lowercase() {
  if [[ $password != *[[:lower:]]* ]]; then
    VIOLATIONS+=("Password must contain at least one lowercase letter.")
    return 1
  fi
  return 0
}

rule_require_uppercase() {
  if [[ $password != *[[:upper:]]* ]]; then
    VIOLATIONS+=("Password must contain at least one uppercase letter.")
    return 1
  fi
  return 0
}

rule_require_digit() {
  if [[ $password != *[[:digit:]]* ]]; then
    VIOLATIONS+=("Password must contain at least one numeric digit.")
    return 1
  fi
  return 0
}

# ---- Policies ----
# One policy_<name> per password type, running the relevant rule_* checks.
# Adding a type means adding a function here and a matching case arm in
# main()'s dispatch below.

policy_user() {
  VIOLATIONS=()
  local ok=0
  rule_max_length "$MAX_PASSWORD_LEN" || ok=1
  rule_min_length 10 || ok=1
  rule_require_symbol || ok=1
  rule_require_lowercase || ok=1
  rule_require_uppercase || ok=1
  rule_require_digit || ok=1
  return "$ok"
}

policy_disk() {
  VIOLATIONS=()
  local ok=0
  rule_max_length "$MAX_PASSWORD_LEN" || ok=1
  rule_min_length 16 || ok=1
  rule_require_symbol || ok=1
  rule_require_lowercase || ok=1
  rule_require_uppercase || ok=1
  rule_require_digit || ok=1
  return "$ok"
}

# ============================================================
# 3. I/O
# ============================================================
# Only section allowed to touch stdin or hold the password.

read_password() {
  # read exits non-zero on EOF, both for a truly empty stream and for a final
  # line with no trailing newline. That is not an error: an empty stream just
  # yields an empty password, which the policies reject like any other
  # too-weak password.
  IFS= read -r password || true
}

# ============================================================
# 4. Argument parsing / output formatting
# ============================================================
# No password access below this point except via read_password() above.

usage() {
  cat << EOF
Usage: ${PROGNAME} --type <name> < password
       ${PROGNAME} --help

Reads one line from stdin as the password and validates it against the
policy selected by --type (user, disk).

Exit status:
  0  password accepted (stdout empty)
  1  password rejected (one error message per line on stdout)
  2  usage error
  3  internal error

See SPECIFICATION.md and SECURITY.md for the full contract.
EOF
}

# ============================================================
# 5. main
# ============================================================

main() {
  local type=""

  while (($# > 0)); do
    case "$1" in
      --type)
        if (($# < 2)); then
          printf 'Error: --type requires an argument.\n' >&2
          exit 2
        fi
        type="$2"
        shift 2
        ;;
      --help)
        usage
        exit 0
        ;;
      *)
        printf 'Error: unrecognized argument: %s\n' "$1" >&2
        exit 2
        ;;
    esac
  done

  if [[ -z $type ]]; then
    printf 'Error: --type is required.\n' >&2
    exit 2
  fi

  read_password

  local status=0
  case "$type" in
    user) policy_user || status=$? ;;
    disk) policy_disk || status=$? ;;
    *)
      printf 'Error: unknown password type: %s\n' "$type" >&2
      exit 2
      ;;
  esac

  if ((status != 0)); then
    local msg
    for msg in "${VIOLATIONS[@]}"; do
      printf '%s\n' "$msg"
    done
    exit 1
  fi

  exit 0
}

# do not run the script (the main() function) when the file is sourced in tests
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi
