#!/usr/bin/env bash
set -euo pipefail

# Chrome 137 removed --load-extension, so 1Password's unpacked dev extension is
# packed into a CRX signed with a per-build key and force-installed by
# policy, the same way the WebAuthn proxy ships: https://developer.chrome.com/blog/extension-news-june-2025

# The public box image carries only this script. The in-box host runs it at
# startup for owners with sand_new_1pass on, with what it downloaded through a
# backend-presigned URL, the digest the backend sent, and an install dir the
# host itself owns. With --crx the input is the Chrome Web Store CRX whose
# signature the backend verified, and the digest is its sha256; otherwise it
# is the chromium/ tarball and the digest is that tree's. The digest file is
# written last, so it names a complete install.

USAGE="usage: pack-onepassword-extension [--crx] <chromium.tar.gz | extension.crx> <expected digest> [<install dir>]"
FORMAT=tarball
if [ "${1:-}" = "--crx" ]; then
	FORMAT=crx
	shift
fi
INPUT="${1:?${USAGE}}"
EXPECTED_DIGEST="${2:?${USAGE}}"
INSTALL_DIR="${3:-/usr/local/share}"
CRX="${INSTALL_DIR}/sand-onepassword-extension.crx"
UPDATE_XML="${INSTALL_DIR}/sand-onepassword-extension-update.xml"
ID_FILE="${INSTALL_DIR}/sand-onepassword-extension.id"
VERSION_FILE="${INSTALL_DIR}/sand-onepassword-extension.version"
DIGEST_FILE="${INSTALL_DIR}/sand-onepassword-extension.sha256"

if ! [[ "${EXPECTED_DIGEST}" =~ ^[0-9a-f]{64}$ ]]; then
	echo "sand-onepassword-extension: expected digest is not a sha256 hex digest; refusing to sign an unpinned extension" >&2
	exit 1
fi
if [ ! -d "${INSTALL_DIR}" ]; then
	mkdir -m 755 -- "${INSTALL_DIR}"
fi

WORK="$(mktemp -d)"
# The signing key stays out of ${WORK}, where a tarball entry outside the
# pinned chromium/ tree could redirect the write, and must not outlive this
# run: the image is public, and in a box the agent must not be able to sign an
# extension under the installed id: https://docs.docker.com/engine/storage/drivers/
KEY_DIR="$(mktemp -d)"
# Chrome handles --pack-extension only after it takes the process singleton of
# its user-data-dir, and hands the command line to a running Chrome that holds
# it instead, so packing beside the box's Chrome uses a private one: https://chromium.googlesource.com/chromium/src/+/151.0.7922.169/chrome/browser/chrome_browser_main.cc
PROFILE_DIR="$(mktemp -d)"
trap 'rm -rf "${WORK}" "${KEY_DIR}" "${PROFILE_DIR}"' EXIT
KEY="${KEY_DIR}/key.pem"
EXT_SRC="${WORK}/chromium"
if [ "${FORMAT}" = crx ]; then
	ACTUAL_DIGEST="$(sha256sum "${INPUT}" | cut -d' ' -f1)"
	if [ "${ACTUAL_DIGEST}" != "${EXPECTED_DIGEST}" ]; then
		echo "sand-onepassword-extension: CRX digest ${ACTUAL_DIGEST} does not match the expected ${EXPECTED_DIGEST}; refusing to sign" >&2
		exit 1
	fi
	# A CRX3 is "Cr24", format version 3, a little-endian header length, the
	# header, then the zip: https://chromium.googlesource.com/chromium/src/+/refs/tags/154.0.8037.57/components/crx_file/crx3.proto
	python3 -I - "${INPUT}" "${EXT_SRC}" <<'PY'
import io, struct, sys, zipfile
data = open(sys.argv[1], "rb").read()
if data[:4] != b"Cr24" or struct.unpack("<I", data[4:8])[0] != 3:
    sys.exit("sand-onepassword-extension: input is not a CRX3")
