#!/bin/bash -e # vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: # # Copyright (c) 2019 Red Hat, Inc. # Author: Sergio Correia # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # CLEVIS_UUID="cb6e8904-81ff-40da-a84a-07ab9ab5715e" # valid_slot() will check whether a given slot is possibly valid, i.e., if it # is a numeric value within the specified range. valid_slot() { local SLT="${1}" local MAX_SLOTS="${2}" case "${SLT}" in ''|*[!0-9]*) return 1 ;; *) # We got an integer, now let's make sure it is within the # supported range. if [ "${SLT}" -ge "${MAX_SLOTS}" ]; then return 1 fi ;; esac } # clevis_luks_read_slot() will read a particular slot of a given device, which # should be either LUKS1 or LUKS2. Returns 1 in case of failure; 0 in case of # success. clevis_luks_read_slot() { local DEV="${1}" local SLT="${2}" if [ -z "${DEV}" ] || [ -z "${SLT}" ]; then echo "Need both a device and a slot as arguments." >&2 return 1 fi local DATA_CODED='' local MAX_LUKS1_SLOTS=8 local MAX_LUKS2_SLOTS=32 if cryptsetup isLuks --type luks1 "${DEV}"; then if ! valid_slot "${SLT}" "${MAX_LUKS1_SLOTS}"; then echo "Please, provide a valid key slot number; 0-7 for LUKS1" >&2 return 1 fi if ! luksmeta test -d "${DEV}"; then echo "The ${DEV} device is not valid!" >&2 return 1 fi local uuid # Pattern from luksmeta: active slot uuid. read -r _ _ uuid <<< "$(luksmeta show -d "${DEV}" | grep "^${SLT} *")" if [ "${uuid}" != ${CLEVIS_UUID}"" ]; then echo "Not a clevis slot!" >&2 return 1 fi if ! DATA_CODED="$(luksmeta load -d "${DEV}" -s "${SLT}")"; then echo "Cannot load data from ${DEV} slot:${SLT}!" >&2 return 1 fi elif cryptsetup isLuks --type luks2 "${DEV}"; then if ! valid_slot "${SLT}" "${MAX_LUKS2_SLOTS}"; then echo "Please, provide a valid key slot number; 0-31 for LUKS2" >&2 return 1 fi local token_id token_id=$(cryptsetup luksDump "${DEV}" \ | grep -E -B1 "^\s+Keyslot:\s+${SLT}$" \ | sed -n 1p | sed -rn 's|^\s+([0-9]+): clevis|\1|p') if [ -z "${token_id}" ]; then echo "Cannot load data from ${DEV} slot:${SLT}. No token found!" >&2 return 1 fi local token token=$(cryptsetup token export --token-id "${token_id}" "${DEV}") DATA_CODED=$(jose fmt -j- -Og jwe -o- <<< "${token}" \ | jose jwe fmt -i- -c) if [ -z "${DATA_CODED}" ]; then echo "Cannot load data from ${DEV} slot:${SLT}!" >&2 return 1 fi else echo "${DEV} is not a supported LUKS device!" >&2 return 1 fi echo "${DATA_CODED}" } # clevis_luks_used_slots() will return the list of used slots for a given LUKS # device. clevis_luks_used_slots() { local DEV="${1:-}" [ -z "${DEV}" ] && return 1 local used_slots if cryptsetup isLuks --type luks1 "${DEV}"; then if ! used_slots=$(cryptsetup luksDump "${DEV}" 2>/dev/null \ | sed -rn 's|^Key Slot ([0-7]): ENABLED$|\1|p'); then return 1 fi elif cryptsetup isLuks --type luks2 "${DEV}"; then if ! used_slots=$(cryptsetup luksDump "${DEV}" 2>/dev/null \ | sed -rn 's|^\s+([0-9]+): luks2$|\1|p'); then return 1 fi else echo "${DEV} is not a supported LUKS device!" >&2 return 1 fi echo "${used_slots}" } # clevis_luks_decode_jwe() will decode a given JWE. clevis_luks_decode_jwe() { local jwe="${1}" local coded read -r -d . coded <<< "${jwe}" jose b64 dec -i- <<< "${coded}" } # clevis_luks_print_pin_config() will print the config of a given pin; i.e. # for tang it will display the associated url address, and for tpm2, the # properties in place, like the hash, for instance. clevis_luks_print_pin_config() { local P="${1}" local decoded="${2}" local content if ! content="$(jose fmt -j- -g clevis -g "${P}" -o- <<< "${decoded}")" \ || [ -z "${content}" ]; then return 1 fi local pin= case "${P}" in tang) local url url="$(jose fmt -j- -g url -u- <<< "${content}")" pin=$(printf '{"url":"%s"}' "${url}") printf "tang '%s'" "${pin}" ;; tpm2) # Valid properties for tpm2 pin are the following: # hash, key, pcr_bank, pcr_ids, pcr_digest. local key local value for key in 'hash' 'key' 'pcr_bank' 'pcr_ids' 'pcr_digest'; do if value=$(jose fmt -j- -g "${key}" -u- <<< "${content}"); then pin=$(printf '%s,"%s":"%s"' "${pin}" "${key}" "${value}") fi done # Remove possible leading comma. pin=${pin/#,/} printf "tpm2 '{%s}'" "${pin}" ;; sss) local threshold threshold=$(jose fmt -j- -Og t -o- <<< "${content}") clevis_luks_process_sss_pin "${content}" "${threshold}" ;; *) printf "unknown pin '%s'" "${P}" ;; esac } # clevis_luks_decode_pin_config() will receive a JWE and extract a pin config # from it. clevis_luks_decode_pin_config() { local jwe="${1}" local decoded if ! decoded=$(clevis_luks_decode_jwe "${jwe}"); then return 1 fi local P if ! P=$(jose fmt -j- -Og clevis -g pin -u- <<< "${decoded}"); then return 1 fi clevis_luks_print_pin_config "${P}" "${decoded}" } # clevis_luks_join_sss_cfg() will receive a list of configurations for a given # pin and returns it as list, in the format PIN [cfg1, cfg2, ..., cfgN]. clevis_luks_join_sss_cfg() { local pin="${1}" local cfg="${2}" cfg=$(echo "${cfg}" | tr -d "'" | sed -e 's/^,//') printf '"%s":[%s]' "${pin}" "${cfg}" } # clevis_luks_process_sss_pin() will receive a JWE with information on the sss # pin config, and also its associated threshold, and will extract the info. clevis_luks_process_sss_pin() { local jwe="${1}" local threshold="${2}" local sss_tang local sss_tpm2 local sss local pin_cfg local pin local cfg local coded for coded in $(jose fmt -j- -Og jwe -Af- <<< "${jwe}"| tr -d '"'); do if ! pin_cfg="$(clevis_luks_decode_pin_config "${coded}")"; then continue fi read -r pin cfg <<< "${pin_cfg}" case "${pin}" in tang) sss_tang="${sss_tang},${cfg}" ;; tpm2) sss_tpm2="${sss_tpm2},${cfg}" ;; sss) sss=$(echo "${cfg}" | tr -d "'") ;; esac done cfg= if [ -n "${sss_tang}" ]; then cfg=$(clevis_luks_join_sss_cfg "tang" "${sss_tang}") fi if [ -n "${sss_tpm2}" ]; then cfg="${cfg},"$(clevis_luks_join_sss_cfg "tpm2" "${sss_tpm2}") fi if [ -n "${sss}" ]; then cfg=$(printf '%s,"sss":%s' "${cfg}" "${sss}") fi # Remove possible leading comma. cfg=${cfg/#,/} pin=$(printf '{"t":%d,"pins":{%s}}' "${threshold}" "${cfg}") printf "sss '%s'" "${pin}" } # clevis_luks_read_pins_from_slot() will receive a given device and slot and # will then output its associated policy configuration. clevis_luks_read_pins_from_slot() { local DEV="${1}" local SLOT="${2}" local jwe if ! jwe=$(clevis_luks_read_slot "${DEV}" "${SLOT}" 2>/dev/null); then return 1 fi local cfg if ! cfg="$(clevis_luks_decode_pin_config "${jwe}")"; then return 1 fi printf "%s: %s\n" "${SLOT}" "${cfg}" } # clevis_luks_check_valid_key_or_keyfile() receives a devices and either a # passphrase or keyfile and then checks whether it is able to unlock the # device wih the received passphrase/keyfile. clevis_luks_check_valid_key_or_keyfile() { local DEV="${1}" local KEY="${2:-}" local KEYFILE="${3:-}" local SLT="${4:-}" [ -z "${DEV}" ] && return 1 [ -z "${KEYFILE}" ] && [ -z "${KEY}" ] && return 1 local extra_args extra_args="$([ -n "${SLT}" ] && printf -- '--key-slot %s' "${SLT}")" if [ -n "${KEYFILE}" ]; then cryptsetup open --test-passphrase "${DEV}" --key-file "${KEYFILE}" \ ${extra_args} return fi printf '%s' "${KEY}" | cryptsetup open --test-passphrase "${DEV}" \ ${extra_args} } # clevis_luks_unlock_device_by_slot() does the unlock of the device and slot # passed as parameters and returns the decoded passphrase. clevis_luks_unlock_device_by_slot() { local DEV="${1}" local SLT="${2}" [ -z "${DEV}" ] && return 1 [ -z "${SLT}" ] && return 1 local jwe passphrase if ! jwe="$(clevis_luks_read_slot "${DEV}" "${SLT}" 2>/dev/null)" \ || [ -z "${jwe}" ]; then return 1 fi if ! passphrase="$(printf '%s' "${jwe}" | clevis decrypt 2>/dev/null)" \ || [ -z "${passphrase}" ]; then return 1 fi clevis_luks_check_valid_key_or_keyfile "${DEV}" "${passphrase}" || return 1 printf '%s' "${passphrase}" } # clevis_luks_unlock_device() does the unlock of the device passed as # parameter and returns the decoded passphrase. clevis_luks_unlock_device() { local DEV="${1}" [ -z "${DEV}" ] && return 1 local used_slots if ! used_slots=$(clevis_luks_used_slots "${DEV}") \ || [ -z "${used_slots}" ]; then return 1 fi local slt pt for slt in ${used_slots}; do if ! pt=$(clevis_luks_unlock_device_by_slot "${DEV}" "${slt}") \ || [ -z "${pt}" ]; then continue fi printf '%s' "${pt}" return 0 done return 1 } # clevis_map_device() tries to map the device received as a parameter to a # block device. As per crypttab(5), we support /path/to/encrypted/blockdev # or UUID=. clevis_map_device() { local CDEV="${1}" if [[ "${CDEV}" == UUID=* ]]; then CDEV=/dev/disk/by-uuid/${CDEV#UUID=} fi if [[ "${CDEV}" == /* ]] && [ -b "${CDEV}" ]; then echo "${CDEV}" else # Invalid crypttab entry. return 1 fi } # clevis_is_luks_device_by_uuid_open() checks whether the LUKS device whose # UUID was passed as a parameter is already open. clevis_is_luks_device_by_uuid_open() { local dev_luks_uuid="${1}" [ -z "${dev_luks_uuid}" ] && return 1 dev_luks_uuid="$(echo "${dev_luks_uuid}" | sed -e 's/-//g')" test -b /dev/disk/by-id/dm-uuid-*"${dev_luks_uuid}"* } # clevis_devices_to_unlock() returns a list of devices to be unlocked, as per # the info from crypttab. clevis_devices_to_unlock() { local list_open_devices="${1:-}" [ ! -r /etc/crypttab ] && return 1 local dev clevis_devices crypt_device dev_uuid bindings clevis_devices= # Build list of devices to unlock. while read -r _ crypt_device _; do if ! dev=$(clevis_map_device "${crypt_device}") \ || [ -z "${dev}" ]; then # Unable to get the device - maybe it's not available, e.g. a # device on a volume group that has not been activated yet. # Add it to the list anyway, since it's a pending device. clevis_devices="${clevis_devices} ${dev}" continue fi # Check if this device has clevis bindings. if ! bindings="$(clevis luks list -d "${dev}" 2>/dev/null)" \ || [ -z "${bindings}" ]; then continue fi if [ -z "${list_open_devices}" ]; then # Check if this device is already open. dev_uuid="$(cryptsetup luksUUID "${dev}")" if clevis_is_luks_device_by_uuid_open "${dev_uuid}"; then continue fi fi clevis_devices="${clevis_devices} ${dev}" done < /etc/crypttab echo "${clevis_devices}" | sed -e 's/^ //' } # clevis_luks1_save_slot() works with LUKS1 devices and it saves a given JWE # to a specific device and slot. The last parameter indicates whether we # should overwrite existing metadata. clevis_luks1_save_slot() { local DEV="${1}" local SLOT="${2}" local JWE="${3}" local SHOULD_OVERWRITE="${4:-}" luksmeta test -d "${DEV}" || return 1 if luksmeta load -d "${DEV}" -s "${SLOT}" -u "${CLEVIS_UUID}" \ >/dev/null 2>/dev/null; then [ -z "${SHOULD_OVERWRITE}" ] && return 1 if ! luksmeta wipe -f -d "${DEV}" -s "${SLOT}" \ -u "${CLEVIS_UUID}"; then echo "Error wiping slot ${SLOT} from ${DEV}" >&2 return 1 fi fi if ! echo -n "${JWE}" | luksmeta save -d "${DEV}" -s "${SLOT}" \ -u "${CLEVIS_UUID}"; then echo "Error saving metadata to LUKSMeta slot ${SLOT} from ${DEV}" >&2 return 1 fi return 0 } # clevis_luks2_save_slot() works with LUKS2 devices and it saves a given JWE # to a specific device and slot. The last parameter indicates whether we # should overwrite existing metadata. clevis_luks2_save_slot() { local DEV="${1}" local SLOT="${2}" local TKN_ID="${3}" local JWE="${4}" local SHOULD_OVERWRITE="${5:-}" # Sanitize clevis LUKS2 tokens. Remove "orphan" clevis tokens, i.e., # tokens that are not linked to any key slots. local token array_len for token in $(cryptsetup luksDump "${DEV}" \ | sed -rn 's|^\s+([0-9]+): clevis|\1|p'); do # Let's check the length of the "keyslots" array. If zero, it means # no key slots are linked, which is a problem. if ! array_len=$(cryptsetup token export --token-id \ "${token}" "${DEV}" \ | jose fmt --json=- --get keyslots --array --length \ --output=-) || [ "${array_len}" -eq 0 ]; then # Remove bad token. cryptsetup token remove --token-id "${token}" "${DEV}" fi done if ! token="$(cryptsetup luksDump "${DEV}" \ | grep -E -B1 "^\s+Keyslot:\s+${SLOT}$" \ | sed -rn 's|^\s+([0-9]+): clevis|\1|p')"; then echo "Error trying to read token from LUKS2 device ${DEV}, slot ${SLOT}" >&2 return 1 fi if [ -n "${token}" ]; then [ -z "${SHOULD_OVERWRITE}" ] && return 1 if ! cryptsetup token remove --token-id "${token}" "${DEV}"; then echo "Error while removing token ${token} from LUKS2 device ${DEV}" >&2 return 1 fi fi if [ -n "${SHOULD_OVERWRITE}" ] && [ -n "${TKN_ID}" ]; then cryptsetup token remove --token-id "${TKN_ID}" "${DEV}" 2>/dev/null || : fi local metadata metadata=$(printf '{"type":"clevis","keyslots":["%s"],"jwe":%s}' \ "${SLOT}" "$(jose jwe fmt --input="${JWE}")") if ! printf '%s' "${metadata}" | cryptsetup token import \ $([ -n "${TKN_ID}" ] && printf -- '--token-id %s' "${TKN_ID}") \ "${DEV}"; then echo "Error saving metadata to LUKS2 header in device ${DEV}" >&2 return 1 fi return 0 } # clevis_luks_save_slot() saves a given JWE to a LUKS device+slot. It can also # overwrite existing metadata. clevis_luks_save_slot() { local DEV="${1}" local SLOT="${2}" local TKN_ID="${3}" local JWE="${4}" local SHOULD_OVERWRITE="${5:-}" if cryptsetup isLuks --type luks1 "${DEV}"; then clevis_luks1_save_slot "${DEV}" "${SLOT}" "${JWE}" \ "${SHOULD_OVERWRITE}" || return 1 elif cryptsetup isLuks --type luks2 "${DEV}"; then clevis_luks2_save_slot "${DEV}" "${SLOT}" "${TKN_ID}" "${JWE}" \ "${SHOULD_OVERWRITE}" || return 1 else return 1 fi return 0 } # clevis_luks1_backup_dev() backups the LUKSMeta slots from a LUKS device, # which can be restored with clevis_luks1_restore_dev(). clevis_luks1_backup_dev() { local DEV="${1}" local TMP="${2}" [ -z "${DEV}" ] && return 1 [ -z "${TMP}" ] && return 1 luksmeta test -d "${DEV}" || return 0 touch "${TMP}/initialized" local used_slots slt uuid jwe fname if ! used_slots=$(clevis_luks_used_slots "${DEV}") \ || [ -z "${used_slots}" ]; then return 1 fi for slt in ${used_slots}; do if ! uuid=$(luksmeta show -d "${DEV}" -s "${slt}") \ || [ -z "${uuid}" ]; then continue fi if ! jwe=$(luksmeta load -d "${DEV}" -s "${slt}") \ || [ -z "${jwe}" ]; then continue fi fname=$(printf "slot_%s_%s" "${slt}" "${uuid}") printf "%s" "${jwe}" > "${TMP}/${fname}" done return 0 } # clevis_luks1_restore_dev() takes care of restoring the LUKSMeta slots from # a LUKS device that was backup'ed by clevis_luks1_backup_dev(). clevis_luks1_restore_dev() { local DEV="${1}" local TMP="${2}" [ -z "${DEV}" ] && return 1 [ -z "${TMP}" ] && return 1 [ -e "${TMP}/initialized" ] || return 0 luksmeta test -d "${DEV}" || luksmeta init -f -d "${DEV}" local slt uuid jwe fname for fname in "${TMP}"/slot_*; do [ -f "${fname}" ] || break if ! slt=$(echo "${fname}" | cut -d '_' -f 2) \ || [ -z "${slt}" ]; then continue fi if ! uuid=$(echo "${fname}" | cut -d '_' -f 3) \ || [ -z "${uuid}" ]; then continue fi if ! jwe=$(cat "${fname}") || [ -z "${jwe}" ]; then continue fi if ! clevis_luks1_save_slot "${DEV}" "${slt}" \ "${jwe}" "overwrite"; then echo "Error restoring LUKSmeta slot ${slt} from ${DEV}" >&2 return 1 fi done return 0 } # clevis_luks_backup_dev() backups a particular LUKS device, which can then # be restored with clevis_luks_restore_dev(). clevis_luks_backup_dev() { local DEV="${1}" local TMP="${2}" [ -z "${DEV}" ] && return 1 [ -z "${TMP}" ] && return 1 printf '%s' "${DEV}" > "${TMP}/device" printf '%s' "${DEV}" > "${TMP}/device" local HDR HDR="${TMP}/$(basename "${DEV}").header" if ! cryptsetup luksHeaderBackup "${DEV}" --batch-mode \ --header-backup-file "${HDR}"; then echo "Error backing up LUKS header from ${DEV}" >&2 return 1 fi # If LUKS1, we need to manually back up (and later restore) the # LUKSmeta slots. For LUKS2, simply saving the header also saves # the associated tokens. if cryptsetup isLuks --type luks1 "${DEV}"; then if ! clevis_luks1_backup_dev "${DEV}" "${TMP}"; then return 1 fi fi return 0 } # clevis_luks_restore_dev() restores a given device that was backup'ed by # clevis_luks_backup_dev(). clevis_luks_restore_dev() { local TMP="${1}" [ -z "${TMP}" ] && return 1 [ -r "${TMP}"/device ] || return 1 local DEV DEV="$(cat "${TMP}"/device)" local HDR HDR="${TMP}/$(basename "${DEV}").header" if [ ! -e "${HDR}" ]; then echo "LUKS header backup does not exist" >&2 return 1 fi if ! cryptsetup luksHeaderRestore "${DEV}" --batch-mode \ --header-backup-file "${HDR}"; then echo "Error restoring LUKS header from ${DEV}" >&2 return 1 fi # If LUKS1, we need to manually back up (and later restore) the # LUKSmeta slots. For LUKS2, simply saving the header also saves # the associated tokens. if cryptsetup isLuks --type luks1 "${DEV}"; then if ! clevis_luks1_restore_dev "${DEV}" "${TMP}"; then return 1 fi fi return 0 } # clevis_luks_get_existing_key() may try to recover a valid password from # existing bindings and additionally prompt the user for the passphrase. clevis_luks_get_existing_key() { local DEV="${1}" local PROMPT="${2}" local RECOVER="${3:-}" [ -z "${DEV}" ] && return 1 local pt if [ -n "${RECOVER}" ] && pt="$(clevis_luks_unlock_device "${DEV}")" \ && [ -n "${pt}" ]; then printf '%s' "${pt}" return 0 fi # Let's prompt the user for the password. read -r -s -p "${PROMPT}" pt; echo >&2 # Check if key is valid. clevis_luks_check_valid_key_or_keyfile "${DEV}" "${pt}" || return 1 printf '%s' "${pt}" } # clevis_luks_luksmeta_sync_fix() makes sure LUKSmeta slots are sync'ed with # cryptsetup, in order to prevent issues when saving clevis metadata. clevis_luks_luksmeta_sync_fix() { local DEV="${DEV}" [ -z "${DEV}" ] && return 1 # This applies only to LUKS1 devices. cryptsetup isLuks --type luks1 "${DEV}" || return 0 # No issues if the LUKSmeta metadata is not initialized. luksmeta test -d "${DEV}" || return 0 local first_free_slot if ! first_free_slot=$(clevis_luks_first_free_slot "${DEV}") \ || [ -z "${first_free_slot}" ]; then echo "There are possibly no free slots in ${DEV}" >&2 return 1 fi # In certain circumstances, we may have LUKSMeta slots "not in sync" with # cryptsetup, which means we will try to save LUKSMeta metadata over an # already used or partially used slot -- github issue #70. # If that is the case, let's wipe the LUKSMeta slot here prior to using # the LUKSMeta slot. local lmeta_slot lmeta_status lmeta_uuid lmeta_slot="$(luksmeta show -d "${DEV}" | grep "^${first_free_slot}")" # 1 active cb6e8904-81ff-40da-a84a-07ab9ab5715e # 2 inactive cb6e8904-81ff-40da-a84a-07ab9ab5715e lmeta_status="$(echo "${lmeta_slot}" | awk '{print $2}')" [ "${lmeta_status}" != 'inactive' ] && return 0 lmeta_uuid="$(echo "${lmeta_slot}" | awk '{print $3}')" [ "${lmeta_uuid}" != "${CLEVIS_UUID}" ] && return 0 luksmeta wipe -f -d "${DEV}" -s "${first_free_slot}" } # clevis_luks_add_key() adds a new key to a key slot. clevis_luks_add_key() { local DEV="${1}" local SLT="${2}" local NEWKEY="${3}" local KEY="${4}" local KEYFILE="${5:-}" [ -z "${DEV}" ] && return 1 [ -z "${NEWKEY}" ] && return 1 [ -z "${KEY}" ] && [ -z "${KEYFILE}" ] && return 1 local extra_args='' input input="$(printf '%s\n%s' "${KEY}" "${NEWKEY}")" if [ -n "${KEYFILE}" ]; then extra_args="$(printf -- '--key-file %s' "${KEYFILE}")" input="$(printf '%s' "${NEWKEY}")" fi printf '%s' "${input}" | cryptsetup luksAddKey --batch-mode \ --key-slot "${SLT}" \ "${DEV}" \ ${extra_args} } # clevis_luks_update_key() will update a key slot with a new key. clevis_luks_update_key() { local DEV="${1}" local SLT="${2}" local NEWKEY="${3}" local KEY="${4}" local KEYFILE="${5:-}" [ -z "${DEV}" ] && return 1 [ -z "${NEWKEY}" ] && return 1 # Update the key slot with the new key. If we have the key for this slot, # the change happens in-place. Otherwise, we kill the slot and re-add it. local in_place clevis_luks_check_valid_key_or_keyfile "${DEV}" \ "${KEY}" "${KEYFILE}" \ "${SLT}" 2>/dev/null \ && in_place=true local input extra_args= input="$(printf '%s\n%s' "${KEY}" "${NEWKEY}")" if [ -n "${KEYFILE}" ]; then extra_args="$(printf -- '--key-file %s' "${KEYFILE}")" input="$(printf '%s' "${NEWKEY}")" fi if [ -n "${in_place}" ]; then printf '%s' "${input}" | cryptsetup luksChangeKey "${DEV}" \ --key-slot "${SLT}" \ --batch-mode ${extra_args} return fi if ! printf '%s' "${input}" | cryptsetup luksKillSlot "${DEV}" \ "${SLT}" \ ${extra_args}; then echo "Error wiping slot ${SLT} from ${DEV}" >&2 return 1 fi clevis_luks_add_key "${DEV}" "${SLT}" "${NEWKEY}" "${KEY}" "${KEYFILE}" } # clevis_luks_save_key_to_slot() will save a new key to a slot. It can either # add a new key to a slot or updating an already used slot. clevis_luks_save_key_to_slot() { local DEV="${1}" local SLT="${2}" local NEWKEY="${3}" local KEY="${4}" local KEYFILE="${5:-}" local OVERWRITE="${6:-}" [ -z "${DEV}" ] && return 1 [ -z "${SLT}" ] && return 1 [ -z "${NEWKEY}" ] && return 1 # Make sure LUKSmeta slots are in sync with cryptsetup, to avoid the # problem reported in github issue #70. Applies to LUKS1 only. clevis_luks_luksmeta_sync_fix "${DEV}" # Let's check if we are adding a new key or updating an existing one. local update update="$(clevis_luks_used_slots "${DEV}" | grep "^${SLT}$")" if [ -n "${update}" ]; then # Replace an existing key. [ -n "${OVERWRITE}" ] || return 1 clevis_luks_update_key "${DEV}" "${SLT}" \ "${NEWKEY}" "${KEY}" "${KEYFILE}" return fi # Add a new key. clevis_luks_add_key "${DEV}" "${SLT}" \ "${NEWKEY}" "${KEY}" "${KEYFILE}" } # clevis_luks_generate_key() generates a new key for use with clevis. clevis_luks_generate_key() { local DEV="${1}" [ -z "${DEV}" ] && return 1 local dump filter bits dump=$(cryptsetup luksDump "${DEV}") if cryptsetup isLuks --type luks1 "${DEV}"; then filter="$(echo "${dump}" | sed -rn 's|MK bits:[ \t]*([0-9]+)|\1|p')" elif cryptsetup isLuks --type luks2 "${DEV}"; then filter="$(echo -n "${dump}" | \ sed -rn 's|^\s+Key:\s+([0-9]+) bits\s*$|\1|p')" else return 1 fi bits="$(echo -n "${filter}" | sort -n | tail -n 1)" pwmake "${bits}" } # clevis_luks_token_id_by_slot() returns the token ID linked to a # particular LUKS2 key slot. clevis_luks_token_id_by_slot() { local DEV="${1}" local SLT="${2}" [ -z "${DEV}" ] && return 1 [ -z "${SLT}" ] && return 1 cryptsetup isLuks --type luks1 "${DEV}" && echo && return local tkn_id tkn_id="$(cryptsetup luksDump "${DEV}" \ | grep -E -B1 "^\s+Keyslot:\s+${SLT}$" \ | sed -rn 's|^\s+([0-9]+): clevis|\1|p')" printf '%s' "${tkn_id}" } # clevis_luks_cleanup() removes the temporary directory used to store the data # relevant to device backup and restore. clevis_luks_cleanup() { [ -z "${CLEVIS_TMP_DIR}" ] && return 0 [ -d "${CLEVIS_TMP_DIR}" ] || return 0 if ! rm -rf "${CLEVIS_TMP_DIR}"; then echo "Deleting temporary files failed!" >&2 echo "You may need to clean up '${CLEVIS_TMP_DIR}'" >&2 exit 1 fi unset CLEVIS_TMP_DIR } # clevis_luks_first_free_slot() returns the first key slot that is available # in a LUKS device. clevis_luks_first_free_slot() { local DEV="${1}" [ -z "${DEV}" ] && return 1 local first_free_slot if cryptsetup isLuks --type luks1 "${DEV}"; then first_free_slot=$(cryptsetup luksDump "${DEV}" \ | sed -rn 's|^Key Slot ([0-7]): DISABLED$|\1|p' \ | sed -n 1p) elif cryptsetup isLuks --type luks2 "${DEV}"; then local used_slots slt used_slots="$(clevis_luks_used_slots "${DEV}")" for slt in $(seq 0 31); do if ! echo "${used_slots}" | grep -q "^${slt}$"; then first_free_slot="${slt}" break fi done else echo "Unsupported device ${DEV}" >&2 return 1 fi echo "${first_free_slot}" } # clevis_luks_do_bind() creates or updates a particular binding. clevis_luks_do_bind() { local DEV="${1}" local SLT="${2}" local TKN_ID="${3}" local PIN="${4}" local CFG="${5}" local YES="${6:-}" local OVERWRITE="${7:-}" local KEY="${8:-}" local KEYFILE="${9:-}" [ -z "${DEV}" ] && return 1 [ -z "${PIN}" ] && return 1 [ -z "${CFG}" ] && return 1 if ! clevis_luks_check_valid_key_or_keyfile "${DEV}" \ "${KEY}" \ "${KEYFILE}" \ && ! KEY="$(clevis_luks_get_existing_key "${DEV}" \ "Enter existing LUKS password: " \ "recover")" || [ -z "${KEY}" ]; then return 1 fi local newkey jwe if ! newkey="$(clevis_luks_generate_key "${DEV}")" \ || [ -z "${newkey}" ]; then echo "Unable to generate a new key" >&2 return 1 fi # Encrypt the new key. jwe="$(printf '%s' "${newkey}" | clevis encrypt "${PIN}" "${CFG}" ${YES})" # We can proceed to binding, after backing up the LUKS header and # metadata. local CLEVIS_TMP_DIR if ! CLEVIS_TMP_DIR="$(mktemp -d)" || [ -z "${CLEVIS_TMP_DIR}" ]; then echo "Unable to create a a temporary dir for device backup/restore" >&2 return 1 fi export CLEVIS_TMP_DIR trap 'clevis_luks_cleanup' EXIT # Backup LUKS header. if ! clevis_luks_backup_dev "${DEV}" "${CLEVIS_TMP_DIR}"; then echo "Unable to back up LUKS header from ${DEV}" >&2 return 1 fi if [ -z "${SLT}" ] && ! SLT=$(clevis_luks_first_free_slot "${DEV}") \ || [ -z "${SLT}" ]; then echo "Unable to find a free slot in ${DEV}" >&2 return 1 fi [ -z "${TKN_ID}" ] && ! TKN_ID="$(clevis_luks_token_id_by_slot "${DEV}" \ "${SLT}")" && return 1 if ! clevis_luks_save_key_to_slot "${DEV}" "${SLT}" \ "${newkey}" "${KEY}" "${KEYFILE}" \ "${OVERWRITE}"; then echo "Unable to save/update key slot; operation cancelled" >&2 clevis_luks_restore_dev "${CLEVIS_TMP_DIR}" || : rm -rf "${CLEVIS_TMP_DIR}" return 1 fi if ! clevis_luks_save_slot "${DEV}" "${SLT}" "${tkn_id}" \ "${jwe}" "${OVERWRITE}"; then echo "Unable to update metadata; operation cancelled" >&2 clevis_luks_restore_dev "${CLEVIS_TMP_DIR}" || : rm -rf "${CLEVIS_TMP_DIR}" return 1 fi clevis_luks_cleanup trap - EXIT return 0 }