# Copyright (c) 2022, Google Inc.
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
# OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

# This script runs test_fips repeatedly with different FIPS tests broken. It is
# intended to be observed to demonstrate that the various tests are working and
# thus pauses for a keystroke between tests.
#
# Runs in either device mode (on an attached Android device) or in a locally built
# BoringSSL checkout.
#
# On Android static binaries are not built using FIPS mode, so in device mode each
# test makes changes to libcrypto.so rather than the test binary, test_fips.

set -e

die () {
  echo "ERROR: $@"
  exit 1
}

usage() {
  echo "USAGE: $0 [local|device]"
  exit 1
}

inferred_mode() {
  # Try and infer local or device mode based on makefiles and artifacts.
  if [ -f Android.bp -o -f external/boringssl/Android.bp ]; then
    echo device
  elif [ -f CMakeLists.txt -a -d build/crypto -a -d build/ssl ]; then
    echo local
  else
    echo "Unable to infer mode, please specify on the command line."
    usage
  fi
}

MODE=`inferred_mode`
# Prefer mode from command line if present.
while [ "$1" ]; do
  case "$1" in
    local|device)
      MODE=$1
      ;;

    "32")
      TEST32BIT="true"
      ;;

    *)
      usage
      ;;
  esac
  shift
done

check_directory() {
  test -d "$1" || die "Directory $1 not found."
}

check_file() {
  test -f "$1" || die "File $1 not found."
}

run_test_locally() {
  eval "$1" || true
}

run_test_on_device() {
  EXECFILE="$1"
  LIBRARY="$2"
  adb shell rm -rf "$DEVICE_TMP"
  adb shell mkdir -p "$DEVICE_TMP"
  adb push "$EXECFILE" "$DEVICE_TMP" > /dev/null
  EXECPATH=$(basename "$EXECFILE")
  adb push "$LIBRARY" "$DEVICE_TMP" > /dev/null
  adb shell "LD_LIBRARY_PATH=$DEVICE_TMP" "$DEVICE_TMP/$EXECPATH" || true
}

device_integrity_break_test() {
  go run "$BORINGSSL/util/fipstools/break-hash.go" "$LIBCRYPTO_BIN" ./libcrypto.so
  $RUN "$TEST_FIPS_BIN" ./libcrypto.so
  rm ./libcrypto.so
}

local_integrity_break_test() {
  go run $BORINGSSL/util/fipstools/break-hash.go "$TEST_FIPS_BIN" ./break-bin
  chmod u+x ./break-bin
  $RUN ./break-bin
  rm ./break-bin
}

local_runtime_break_test() {
  BORINGSSL_FIPS_BREAK_TEST=$1 "$RUN" "$TEST_FIPS_BREAK_BIN"
}

# TODO(prb): make break-hash and break-kat take similar arguments to save having
# separate functions for each.
device_kat_break_test() {
  KAT="$1"
  go run "$BORINGSSL/util/fipstools/break-kat.go" "$LIBCRYPTO_BREAK_BIN" "$KAT" > ./libcrypto.so
  $RUN "$TEST_FIPS_BIN" ./libcrypto.so
  rm ./libcrypto.so
}

local_kat_break_test() {
  KAT="$1"
  go run "$BORINGSSL/util/fipstools/break-kat.go" "$TEST_FIPS_BREAK_BIN" "$KAT" > ./break-bin
  chmod u+x ./break-bin
  $RUN ./break-bin
  rm ./break-bin
}

pause () {
  echo -n "Press <Enter> "
  read
}

if [ "$MODE" = "local" ]; then
  TEST_FIPS_BIN=${TEST_FIPS_BIN:-build/util/fipstools/test_fips}
  TEST_FIPS_BREAK_BIN=${TEST_FIPS_BREAK_BIN:-./test_fips_break}
  check_file "$TEST_FIPS_BIN"
  check_file "$TEST_FIPS_BREAK_BIN"

  BORINGSSL=.
  RUN=run_test_locally
  BREAK_TEST=local_break_test
  INTEGRITY_BREAK_TEST=local_integrity_break_test
  KAT_BREAK_TEST=local_kat_break_test
  RUNTIME_BREAK_TEST=local_runtime_break_test
  if [ ! -f "$TEST_FIPS_BIN" ]; then
    echo "$TEST_FIPS_BIN is missing. Run this script from the top level of a"
    echo "BoringSSL checkout and ensure that BoringSSL has been built in"
    echo "build/ with -DFIPS_BREAK_TEST=TESTS passed to CMake."
    exit 1
  fi
else # Device mode
  test "$ANDROID_BUILD_TOP" || die "'lunch aosp_arm64-eng' first"
  check_directory "$ANDROID_PRODUCT_OUT"

  if [ "$TEST32BIT" ]; then
    TEST_FIPS_BIN="$ANDROID_PRODUCT_OUT/system/bin/test_fips32"
    LIBCRYPTO_BIN="$ANDROID_PRODUCT_OUT/system/lib/libcrypto.so"
    LIBCRYPTO_BREAK_BIN="$ANDROID_PRODUCT_OUT/system/lib/libcrypto_for_testing.so"
  else
    TEST_FIPS_BIN="$ANDROID_PRODUCT_OUT/system/bin/test_fips"
    LIBCRYPTO_BIN="$ANDROID_PRODUCT_OUT/system/lib64/libcrypto.so"
    LIBCRYPTO_BREAK_BIN="$ANDROID_PRODUCT_OUT/system/lib64/libcrypto_for_testing.so"
  fi
  check_file "$TEST_FIPS_BIN"
  check_file "$LIBCRYPTO_BIN"
  check_file "$LIBCRYPTO_BREAK_BIN"

  test "$ANDROID_SERIAL" || die "ANDROID_SERIAL not set"
  DEVICE_TMP=/data/local/tmp

  BORINGSSL="$ANDROID_BUILD_TOP/external/boringssl/src"
  RUN=run_test_on_device
  INTEGRITY_BREAK_TEST=device_integrity_break_test
  KAT_BREAK_TEST=device_kat_break_test
fi


KATS=$(go run "$BORINGSSL/util/fipstools/break-kat.go" --list-tests)

echo -e '\033[1mNormal output\033[0m'
$RUN "$TEST_FIPS_BIN" "$LIBCRYPTO_BIN"
pause

echo
echo -e '\033[1mIntegrity test failure\033[0m'
$INTEGRITY_BREAK_TEST
pause

for kat in $KATS; do
  echo
  echo -e "\033[1mKAT failure ${kat}\033[0m"
  $KAT_BREAK_TEST $kat
  pause
done

if [ "$MODE" = "local" ]; then
  # TODO(prb): add support for Android devices.
  for runtime_test in ECDSA_PWCT RSA_PWCT CRNG; do
    echo
    echo -e "\033[1m${runtime_test} failure\033[0m"
    $RUNTIME_BREAK_TEST ${runtime_test}
    pause
  done
fi
