secrets/common.sh (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 |
this_dir="$(cd "$(dirname "$0")" && pwd)"
exists() { command -v "$1" >/dev/null 2>&1; } # Check if command exists (is in path).
if [[ "$(whoami)" == "root" ]]; then
# find out who the normal user is based on $this_dir,
# since $this_dir will be somewhere within /Users/<user>/... or /home/<user>/...
# and then switch to that user, using exec to avoid forking
# Cross-platform user detection
user=""
# Use uname to detect OS and choose appropriate stat command
if command -v stat >/dev/null 2>&1; then
if [[ "$(uname)" == "Darwin" ]]; then
user="$(stat -f '%Su' "${this_dir}" 2>/dev/null || true)"
elif [[ "$(uname)" == "Linux" ]]; then
user="$(stat -c '%U' "${this_dir}" 2>/dev/null || true)"
else
>&2 echo "ERROR: Unsupported OS: $(uname)"
exit 1
fi
fi
# Fallback method: Parse user from directory path
if [[ -z "${user}" ]]; then
# Extract username from path like /Users/username/... or /home/username/...
if [[ "$(uname)" == "Darwin" ]] && [[ "${this_dir}" =~ ^/Users/([^/]+) ]]; then
user="${BASH_REMATCH[1]}"
elif [[ "$(uname)" == "Linux" ]] && [[ "${this_dir}" =~ ^/home/([^/]+) ]]; then
user="${BASH_REMATCH[1]}"
fi
fi
if [[ -n "${user}" ]]; then
if id "${user}" >/dev/null 2>&1; then
# exec to avoid forking,
# and make sure to use a login shell
# propagate all arguments using "$@"
# The command structure: su - user -c "command" -- args
# - su - user: switch to user with login shell
# - -c "command": execute the quoted command
# - -- args: separator, everything after -- goes to the command
# - "$@": all original script arguments
# - \"\$@\": escaped version for the inner command string
exec su - "${user}" -c "exec bash -l $0 \"\$@\"" -- "$@"
>&2 echo "ERROR: Failed to switch to user ${user}"
else
>&2 echo "ERROR: Detected user ${user} does not exist"
fi
else
>&2 echo "WARN: Failed to determine user from szecret dir: ${this_dir}"
fi
>&2 echo "ERROR: Refusing to run szecret as root"
exit 1
fi
if ! exists age ; then
>&2 cat <<EOF
ERROR: age tool not found
Install from: https://github.com/FiloSottile/age
EOF
exit 1
fi
XDG_DATA_HOME=${XDG_DATA_HOME:-"$HOME/.local/share"}
# Private key directory. Files must be named after their public key (age1...).
# SZECRET_PRIVATE_KEY can still be used as a full single-file override (mainly for tests).
private_key_dir="${SZECRET_PRIVATE_KEY_DIR:-${XDG_DATA_HOME}/szecret}"
# The global store and the public key its secrets are encrypted to. This code
# carries neither: a machine with only these scripts holds no secrets. Looked
# for in order:
#
# 1. SZECRET_STORE / SZECRET_PUBLIC_KEY, if set (tests, or a separate store).
# 2. store/ and id.pub in ~/.local/share/szecret, beside the private keys.
# Normally links into whichever checkout holds the store.
#
# With none of these, every command fails with the instructions below.
szecret_data_dir="${XDG_DATA_HOME}/szecret"
if [[ -n "${SZECRET_STORE:-}" ]]; then
store="${SZECRET_STORE}"
mkdir -p "${store}"
elif [[ -d "${szecret_data_dir}/store" ]]; then
store="${szecret_data_dir}/store"
else
store=""
fi
if [[ -n "${SZECRET_PUBLIC_KEY:-}" ]]; then
public_key="${SZECRET_PUBLIC_KEY}"
elif [[ -r "${szecret_data_dir}/id.pub" ]]; then
public_key="${szecret_data_dir}/id.pub"
else
public_key="${szecret_data_dir}/id.pub"
fi
if [[ -z "${store}" || ! -r "${public_key}" ]]; then
# `ready` is asked by every interactive shell; a machine with no store is
# simply not ready, which needs no explanation each time.
if [[ -n "${szecret_quiet:-}" ]]; then
exit 1
fi
>&2 cat <<EOF
ERROR: no szecret store on this machine.
Looked for ${szecret_data_dir}/store and ${szecret_data_dir}/id.pub.
Link them to an existing store, point SZECRET_STORE and SZECRET_PUBLIC_KEY at
one, or start a new one:
age-keygen -o private
pubkey="\$(age-keygen -y private)"
mkdir -p ${szecret_data_dir}/store
echo "\${pubkey}" > ${szecret_data_dir}/id.pub
mv private "${private_key_dir}/\${pubkey}"
EOF
exit 1
fi
key_id="$(cat "${public_key}" 2>/dev/null || true)"
if [[ -z "$key_id" ]]; then
>&2 echo "WARN: Invalid public key: ${public_key}"
exit 1
fi
private_key="${SZECRET_PRIVATE_KEY:-${private_key_dir}/${key_id}}"
file_for() {
local name="$1"
echo "${store}/${name}.age"
}
local_key_name() {
local local_store_name="$1"
echo "local-key-${local_store_name}"
}
ensure_local_store() {
local local_store_name="$1"
local local_key_name
local local_key_file
local_key_name="$(local_key_name "${local_store_name}")"
local_key_file="$(file_for "${local_key_name}")"
if ! [[ -r "${local_key_file}" ]]; then
>&2 echo "ERROR: Local store '${local_store_name}' does not exist (key: ${local_key_name})"
exit 1
fi
}
get_local_private_key() {
local local_store_name="$1"
local local_key_name
local_key_name="$(local_key_name "${local_store_name}")"
"${this_dir}/get.sh" "${local_key_name}"
}
exists() { command -v "$1" >/dev/null 2>&1; } # Check if command exists (is in path).
pathadd() { [[ -d "$1" ]] && PATH="$1"$(echo ":$PATH:" | sed "s|:$1:|:|" | sed 's|:$||'); }
ensure_age() {
if exists age ; then
return
fi
local bin="${this_dir}/.bin"
pathadd "${bin}"
if [[ -x "${bin}/age" ]]; then
return
fi
local os
local arch
os="$(uname)"
os=${os,,}
arch="$(uname -m)"
if [[ "$arch" == "aarch64" ]]; then
arch="arm64"
elif [[ "$arch" == "x86_64" ]]; then
arch="amd64"
fi
tar="$bin/age.tar.gz"
curl -LSsfo "$tar" "https://dl.filippo.io/age/latest?for=${os}/${arch}"
tar xf "$tar" -C "$bin" --strip-components=1
}
private_key_is_setup() {
check_legacy_private_key_migration
if ! [[ -n "${private_key}" && -r "${private_key}" ]]; then
>&2 echo "WARN: Missing age key: ${private_key}"
fi
[[ -r "${private_key}" ]]
}
ensure_private_key() {
mkdir -p "${private_key_dir}"
if private_key_is_setup; then
return
fi
if [ ! -t 0 ]; then
>&2 echo "ERROR: szecret private key not installed: ${public_key}"
exit 1
fi
ensure_age
echo -n "Enter age private key ${public_key}: "
read -s private_key_value
echo
local derived
derived="$(age-keygen -y <<<"${private_key_value}" 2>/dev/null || true)"
if [[ "$derived" != "$(cat "${public_key}")" ]]; then
>&2 echo "ERROR: Invalid private key (does not match ${public_key})"
exit 1
fi
mkdir -p "${private_key_dir}"
local target="${private_key_dir}/${derived}"
echo "${private_key_value}" > "${target}"
chmod 600 "${target}" 2>/dev/null || true
}
check_legacy_private_key_migration() {
local f="${private_key_dir}/.id"
if [[ ! -r "${f}" ]]; then
return
fi
local pub
pub="$(age-keygen -y "$f" 2>/dev/null || true)"
if [[ -z "$pub" || "$pub" != age1* ]]; then
>&2 echo "WARN: stray file in ${private_key_dir} that does not belong there: $f"
return
fi
local target="${private_key_dir}/${pub}"
mv "$f" "$target"
}
age_encrypt() {
local pubkeyfile="$1"
local outputfile="$2"
cat | age --encrypt --recipients-file "${pubkeyfile}" --output "${outputfile}"
}
age_decrypt() {
local privatekeyfile="$1"
local inputfile="$2"
age --decrypt --identity "${privatekeyfile}" "${inputfile}"
}
# Local store metadata helpers
write_local_metadata() {
local metadata_file="$1"
local store_name="$2"
local pubkey="$3"
cat > "${metadata_file}" <<EOF
store=${store_name}
pubkey=${pubkey}
EOF
}
# Read local metadata file
# Sets global variables: _metadata_store and _metadata_pubkey
read_local_metadata() {
local metadata_file="$1"
_metadata_store=""
_metadata_pubkey=""
if [[ ! -r "${metadata_file}" ]]; then
return 1
fi
_metadata_store="$(grep '^store=' "${metadata_file}" | cut -d= -f2)"
_metadata_pubkey="$(grep '^pubkey=' "${metadata_file}" | cut -d= -f2)"
if [[ -z "${_metadata_store}" || -z "${_metadata_pubkey}" ]]; then
>&2 echo "ERROR: Invalid metadata format in ${metadata_file}"
>&2 echo "Run 'szecret create-local <store-name> <directory>' to recreate"
return 1
fi
}
get_local_public_key() {
local local_store_name="$1"
local local_private_key
local_private_key="$(get_local_private_key "${local_store_name}")"
age-keygen -y <<<"${local_private_key}"
}
verify_local_pubkey() {
local expected_pubkey="$1"
local actual_pubkey="$2"
local store_name="$3"
if [[ "${expected_pubkey}" != "${actual_pubkey}" ]]; then
>&2 echo "ERROR: Public key mismatch for store '${store_name}'"
>&2 echo " Expected: ${expected_pubkey}"
>&2 echo " Actual: ${actual_pubkey}"
return 1
fi
}
|