diff --git a/README.md b/README.md index 1f4f287..458bcc0 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,7 @@ This repository holds shell scripts that Intersect uses to engage in Cardano on- - Checks if a file is accessible via free IPFS gateways - [ipfs-pin.sh](./scripts/ipfs-pin.sh) - Pins file(s) across local IPFS node, Pinata, Blockfrost, and NMKR (enabled by default; disable individually with `--no-local`, `--no-pinata`, `--no-blockfrost`, `--no-nmkr`) - - Accepts a single file or a directory; use `--just-jsonld` to limit a directory walk to `.jsonld` files - - `--check-too` first runs `ipfs-check.sh` and skips pinning if the file is already discoverable + - Accepts a single file by default. To recursively pin a directory you must explicitly pass `--directory` (think of it as the equivalent of `rm -r`); without it, a directory path is rejected to prevent accidental bulk uploads. Combine with `--just-jsonld` to limit the walk to `.jsonld` files. The walk skips `.git`/`.svn`/`.hg`, symlinks, and any file whose name matches a sensitive pattern (`*.skey`, `*.vkey`, `.env*`, `id_rsa*`, `*.pem`, `*.p12`, `*.pfx`). #### CIP-108 Scripts diff --git a/scripts/ipfs-pin.sh b/scripts/ipfs-pin.sh index e5bd4e2..30c3f68 100755 --- a/scripts/ipfs-pin.sh +++ b/scripts/ipfs-pin.sh @@ -4,8 +4,6 @@ # Can change if you want! -# Default behavior is to not check if file is discoverable on IPFS -CHECK_TOO="false" JUST_JSONLD="false" # Pinning services to host the file on IPFS @@ -42,15 +40,40 @@ if ! command -v ipfs >/dev/null 2>&1; then exit 1 fi +# Auth-header tmp file plumbing. We pass the API key to curl via `-H @file` +# (header-file syntax, curl >= 7.55) so the secret never appears in this +# process's argv, where it would otherwise be visible to any user on the +# system via `ps`. Files are mktemp'd with mode 600 and removed on exit. +SECRET_TMP_FILES=() +cleanup_secret_files() { + local f + for f in "${SECRET_TMP_FILES[@]:-}"; do + [ -n "$f" ] && [ -f "$f" ] && rm -f "$f" + done +} +trap cleanup_secret_files EXIT INT TERM + +# Create a 0600 tmp file containing one HTTP header line and emit its path on +# stdout. The caller passes that path to `curl -H @path`. Track it for cleanup. +write_auth_header_file() { + local header_line="$1" + local f + f=$(mktemp "${TMPDIR:-/tmp}/ipfs-pin-auth.XXXXXX") + chmod 600 "$f" + printf '%s\n' "$header_line" > "$f" + SECRET_TMP_FILES+=("$f") + printf '%s' "$f" +} + # Usage message usage() { local col=50 echo -e "${UNDERLINE}${BOLD}Pin files to local IPFS node and via Blockfrost, NMKR and Pinata${NC}" echo -e "\n" - echo -e "Syntax:${BOLD} $0 ${GREEN}${NC} [${GREEN}--check-too${NC}] [${GREEN}--no-local${NC}] [${GREEN}--no-pinata${NC}] [${GREEN}--no-blockfrost${NC}] [${GREEN}--no-nmkr${NC}]" + echo -e "Syntax:${BOLD} $0 ${GREEN}${NC} [${GREEN}--directory${NC}] [${GREEN}--no-local${NC}] [${GREEN}--no-pinata${NC}] [${GREEN}--no-blockfrost${NC}] [${GREEN}--no-nmkr${NC}]" printf "Params: ${GREEN}%-*s${GRAY}%s${NC}\n" $((col-8)) "" "- Path to your file or directory containing files" - printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--just-jsonld]" "- If a directory is provided, only .jsonld files will be processed (default: $JUST_JSONLD)" - printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--check-too]" "- Run a check if file is discoverable on ipfs, only pin if not discoverable (default: $CHECK_TOO)" + printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--directory]" "- REQUIRED if the path is a directory. Without this flag, a directory path is rejected to prevent accidental bulk uploads (e.g. pointing at a project root and pinning every file in it). Treat this as the equivalent of rm's '-r'." + printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--just-jsonld]" "- If --directory is set, only .jsonld files will be processed (default: $JUST_JSONLD)" printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--no-local]" "- Don't try to pin file on local ipfs node (default: $DEFAULT_HOST_ON_LOCAL_NODE)" printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--no-pinata]" "- Don't try to pin file on pinata service (default: $DEFAULT_HOST_ON_PINATA)" printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--no-blockfrost]" "- Don't try to pin file on blockfrost service (default: $DEFAULT_HOST_ON_BLOCKFROST)" @@ -61,8 +84,8 @@ usage() { # Initialize variables with defaults input_path="" -check_discoverable="$CHECK_TOO" just_jsonld="$JUST_JSONLD" +allow_directory="false" local_host="$DEFAULT_HOST_ON_LOCAL_NODE" pinata_host="$DEFAULT_HOST_ON_PINATA" blockfrost_host="$DEFAULT_HOST_ON_BLOCKFROST" @@ -71,8 +94,8 @@ nmkr_host="$DEFAULT_HOST_ON_NMKR" # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in - --check-too) - check_discoverable="true" + --directory) + allow_directory="true" shift ;; --just-jsonld) @@ -151,13 +174,29 @@ if [ "$nmkr_host" = "true" ]; then fi fi +# Refuse to pin obviously-secret files. Public-IPFS pinning a Cardano signing +# key (.skey) or an .env credentials file would publish it permanently +SENSITIVE_BASENAME_REGEX='\.(skey|vkey|env)$|^\.env(\..*)?$|^id_(rsa|ed25519|ecdsa|dsa)(\.pub)?$|\.(pem|p12|pfx)$' +is_sensitive_filename() { + local name + name=$(basename "$1") + [[ "$name" =~ $SENSITIVE_BASENAME_REGEX ]] +} + # Function to pin a single file pin_single_file() { local file="$1" - + + if is_sensitive_filename "$file"; then + echo -e " " + echo -e "${RED}Refusing to pin '${YELLOW}$file${RED}': filename matches a sensitive pattern (signing key / env / private key / cert). Pinning would publish it permanently to public IPFS.${NC}" >&2 + echo -e "${GRAY}If you genuinely intend to pin this file, rename it first or remove it from the directory tree.${NC}" >&2 + return 1 + fi + echo -e " " echo -e "${CYAN}Processing file: ${YELLOW}$file${NC}" - + # Generate CID from the given file echo -e "${CYAN}Generating CID for the file...${NC}" @@ -165,23 +204,9 @@ pin_single_file() { # use CIDv1 ipfs_cid=$(ipfs add -Q --cid-version 1 "$file") echo -e "CID: ${YELLOW}$ipfs_cid${NC}" - - # If user wants to check if file is discoverable on IPFS - if [ "$check_discoverable" = "true" ]; then - echo -e "${CYAN}Using ./scripts/ipfs-check.sh script to check if file is discoverable on IPFS...${NC}" - # check if file is discoverable on IPFS - if ! ./scripts/ipfs-check.sh "$file"; then - echo -e "${YELLOW}File is not discoverable on IPFS. Proceeding to pin it.${NC}" - else - echo -e "${GREEN}File is already discoverable on IPFS. No need to pin it.${NC}" - return 0 - fi - else - echo -e "${CYAN}Skipping check of file on ipfs...${NC}" - fi - + echo -e " " - echo -e "${CYAN}File is not hosted on IPFS, so pinning it...${NC}" + echo -e "${CYAN}Pinning file to enabled services...${NC}" # Pin on local node if [ "$local_host" = "true" ]; then @@ -210,8 +235,9 @@ pin_single_file() { fi echo -e "${CYAN}Uploading file to Pinata service...${NC}" + pinata_auth_file=$(write_auth_header_file "Authorization: Bearer ${PINATA_API_KEY}") response=$(curl -s -X POST "https://uploads.pinata.cloud/v3/files" \ - -H "Authorization: Bearer ${PINATA_API_KEY}" \ + -H "@${pinata_auth_file}" \ -F "file=@$file" \ -F "network=public" \ ) @@ -240,8 +266,9 @@ pin_single_file() { fi echo -e "${CYAN}Uploading file to Blockfrost service...${NC}" + blockfrost_auth_file=$(write_auth_header_file "project_id: $BLOCKFROST_API_KEY") response=$(curl -s -X POST "https://ipfs.blockfrost.io/api/v0/ipfs/add" \ - -H "project_id: $BLOCKFROST_API_KEY" \ + -H "@${blockfrost_auth_file}" \ -F "file=@$file" \ ) # Check response for errors @@ -278,18 +305,18 @@ pin_single_file() { base64_content=$(base64 -i "$file") echo -e "${CYAN}Uploading file to NMKR service...${NC}" + nmkr_auth_file=$(write_auth_header_file "Authorization: Bearer ${NMKR_API_KEY}") + # Build the JSON body with jq so the basename and base64 payload are + # properly escaped + nmkr_body=$(jq -n \ + --arg b64 "$base64_content" \ + --arg name "$(basename "$file")" \ + '{fileFromBase64: $b64, name: $name, mimetype: "application/json"}') response=$(curl -s -X POST "https://studio-api.nmkr.io/v2/UploadToIpfs/${NMKR_USER_ID}" \ -H 'accept: text/plain' \ -H 'Content-Type: application/json' \ - -H "Authorization: Bearer ${NMKR_API_KEY}" \ - -d @- <&2 @@ -309,22 +336,36 @@ EOF # Main processing logic if [ -d "$input_path" ]; then - # If input is a directory: pin files (optionally only .jsonld files) including subdirectories + # Directory uploads must be opted into explicitly + if [ "$allow_directory" != "true" ]; then + echo -e "${RED}Error: '${YELLOW}$input_path${RED}' is a directory.${NC}" >&2 + echo -e "${YELLOW}Pass ${GREEN}--directory${YELLOW} to confirm you want to recursively pin its contents (think of this as the equivalent of ${GREEN}rm -r${YELLOW} — it will publish every regular non-symlink, non-VCS, non-sensitive file under the tree to public IPFS, irreversibly).${NC}" >&2 + echo -e "${GRAY}Combine with ${GREEN}--just-jsonld${GRAY} to limit the walk to *.jsonld files.${NC}" >&2 + exit 1 + fi echo -e " " + echo -e "${YELLOW}Warning: ${GREEN}--directory${YELLOW} is set — this will recursively pin files under '${BRIGHTWHITE}$input_path${YELLOW}' to every enabled pinning service. Pinning is publishing: anything uploaded becomes permanently retrievable from public IPFS gateways.${NC}" >&2 echo -e "${CYAN}Processing directory: ${YELLOW}$input_path${NC}" - - # if just jsonld is true, only process .jsonld files + + # Pruning rules for the recursive walk: + # - Skip .git / .svn / .hg metadata directories + # - Skip symlinks (` ! -type l`) + PRUNE_EXPR=( + \( -type d \( -name .git -o -name .svn -o -name .hg \) \) -prune + -o + ) + files_to_process=() if [ "$just_jsonld" = "true" ]; then - # Only .jsonld files + # Only .jsonld files (still skipping VCS dirs and symlinks) while IFS= read -r -d '' file; do files_to_process+=("$file") - done < <(find "$input_path" -type f -name "*.jsonld" -print0) + done < <(find "$input_path" "${PRUNE_EXPR[@]}" -type f ! -type l -name "*.jsonld" -print0) else - # else do all files + # All files (still skipping VCS dirs and symlinks) while IFS= read -r -d '' file; do files_to_process+=("$file") - done < <(find "$input_path" -type f -print0) + done < <(find "$input_path" "${PRUNE_EXPR[@]}" -type f ! -type l -print0) fi # check if any files were found @@ -360,7 +401,15 @@ if [ -d "$input_path" ]; then echo -e "${GREEN}All files processed successfully!${NC}" elif [ -f "$input_path" ]; then - # Input is a single file + if [ "$allow_directory" = "true" ]; then + echo -e "${RED}Error: ${GREEN}--directory${RED} was set, but '${YELLOW}$input_path${RED}' is a single file. Drop ${GREEN}--directory${RED} for single-file uploads.${NC}" >&2 + exit 1 + fi + # Input is a single file. Reject symlinks: `[ -f X ]` follows the link. + if [ -L "$input_path" ]; then + echo -e "${RED}Error: '${YELLOW}$input_path${RED}' is a symbolic link. Refusing to pin a symlink target — pass the real file path instead.${NC}" >&2 + exit 1 + fi echo -e " " echo -e "${CYAN}Processing single file: ${YELLOW}$input_path${NC}" pin_single_file "$input_path"