#!/bin/sh #=============================================================================== # # recovery-initramfs-init # # Copyright (C) 2016, 2017 by Digi International Inc. # All rights reserved. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # # !Description: Init script for recovery initramfs # #=============================================================================== # Variables. #------------------------------------------------------------------------------ ENV_BOOT_RECOVERY="boot_recovery" ENV_RECOVERY_COMMAND="recovery_command" SW_CONFIG="/etc/swupdate.cfg" PUBLIC_KEY="/etc/ssl/certs/key.pub" USB_MOUNT_DIR="/run/media" UPDATE_MOUNT_DIR="/mnt/update" ROOTFS_NEEDS_FORMAT="no" SWUPDATE_OUTPUT="swupdate_output.txt" PART_LIST="" ENC_PARTS="" DEFAULT_ENC_PARTS="yes" NAND_PARTS_BLACKLIST="bootloader environment linux recovery safe" EMMC_PARTS_BLACKLIST="linux recovery safe" ENC_DIFF="" UNENC_DIFF="" REBOOT_TIME=10 # Functions. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # Function - log # # Prints the given text in the console. # # @param ${1} - Text to print. #------------------------------------------------------------------------------ log() { echo "[RECOVERY] ${1}" 1>&2 } #------------------------------------------------------------------------------ # Function - log_warning # # Prints the given text in the console as a warning. # # @param ${1} - Warning text to print. #------------------------------------------------------------------------------ log_warning() { log "[WARNING] ${1}" } #------------------------------------------------------------------------------ # Function - log_error # # Prints the given text in the console as an error. # # @param ${1} - Error text to print. #------------------------------------------------------------------------------ log_error() { log "[ERROR] ${1}" psplash_message "ERROR: ${1}" psplash_progress "0" } #------------------------------------------------------------------------------ # Function - clear_uboot_vars # # Clears recovery U-Boot variables. #------------------------------------------------------------------------------ clear_uboot_vars() { fw_setenv "${ENV_BOOT_RECOVERY}" fw_setenv "${ENV_RECOVERY_COMMAND}" } #------------------------------------------------------------------------------ # Function - read_uboot_var # # Reads the given U-Boot variable. # # @param ${1} - U-Boot variable to read. # @param ${2} - Where to store the value of the read variable. #------------------------------------------------------------------------------ read_uboot_var() { eval "${2}=\"$(fw_printenv -n ${1} 2>/dev/null)\"" } #------------------------------------------------------------------------------ # Function - set_uboot_var # # Sets the given U-Boot variable. # # @param ${1} - U-Boot variable to set. # @param ${2} - Value to set. #------------------------------------------------------------------------------ set_uboot_var() { fw_setenv ${1} ${2} 2>/dev/null } #------------------------------------------------------------------------------ # Function - psplash_message # # Shows the given message in the psplash screen. # # @param ${1} - Message to show. #------------------------------------------------------------------------------ psplash_message() { echo "MSG ${1}" > /tmp/psplash_fifo sleep 0.2 } #------------------------------------------------------------------------------ # Function - psplash_progress # # Sets the psplash progress bar percentage to the given one. # # @param ${1} - Progress percentage. #------------------------------------------------------------------------------ psplash_progress() { echo "PROGRESS ${1}" > /tmp/psplash_fifo sleep 0.2 } #------------------------------------------------------------------------------ # Function - reboot_system # # Reboots the system. #------------------------------------------------------------------------------ reboot_system() { sync && reboot -f } #------------------------------------------------------------------------------ # Function - quit_with_error # # Ends the recovery process with the given error message. # # @param ${1} - Error message. #------------------------------------------------------------------------------ quit_with_error() { clear_uboot_vars log_error "${1}" log "The system will now reboot in ${REBOOT_TIME} seconds" sleep "${REBOOT_TIME}" reboot_system } #------------------------------------------------------------------------------ # Function - is_nand # # Verifies if the system is running in a NAND flash. # # @return - "yes" if the system is running in NAND, "no" otherwise #------------------------------------------------------------------------------ is_nand() { if [ -f /proc/mtd ] && grep -qs mtd /proc/mtd; then echo "yes" else echo "no" fi } #------------------------------------------------------------------------------ # Function - contains # # Returns true if and only if the list $1 contains $2 #------------------------------------------------------------------------------ contains() { echo "${1}" | grep -qs "\b${2}\b" return $? } #------------------------------------------------------------------------------ # Function - remove_duplicates # # Removes duplicate entries from a list #------------------------------------------------------------------------------ remove_duplicates () { echo "${1}" | tr ' ' '\n' | sort | uniq | tr '\n' ' ' | xargs } #------------------------------------------------------------------------------ # Function - remove_entry # # Removes an entry from a list #------------------------------------------------------------------------------ remove_entry () { echo "${1}" | sed "s/\b${2}\b//g" | xargs } #------------------------------------------------------------------------------ # Function - format_partition # # Formats the given partition. # # @param ${1} - Partition name to format. #------------------------------------------------------------------------------ format_partition() { if [ "$(is_nand)" = "yes" ]; then format_ubi_volume "${1}" else format_emmc_block "${1}" fi } #------------------------------------------------------------------------------ # Function - format_ubi_volume # # Formats and re-creates the given UBI volume. # # @param ${1} - UBI Volume name to format. #------------------------------------------------------------------------------ format_ubi_volume() { log "Formatting '${1}' ubi volume" psplash_message "Formatting '${1}' partition..." psplash_progress "0" # Find the MTD partition. local mtd_num="$(sed -ne "s/mtd\([0-9]\+\):.*\<${1}\>.*/\1/g;T;p" /proc/mtd 2>/dev/null)" if [ -z "${mtd_num}" ]; then quit_with_error "Could not find MTD partition for volume '${1}'" else # Umount in case partition is mounted, ignore errors. if grep -qs "${1}" /proc/mounts; then local path="$(sed -ne "s/.*:${1} \(.*\) ubifs.*/\1/g;T;p" /proc/mounts 2>/dev/null)" umount "${path}" >/dev/null 2>&1 fi ubidetach -p "/dev/mtd${mtd_num}" >/dev/null 2>&1 # Format MTD partition. if ! ubiformat "/dev/mtd${mtd_num}" -q -y; then quit_with_error "Error erasing '/dev/mtd${mtd_num}' block" fi psplash_progress "50" # Attach and get UBI device number local dev_number="$(ubiattach -p /dev/mtd${mtd_num} 2>/dev/null | sed -ne 's,.*device number \([0-9]\).*,\1,g;T;p' 2>/dev/null)" # Create UBI Vol. ubimkvol "/dev/ubi${dev_number}" -m -N "${1}" >/dev/null 2>&1 if [ "$?" = "0" ]; then # Configure the empty UBIFS partition to use ZLIB [ "${1}" = "update" ] && UBIFS_COMPRESSION="-x zlib" volid="$(ubinfo "/dev/ubi${dev_number}" -N "${1}" | sed -ne 's,Volume ID:[[:blank:]]\+\([0-9]\+\)[[:blank:]]\+.*,\1,g;T;p')" mkfs.ubifs ${UBIFS_COMPRESSION} -F /dev/ubi${dev_number}_${volid} psplash_progress "100" log "Partition '${1}' successfully erased!" # Detach MTD partition. ubidetach -p "/dev/mtd${mtd_num}" >/dev/null 2>&1 else quit_with_error "Error creating '${1}' UBI volume" fi fi } #------------------------------------------------------------------------------ # Function - format_emmc_block # # Formats the given emmc partition block name. # # @param ${1} - Partition name to format. #------------------------------------------------------------------------------ format_emmc_block() { log "Formatting '${1}' eMMC block" psplash_message "Formatting '${1}' partition..." psplash_progress "0" # If partition is encrypted, it might be open and even mounted at this point. local mapped_block="/dev/mapper/crypt${1}" if [ -e "${mapped_block}" ]; then # Umount in case partition is mounted, ignore errors. if grep -qs "${mapped_block}" /proc/mounts; then umount "${mapped_block}" >/dev/null 2>&1 fi # Close mapped device cryptsetup close crypt${1} fi # Find partition block number. local partition_block="/dev/mmcblk0p$(fdisk -l /dev/mmcblk0 | sed -ne "s,^[^0-9]*\([0-9]\+\).*\<${1}\>.*,\1,g;T;p")" if [ -b "${partition_block}" ]; then # Umount in case partition is mounted, ignore errors. if grep -qs "${partition_block}" /proc/mounts; then umount "${partition_block}" >/dev/null 2>&1 fi # If partition is encrypted, format with trustfence-tool first if contains "${encrypt_partitions}" "${1}"; then trustfence-tool --format ${partition_block} crypt${1} if [ ! "$?" = "0" ]; then quit_with_error "Error formatting '${1}' partition for encryption" fi partition_block="/dev/mapper/crypt${1}" psplash_progress "50" fi # Format emmc block. mkfs.ext4 "${partition_block}" >/dev/null 2>&1 if [ "$?" = "0" ]; then psplash_progress "100" log "Partition '${1}' successfully erased!" else quit_with_error "Error erasing '${1}' partition" fi else quit_with_error "Could not find partition block for '${1}'" fi } #------------------------------------------------------------------------------ # Function - swu_package_path # # Get absolute path of update package searching in local media # # @param ${1} - SWU package #------------------------------------------------------------------------------ swu_package_path() { # Check whether the package is local and get the absolute path. if echo "${1}" | grep -qs '^file://'; then local pkg_name="$(basename ${1})" for i in ${UPDATE_MOUNT_DIR} $(echo ${USB_MOUNT_DIR}/*); do echo $i | grep -qs "${USB_MOUNT_DIR}/\*" && continue if [ -f "${i}/${pkg_name}" ]; then swu_abspath="${i}/${pkg_name}" break fi done if [ -n "${swu_abspath}" ]; then echo "${swu_abspath}" else quit_with_error "Unable to find update package '${pkg_name}'" fi else echo "${1}" fi } #------------------------------------------------------------------------------ # Function - check_swu_package # # Check if the update package is a valid one and get its absolute path # # @param ${1} - SWU package #------------------------------------------------------------------------------ check_swu_package() { log "Checking update package '$(basename ${1})'" if [ -z "${1}" ]; then quit_with_error "Firmware update package not specified" fi update_package="$(swu_package_path ${1})" # Check software update package. if [ -f "${PUBLIC_KEY}" ]; then swupdate -c -v -i "${update_package}" -k "${PUBLIC_KEY}" > "${SWUPDATE_OUTPUT}" else swupdate -c -v -i "${update_package}" > "${SWUPDATE_OUTPUT}" fi if [ "$?" != "0" ]; then quit_with_error "Invalid update package '$(basename ${1})'" fi # Format the rootfs partition if and only if the package contains a rootfs image. if [ "$(is_nand)" = "yes" ]; then grep "Found Image" "${SWUPDATE_OUTPUT}" | grep -qs "rootfs" && ROOTFS_NEEDS_FORMAT="yes" fi } #------------------------------------------------------------------------------ # Function - update_mtdparts # Updates the encryption flags on the mtdparts environment variable #------------------------------------------------------------------------------ update_mtdparts() { # Read the mtdparts variable. read_uboot_var mtdparts mtdparts # Check if there is any command. if [ -z "${mtdparts}" ]; then quit_with_error "No mtdparts found" fi # Add encryption flag to new encrypted partitions for p in ${ENC_DIFF}; do mtdparts=$(echo "${mtdparts}" | sed "s/(${p})/(${p})enc/g") done # Remove encryption flag from unencrypted partitions for p in ${UNENC_DIFF}; do mtdparts=$(echo "${mtdparts}" | sed "s/(${p})enc/(${p})/g") done set_uboot_var mtdparts "${mtdparts}" } #------------------------------------------------------------------------------ # Function - parse_partition_info # # Obtain a list of all partitions and a list of encrypted partitions #------------------------------------------------------------------------------ parse_partition_info() { if [ "$(is_nand)" = "yes" ]; then # Read the mtdparts variable. read_uboot_var mtdparts mtdparts if [ -z "${mtdparts}" ]; then quit_with_error "No mtdparts found" fi TMP_LIST=$(echo ${mtdparts} | cut -d ':' -f2 | tr "," " ") for p in ${TMP_LIST}; do PART=$(echo $p | sed 's,.*(\(.*\)).*,\1,') PART_LIST="${PART_LIST} ${PART}" echo $p | grep -qs ")enc" && ENC_PARTS="${ENC_PARTS} ${PART}" done else PART_LIST=$(fdisk -l /dev/mmcblk0 | grep '^ *' | rev | cut -d ' ' -f1 | rev) # If the partition list doesn't exist, we can consider it empty read_uboot_var encrypted_parts_list ENC_PARTS ENC_PARTS=$(remove_duplicates "${ENC_PARTS}") fi # If a new partition list wasn't explicitly passed in the recovery # command, assume the list hasn't changed. This prevents having to # always pass the list even if it doesn't have any changes. [ "${DEFAULT_ENC_PARTS}" = "yes" ] && encrypt_partitions="${ENC_PARTS}" } #------------------------------------------------------------------------------ # Function - get_blacklist # # Return the list of partitions that can't be encrypted on the storage media #------------------------------------------------------------------------------ get_blacklist() { if [ "$(is_nand)" = "yes" ]; then echo "${NAND_PARTS_BLACKLIST}" else echo "${EMMC_PARTS_BLACKLIST}" fi } # Main #------------------------------------------------------------------------------ # Setup the environment. export PATH=/bin:/sbin:/usr/bin:/usr/sbin # Mount virtual file system. mkdir -p /proc /sys /dev /tmp mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs devtmpfs /dev mount -t tmpfs tmpfs /tmp # Give some time for the devices to settle down so mdev can mount all of them sleep 2 # Register mdev as device manager echo > /dev/mdev.seq echo > /dev/mdev.log mdev -s # Run all shell scripts in postinstall folder run-parts /etc/*-postinsts # Setup fw_printenv. mkdir -p /var/lock # Set kernel console loglevel. sysctl -q -w kernel.printk=4 # Start psplash. psplash & # Parse the kernel command line. for arg in $(cat /proc/cmdline); do case "${arg}" in rescue=1) eval "${arg}";; esac done # Jump to a rescue shell if requested. if [ -n "${rescue}" ]; then # Expand console and respawn if exited while true; do setsid cttyhack sh -l sleep 1 done fi log "Starting recovery..." psplash_message "Starting recovery..." # Read the recovery command. read_uboot_var "${ENV_RECOVERY_COMMAND}" COMMAND # Check if there is any command. if [ -z "${COMMAND}" ]; then quit_with_error "No command found" fi # Parse the recovery command. for arg in ${COMMAND}; do case "${arg}" in wipe_update) wipe_update_bool=true;; encryption_key=*) encryption_key_bool=true; eval "${arg}";; update_package=*) update_package_bool=true; eval "${arg}";; encrypt_partitions=*) eval "${arg}"; DEFAULT_ENC_PARTS="no"; encrypt_partitions=$(echo ${encrypt_partitions} | tr "," " "); encrypt_partitions=$(remove_duplicates "${encrypt_partitions}");; wipe_ubi_partitions=*) eval "${arg}"; wipe_ubi_partitions=$(echo ${wipe_ubi_partitions} | tr "," " "); wipe_ubi_partitions=$(remove_duplicates "${wipe_ubi_partitions}");; *) # Not supported command quit_with_error "Unknown recovery command '${arg}'";; esac done # Get current partition information parse_partition_info # Sanitize the encrypted partition list BLACKLISTED="" for p in ${encrypt_partitions}; do contains "${PART_LIST}" "${p}" || quit_with_error "Cannot encrypt nonexistant partition '${p}'" # Take note of any blacklisted partitions to remove them from the list later contains "$(get_blacklist)" "${p}" && BLACKLISTED="${BLACKLISTED} ${p}" done for p in ${BLACKLISTED}; do log_warning "Encryption of partition '${p}' is forbidden, skipping" encrypt_partitions=$(remove_entry "${encrypt_partitions}" "${p}") done # On eMMC, if the 'update' partition is encrypted, we need to mount it manually if [ "$(is_nand)" = "no" ] && contains "${ENC_PARTS}" "update"; then update_block="/dev/mmcblk0p$(fdisk -l /dev/mmcblk0 | sed -ne "s,^[^0-9]*\([0-9]\+\).*\.*,\1,g;T;p")" trustfence-tool ${update_block} cryptupdate if [ "$?" = "0" ]; then # Reset block path to the decrypted mapped device update_block="/dev/mapper/cryptupdate" mkdir -p ${UPDATE_MOUNT_DIR} FSTYPE="$(blkid ${update_block} | sed -e 's,.*TYPE="\([^"]\+\)".*,\1,g')" mount ${FSTYPE:+-t ${FSTYPE}} ${update_block} ${UPDATE_MOUNT_DIR} else quit_with_error "Error mounting encrypted update partition" fi fi # Sanity checks. if [ -n "${update_package_bool}" ]; then check_swu_package "${update_package}" fi # Format UBI partitions if [ "$(is_nand)" = "yes" -a -n "${wipe_ubi_partitions}" ]; then for p in ${wipe_ubi_partitions}; do # Only format 'update' partition if it doesn't have a pending # update package. [ "${p}" = "update" ] && \ echo "${update_package}" | grep -qs "^${UPDATE_MOUNT_DIR}" && continue # To protect against manually injected commands, only format # partitions that exist and aren't blacklisted. contains "${PART_LIST}" "${p}" && \ ! contains "${NAND_PARTS_BLACKLIST}" "${p}" && \ format_ubi_volume "${p}" done fi # Compare the current and new encrypted partition lists and take note of the # differences, if any. If we know the list hasn't changed, don't bother # checking. if [ "${DEFAULT_ENC_PARTS}" = "no" ]; then # Iterate through the old encrypted partition list for p in ${ENC_PARTS}; do if ! contains "${encrypt_partitions}" "${p}"; then # Partition is no longer in the list, unencrypt it UNENC_DIFF="${UNENC_DIFF} ${p}" fi done # Iterate through the new encrypted partition list for p in ${encrypt_partitions}; do if ! contains "${ENC_PARTS}" "${p}"; then # Partition is new in the list, encrypt it ENC_DIFF="${ENC_DIFF} ${p}" fi done fi # If we have an update package in the 'update' partition and we also need to # format said partition, avoid doing so by aborting the operation or by # avoiding the formatting. if echo "${update_package}" | grep -qs "^${UPDATE_MOUNT_DIR}"; then # In the case of a state change (encrypted->unencrypted or # unencrypted->encrypted), we can simply cancel it and continue with # the update. Make sure to remove leading/trailing whitespaces from # the diffs. if contains "${UNENC_DIFF}" "update"; then log_warning "Update package in 'update' partition, skip the partition's unencryption process." encrypt_partitions="${encrypt_partitions} update" UNENC_DIFF=$(remove_entry "${UNENC_DIFF}" "update") elif contains "${ENC_DIFF}" "update"; then log_warning "Update package in 'update' partition, skip the partition's encryption process." encrypt_partitions=$(remove_entry "${encrypt_partitions}" "update") ENC_DIFF=$(remove_entry "${ENC_DIFF}" "update") fi # Worst case scenario: the partition remains encrypted, but the key is # going to be changed. Since this affects all encrypted partitions, # which might also include the rootfs, we can't cancel the key change # and continue with the update. To avoid unexpected behaviour, abort # both operations and quit. if [ -n "${encryption_key_bool}" ] && \ contains "${ENC_PARTS}" "update" && \ contains "${encrypt_partitions}" "update"; then quit_with_error "Cannot change the encryption key with an update package in an encrypted 'update' partition." fi fi # Remove leading/trailing whitespaces from the new encrypted partitions list. encrypt_partitions=$(echo "${encrypt_partitions}" | xargs) # Check if encryption key command is configured. if [ -n "${encryption_key_bool}" ]; then log "Trustfence encryption key setup requested (new key: ${encryption_key:-random})" psplash_message "Configuring new encryption key..." psplash_progress "0" trustfence-tool "--newkey${encryption_key:+=${encryption_key}}" if [ "$?" = "0" ]; then psplash_progress "100" log "Trustfence encryption key setup succeed!" else quit_with_error "Error configuring trustfence encryption key" fi fi # Proceed with the formatting if any partitions require it if [ -n "${encryption_key_bool}" -a -n "${encrypt_partitions}" ] || [ -n "${ENC_DIFF}" -o -n "${UNENC_DIFF}" ]; then log "Proceeding to format partitions" # On NAND devices, if the list has changed, reflect the change in the # environment now so that the MTD driver loads the partitions with # their new encryption status on the second run of the recovery script. if [ "$(is_nand)" = "yes" ] && [ -n "${ENC_DIFF}" -o -n "${UNENC_DIFF}" ]; then update_mtdparts fi # Format all currently encrypted partitions with the new encryption key if [ -n "${encryption_key_bool}" ]; then ENC_DIFF="${encrypt_partitions}" fi if [ "$(is_nand)" = "yes" ]; then psplash_message "Preparing new recovery command..." psplash_progress "0" # Remove the encrypted parts list from the recovery command. new_command="${COMMAND}" new_command=$(echo "${new_command}" | sed "s/encrypt_partitions=[^ ]*//g") if [ -n "${encryption_key_bool}" ]; then # Modify the recovery command to not set the key again. new_command=$(echo "${new_command}" | sed "s/encryption_key=[^ ]*//g") fi # Append the list of partitions to format to the recovery command. wipe_ubi_partitions=$(echo "${ENC_DIFF} ${UNENC_DIFF}" | tr " " ",") new_command="${new_command} wipe_ubi_partitions=${wipe_ubi_partitions}" set_uboot_var "${ENV_RECOVERY_COMMAND}" "${new_command}" log "Rebooting to complete partition formatting..." psplash_progress "100" reboot_system else # On eMMC devices, update the new encrypted partition list # gradually using the old list as a base. By the end, the list # should be the same as the new one. for p in ${ENC_DIFF}; do format_emmc_block ${p} ENC_PARTS=$(remove_duplicates "${ENC_PARTS} ${p}") set_uboot_var encrypted_parts_list "${ENC_PARTS}" done for p in ${UNENC_DIFF}; do format_emmc_block ${p} ENC_PARTS=$(remove_entry "${ENC_PARTS}" "${p}") set_uboot_var encrypted_parts_list "${ENC_PARTS}" done fi fi # Check if update package command is configured. if [ -n "${update_package_bool}" ]; then log "Firmware update requested" psplash_message "Updating firmware..." psplash_progress "0" # Format the UBI volume before updating if needed. if [ "$(is_nand)" = "yes" ]; then if [ "$ROOTFS_NEEDS_FORMAT" = "yes" ]; then psplash_message "Formatting rootfs partition..." format_ubi_volume rootfs psplash_progress "0" fi fi log "Update package location: ${update_package}" # Execute the progress binary. progress -wp & # Execute the software update. if [ -f "${PUBLIC_KEY}" ]; then swupdate -f "${SW_CONFIG}" -i "${update_package}" -k "${PUBLIC_KEY}" else swupdate -f "${SW_CONFIG}" -i "${update_package}" fi if [ "$?" = "0" ]; then log "Firmware update process succeed!" else quit_with_error "Error executing the firmware update" fi fi # Check if wipe update patition command is configured. if [ -n "${wipe_update_bool}" ]; then log "Wipe 'update' partition requested" format_partition update fi # End the recovery process. clear_uboot_vars reboot_system