#!/bin/sh # # hh — the hushhh.cc tools, from a terminal. # # GENERATED by scripts/build-cli.js from src/tools/api.js and this template. # Do not edit an installed copy: the commands, their flags and their help all # come from the same contract the API validates against, so a tool added to the # bench turns up here without anybody having to remember this file exists. # # Everything it does is one HTTPS request. The work happens on the server, # which is the trade this makes against the web pages, where the work happens # in your own browser and no file is sent anywhere: https://hushhh.cc/tools/api # # POSIX sh on purpose — no bash, no jq, no python. It has to run on whatever is # already in the box. set -eu HUSHHH_HOST=${HUSHHH_HOST:-https://hushhh.cc} HH_VERSION='b6bf4943' die() { printf 'hh: %s\n' "$*" >&2; exit 1; } command -v curl >/dev/null 2>&1 || die 'curl is needed and was not found' # --- temporary files --------------------------------------------------------- # # One directory, made once and removed by one trap. Everything temporary lives # inside it — the downloaded body, the response headers, the config file that # holds the password — so a failure halfway through leaves nothing behind. # # A directory rather than a list of files, because the files are named inside # `$(...)`: a command substitution is a subshell, and a variable assigned in # one is gone the moment it closes. Registering each file in a variable looked # right, ran clean, and quietly left three files in /tmp per invocation. HH_DIR=$(mktemp -d "${TMPDIR:-/tmp}/hh.XXXXXX") || die 'could not create a temporary directory' trap 'rm -rf "$HH_DIR"' EXIT INT TERM hh_tmpfile() { : > "$HH_DIR/$1" || die 'could not write to the temporary directory' printf '%s' "$HH_DIR/$1" } # --- parameters, as headers -------------------------------------------------- # # Every parameter this sends travels as an `X-Hushhh-` header rather than # in the query string, and the headers are written into a curl config file # instead of the command line. Two different leaks, closed the same way: # # - A query string is written down by things nobody controls from here — the # shell history this was typed into, a proxy, the edge provider's logs — # and `?password=` opens somebody's document. # - An argument to curl is visible in `ps` to every other user on the box for # as long as the request takes. # # The API accepts both forms (see readParams in routes/tools-api.js); this # picks the one that leaves nothing behind. HH_CONF='' param_add() { [ -n "${2:-}" ] || return 0 value=$2 case $value in *[[:cntrl:]]*) die "$1 cannot contain control characters" ;; esac # curl's config format quotes with "..."; backslash and quote are escaped. escaped=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g') printf 'header = "X-Hushhh-%s: %s"\n' "$1" "$escaped" >> "$HH_CONF" } # --- secrets ----------------------------------------------------------------- # Reads a password without echoing it. # # Asked for rather than required on the command line: --password exists for # scripts, which have no terminal to type into, but a person at a prompt should # not have to put a password into their history to unlock a PDF. ask_secret() { if [ ! -t 0 ]; then die "$1 is needed (use --password or --password-stdin)"; fi printf '%s: ' "$1" >&2 if stty -echo 2>/dev/null; then read -r HH_SECRET stty echo 2>/dev/null || true printf '\n' >&2 else read -r HH_SECRET fi } # --- JSON, by hand ----------------------------------------------------------- # A JSON string from stdin, quotes included. # # awk rather than jq, because jq is not installed on most machines and this is # the only thing it would have been for. UTF-8 passes through untouched — JSON # is defined over text — and the characters that would break the document are # the ones named here. json_string() { awk ' BEGIN { ORS = ""; print "\"" } { gsub(/\\/, "\\\\") gsub(/"/, "\\\"") gsub(/\t/, "\\t") gsub(/\r/, "\\r") if (NR > 1) print "\\n" print } END { print "\"" } ' } # Base64 with no line breaks, on both GNU and BSD. b64() { base64 < "$1" | tr -d '\r\n'; } # The `error` field of a JSON body, or nothing. json_error() { sed -n 's/.*"error"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1" | head -n 1 } # --- requests ---------------------------------------------------------------- HH_BODY='' HH_HEAD='' # One place that knows what a failure looks like. # # curl is deliberately not given -f: -f throws the body away, and the body is # where the server explains itself ("This PDF is password-protected. Send …"). request() { url=$1; shift HH_BODY=$(hh_tmpfile body) HH_HEAD=$(hh_tmpfile head) code=$(curl -sS -K "$HH_CONF" -D "$HH_HEAD" -o "$HH_BODY" -w '%{http_code}' "$@" "$url") \ || die 'the request failed' case $code in 2*) return 0 ;; esac message=$(json_error "$HH_BODY") [ -n "$message" ] || message="HTTP $code" die "$message" } # The name the server chose, out of the Content-Disposition. # # Read from the response rather than worked out here: the server already knows # what it made — "-unlocked.pdf", "pages.zip", the name stored inside a # .hushhh — and a second opinion about it would eventually disagree with the # first. suggested_name() { sed -n 's/.*[Ff]ilename="\([^"]*\)".*/\1/p' "$HH_HEAD" | head -n 1 } # Puts the body where it belongs and says where that was. # # It will not overwrite without --force: a tool that silently replaces the file # you pointed it at is one you find out about later. save() { want=${HH_OUT:-} if [ "$want" = '-' ]; then cat "$HH_BODY" return 0 fi if [ -z "$want" ]; then want=$(suggested_name) [ -n "$want" ] || want='hh-output' fi if [ -e "$want" ] && [ "$HH_FORCE" != 1 ]; then die "$want already exists (use --force to overwrite)" fi cp "$HH_BODY" "$want" || die "could not write $want" printf '%s\n' "$want" } # For the answers that were never a file. show() { cat "$HH_BODY"; printf '\n'; } # --- argument handling ------------------------------------------------------- # # Shared by every generated command. What differs between them is only which # flags are legal, and that part is generated from the contract below. hh_reset() { HH_CONF=$(hh_tmpfile conf) HH_JSON='' HH_SECRET='' HH_OUT='' HH_FORCE=0 HH_POS='' HH_POSN=0 } json_add() { if [ -z "$HH_JSON" ]; then HH_JSON="\"$1\":$2" else HH_JSON="$HH_JSON,\"$1\":$2"; fi } # Positional arguments are kept one per line rather than in variables numbered # by hand, because pdf-merge takes as many files as you give it. A newline in a # filename would break this; so would a great many other things, and there is # no array type in POSIX sh to do it properly with. positional() { HH_POS="$HH_POS$1 " HH_POSN=$((HH_POSN + 1)) } hh_pos_at() { printf '%s' "$HH_POS" | sed -n "$1p"; } # A text argument: a file, or `-` for standard input, as a JSON string. hh_read_text() { if [ "$1" = '-' ]; then json_string; else need_file "$1" 'a file to compare' json_string < "$1" fi } need_file() { [ -n "$1" ] || die "$2" [ -f "$1" ] || die "no such file: $1" [ -r "$1" ] || die "cannot read: $1" } read_secret_stdin() { HH_SECRET=$(cat) HH_SECRET=${HH_SECRET%"${HH_SECRET##*[![:space:]]}"} } # --- the commands ------------------------------------------------------------ cmd_pdf_unlock() { hh_reset while [ $# -gt 0 ]; do case $1 in --password) shift; [ $# -gt 0 ] || die 'missing value for --password'; HH_SECRET=$1 ;; --password=*) HH_SECRET=${1#*=} ;; --password-stdin) read_secret_stdin ;; -o) shift; [ $# -gt 0 ] || die 'missing value for -o'; HH_OUT=$1 ;; -o=*|--output=*) HH_OUT=${1#*=} ;; --output) shift; [ $# -gt 0 ] || die 'missing value for --output'; HH_OUT=$1 ;; -f|--force) HH_FORCE=1 ;; -h|--help) cat <<'HELP' usage: hh pdf-unlock [--password …] Remove PDF passwords --password The password the document opens with. Leave it out first: a PDF with owner-only restrictions needs none. HELP return 0 ;; --) shift; while [ $# -gt 0 ]; do positional "$1"; shift; done; break ;; -*) die "unknown option: $1 (try 'hh pdf-unlock --help')" ;; *) positional "$1" ;; esac shift done hh_in=$(hh_pos_at 1) need_file "$hh_in" "usage: hh pdf-unlock " param_add filename "$(basename "$hh_in")" param_add password "$HH_SECRET" request "$HUSHHH_HOST/api/tools/pdf-unlock" -X POST --data-binary "@$hh_in" save } cmd_pdf_merge() { hh_reset while [ $# -gt 0 ]; do case $1 in -o) shift; [ $# -gt 0 ] || die 'missing value for -o'; HH_OUT=$1 ;; -o=*|--output=*) HH_OUT=${1#*=} ;; --output) shift; [ $# -gt 0 ] || die 'missing value for --output'; HH_OUT=$1 ;; -f|--force) HH_FORCE=1 ;; -h|--help) cat <<'HELP' usage: hh pdf-merge [...] Join several into one HELP return 0 ;; --) shift; while [ $# -gt 0 ]; do positional "$1"; shift; done; break ;; -*) die "unknown option: $1 (try 'hh pdf-merge --help')" ;; *) positional "$1" ;; esac shift done [ "$HH_POSN" -ge 2 ] || die "usage: hh pdf-merge [...]" hh_n=1 while [ "$hh_n" -le "$HH_POSN" ]; do need_file "$(hh_pos_at "$hh_n")" "usage: hh pdf-merge [...]" hh_n=$((hh_n + 1)) done hh_json_file=$(hh_tmpfile json) printf '{"files":[' > "$hh_json_file" hh_n=1 while [ "$hh_n" -le "$HH_POSN" ]; do hh_each=$(hh_pos_at "$hh_n") [ "$hh_n" = 1 ] || printf ',' >> "$hh_json_file" printf '{"name":' >> "$hh_json_file" basename "$hh_each" | json_string >> "$hh_json_file" printf ',"data":"%s"}' "$(b64 "$hh_each")" >> "$hh_json_file" hh_n=$((hh_n + 1)) done printf ']}' >> "$hh_json_file" request "$HUSHHH_HOST/api/tools/pdf-merge" -X POST -H 'Content-Type: application/json' --data-binary "@$hh_json_file" save } cmd_pdf_split() { hh_reset HH_P_mode='' HH_P_range='' while [ $# -gt 0 ]; do case $1 in --mode) shift; [ $# -gt 0 ] || die 'missing value for --mode'; HH_P_mode=$1 ;; --mode=*) HH_P_mode=${1#*=} ;; --range) shift; [ $# -gt 0 ] || die 'missing value for --range'; HH_P_range=$1 ;; --range=*) HH_P_range=${1#*=} ;; --password) shift; [ $# -gt 0 ] || die 'missing value for --password'; HH_SECRET=$1 ;; --password=*) HH_SECRET=${1#*=} ;; --password-stdin) read_secret_stdin ;; -o) shift; [ $# -gt 0 ] || die 'missing value for -o'; HH_OUT=$1 ;; -o=*|--output=*) HH_OUT=${1#*=} ;; --output) shift; [ $# -gt 0 ] || die 'missing value for --output'; HH_OUT=$1 ;; -f|--force) HH_FORCE=1 ;; -h|--help) cat <<'HELP' usage: hh pdf-split [--mode …] [--range …] [--password …] Extract or remove pages --mode each: one file per page, returned as a ZIP. keep: only the pages in range. drop: everything except them. One of: each, keep, drop. Default: each. --range Which pages, for keep and drop. "end" is the last page, so "end-2" is the last three. --password The password the document opens with. Leave it out first: a PDF with owner-only restrictions needs none. HELP return 0 ;; --) shift; while [ $# -gt 0 ]; do positional "$1"; shift; done; break ;; -*) die "unknown option: $1 (try 'hh pdf-split --help')" ;; *) positional "$1" ;; esac shift done hh_in=$(hh_pos_at 1) need_file "$hh_in" "usage: hh pdf-split " param_add filename "$(basename "$hh_in")" param_add mode "$HH_P_mode" param_add range "$HH_P_range" param_add password "$HH_SECRET" request "$HUSHHH_HOST/api/tools/pdf-split" -X POST --data-binary "@$hh_in" save } cmd_pdf_rotate() { hh_reset HH_P_angle='' HH_P_range='' while [ $# -gt 0 ]; do case $1 in --angle) shift; [ $# -gt 0 ] || die 'missing value for --angle'; HH_P_angle=$1 ;; --angle=*) HH_P_angle=${1#*=} ;; --range) shift; [ $# -gt 0 ] || die 'missing value for --range'; HH_P_range=$1 ;; --range=*) HH_P_range=${1#*=} ;; --password) shift; [ $# -gt 0 ] || die 'missing value for --password'; HH_SECRET=$1 ;; --password=*) HH_SECRET=${1#*=} ;; --password-stdin) read_secret_stdin ;; -o) shift; [ $# -gt 0 ] || die 'missing value for -o'; HH_OUT=$1 ;; -o=*|--output=*) HH_OUT=${1#*=} ;; --output) shift; [ $# -gt 0 ] || die 'missing value for --output'; HH_OUT=$1 ;; -f|--force) HH_FORCE=1 ;; -h|--help) cat <<'HELP' usage: hh pdf-rotate [--angle …] [--range …] [--password …] Turn pages the right way up --angle Degrees clockwise. One of: 90, 180, 270, -90. Default: 90. --range Which pages to turn. All of them if left out. --password The password the document opens with. Leave it out first: a PDF with owner-only restrictions needs none. HELP return 0 ;; --) shift; while [ $# -gt 0 ]; do positional "$1"; shift; done; break ;; -*) die "unknown option: $1 (try 'hh pdf-rotate --help')" ;; *) positional "$1" ;; esac shift done hh_in=$(hh_pos_at 1) need_file "$hh_in" "usage: hh pdf-rotate " param_add filename "$(basename "$hh_in")" param_add angle "$HH_P_angle" param_add range "$HH_P_range" param_add password "$HH_SECRET" request "$HUSHHH_HOST/api/tools/pdf-rotate" -X POST --data-binary "@$hh_in" save } cmd_image_convert() { hh_reset HH_P_format='' HH_P_quality='' while [ $# -gt 0 ]; do case $1 in --format) shift; [ $# -gt 0 ] || die 'missing value for --format'; HH_P_format=$1 ;; --format=*) HH_P_format=${1#*=} ;; --quality) shift; [ $# -gt 0 ] || die 'missing value for --quality'; HH_P_quality=$1 ;; --quality=*) HH_P_quality=${1#*=} ;; -o) shift; [ $# -gt 0 ] || die 'missing value for -o'; HH_OUT=$1 ;; -o=*|--output=*) HH_OUT=${1#*=} ;; --output) shift; [ $# -gt 0 ] || die 'missing value for --output'; HH_OUT=$1 ;; -f|--force) HH_FORCE=1 ;; -h|--help) cat <<'HELP' usage: hh image-convert [--format …] [--quality …] WebP, JPG, PNG, AVIF --format What to convert to. One of: webp, jpeg, png, avif. --quality 1–100, for the lossy formats. Ignored for PNG, which is lossless. Default: 82. HELP return 0 ;; --) shift; while [ $# -gt 0 ]; do positional "$1"; shift; done; break ;; -*) die "unknown option: $1 (try 'hh image-convert --help')" ;; *) positional "$1" ;; esac shift done hh_in=$(hh_pos_at 1) need_file "$hh_in" "usage: hh image-convert " param_add filename "$(basename "$hh_in")" param_add format "$HH_P_format" param_add quality "$HH_P_quality" request "$HUSHHH_HOST/api/tools/image-convert" -X POST --data-binary "@$hh_in" save } cmd_text_compare() { hh_reset HH_P_granularity='' HH_P_ignoreCase='' HH_P_ignoreWhitespace='' HH_P_format='' while [ $# -gt 0 ]; do case $1 in --granularity) shift; [ $# -gt 0 ] || die 'missing value for --granularity'; HH_P_granularity=$1 ;; --granularity=*) HH_P_granularity=${1#*=} ;; --ignore-case) HH_P_ignoreCase=true ;; --no-ignore-case) HH_P_ignoreCase=false ;; --ignore-whitespace) HH_P_ignoreWhitespace=true ;; --no-ignore-whitespace) HH_P_ignoreWhitespace=false ;; --format) shift; [ $# -gt 0 ] || die 'missing value for --format'; HH_P_format=$1 ;; --format=*) HH_P_format=${1#*=} ;; -o) shift; [ $# -gt 0 ] || die 'missing value for -o'; HH_OUT=$1 ;; -o=*|--output=*) HH_OUT=${1#*=} ;; --output) shift; [ $# -gt 0 ] || die 'missing value for --output'; HH_OUT=$1 ;; -f|--force) HH_FORCE=1 ;; -h|--help) cat <<'HELP' usage: hh text-compare [--granularity …] [--ignore-case] [--ignore-whitespace] [--format …] Every difference, marked The first text. The second text. --granularity What to compare by. Word and character return one flowing run of spans instead of rows, because at that grain a line number answers nothing. One of: line, word, character. Default: line. --ignore-case Treat upper and lower case as the same. Default: false. --ignore-whitespace Treat runs of spaces and tabs as one, and ignore them at the ends of lines. Default: false. --format json: the edit script. unified: a real unified diff, line granularity only. One of: json, unified. Default: json. HELP return 0 ;; --) shift; while [ $# -gt 0 ]; do positional "$1"; shift; done; break ;; -*) die "unknown option: $1 (try 'hh text-compare --help')" ;; *) positional "$1" ;; esac shift done [ "$HH_POSN" = 2 ] || die "usage: hh text-compare " json_add a "$(hh_read_text "$(hh_pos_at 1)")" json_add b "$(hh_read_text "$(hh_pos_at 2)")" [ -z "$HH_P_granularity" ] || json_add granularity "$(printf '%s' "$HH_P_granularity" | json_string)" [ -z "$HH_P_ignoreCase" ] || json_add ignoreCase "$HH_P_ignoreCase" [ -z "$HH_P_ignoreWhitespace" ] || json_add ignoreWhitespace "$HH_P_ignoreWhitespace" [ -z "$HH_P_format" ] || json_add format "$(printf '%s' "$HH_P_format" | json_string)" request "$HUSHHH_HOST/api/tools/text-compare" -X POST -H 'Content-Type: application/json' --data-binary "{$HH_JSON}" if [ "$HH_P_format" = unified ]; then save; else show; fi } cmd_file_encrypt() { hh_reset HH_P_direction='' HH_P_filename='' while [ $# -gt 0 ]; do case $1 in --direction) shift; [ $# -gt 0 ] || die 'missing value for --direction'; HH_P_direction=$1 ;; --direction=*) HH_P_direction=${1#*=} ;; --password) shift; [ $# -gt 0 ] || die 'missing value for --password'; HH_SECRET=$1 ;; --password=*) HH_SECRET=${1#*=} ;; --password-stdin) read_secret_stdin ;; --filename) shift; [ $# -gt 0 ] || die 'missing value for --filename'; HH_P_filename=$1 ;; --filename=*) HH_P_filename=${1#*=} ;; -o) shift; [ $# -gt 0 ] || die 'missing value for -o'; HH_OUT=$1 ;; -o=*|--output=*) HH_OUT=${1#*=} ;; --output) shift; [ $# -gt 0 ] || die 'missing value for --output'; HH_OUT=$1 ;; -f|--force) HH_FORCE=1 ;; -h|--help) cat <<'HELP' usage: hh file-encrypt [--direction …] [--password …] [--filename …] Lock and unlock with a password --direction lock to encrypt, unlock to decrypt. One of: lock, unlock. Default: lock. --password The password. There is no recovery: a file nobody can open is a file you have lost. Asked for at the terminal, without echo, if not given. --filename The original name, stored inside the container and given back when it is opened. HELP return 0 ;; --) shift; while [ $# -gt 0 ]; do positional "$1"; shift; done; break ;; -*) die "unknown option: $1 (try 'hh file-encrypt --help')" ;; *) positional "$1" ;; esac shift done hh_in=$(hh_pos_at 1) need_file "$hh_in" "usage: hh file-encrypt " [ -n "$HH_P_filename" ] || HH_P_filename=$(basename "$hh_in") [ -n "$HH_SECRET" ] || ask_secret Password param_add direction "$HH_P_direction" param_add password "$HH_SECRET" param_add filename "$HH_P_filename" request "$HUSHHH_HOST/api/tools/file-encrypt" -X POST --data-binary "@$hh_in" save } cmd_password() { hh_reset HH_P_mode='' HH_P_length='' HH_P_words='' HH_P_separator='' HH_P_count='' HH_P_uppercase='' HH_P_digits='' HH_P_symbols='' while [ $# -gt 0 ]; do case $1 in --mode) shift; [ $# -gt 0 ] || die 'missing value for --mode'; HH_P_mode=$1 ;; --mode=*) HH_P_mode=${1#*=} ;; --length) shift; [ $# -gt 0 ] || die 'missing value for --length'; HH_P_length=$1 ;; --length=*) HH_P_length=${1#*=} ;; --words) shift; [ $# -gt 0 ] || die 'missing value for --words'; HH_P_words=$1 ;; --words=*) HH_P_words=${1#*=} ;; --separator) shift; [ $# -gt 0 ] || die 'missing value for --separator'; HH_P_separator=$1 ;; --separator=*) HH_P_separator=${1#*=} ;; --count) shift; [ $# -gt 0 ] || die 'missing value for --count'; HH_P_count=$1 ;; --count=*) HH_P_count=${1#*=} ;; --uppercase) HH_P_uppercase=true ;; --no-uppercase) HH_P_uppercase=false ;; --digits) HH_P_digits=true ;; --no-digits) HH_P_digits=false ;; --symbols) HH_P_symbols=true ;; --no-symbols) HH_P_symbols=false ;; -o) shift; [ $# -gt 0 ] || die 'missing value for -o'; HH_OUT=$1 ;; -o=*|--output=*) HH_OUT=${1#*=} ;; --output) shift; [ $# -gt 0 ] || die 'missing value for --output'; HH_OUT=$1 ;; -f|--force) HH_FORCE=1 ;; -h|--help) cat <<'HELP' usage: hh password [--mode …] [--length …] [--words …] [--separator …] [--count …] [--uppercase] [--digits] [--symbols] Strong passwords and passphrases --mode characters for a random string, words for a passphrase from the 512-word list. One of: characters, words. Default: characters. --length Characters, in characters mode. Default: 20. --words Words, in words mode. Default: 5. --separator What goes between the words. Default: -. --count How many to generate in one call. Default: 1. --uppercase Include A–Z. Default: true. --digits Include 0–9. Default: true. --symbols Include punctuation. Default: false. HELP return 0 ;; --) shift; while [ $# -gt 0 ]; do positional "$1"; shift; done; break ;; -*) die "unknown option: $1 (try 'hh password --help')" ;; *) positional "$1" ;; esac shift done [ -z "$HH_P_mode" ] || json_add mode "$(printf '%s' "$HH_P_mode" | json_string)" [ -z "$HH_P_length" ] || json_add length "$HH_P_length" [ -z "$HH_P_words" ] || json_add words "$HH_P_words" [ -z "$HH_P_separator" ] || json_add separator "$(printf '%s' "$HH_P_separator" | json_string)" [ -z "$HH_P_count" ] || json_add count "$HH_P_count" [ -z "$HH_P_uppercase" ] || json_add uppercase "$HH_P_uppercase" [ -z "$HH_P_digits" ] || json_add digits "$HH_P_digits" [ -z "$HH_P_symbols" ] || json_add symbols "$HH_P_symbols" request "$HUSHHH_HOST/api/tools/password" -X POST -H 'Content-Type: application/json' --data-binary "{$HH_JSON}" show } cmd_file_hash() { hh_reset HH_P_algorithms='' HH_P_expected='' while [ $# -gt 0 ]; do case $1 in --algorithms) shift; [ $# -gt 0 ] || die 'missing value for --algorithms'; HH_P_algorithms=$1 ;; --algorithms=*) HH_P_algorithms=${1#*=} ;; --expected) shift; [ $# -gt 0 ] || die 'missing value for --expected'; HH_P_expected=$1 ;; --expected=*) HH_P_expected=${1#*=} ;; -o) shift; [ $# -gt 0 ] || die 'missing value for -o'; HH_OUT=$1 ;; -o=*|--output=*) HH_OUT=${1#*=} ;; --output) shift; [ $# -gt 0 ] || die 'missing value for --output'; HH_OUT=$1 ;; -f|--force) HH_FORCE=1 ;; -h|--help) cat <<'HELP' usage: hh file-hash [--algorithms …] [--expected …] MD5, SHA-1, SHA-256 --algorithms Which digests to compute, comma-separated. One of: md5, sha1, sha256. Default: sha256. --expected The checksum you were given. The answer comes back with matches: true or false, compared case-insensitively. HELP return 0 ;; --) shift; while [ $# -gt 0 ]; do positional "$1"; shift; done; break ;; -*) die "unknown option: $1 (try 'hh file-hash --help')" ;; *) positional "$1" ;; esac shift done hh_in=$(hh_pos_at 1) need_file "$hh_in" "usage: hh file-hash " param_add filename "$(basename "$hh_in")" param_add algorithms "$HH_P_algorithms" param_add expected "$HH_P_expected" request "$HUSHHH_HOST/api/tools/file-hash" -X POST --data-binary "@$hh_in" show } # --- help -------------------------------------------------------------------- usage() { cat <<'USAGE' hh — the hushhh.cc tools, from a terminal. Usage: hh [options] [file …] Commands: pdf-unlock Remove PDF passwords pdf-merge [...] Join several into one pdf-split Extract or remove pages pdf-rotate Turn pages the right way up image-convert WebP, JPG, PNG, AVIF text-compare Every difference, marked file-encrypt Lock and unlock with a password password Strong passwords and passphrases file-hash MD5, SHA-1, SHA-256 Short names: webp image-convert (--format webp) jpg image-convert (--format jpeg) jpeg image-convert (--format jpeg) png image-convert (--format png) avif image-convert (--format avif) lock file-encrypt (--direction lock) unlock file-encrypt (--direction unlock) hash file-hash pass password diff text-compare Everywhere: -o FILE, --output FILE where to write the result ("-" for stdout) -f, --force overwrite an existing file --password-stdin read the password from stdin instead of asking -V, --version print the version The file is sent to the server, which is the difference between this and the web pages, where the work happens in your own browser. Passwords go in a header, never in the URL or in an argument. Nothing is stored either way. Docs: https://hushhh.cc/tools/api Set HUSHHH_HOST to point somewhere else. USAGE } # --- main -------------------------------------------------------------------- [ $# -gt 0 ] || { usage; exit 0; } hh_command=$1 shift case $hh_command in -h|--help|help) usage; exit 0 ;; -V|--version|version) printf 'hh %s (%s)\n' "$HH_VERSION" "$HUSHHH_HOST"; exit 0 ;; pdf-unlock) cmd_pdf_unlock "$@" ;; pdf-merge) cmd_pdf_merge "$@" ;; pdf-split) cmd_pdf_split "$@" ;; pdf-rotate) cmd_pdf_rotate "$@" ;; image-convert) cmd_image_convert "$@" ;; text-compare) cmd_text_compare "$@" ;; file-encrypt) cmd_file_encrypt "$@" ;; password) cmd_password "$@" ;; file-hash) cmd_file_hash "$@" ;; webp) cmd_image_convert '--format' 'webp' "$@" ;; jpg) cmd_image_convert '--format' 'jpeg' "$@" ;; jpeg) cmd_image_convert '--format' 'jpeg' "$@" ;; png) cmd_image_convert '--format' 'png' "$@" ;; avif) cmd_image_convert '--format' 'avif' "$@" ;; lock) cmd_file_encrypt '--direction' 'lock' "$@" ;; unlock) cmd_file_encrypt '--direction' 'unlock' "$@" ;; hash) cmd_file_hash "$@" ;; pass) cmd_password "$@" ;; diff) cmd_text_compare "$@" ;; *) die "unknown command: $hh_command (try 'hh help')" ;; esac