Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 86 additions & 30 deletions SSH_and_GPG/create_ssh_key.sh
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ fi

prompt KEY_NAME "Key file name (no path)" "${GIT_HOST%%.*}"

if [[ "$KEY_NAME" == "." || "$KEY_NAME" == ".." || ! "$KEY_NAME" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
echo "Error: KEY_NAME must be a simple key file name without a path, whitespace, or control characters." >&2
exit 1
fi

# ---- Optional passphrase ----
SSH_PASSPHRASE="${SSH_PASSPHRASE:-}"
if [[ -z "$SSH_PASSPHRASE" ]]; then
Expand Down Expand Up @@ -115,6 +120,33 @@ if [[ -n "$SSH_PASSPHRASE" ]]; then
fi
fi

validate_host_token() {
local value="$1" label="$2"
if [[ -z "$value" ]]; then
printf 'Error: %s cannot be empty.\n' "$label" >&2
exit 1
fi
if [[ "$value" == *://* || "$value" == *[[:space:][:cntrl:]]* || "$value" == -* || "$value" == *\** || "$value" == *\?* || "$value" == *\!* || "$value" == */* ]]; then
printf 'Error: %s must be one literal SSH host token, not a URL, wildcard, negated pattern, or value containing whitespace/control characters.\n' "$label" >&2
exit 1
fi
}

if [[ -n "$GIT_HOSTNAME" && -z "$IS_SELF_HOSTED" ]]; then
echo "Error: GIT_HOSTNAME requires IS_SELF_HOSTED=true." >&2
exit 1
fi

validate_host_token "$GIT_HOST" "GIT_HOST"
if [[ -n "$IS_SELF_HOSTED" ]]; then
validate_host_token "$GIT_HOSTNAME" "GIT_HOSTNAME"
GIT_SSH_PORT="${GIT_SSH_PORT:-22}"
if [[ ! "$GIT_SSH_PORT" =~ ^[0-9]+$ ]] || (( GIT_SSH_PORT < 1 || GIT_SSH_PORT > 65535 )); then
echo "Error: GIT_SSH_PORT must be a number from 1 to 65535." >&2
exit 1
fi
fi

SSH_DIR="$HOME/.ssh"
KEY_PATH="$SSH_DIR/$KEY_NAME"
PUB_PATH="$KEY_PATH.pub"
Expand Down Expand Up @@ -175,38 +207,62 @@ fi
touch "$CFG_PATH"
chmod 600 "$CFG_PATH"

# Remove any existing block for this host (simple, robust approach).
# This deletes from line "Host <GIT_HOST>" up to the next "Host " line (or EOF).
tmp_cfg="$(mktemp)"
awk -v host="$GIT_HOST" '
BEGIN {skip=0}
$1=="Host" && $2==host {skip=1; next}
$1=="Host" && skip==1 {skip=0}
skip==0 {print}
managed_begin="# BEGIN create_ssh_key.sh: $KEY_NAME"
legacy_begin='# BEGIN create_ssh_key.sh'
legacy_end='# END create_ssh_key.sh'
managed_end="# END create_ssh_key.sh: $KEY_NAME"
tmp_cfg="$(mktemp "$SSH_DIR/config.XXXXXX")"
trap 'rm -f "$tmp_cfg"' EXIT
awk -v begin="$managed_begin" -v legacy_begin="$legacy_begin" -v legacy_end="$legacy_end" -v end="$managed_end" '
function flush_block( i) {
for (i = 1; i <= block_lines; i++) print block[i]
block_lines = 0
}
($0 == begin || $0 == legacy_begin) {
in_block = 1
block_lines = 1
block[block_lines] = $0
next
}
in_block {
block[++block_lines] = $0
if ($0 == end || $0 == legacy_end) {
in_block = 0
block_lines = 0
}
next
}
{print}
END {
if (in_block) flush_block()
}
' "$CFG_PATH" > "$tmp_cfg"
mv "$tmp_cfg" "$CFG_PATH"

if [[ -n "$IS_SELF_HOSTED" ]]; then
{
echo ""
echo "Host $GIT_HOST"
echo " HostName $GIT_HOSTNAME"
# Port 22 is the SSH default — only emit the line for a non-standard port.
[[ -n "$GIT_SSH_PORT" && "$GIT_SSH_PORT" != "22" ]] && echo " Port $GIT_SSH_PORT"
echo " User git"
echo " AddKeysToAgent $ADD_KEYS_TO_AGENT"
echo " IdentityFile $KEY_PATH"
} >> "$CFG_PATH"
else
{
echo ""
echo "Host $GIT_HOST"
echo " AddKeysToAgent $ADD_KEYS_TO_AGENT"
# macOS keychain optional:
# echo " UseKeychain yes"
echo " IdentityFile $KEY_PATH"
} >> "$CFG_PATH"
fi
managed_block="$(mktemp "$SSH_DIR/config-block.XXXXXX")"
final_cfg="$(mktemp "$SSH_DIR/config.XXXXXX")"
trap 'rm -f "$tmp_cfg" "$managed_block" "$final_cfg"' EXIT
{
printf '%s\n' "$managed_begin"
if [[ -n "$IS_SELF_HOSTED" ]]; then
printf 'Host %s %s\n' "$GIT_HOST" "$GIT_HOSTNAME"
printf ' HostName %s\n' "$GIT_HOSTNAME"
[[ "$GIT_SSH_PORT" != "22" ]] && printf ' Port %s\n' "$GIT_SSH_PORT"
printf ' User git\n'
else
printf 'Host %s\n' "$GIT_HOST"
fi
printf ' AddKeysToAgent %s\n' "$ADD_KEYS_TO_AGENT"
printf ' IdentityFile %s\n' "$KEY_PATH"
printf '%s\n' "$managed_end"
} > "$managed_block"

{
cat "$managed_block"
cat "$tmp_cfg"
} > "$final_cfg"
mv "$final_cfg" "$CFG_PATH"
rm -f "$tmp_cfg" "$managed_block"
trap - EXIT

# ---- Show public key ----
echo ""
Expand Down
39 changes: 37 additions & 2 deletions lib/core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,37 @@ run_eval() {
fi
}

core_prime_sudo() {
if [[ "$DRY_RUN" == true ]]; then
printf ' [dry-run] sudo -v (cache credentials + background keepalive)\n'
return 0
fi
printf '==> Caching sudo credentials (you may be prompted once)...\n'
if ! sudo -v; then
printf 'error: sudo authentication is required for this setup.\n' >&2
printf ' Run it from an interactive terminal, or authenticate first with: sudo -v\n' >&2
return 1
fi
( while true; do sudo -n true || true; sleep 60; kill -0 "$$" 2>/dev/null || exit; done ) >/dev/null 2>&1 &
}

prepare_nvm_environment() {
local npmrc="$HOME/.npmrc" backup tmp
unset NPM_CONFIG_PREFIX npm_config_prefix NPM_CONFIG_GLOBALCONFIG npm_config_globalconfig
[[ -f "$npmrc" ]] || return 0
if ! grep -Eq '^[[:space:]]*(prefix|globalconfig)[[:space:]]*=' "$npmrc"; then
return 0
fi
backup="${npmrc}.nvm-preflight.bak"
if [[ ! -e "$backup" ]]; then
cp "$npmrc" "$backup"
fi
tmp="$(mktemp)"
grep -Ev '^[[:space:]]*(prefix|globalconfig)[[:space:]]*=' "$npmrc" > "$tmp" || true
mv "$tmp" "$npmrc"
printf ' removed npm prefix settings incompatible with nvm (backup: %s)\n' "$backup"
}

# npm supply-chain cooldown: refuse to install package versions younger than
# NPM_MIN_RELEASE_AGE days. Compromised releases of popular packages (e.g. the
# axios RAT, Mar 2026) are typically caught and yanked within hours, so a short
Expand Down Expand Up @@ -743,6 +774,7 @@ linux_nvm_flow() {
configure_pnpm
return 0
fi
prepare_nvm_environment
if [ ! -d "$HOME/.nvm" ]; then
printf '==> Installing nvm...\n'
eval "$(custom_cmd nvm)"
Expand Down Expand Up @@ -829,8 +861,10 @@ desktop_pipx_section() {

desktop_pnpm_section() {
printf '\n==> Installing pnpm packages...\n'
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
if [[ "$DRY_RUN" == false ]]; then
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
fi
export PNPM_HOME="$HOME/.local/share/pnpm"
export PATH="$PNPM_HOME/bin:$PATH"
if command -v pnpm &>/dev/null; then
Expand Down Expand Up @@ -879,6 +913,7 @@ linux_main() {
else
CONFIG_SRC_DIR="$SETUP_ROOT/linux-desktop"
fi
core_prime_sudo
platform_bootstrap

printf '\n'
Expand Down
16 changes: 3 additions & 13 deletions platforms/macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,9 @@ mac_pipx_install_tier() { mac_install_list "$(pkg_names pipx "$1")" pipx install

# Cache sudo credentials once up front. Homebrew's cask/pkg installers each shell
# out to `sudo`, so without this a fresh install prompts for the password ~6
# times. Prime the timestamp once, then refresh it in the background until this
# script exits so every later sudo call reuses it silently.
# times.
mac_prime_sudo() {
if [[ "$DRY_RUN" == true ]]; then
printf ' [dry-run] sudo -v (cache credentials + background keepalive)\n'
return 0
fi
printf '==> Caching credentials (you may be prompted for your password once)...\n'
sudo -v || return 0
# || true: the subshell inherits set -e, and one failed refresh (timestamp
# revoked mid-run) must not silently kill the keepalive. stdout is redirected
# so a piped run (setup.sh | tee) sees EOF at exit instead of hanging on the
# fd this subshell holds for up to 60s.
( while true; do sudo -n true || true; sleep 60; kill -0 "$$" 2>/dev/null || exit; done ) >/dev/null 2>&1 &
core_prime_sudo
}

print_app_store_reminders() {
Expand Down Expand Up @@ -154,6 +143,7 @@ platform_main() {
fi

if [[ "$DRY_RUN" == false ]]; then
prepare_nvm_environment
export NVM_DIR="$HOME/.nvm"
[[ -s "$NVM_DIR/nvm.sh" ]] && \. "$NVM_DIR/nvm.sh"
nvm install 'lts/*'
Expand Down
Loading