(header,) = struct.unpack("<I", data[8:12])
zipfile.ZipFile(io.BytesIO(data[12 + header:])).extractall(sys.argv[2])
PY
	# A manifest update_url naming the store makes Chrome treat the repacked
	# copy as a store extension and verify it against content hashes signed
	# for the store id (the CRX's _metadata, or fetched from the store), which
	# fail for the new id, so Chrome disables it as corrupt: https://chromium.googlesource.com/chromium/src/+/refs/tags/154.0.8037.57/extensions/browser/content_verifier/content_verifier.cc
	rm -rf -- "${EXT_SRC}/_metadata"
else
	tar -xzf "${INPUT}" --no-same-owner -C "${WORK}"
fi
if [ ! -f "${EXT_SRC}/manifest.json" ]; then
	echo "sand-onepassword-extension: the ${FORMAT} has no manifest.json" >&2
	exit 1
fi

if [ "${FORMAT}" = tarball ]; then
	ACTUAL_DIGEST="$(cd "${EXT_SRC}" && find . -type f -print0 | LC_ALL=C sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1)"
	if [ "${ACTUAL_DIGEST}" != "${EXPECTED_DIGEST}" ]; then
		echo "sand-onepassword-extension: chromium/ digest ${ACTUAL_DIGEST} does not match the pinned ${EXPECTED_DIGEST}; refusing to sign" >&2
		exit 1
	fi
fi

# Chrome's --pack-extension-key parses the PEM as a PKCS#8 PrivateKeyInfo and rejects any other format, so the key is converted with pkcs8 -topk8 whatever this OpenSSL's genrsa emits: https://chromium.googlesource.com/chromium/src/+/b24e51fe3854abd62fc77477f7f05fc48905a108/extensions/browser/extension_creator.cc
openssl genrsa 2048 2>/dev/null | openssl pkcs8 -topk8 -nocrypt -out "${KEY}"
chmod 600 "${KEY}"

# A manifest `key` would pin the dev channel's extension ID, which the CRX
# signing key cannot reproduce; the host discovers the installed ID from the
# id file instead of hardcoding a channel ID.
python3 -I - "${EXT_SRC}/manifest.json" "${FORMAT}" <<'PY'
import json, sys
path = sys.argv[1]
manifest = json.load(open(path))
manifest.pop("key", None)
if sys.argv[2] == "crx":
    manifest.pop("update_url", None)
json.dump(manifest, open(path, "w"), indent="\t")
PY

VERSION="$(python3 -I -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "${EXT_SRC}/manifest.json")"

google-chrome-stable --no-sandbox --user-data-dir="${PROFILE_DIR}" \
	--pack-extension="${EXT_SRC}" --pack-extension-key="${KEY}"
# --pack-extension writes <source dir>.crx beside the source, so the packed
# file lands at ${WORK}/chromium.crx.
test -f "${EXT_SRC}.crx"
# A run that fails before this line leaves the previous install and its
# digest intact, so the host keeps it armed.
rm -f "${DIGEST_FILE}"
mv "${EXT_SRC}.crx" "${CRX}"
chmod 644 "${CRX}"

# Chromium derives an extension ID from the first 16 bytes of the SHA-256 of the DER public key with each hex digit mapped 0-f to a-p, which this pipeline reproduces: https://chromium.googlesource.com/chromium/src/+/b24e51fe3854abd62fc77477f7f05fc48905a108/components/crx_file/id_util.cc
EXT_ID="$(openssl rsa -in "${KEY}" -pubout -outform DER 2>/dev/null | python3 -I -c '
import hashlib, sys
digest = hashlib.sha256(sys.stdin.buffer.read()).hexdigest()[:32]
print("".join(chr(ord("a") + int(c, 16)) for c in digest))
')"

cat >"${UPDATE_XML}" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
  <app appid="${EXT_ID}">
    <updatecheck codebase="file://${CRX}" version="${VERSION}" />
  </app>
</gupdate>
EOF
chmod 644 "${UPDATE_XML}"

printf '%s' "${EXT_ID}" >"${ID_FILE}"
chmod 644 "${ID_FILE}"

printf '%s' "${VERSION}" >"${VERSION_FILE}"
chmod 644 "${VERSION_FILE}"

printf '%s' "${EXPECTED_DIGEST}" >"${DIGEST_FILE}"
chmod 644 "${DIGEST_FILE}"

echo "packed sand-onepassword-extension ${VERSION} as ${EXT_ID}"
