#!/usr/bin/env bash # Shared Hash/Checksum Utilities # Sprint: CI/CD Enhancement - Script Consolidation # # Purpose: Cryptographic hash and checksum operations for CI/CD scripts # Usage: source "$(dirname "${BASH_SOURCE[0]}")/lib/hash-utils.sh" # Prevent multiple sourcing if [[ -n "${__STELLAOPS_HASH_UTILS_LOADED:-}" ]]; then return 0 fi export __STELLAOPS_HASH_UTILS_LOADED=1 # Source dependencies SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/logging.sh" 2>/dev/null || true source "${SCRIPT_DIR}/exit-codes.sh" 2>/dev/null || true # ============================================================================ # Hash Computation # ============================================================================ # Compute SHA-256 hash of a file compute_sha256() { local file="$1" if [[ ! -f "$file" ]]; then log_error "File not found: $file" return "${EXIT_NOT_FOUND:-4}" fi if command -v sha256sum >/dev/null 2>&1; then sha256sum "$file" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$file" | awk '{print $1}' elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 "$file" | awk '{print $NF}' else log_error "No SHA-256 tool available" return "${EXIT_MISSING_TOOL:-10}" fi } # Compute SHA-512 hash of a file compute_sha512() { local file="$1" if [[ ! -f "$file" ]]; then log_error "File not found: $file" return "${EXIT_NOT_FOUND:-4}" fi if command -v sha512sum >/dev/null 2>&1; then sha512sum "$file" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 512 "$file" | awk '{print $1}' elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha512 "$file" | awk '{print $NF}' else log_error "No SHA-512 tool available" return "${EXIT_MISSING_TOOL:-10}" fi } # Compute MD5 hash of a file (for compatibility, not security) compute_md5() { local file="$1" if [[ ! -f "$file" ]]; then log_error "File not found: $file" return "${EXIT_NOT_FOUND:-4}" fi if command -v md5sum >/dev/null 2>&1; then md5sum "$file" | awk '{print $1}' elif command -v md5 >/dev/null 2>&1; then md5 -q "$file" elif command -v openssl >/dev/null 2>&1; then openssl dgst -md5 "$file" | awk '{print $NF}' else log_error "No MD5 tool available" return "${EXIT_MISSING_TOOL:-10}" fi } # Compute hash of string compute_string_hash() { local string="$1" local algorithm="${2:-sha256}" case "$algorithm" in sha256) echo -n "$string" | sha256sum 2>/dev/null | awk '{print $1}' || \ echo -n "$string" | shasum -a 256 2>/dev/null | awk '{print $1}' ;; sha512) echo -n "$string" | sha512sum 2>/dev/null | awk '{print $1}' || \ echo -n "$string" | shasum -a 512 2>/dev/null | awk '{print $1}' ;; md5) echo -n "$string" | md5sum 2>/dev/null | awk '{print $1}' || \ echo -n "$string" | md5 2>/dev/null ;; *) log_error "Unknown algorithm: $algorithm" return "${EXIT_USAGE:-2}" ;; esac } # ============================================================================ # Checksum Files # ============================================================================ # Write checksum file for a single file write_checksum() { local file="$1" local checksum_file="${2:-${file}.sha256}" local algorithm="${3:-sha256}" local hash case "$algorithm" in sha256) hash=$(compute_sha256 "$file") ;; sha512) hash=$(compute_sha512 "$file") ;; md5) hash=$(compute_md5 "$file") ;; *) log_error "Unknown algorithm: $algorithm" return "${EXIT_USAGE:-2}" ;; esac if [[ -z "$hash" ]]; then return "${EXIT_ERROR:-1}" fi local basename basename=$(basename "$file") echo "$hash $basename" > "$checksum_file" log_debug "Wrote checksum to $checksum_file" } # Write checksums for multiple files write_checksums() { local output_file="$1" shift local files=("$@") : > "$output_file" for file in "${files[@]}"; do if [[ -f "$file" ]]; then local hash basename hash=$(compute_sha256 "$file") basename=$(basename "$file") echo "$hash $basename" >> "$output_file" fi done log_debug "Wrote checksums to $output_file" } # ============================================================================ # Checksum Verification # ============================================================================ # Verify checksum of a file verify_checksum() { local file="$1" local expected_hash="$2" local algorithm="${3:-sha256}" local actual_hash case "$algorithm" in sha256) actual_hash=$(compute_sha256 "$file") ;; sha512) actual_hash=$(compute_sha512 "$file") ;; md5) actual_hash=$(compute_md5 "$file") ;; *) log_error "Unknown algorithm: $algorithm" return "${EXIT_USAGE:-2}" ;; esac if [[ "$actual_hash" == "$expected_hash" ]]; then log_debug "Checksum verified: $file" return 0 else log_error "Checksum mismatch for $file" log_error " Expected: $expected_hash" log_error " Actual: $actual_hash" return "${EXIT_VERIFY_FAILED:-64}" fi } # Verify checksums from file (sha256sum -c style) verify_checksums_file() { local checksum_file="$1" local base_dir="${2:-.}" if [[ ! -f "$checksum_file" ]]; then log_error "Checksum file not found: $checksum_file" return "${EXIT_NOT_FOUND:-4}" fi local failures=0 while IFS= read -r line; do # Skip empty lines and comments [[ -z "$line" ]] && continue [[ "$line" == \#* ]] && continue local hash filename hash=$(echo "$line" | awk '{print $1}') filename=$(echo "$line" | awk '{print $2}') local filepath="${base_dir}/${filename}" if [[ ! -f "$filepath" ]]; then log_error "File not found: $filepath" ((failures++)) continue fi if ! verify_checksum "$filepath" "$hash"; then ((failures++)) fi done < "$checksum_file" if [[ $failures -gt 0 ]]; then log_error "$failures checksum verification(s) failed" return "${EXIT_VERIFY_FAILED:-64}" fi log_info "All checksums verified" return 0 } # ============================================================================ # Helpers # ============================================================================ # Check if two files have the same content files_identical() { local file1="$1" local file2="$2" [[ -f "$file1" ]] && [[ -f "$file2" ]] || return 1 local hash1 hash2 hash1=$(compute_sha256 "$file1") hash2=$(compute_sha256 "$file2") [[ "$hash1" == "$hash2" ]] } # Get short hash for display short_hash() { local hash="$1" local length="${2:-8}" echo "${hash:0:$length}" } # Generate deterministic ID from inputs generate_id() { local inputs="$*" compute_string_hash "$inputs" sha256 | head -c 16 }