#!/bin/bash
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Common key generation functions.

SCRIPT_DIR="$(dirname "$(readlink -f -- "$0")")"
PROG=$(basename "$0")
CROS_LOG_PREFIX="${PROG}: "

# Prints an informational message.
info() {
  echo "${CROS_LOG_PREFIX}INFO: $*" >&2
}

# Prints a warning message.
warn() {
  echo "${CROS_LOG_PREFIX}WARNING: $*" >&2
}

# Prints an error message.
error() {
  echo "${CROS_LOG_PREFIX}ERROR: $*" >&2
}

# Print an error message and then exit the script.
die() {
  error "$@"
  exit 1
}

# Algorithm ID mappings:
RSA1024_SHA1_ALGOID=0
RSA1024_SHA256_ALGOID=1
RSA1024_SHA512_ALGOID=2
RSA2048_SHA1_ALGOID=3
RSA2048_SHA256_ALGOID=4
RSA2048_SHA512_ALGOID=5
RSA4096_SHA1_ALGOID=6
RSA4096_SHA256_ALGOID=7
RSA4096_SHA512_ALGOID=8
RSA8192_SHA1_ALGOID=9
RSA8192_SHA256_ALGOID=10
RSA8192_SHA512_ALGOID=11
alg_to_keylen() {
  echo $(( 1 << (10 + ($1 / 3)) ))
}

# Default algorithms.
EC_ROOT_KEY_ALGOID=${RSA4096_SHA256_ALGOID}
EC_DATAKEY_ALGOID=${RSA4096_SHA256_ALGOID}

ROOT_KEY_ALGOID=${RSA4096_SHA512_ALGOID}
RECOVERY_KEY_ALGOID=${RSA4096_SHA512_ALGOID}

FIRMWARE_DATAKEY_ALGOID=${RSA4096_SHA256_ALGOID}
DEV_FIRMWARE_DATAKEY_ALGOID=${RSA4096_SHA256_ALGOID}

RECOVERY_KERNEL_ALGOID=${RSA4096_SHA512_ALGOID}
INSTALLER_KERNEL_ALGOID=${RSA4096_SHA512_ALGOID}
KERNEL_SUBKEY_ALGOID=${RSA4096_SHA256_ALGOID}
KERNEL_DATAKEY_ALGOID=${RSA2048_SHA256_ALGOID}

# Keyblock modes determine which boot modes a signing key is valid for use
# in verification.
EC_KEYBLOCK_MODE=7  # Only allow RW EC firmware in non-recovery.
FIRMWARE_KEYBLOCK_MODE=7  # Only allow RW firmware in non-recovery.
DEV_FIRMWARE_KEYBLOCK_MODE=6  # Only allow in dev mode.
RECOVERY_KERNEL_KEYBLOCK_MODE=11 # Only in recovery mode.
KERNEL_KEYBLOCK_MODE=7  # Only allow in non-recovery.
INSTALLER_KERNEL_KEYBLOCK_MODE=10  # Only allow in Dev + Recovery.

# Emit .vbpubk and .vbprivk using given basename and algorithm
# NOTE: This function also appears in ../../utility/dev_make_keypair. Making
# the two implementations the same would require some common.sh, which is more
# likely to cause problems than just keeping an eye out for any differences. If
# you feel the need to change this file, check the history of that other file
# to see what may need updating here too.
make_pair() {
  local base=$1
  local alg=$2
  local key_version=${3:-1}
  local len=$(alg_to_keylen $alg)

  echo "creating $base keypair (version = $key_version)..."

  # make the RSA keypair
  openssl genrsa -F4 -out "${base}_${len}.pem" $len
  # create a self-signed certificate
  openssl req -batch -new -x509 -key "${base}_${len}.pem" \
    -out "${base}_${len}.crt"
  # generate pre-processed RSA public key
  dumpRSAPublicKey -cert "${base}_${len}.crt" > "${base}_${len}.keyb"

  # wrap the public key
  vbutil_key \
    --pack "${base}.vbpubk" \
    --key "${base}_${len}.keyb" \
    --version  "${key_version}" \
    --algorithm $alg

  # wrap the private key
  vbutil_key \
    --pack "${base}.vbprivk" \
    --key "${base}_${len}.pem" \
    --algorithm $alg

  # remove intermediate files
  rm -f "${base}_${len}.pem" "${base}_${len}.crt" "${base}_${len}.keyb"
}

# Used to generate keys for signing update payloads.
make_au_payload_key() {
  local dir=$1
  local priv="${dir}/update_key.pem"
  local pub="${dir}/update-payload-key-pub.pem"
  openssl genrsa -out "${priv}" 2048
  openssl rsa -pubout -in "${priv}" -out "${pub}"
}

# Emit a .keyblock containing flags and a public key, signed by a private key
# flags are the bitwise OR of these (passed in decimal, though)
#   0x01  Developer switch off
#   0x02  Developer switch on
#   0x04  Not recovery mode
#   0x08  Recovery mode
make_keyblock() {
  local base=$1
  local flags=$2
  local pubkey=$3
  local signkey=$4

  echo "creating $base keyblock..."

  # create it
  vbutil_keyblock \
    --pack "${base}.keyblock" \
    --flags $flags \
    --datapubkey "${pubkey}.vbpubk" \
    --signprivate "${signkey}.vbprivk"

  # verify it
  vbutil_keyblock \
    --unpack "${base}.keyblock" \
    --signpubkey "${signkey}.vbpubk"
}

