#!/usr/bin/env bash CUR_IFS=$IFS script_name="$(basename $(realpath $0))" script_dir="$(dirname $(realpath $0))" conf_dir="${script_dir}/conf" snippet_dir="${script_dir}/snippets" report_dir="${script_dir}/reports" declare -a unsorted_website_arr declare -a website_arr declare -a unsorted_account_arr declare -a account_arr declare -a unsorted_selected_user_arr declare -a selected_user_arr LOCK_DIR="/tmp/${script_name%%.*}.LOCK" log_file="${LOCK_DIR}/${script_name%%.*}.log" run_date=$(date +%Y-%m-%d-%H%M) # ============= # --- Some functions # ============= usage() { [[ -n "$1" ]] && error "$1" [[ $terminal ]] && echo -e " \033[1mUsage:\033[m $(basename $0) -s \033[1mDescription\033[m READ-ONLY diagnostic for the 'Cannot decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.' and 'OCA\\Encryption\\Exceptions\\MultiKeyDecryptException' errors that recover_bad_signature.sh reports as READ_ERROR (and restore_bad_signature.sh can separately hit as WRITE_ERROR, even for files that read back fine during recovery) and neither script can fix - these are NOT signature problems, they are Server-Side Encryption KEY-MATERIAL problems (a per-user 'share key' for a file is either missing on disk or fails to decrypt with that user's private key). \033[1mThis script accepts either of two report types as input, and\033[m \033[1mevaluates a different row/status from each:\033[m \033[1mrecovery_*.tsv\033[m report (from recover_bad_signature.sh) -> evaluates every row whose validation is \033[1mREAD_ERROR\033[m \033[1mrestore_*.tsv\033[m report (from restore_bad_signature.sh) -> evaluates every row whose status is \033[1mWRITE_ERROR\033[m This script changes NOTHING. It never decrypts a file, never touches 'encryption_skip_signature_check', and never writes anywhere except its own report file. It only: 1. Reads the chosen report and picks out the matching rows: - from a \033[1mrecovery_*.tsv\033[m report: every row whose validation is \033[1mREAD_ERROR\033[m - from a \033[1mrestore_*.tsv\033[m report: every row whose status is \033[1mWRITE_ERROR\033[m ...and, in both cases, whose detail text matches one of the two known key-material error signatures above. 2. For each such file, resolves - via Nextcloud's normal, read-only Files API (\$node->getOwner(), \$folder->getById()) - who currently owns the file, i.e. whether the affected account is the owner or only a share recipient. 3. Checks, directly and only via plain filesystem existence tests (no content is read), whether the expected Server-Side-Encryption key files are present on disk: - the affected user's OWN private/public keypair (files_encryption/OC_DEFAULT_MODULE/.{private,public}Key) - the file's 'shareKey' entry for that user, under the file's OWNER's files_encryption tree 4. Classifies every diagnosed file into one of: NO_OWN_KEYPAIR - the affected user has no encryption keypair on disk at all. This explains EVERY file failing for that user, not just this one - the account's own key material is broken/missing, independent of any single file or share. OWNER_UNRESOLVED - the file's current owner could not be determined via the Files API (e.g. the file/share no longer exists as such). NO_SHAREKEY - the owner could be resolved, and that user HAS their own keypair, but no shareKey file exists for them on this specific file. KEY_MATERIAL_PRESENT - both the user's keypair and this file's shareKey exist on disk. The failure is then a genuine decrypt/mismatch problem for this specific key (as with the MultiKeyDecryptException case), not a missing-key problem. Read the report's per-account summary first: if an account shows NO_OWN_KEYPAIR for (almost) all its files, that account's own encryption keypair is the thing to fix/restore, not the individual files. \033[1mOptions\033[m -s The site of the nextcloud instance. \033[1mExample:\033[m $(basename $0) -s cloud-02.oopen.de " clean_up 1 } clean_up() { # Perform program exit housekeeping [[ -n "$diag_php_file" ]] && rm -f "$diag_php_file" 2> /dev/null rm -rf "$LOCK_DIR" blank_line exit $1 } is_number() { return $(test ! -z "${1##*[!0-9]*}" > /dev/null 2>&1); } echononl(){ if $terminal ; then echo X\\c > /tmp/shprompt$$ if [ `wc -c /tmp/shprompt$$ | awk '{print $1}'` -eq 1 ]; then echo -e -n "$*\\c" 1>&2 else echo -e -n "$*" 1>&2 fi rm /tmp/shprompt$$ fi } echo_done() { if $terminal ; then echo -e "\033[75G[ \033[32mdone\033[m ]" fi } echo_ok() { if $terminal ; then echo -e "\033[75G[ \033[32mok\033[m ]" fi } echo_warning() { if $terminal ; then echo -e "\033[75G[ \033[33m\033[1mwarn\033[m ]" fi } echo_failed(){ if $terminal ; then echo -e "\033[75G[ \033[1;31mfailed\033[m ]" fi } echo_skipped() { if $terminal ; then echo -e "\033[75G[ \033[37mskipped\033[m ]" fi } fatal (){ echo "" echo "" if $terminal ; then echo -e " [ \033[31m\033[1mFatal\033[m ]: \033[37m\033[1m$*\033[m" echo "" echo -e " \033[31m\033[1mScript will be interrupted..\033[m!" else echo " [ Fatal ]: $*" echo "" echo " Script was terminated...." fi clean_up 1 } error(){ echo "" if $terminal ; then echo -e " [ \033[31m\033[1mError\033[m ]: $*" else echo " [ Error ]: $*" fi echo "" } warn (){ if $terminal ; then echo "" echo -e " [ \033[33m\033[1mWarning\033[m ]: $*" echo "" fi } info (){ if $terminal ; then echo "" echo -e " [ \033[32m\033[1mInfo\033[m ]: $*" echo "" fi } # - Remove leading/trailling whitespaces # - trim() { local var="$*" var="${var#"${var%%[![:space:]]*}"}" # remove leading whitespace characters var="${var%"${var##*[![:space:]]}"}" # remove trailing whitespace characters echo -n "$var" } ## - Check if a given array (parameter 2) contains a given string (parameter 1) ## - containsElement () { local e for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done return 1 } ## - Percentage (1 decimal) of parameter 1 (part) against parameter 2 (total) ## - Returns "0.0" if total is empty, zero or non-numeric. ## - calc_percent() { local _part="$1" local _total="$2" if [[ -z "$_total" ]] || ! [[ "$_total" =~ ^[0-9]+$ ]] || [[ "$_total" -eq 0 ]] ; then echo "0.0" else awk -v b="$_part" -v t="$_total" 'BEGIN { printf "%.1f", (b/t)*100 }' fi } blank_line() { if $terminal ; then echo "" fi } # - Running in a terminal? # - if [[ -t 1 ]] ; then terminal=true else terminal=false fi # - This script needs root privileges (reads other users' key files # - under the Nextcloud data directory, 'su' into the webserver user # - to resolve file ownership through the normal Files API). # - if [[ "$(id -u)" -ne 0 ]] ; then fatal "This script must be run as root (it needs to read the Nextcloud data directory and 'su' into the webserver user). Please re-run as root, e.g. via sudo." fi # ---------- # - Jobhandling # ---------- if pgrep -f "$(basename $0)" | grep -q -v $$ ; then msg="A previos instance of script \"`basename $0`\" seems already be running." echo "" if $terminal ; then echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg" echo "" echo -e " \033[31m\033[1mScript was interupted\033[m!" else echo " [ Fatal ]: $msg" echo "" echo " Script was interupted!" fi echo exit 1 else if [[ -d "$LOCK_DIR" ]] ; then rm -rf "$LOCK_DIR" 2> /dev/null fi fi # - If job already runs, stop execution.. # - if mkdir "$LOCK_DIR" 2> /dev/null ; then # - Remove lockdir when the script finishes, or when it receives a signal # - trap clean_up SIGHUP SIGINT SIGTERM else msg="A previos instance of script \"`basename $0`\" seems already be running." echo "" if $terminal ; then echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg" echo "" echo -e " \033[31m\033[1mScript was interupted\033[m!" else echo " [ Fatal ]: $msg" echo "" echo " Script was interupted!" fi echo exit 1 fi # ------------- # - Read in Commandline arguments # ------------- while getopts hs: opt ; do case $opt in h) usage ;; s) WEBSITE=$OPTARG ;; \?) usage esac done if [[ -z "$WEBSITE" ]] ; then while IFS='' read -r -d '' _conf_file ; do source $_conf_file if [[ -n "$WEBSITE" ]] ; then unsorted_website_arr+=("${WEBSITE}:$_conf_file") fi WEBSITE="" done < <(find "${conf_dir}" -maxdepth 1 -type f -name "*.conf" -print0) # - Sort array # - IFS=$'\n' website_arr=($(sort <<<"${unsorted_website_arr[*]}")) # Which cloud instance (website) would you like to update # source ${snippet_dir}/get-cloud-instance-to-update.sh else while IFS='' read -r -d '' _conf_file ; do if $(grep -E -q "WEBSITE=\"?${WEBSITE}\"?" ${_conf_file} 2> /dev/null) ; then conf_file="${_conf_file}" break fi done < <(find "${conf_dir}" -maxdepth 1 -type f -name "*.conf" -print0) fi # - Reset IFS # - IFS=$CUR_IFS DEFAULT_SRC_BASE_DIR="/usr/local/src/nextcloud" DEFAULT_HTTP_USER="www-data" DEFAULT_HTTP_GROUP="www-data" DEFAULT_PHP_ENGINE='FPM' blank_line echononl " Include Configuration file '$(basename "${conf_file}")'.." if [[ ! -f $conf_file ]]; then echo_skipped fatal "Missing configuration file '$conf_file'." else source $conf_file echo_ok fi DEFAULT_WEB_BASE_DIR="/var/www/${WEBSITE}" [[ -n "$WEB_BASE_DIR" ]] || WEB_BASE_DIR=$DEFAULT_WEB_BASE_DIR if [[ ! -d ${WEB_BASE_DIR} ]] ; then fatal "Web base directory '$WEB_BASE_DIR' not found!" fi DATA_DIR="$(realpath ${WEB_BASE_DIR}/data)" [[ -n "$PHP_ENGINE" ]] || PHP_ENGINE=$DEFAULT_PHP_ENGINE INSTALL_DIR="$(realpath ${WEB_BASE_DIR}/nextcloud)" CURRENT_VERSION="$(basename $INSTALL_DIR | cut -d"-" -f2)" # ============= # --- Some # ============= if $terminal ; then echo "" echo -e "\033[32m-----\033[m" echo -e "Diagnose Server-Side-Encryption \033[1mkey-material\033[m problems on system \033[1m${WEB_BASE_DIR}\033[m" echo -e "\033[1m READ-ONLY: this script never decrypts anything, never writes to Nextcloud storage and never touches 'encryption_skip_signature_check'. It only inspects, on disk and via the normal Files API, whether the expected encryption key files are present for accounts/files that recover_bad_signature.sh or restore_bad_signature.sh could not process (not 'Bad Signature').\033[m" echo "" echo -e " \033[1mrecovery_*.tsv\033[m report -> evaluates \033[1mREAD_ERROR\033[m rows" echo -e " \033[1mrestore_*.tsv\033[m report -> evaluates \033[1mWRITE_ERROR\033[m rows" echo -e "\033[32m-----\033[m" fi # ============= # --- Some checks # ============= DEFAULT_HTTP_USER="www-data" DEFAULT_HTTP_GROUP="www-data" NGINX_IS_ENABLED=false APACHE2_IS_ENABLED=false systemd=$(which systemd) systemctl=$(which systemctl) # Get Webservice environment as IS_HTTPD_RUNNING, HTTP_USER, HTTP_GROUP.. # source ${snippet_dir}/get-webservice-environment.sh # Check PHP Version # source ${snippet_dir}/get-php-major-version.sh # Get full qualified PHP command # source ${snippet_dir}/get-path-of-php-command.sh if [[ ! -x "$PHP_BIN" ]]; then fatal "No PHP binary found!" fi # ============= # --- Determine available reports to diagnose from - either a # --- recover_bad_signature.sh report (recovery_*.tsv, READ_ERROR rows) # --- or a restore_bad_signature.sh report (restore_*.tsv, WRITE_ERROR # --- rows) - both can carry the same key-material error signatures, # --- just at a different step (read vs. write). # ============= mapfile -t _available_reports < <(ls -t "${report_dir}"/recovery_*.tsv "${report_dir}"/restore_*.tsv 2> /dev/null) if [[ ${#_available_reports[@]} -eq 0 ]] ; then fatal "No recovery_*.tsv or restore_*.tsv report files found in '${report_dir}'. Run recover_bad_signature.sh (and/or restore_bad_signature.sh) first." fi blank_line if $terminal ; then echo -e "\033[37m\033[1mAvailable reports (newest first, recovery_*.tsv and restore_*.tsv)\033[m" echo "" _i=1 for _f in "${_available_reports[@]}" ; do printf " [%2d] %s\n" "$_i" "$(basename "$_f")" (( _i++ )) done echo "" fi echo -n " Select report by number: " read _report_choice if ! [[ "$_report_choice" =~ ^[0-9]+$ ]] || [[ "$_report_choice" -lt 1 ]] || [[ "$_report_choice" -gt ${#_available_reports[@]} ]] ; then fatal "Invalid selection '$_report_choice'." fi chosen_report="${_available_reports[$((_report_choice-1))]}" # ============= # --- Extract "userpathdetail" for every row whose detail # --- text matches one of the two known key-material error signatures # --- - a recovery report's READ_ERROR rows (6 tab-separated fields, # --- validation in $5) as well as a restore report's WRITE_ERROR rows # --- (4 tab-separated fields, status in $3) both match: the same # --- underlying key-material problem can surface at either step. Same # --- section-header state machine as restore_bad_signature.sh uses # --- for VALID rows. # ============= key_error_entries_file="${LOCK_DIR}/key_error_entries.tsv" awk -F'\t' ' /^-+$/ { if (state == 0) { state = 1 } else if (state == 2) { state = 0 } next } { if (state == 1) { user = $0; state = 2; next } if (NF == 6 && $1 != "path" && $5 == "READ_ERROR") { if (index($6, "probably this is a shared file") > 0 || index($6, "MultiKeyDecryptException") > 0) { print user "\t" $1 "\t" $6 } } else if (NF == 4 && $1 != "path" && $3 == "WRITE_ERROR") { if (index($4, "probably this is a shared file") > 0 || index($4, "MultiKeyDecryptException") > 0) { print user "\t" $1 "\t" $4 } } } ' "$chosen_report" > "$key_error_entries_file" unsorted_account_arr=($(cut -f1 "$key_error_entries_file" | sort -u)) IFS=$'\n' account_arr=($(sort <<<"${unsorted_account_arr[*]}")) IFS=$CUR_IFS if [[ ${#account_arr[@]} -eq 0 ]] ; then fatal "No key-material READ_ERROR/WRITE_ERROR entries ('...probably this is a shared file...' or 'MultiKeyDecryptException') found in '$(basename "$chosen_report")' - nothing to diagnose." fi blank_line if $terminal ; then echo -e "\033[37m\033[1mAccounts with key-material READ_ERROR entries in this report\033[m" echo "" for _a in "${account_arr[@]}" ; do _a_count=$(awk -F'\t' -v u="$_a" '$1==u' "$key_error_entries_file" | wc -l) echo " - $_a (${_a_count})" done echo "" fi echo -n " Account(s) to diagnose (blank separated, or 'all'): " read _input while true ; do IFS=' ' read -r -a unsorted_selected_user_arr <<< "$(trim "$_input")" if [[ "${#unsorted_selected_user_arr[@]}" -eq 1 && "${unsorted_selected_user_arr[0],,}" = "all" ]] ; then selected_user_arr=("${account_arr[@]}") break fi _all_valid=true for _u in "${unsorted_selected_user_arr[@]}" ; do if ! containsElement "$_u" "${account_arr[@]}" ; then error "Unknown account '$_u' (not found in the selected report)." _all_valid=false fi done if $_all_valid && [[ "${#unsorted_selected_user_arr[@]}" -gt 0 ]] ; then IFS=$'\n' selected_user_arr=($(sort <<<"${unsorted_selected_user_arr[*]}")) IFS=$CUR_IFS break fi echo -n " Account(s) to diagnose (blank separated, or 'all'): " read _input done mkdir -p "$report_dir" 2> /dev/null diag_report_file="${report_dir}/diagnose_share_key_${WEBSITE}_${run_date}.tsv" if $terminal ; then echo "" echo -e "\033[1;32mStarting key-material diagnosis for \033[1;37m${WEBSITE}\033[m" echo "" echo -e " Cloud instance..........................: $WEBSITE" echo -e " Recovery report used....................: $(basename "$chosen_report")" echo -e " Account(s) to diagnose..................: \033[33m${selected_user_arr[*]}\033[m" echo -e " Diagnose report..........................: reports/$(basename "${diag_report_file}")" echo "" info "Read-only - nothing is decrypted, written or changed anywhere." fi { echo "==================================================================" echo " Share-Key-Diagnose ${WEBSITE} ${run_date}" echo "==================================================================" echo "" echo " Basis-Report (Recovery): $(basename "$chosen_report")" echo " Diagnostiziert werden alle READ_ERROR-Eintraege mit den Fehlerbildern" echo " 'probably this is a shared file' und 'MultiKeyDecryptException'." echo " READ-ONLY: nichts wird entschluesselt, geschrieben oder veraendert." echo "" printf 'path\towner\thas_own_keypair\thas_filekey\thas_sharekey\tclassification\toriginal_error\n' } > "$diag_report_file" # ============= # --- Write the embedded PHP diagnosis helper (read-only: resolves the # --- current owner of each file via the normal Files API) # ============= diag_php_file="${INSTALL_DIR}/.diagnose_share_key_$$.php" cat > "$diag_php_file" <<'PHP_DIAG_EOF' * listFile: one path per line */ error_reporting(E_ALL); ini_set('display_errors', '1'); define('OC_CONSOLE', 1); $oldWorkingDir = getcwd(); if ($oldWorkingDir === false) { fwrite(STDERR, "Konnte aktuelles Arbeitsverzeichnis nicht ermitteln. Bitte mit absolutem Pfad aufrufen.\n"); exit(1); } chdir(__DIR__); require_once __DIR__ . '/lib/base.php'; chdir($oldWorkingDir); if (function_exists('posix_getuid') && posix_getuid() === 0) { fwrite(STDERR, "Bitte NICHT als root ausfuehren, sondern als Webserver-User.\n"); exit(1); } try { $uid = $argv[1] ?? null; $listFile = $argv[2] ?? null; if ($uid === null || $listFile === null) { fwrite(STDERR, "Usage: php diagnose_share_key.php \n"); exit(1); } if (!is_file($listFile)) { fwrite(STDERR, "Liste nicht gefunden: $listFile\n"); exit(1); } $rootFolder = \OC::$server->get(\OCP\Files\IRootFolder::class); \OC_Util::setupFS($uid); $lines = file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if ($lines === false) { $lines = []; } echo "path\towner\townerpath\tstatus\tdetail\n"; foreach ($lines as $path) { if ($path === '') { continue; } try { $node = $rootFolder->get($path); } catch (\Throwable $e) { $msg = str_replace(["\t", "\n", "\r"], ' ', $e->getMessage()); echo "$path\t?\t?\tUNRESOLVED\t" . get_class($e) . ": $msg\n"; flush(); continue; } $ownerUid = null; try { $owner = $node->getOwner(); $ownerUid = $owner ? $owner->getUID() : null; } catch (\Throwable $e) { $ownerUid = null; } if ($ownerUid === null) { echo "$path\t?\t?\tOWNER_UNRESOLVED\tgetOwner() returned no owner\n"; flush(); continue; } $ownerPath = $path; if ($ownerUid !== $uid) { try { $ownerNodes = $rootFolder->getUserFolder($ownerUid)->getById($node->getId()); if (!empty($ownerNodes)) { $ownerPath = $ownerNodes[0]->getPath(); } else { echo "$path\t$ownerUid\t?\tOWNER_UNRESOLVED\tfile not found in owner's ($ownerUid) own tree via getById()\n"; flush(); continue; } } catch (\Throwable $e) { $msg = str_replace(["\t", "\n", "\r"], ' ', $e->getMessage()); echo "$path\t$ownerUid\t?\tOWNER_UNRESOLVED\t" . get_class($e) . ": $msg\n"; flush(); continue; } } echo "$path\t$ownerUid\t$ownerPath\tOK\t\n"; flush(); } } catch (\Throwable $e) { fwrite(STDERR, "\n!!! UNBEHANDELTE AUSNAHME !!!\n"); fwrite(STDERR, get_class($e) . ": " . $e->getMessage() . "\n"); fwrite(STDERR, "in " . $e->getFile() . ":" . $e->getLine() . "\n"); fwrite(STDERR, $e->getTraceAsString() . "\n"); exit(2); } PHP_DIAG_EOF chmod 644 "$diag_php_file" # ----- # - Main part of the script # ----- if $terminal ; then echo "" echo "" echo -e "\033[37m\033[1mMain part of the script\033[m" echo "" fi declare -i total_selected=0 declare -i total_no_own_keypair=0 declare -i total_owner_unresolved=0 declare -i total_no_sharekey=0 declare -i total_no_filekey=0 declare -i total_key_material_present=0 declare -i total_failed_users=0 for _user in "${selected_user_arr[@]}" ; do _sep_len=${#_user} [[ $_sep_len -lt 9 ]] && _sep_len=9 _sep_line="$(printf '%*s' "$_sep_len" '' | tr ' ' '-')" { echo "" echo "" echo "$_sep_line" echo "$_user" echo "$_sep_line" } >> "$diag_report_file" # - This account's own keypair - a single, cheap, bash-only check # - that alone explains an entire account failing on EVERY file, if # - it is missing. # - _user_privkey="${DATA_DIR}/${_user}/files_encryption/OC_DEFAULT_MODULE/${_user}.privateKey" _user_pubkey="${DATA_DIR}/${_user}/files_encryption/OC_DEFAULT_MODULE/${_user}.publicKey" if [[ -f "$_user_privkey" && -f "$_user_pubkey" ]] ; then _user_has_own_keypair=true else _user_has_own_keypair=false fi path_list_file="${LOCK_DIR}/paths_${_user}.txt" awk -F'\t' -v u="$_user" '$1==u { print $2 }' "$key_error_entries_file" > "$path_list_file" declare -i _user_selected=0 _user_selected=$(wc -l < "$path_list_file") owner_result_tsv="${LOCK_DIR}/owner_${_user}.tsv" echononl " Resolving ownership for account \033[1;37m${_user}\033[m (${_user_selected} files).." su -c "$PHP_BIN $diag_php_file $_user $path_list_file" -s /bin/bash $HTTP_USER > "$owner_result_tsv" 2> "$log_file" _rc=$? if [[ $_rc -ne 0 ]]; then echo_failed error "$(cat "$log_file")" echo " [ FEHLER bei der Eigentuemer-Aufloesung - siehe Server-Log ]" >> "$diag_report_file" (( total_failed_users++ )) continue fi echo_done declare -i _user_no_own_keypair=0 declare -i _user_owner_unresolved=0 declare -i _user_no_sharekey=0 declare -i _user_no_filekey=0 declare -i _user_key_material_present=0 declare -a _user_lines=() while IFS=$'\t' read -r _path _owner _ownerpath _status _odetail ; do [[ "$_path" = "path" ]] && continue [[ -z "$_path" ]] && continue _orig_error="$(awk -F'\t' -v u="$_user" -v p="$_path" '$1==u && $2==p { print $3; exit }' "$key_error_entries_file")" if ! $_user_has_own_keypair ; then (( _user_no_own_keypair++ )) _class="NO_OWN_KEYPAIR" _detail="account '${_user}' has no encryption keypair on disk at all (missing: ${_user_privkey} and/or ${_user_pubkey}) - this explains every file failing for this account, independent of sharing" _user_lines+=("${_path}"$'\t'"${_class}: ${_detail}") printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$_path" "${_owner:-?}" "no" "n/a" "n/a" "$_class" "$_orig_error" >> "$diag_report_file" continue fi if [[ "$_status" != "OK" ]] ; then (( _user_owner_unresolved++ )) _class="OWNER_UNRESOLVED" _detail="could not determine current owner/path via the Files API (${_status}: ${_odetail})" _user_lines+=("${_path}"$'\t'"${_class}: ${_detail}") printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$_path" "${_owner:-?}" "yes" "n/a" "n/a" "$_class" "$_orig_error" >> "$diag_report_file" continue fi _owner_rel="${_ownerpath#/${_owner}/files/}" if [[ "$_owner_rel" = "$_ownerpath" ]] ; then # Prefix did not match - path shape was not what we expected. (( _user_owner_unresolved++ )) _class="OWNER_UNRESOLVED" _detail="owner path '${_ownerpath}' did not have the expected '/${_owner}/files/...' shape" _user_lines+=("${_path}"$'\t'"${_class}: ${_detail}") printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$_path" "${_owner:-?}" "yes" "n/a" "n/a" "$_class" "$_orig_error" >> "$diag_report_file" continue fi # - NOTE: an earlier version of this script also required a # - standalone 'fileKey' file next to the shareKey and treated # - its absence as a failure (NO_FILEKEY). That was WRONG: in # - the default (non-legacy, non-masterkey) key storage mode, # - Nextcloud's Encryption module never writes a plain 'fileKey' # - file at all - the file key only ever exists ENCRYPTED, once # - per user with access, as '.shareKey'; getFileKey() # - reconstructs it by decrypting one of those. A uniform # - 'fileKey missing' result across many unrelated accounts (as # - seen in practice) is the tell that this check was testing # - for something that legitimately never exists here, not a # - real defect - so it is now purely informational and never # - drives the classification. # - _filekey_path="${DATA_DIR}/${_owner}/files_encryption/keys/files/${_owner_rel}/OC_DEFAULT_MODULE/fileKey" _sharekey_path="${DATA_DIR}/${_owner}/files_encryption/keys/files/${_owner_rel}/OC_DEFAULT_MODULE/${_user}.shareKey" if [[ -f "$_filekey_path" ]] ; then _has_filekey="yes" else _has_filekey="no (informational only - see note above; NOT used to classify)" fi if [[ -f "$_sharekey_path" ]] ; then _has_sharekey="yes" else _has_sharekey="no" fi if [[ "$_has_sharekey" = "no" ]] ; then (( _user_no_sharekey++ )) _class="NO_SHAREKEY" _detail="no shareKey exists for '${_user}' on this file (${_sharekey_path}) - '${_user}' was probably never (re-)granted a usable key for this file" else (( _user_key_material_present++ )) _class="KEY_MATERIAL_PRESENT" _detail="'${_user}.shareKey' exists on disk (${_sharekey_path}) - the failure is a genuine decrypt/mismatch problem: '${_user}'s current private key cannot decrypt this existing shareKey (owner: ${_owner}$([[ "$_owner" != "$_user" ]] && echo ", shared file"))" fi _user_lines+=("${_path}"$'\t'"${_class}: ${_detail}") printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$_path" "$_owner" "yes" "$_has_filekey" "$_has_sharekey" "$_class" "$_orig_error" >> "$diag_report_file" done < "$owner_result_tsv" if $terminal ; then echo -e " \033[1;37mAccount ${_user}\033[m" echo -e " Diagnostiziert.............................: ${_user_selected}" if [[ $_user_no_own_keypair -gt 0 ]] ; then echo -e " Kein eigenes Schluesselpaar (NO_OWN_KEYPAIR): \033[1;31m${_user_no_own_keypair}\033[m" fi [[ $_user_owner_unresolved -gt 0 ]] && echo -e " Eigentuemer nicht aufloesbar................: \033[33m${_user_owner_unresolved}\033[m" [[ $_user_no_filekey -gt 0 ]] && echo -e " Datei-Schluessel fehlt (NO_FILEKEY)........: \033[33m${_user_no_filekey}\033[m" [[ $_user_no_sharekey -gt 0 ]] && echo -e " Freigabe-Schluessel fehlt (NO_SHAREKEY)....: \033[33m${_user_no_sharekey}\033[m" [[ $_user_key_material_present -gt 0 ]] && echo -e " Schluessel vorhanden, Entschluesseln schlaegt fehl: \033[33m${_user_key_material_present}\033[m" echo "" fi { echo "" for _line in "${_user_lines[@]}" ; do printf ' %s\n' "$_line" done echo "" echo " Account ${_user}" echo " Diagnostiziert.............................: ${_user_selected}" [[ $_user_no_own_keypair -gt 0 ]] && echo " Kein eigenes Schluesselpaar (NO_OWN_KEYPAIR): ${_user_no_own_keypair}" [[ $_user_owner_unresolved -gt 0 ]] && echo " Eigentuemer nicht aufloesbar................: ${_user_owner_unresolved}" [[ $_user_no_filekey -gt 0 ]] && echo " Datei-Schluessel fehlt (NO_FILEKEY)........: ${_user_no_filekey}" [[ $_user_no_sharekey -gt 0 ]] && echo " Freigabe-Schluessel fehlt (NO_SHAREKEY)....: ${_user_no_sharekey}" [[ $_user_key_material_present -gt 0 ]] && echo " Schluessel vorhanden, Entschluesseln schlaegt fehl: ${_user_key_material_present}" } >> "$diag_report_file" (( total_selected += _user_selected )) (( total_no_own_keypair += _user_no_own_keypair )) (( total_owner_unresolved += _user_owner_unresolved )) (( total_no_sharekey += _user_no_sharekey )) (( total_no_filekey += _user_no_filekey )) (( total_key_material_present += _user_key_material_present )) unset _user_no_own_keypair _user_owner_unresolved _user_no_sharekey _user_no_filekey _user_key_material_present _user_lines done { echo "" echo "" echo "==================================================================" echo " Gesamtergebnis" echo "==================================================================" echo "" echo "Ausgewaehlte Accounts.......................: ${#selected_user_arr[@]}" [[ $total_failed_users -gt 0 ]] && echo "Accounts mit Diagnose-Fehler................: ${total_failed_users}" echo "Diagnostiziert insgesamt.....................: ${total_selected}" [[ $total_no_own_keypair -gt 0 ]] && echo "Kein eigenes Schluesselpaar (NO_OWN_KEYPAIR): ${total_no_own_keypair}" [[ $total_owner_unresolved -gt 0 ]] && echo "Eigentuemer nicht aufloesbar.................: ${total_owner_unresolved}" [[ $total_no_filekey -gt 0 ]] && echo "Datei-Schluessel fehlt (NO_FILEKEY).........: ${total_no_filekey}" [[ $total_no_sharekey -gt 0 ]] && echo "Freigabe-Schluessel fehlt (NO_SHAREKEY)......: ${total_no_sharekey}" [[ $total_key_material_present -gt 0 ]] && echo "Schluessel vorhanden, Entschluesseln schlaegt fehl: ${total_key_material_present}" echo "" echo "READ-ONLY: es wurde nichts entschluesselt, geschrieben oder veraendert." } >> "$diag_report_file" blank_line if $terminal ; then echo -e "\033[37m\033[1mErgebnis\033[m" echo "" echo -e " Ausgewaehlte Accounts.......................: ${#selected_user_arr[@]}" [[ $total_failed_users -gt 0 ]] && echo -e " Accounts mit Diagnose-Fehler................: \033[1;31m${total_failed_users}\033[m" echo -e " Diagnostiziert insgesamt.....................: ${total_selected}" [[ $total_no_own_keypair -gt 0 ]] && echo -e " Kein eigenes Schluesselpaar (NO_OWN_KEYPAIR): \033[1;31m${total_no_own_keypair}\033[m" [[ $total_owner_unresolved -gt 0 ]] && echo -e " Eigentuemer nicht aufloesbar.................: \033[33m${total_owner_unresolved}\033[m" [[ $total_no_filekey -gt 0 ]] && echo -e " Datei-Schluessel fehlt (NO_FILEKEY).........: \033[33m${total_no_filekey}\033[m" [[ $total_no_sharekey -gt 0 ]] && echo -e " Freigabe-Schluessel fehlt (NO_SHAREKEY)......: \033[33m${total_no_sharekey}\033[m" [[ $total_key_material_present -gt 0 ]] && echo -e " Schluessel vorhanden, Entschluesseln schlaegt fehl: \033[33m${total_key_material_present}\033[m" echo "" echo -e " Diagnose-Report...............................: reports/$(basename "${diag_report_file}")" echo "" info "Lies zuerst die Account-Zusammenfassung: 'NO_OWN_KEYPAIR' bei (fast) allen Dateien eines Accounts heisst, das eigene Schluesselpaar dieses Accounts ist das Problem - nicht die einzelnen Dateien." fi clean_up 0