# File to read current versions from.
VERSION_FILE="key.versions"

# ARGS: <VERSION_TYPE> [VERSION_FILE]
get_version() {
  local key="$1"
  local file="${2:-${VERSION_FILE}}"
  awk -F= -vkey="${key}" '$1 == key { print $NF }' "${file}"
}

# Loads the current versions prints them to stdout and sets the global version
# variables: CURR_FIRMKEY_VER CURR_FIRM_VER CURR_KERNKEY_VER CURR_KERN_VER
load_current_versions() {
  local key_dir=$1
  local VERSION_FILE="${key_dir}/${VERSION_FILE}"
  if [[ ! -f ${VERSION_FILE} ]]; then
    return 1
  fi
  CURR_FIRMKEY_VER=$(get_version "firmware_key_version")
  # Firmware version is the kernel subkey version.
  CURR_FIRM_VER=$(get_version "firmware_version")
  # Kernel data key version is the kernel key version.
  CURR_KERNKEY_VER=$(get_version "kernel_key_version")
  CURR_KERN_VER=$(get_version "kernel_version")

  cat <<EOF
Current Firmware key version: ${CURR_FIRMKEY_VER}
Current Firmware version: ${CURR_FIRM_VER}
Current Kernel key version: ${CURR_KERNKEY_VER}
Current Kernel version: ${CURR_KERN_VER}
EOF
}

# Make backups of existing kernel subkeys and keyblocks that will be revved.
# Backup format:
# for keyblocks: <keyblock_name>.v<datakey version>.v<subkey version>.keyblock
# Args: SUBKEY_VERSION DATAKEY_VERSION
backup_existing_kernel_keyblock() {
  if [[ ! -e kernel.keyblock ]]; then
    return
  fi
  mv --no-clobber kernel.{keyblock,"v$2.v$1.keyblock"}
}

# Make backups of existing kernel subkeys and keyblocks that will be revved.
# Backup format:
# for keys: <key_name>.v<version>.vb{pub|priv}k
# for keyblocks: <keyblock_name>.v<datakey version>.v<subkey version>.keyblock
# Args: SUBKEY_VERSION DATAKEY_VERSION
backup_existing_kernel_subkeys() {
  local subkey_ver=$1
  local datakey_ver=$2
  # --no-clobber to prevent accidentally overwriting existing
  # backups.
  mv --no-clobber kernel_subkey.{vbprivk,"v${subkey_ver}.vbprivk"}
  mv --no-clobber kernel_subkey.{vbpubk,"v${subkey_ver}.vbpubk"}
  backup_existing_kernel_keyblock ${subkey_ver} ${datakey_ver}
}

# Make backups of existing kernel data keys and keyblocks that will be revved.
# Backup format:
# for keys: <key_name>.v<version>.vb{pub|priv}k
# for keyblocks: <keyblock_name>.v<datakey version>.v<subkey version>.keyblock
# Args: SUBKEY_VERSION DATAKEY_VERSION
backup_existing_kernel_data_keys() {
  local subkey_ver=$1
  local datakey_ver=$2
  # --no-clobber to prevent accidentally overwriting existing
  # backups.
  mv --no-clobber kernel_data_key.{vbprivk,"v${datakey_ver}.vbprivk"}
  mv --no-clobber kernel_data_key.{vbpubk,"v${datakey_ver}.vbpubk"}
  backup_existing_kernel_keyblock ${subkey_ver} ${datakey_ver}
}

# Make backups of existing firmware keys and keyblocks that will be revved.
# Backup format:
# for keys: <key_name>.v<version>.vb{pub|priv}k
# for keyblocks: <keyblock_name>.v<datakey version>.v<subkey version>.keyblock
# Args: SUBKEY_VERSION DATAKEY_VERSION
backup_existing_firmware_keys() {
  local subkey_ver=$1
  local datakey_ver=$2
  mv --no-clobber firmware_data_key.{vbprivk,"v${subkey_ver}.vbprivk"}
  mv --no-clobber firmware_data_key.{vbpubk,"v${subkey_ver}.vbpubk"}
  mv --no-clobber firmware.{keyblock,"v${datakey_ver}.v${subkey_ver}.keyblock"}
}


# Write new key version file with the updated key versions.
# Args: FIRMWARE_KEY_VERSION FIRMWARE_VERSION KERNEL_KEY_VERSION
#       KERNEL_VERSION
write_updated_version_file() {
  local firmware_key_version=$1
  local firmware_version=$2
  local kernel_key_version=$3
  local kernel_version=$4

  cat > ${VERSION_FILE} <<EOF
firmware_key_version=${firmware_key_version}
firmware_version=${firmware_version}
kernel_key_version=${kernel_key_version}
kernel_version=${kernel_version}
EOF
}

# Returns the incremented version number of the passed in key from the version
# file.  The options are "firmware_key_version", "firmware_version",
# "kernel_key_version", or "kernel_version".
# ARGS: KEY_DIR <key_name>
increment_version() {
  local key_dir=$1
  local VERSION_FILE="${key_dir}/${VERSION_FILE}"
  local old_version=$(get_version $2)
  local new_version=$(( ${old_version} + 1 ))

  if [[ ${new_version} -gt 0xffff ]]; then
    echo "Version overflow!" >&2
    return 1
  fi
  echo ${new_version}
}
