From 8e3cafbd5bc70ff53b7f1897eafa55de2b4bf3fc Mon Sep 17 00:00:00 2001 From: pstruebi Date: Thu, 5 Mar 2026 13:56:18 +0100 Subject: [PATCH] add encryption - sb and encryption now working with rauc ota updates --- AGENTS.md | 31 +++++- Config.in | 3 +- board/beacon-cm4/genimage.cfg | 12 --- board/beacon-cm4/linux.fragment | 3 + board/beacon-cm4/post-build.sh | 18 ++-- .../system/beacon-encrypt-data.service | 18 ++++ .../usr/sbin/beacon-encrypt-data.sh | 98 +++++++++++++++++++ configs/beacon_cm4_rauc_defconfig | 2 + package/beacon-otp/Config.in | 6 ++ package/beacon-otp/beacon-otp.mk | 22 +++++ package/beacon-otp/src/beacon-otp-key.c | 84 ++++++++++++++++ scripts/flash-cm4-sb.sh | 96 ++++++++++++++++++ scripts/flash-cm4.sh | 67 ------------- 13 files changed, 372 insertions(+), 88 deletions(-) create mode 100644 board/beacon-cm4/rootfs-overlay/etc/systemd/system/beacon-encrypt-data.service create mode 100644 board/beacon-cm4/rootfs-overlay/usr/sbin/beacon-encrypt-data.sh create mode 100644 package/beacon-otp/Config.in create mode 100644 package/beacon-otp/beacon-otp.mk create mode 100644 package/beacon-otp/src/beacon-otp-key.c create mode 100755 scripts/flash-cm4-sb.sh delete mode 100755 scripts/flash-cm4.sh diff --git a/AGENTS.md b/AGENTS.md index a41ad46..6ebcdf2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,4 +69,33 @@ Expected secure boot log lines: `secure-boot`, `rsa-verify pass (0x0)`, then U-B ```bash update-pieeprom.sh -k private.pem && rpiboot -d secure-boot-recovery ``` -> Use existing `private.pem` — never regenerate it. \ No newline at end of file +> Use existing `private.pem` — never regenerate it. + +## Disk Encryption (/data partition) ✅ COMPLETE +`/dev/mmcblk0p3` is LUKS2-encrypted using the device-unique 256-bit OTP private key. +Key is read from OTP at every boot via `/dev/vcio` (VideoCore mailbox). +No key ever touches disk — tmpfs only. + +**Self-healing:** If service finds a LUKS header that can't be opened (e.g. stale header +surviving a sparse bmaptool flash), it wipes the first 4 MB and re-formats automatically. + +**Note:** `lsblk` is not installed on target — use `cryptsetup status` instead. + +```bash +# Check /data is mounted + encrypted: +sshpass -p beacon ssh user@$CM4 'sudo cryptsetup status data' +# Expected: type: LUKS2, cipher: aes-xts-plain64, keysize: 512 bits, mode: read/write + +# Read OTP key (hex, stable across reboots) — requires root: +sshpass -p beacon ssh user@$CM4 'sudo beacon-otp-key' + +# Verify LUKS2 header + keyslots: +sshpass -p beacon ssh user@$CM4 'sudo cryptsetup luksDump /dev/mmcblk0p3' +``` + +**OTA update behavior:** RAUC only writes to rootfs.0/1 — data partition is untouched. +On reboot into new slot, service opens LUKS with same OTP key → same data accessible. +Verified: A→B and B→A OTA both maintain encrypted /data correctly. + +**Security model:** OTP key is protected by secure boot (only signed boot.img runs). +Root processes within the signed OS can still read OTP via `/dev/vcio` (RPi hardware limitation). \ No newline at end of file diff --git a/Config.in b/Config.in index 32cc7e0..86a114b 100644 --- a/Config.in +++ b/Config.in @@ -1,2 +1 @@ -# Nothing to see here (yet) -#source "$BR2_EXTERNAL_BEACON_PATH/package/blah/Config.in" +source "$BR2_EXTERNAL_BEACON_PATH/package/beacon-otp/Config.in" diff --git a/board/beacon-cm4/genimage.cfg b/board/beacon-cm4/genimage.cfg index 6fb5718..3f89f28 100644 --- a/board/beacon-cm4/genimage.cfg +++ b/board/beacon-cm4/genimage.cfg @@ -1,14 +1,3 @@ -image data.ext4 { - name = "Data" - mountpoint = /data - ext4 { - use-mke2fs = true - label = "Data" - features = "^64bit" - } - size = 128M -} - image upload.ext4 { name = "Upload" empty = true @@ -61,7 +50,6 @@ image sdcard.img { partition data { partition-type = 0x83 - image = "data.ext4" size = 128M } diff --git a/board/beacon-cm4/linux.fragment b/board/beacon-cm4/linux.fragment index fd03e0b..a8c35b1 100644 --- a/board/beacon-cm4/linux.fragment +++ b/board/beacon-cm4/linux.fragment @@ -4,6 +4,9 @@ CONFIG_BLK_DEV_LOOP=y CONFIG_DM_VERITY=y CONFIG_SQUASHFS=y CONFIG_CRYPTO_SHA256=y +CONFIG_CRYPTO_SHA512=y CONFIG_DM_CRYPT=y CONFIG_CRYPTO_AES=y CONFIG_CRYPTO_XTS=y +CONFIG_CRYPTO_USER_API_HASH=y +CONFIG_CRYPTO_USER_API_SKCIPHER=y diff --git a/board/beacon-cm4/post-build.sh b/board/beacon-cm4/post-build.sh index c27b27e..3d671ec 100755 --- a/board/beacon-cm4/post-build.sh +++ b/board/beacon-cm4/post-build.sh @@ -24,17 +24,23 @@ fi # Mount persistent data partitions +# /data is handled by beacon-encrypt-data.service (LUKS2 encrypted, key from OTP) if [ -e ${TARGET_DIR}/etc/fstab ]; then - # For configuration data - # WARNING: data=journal is safest, but potentially slow! - grep -qE 'LABEL=Data' ${TARGET_DIR}/etc/fstab || \ - echo "LABEL=Data /data ext4 defaults,data=journal,noatime 0 0" >> ${TARGET_DIR}/etc/fstab - - # For bulk data (eg: firmware updates) + # Remove any stale LABEL=Data entry left from previous builds + sed -i '/LABEL=Data/d' ${TARGET_DIR}/etc/fstab + # For bulk data (eg: firmware updates) — unencrypted grep -qE 'LABEL=Upload' ${TARGET_DIR}/etc/fstab || \ echo "LABEL=Upload /upload ext4 defaults,noatime 0 0" >> ${TARGET_DIR}/etc/fstab fi +# Enable beacon-encrypt-data.service (runs before local-fs.target to mount /data) +mkdir -p "${TARGET_DIR}/etc/systemd/system/local-fs.target.wants" +ln -sf ../beacon-encrypt-data.service \ + "${TARGET_DIR}/etc/systemd/system/local-fs.target.wants/beacon-encrypt-data.service" + +# Ensure the service script is executable +chmod 0755 "${TARGET_DIR}/usr/sbin/beacon-encrypt-data.sh" 2>/dev/null || true + # Copy custom cmdline.txt file install -D -m 0644 ${BR2_EXTERNAL_BEACON_PATH}/board/beacon-cm4/cmdline.txt ${BINARIES_DIR}/custom/cmdline.txt diff --git a/board/beacon-cm4/rootfs-overlay/etc/systemd/system/beacon-encrypt-data.service b/board/beacon-cm4/rootfs-overlay/etc/systemd/system/beacon-encrypt-data.service new file mode 100644 index 0000000..6ae205e --- /dev/null +++ b/board/beacon-cm4/rootfs-overlay/etc/systemd/system/beacon-encrypt-data.service @@ -0,0 +1,18 @@ +[Unit] +Description=LUKS2 encrypted data partition setup +Documentation=man:cryptsetup(8) +DefaultDependencies=no +Conflicts=umount.target +After=systemd-udevd.service +Before=local-fs.target umount.target +# Wait for the block device to appear +After=dev-mmcblk0p3.device +Wants=dev-mmcblk0p3.device + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/sbin/beacon-encrypt-data.sh + +[Install] +WantedBy=local-fs.target diff --git a/board/beacon-cm4/rootfs-overlay/usr/sbin/beacon-encrypt-data.sh b/board/beacon-cm4/rootfs-overlay/usr/sbin/beacon-encrypt-data.sh new file mode 100644 index 0000000..9353b62 --- /dev/null +++ b/board/beacon-cm4/rootfs-overlay/usr/sbin/beacon-encrypt-data.sh @@ -0,0 +1,98 @@ +#!/bin/sh +# +# beacon-encrypt-data.sh +# +# On first boot: LUKS2-format /dev/mmcblk0p3 with device OTP key, create ext4, mount /data +# On later boots: open LUKS2 container with device OTP key, mount /data +# +# If the OTP key is all-zeros (not programmed) the partition is mounted unencrypted +# so the system is still usable on a non-secure-boot device during development. + +set -e + +DATA_DEV="/dev/mmcblk0p3" +MAPPER_NAME="data" +MAPPER_DEV="/dev/mapper/${MAPPER_NAME}" +MOUNT_POINT="/data" +OTP_TOOL="/usr/sbin/beacon-otp-key" + +log() { echo "beacon-encrypt-data: $*"; } +die() { echo "beacon-encrypt-data: ERROR: $*" >&2; exit 1; } + +# Block device must exist +[ -b "${DATA_DEV}" ] || { log "${DATA_DEV} not found, skipping"; exit 0; } + +# OTP tool must exist +[ -x "${OTP_TOOL}" ] || die "OTP tool not found at ${OTP_TOOL}" + +# Write OTP key into a tmpfs file so it never touches disk +KEY_FILE="$(mktemp /dev/shm/otp-XXXXXX 2>/dev/null || mktemp /tmp/otp-XXXXXX)" +trap 'rm -f "${KEY_FILE}"' EXIT INT TERM + +OTP_READ_OK=1 +if ! ${OTP_TOOL} -b > "${KEY_FILE}"; then + log "WARNING: OTP key read failed — falling back to unencrypted /data" + OTP_READ_OK=0 +fi + +# Check if key is all zeros (OTP not programmed — dev/test device) +KEY_HEX="" +if [ "${OTP_READ_OK}" = "1" ]; then + KEY_HEX="$(${OTP_TOOL} 2>/dev/null || true)" +fi + +mount_unencrypted() { + if ! blkid "${DATA_DEV}" >/dev/null 2>&1; then + log "No filesystem on ${DATA_DEV} — creating ext4 (unencrypted)" + mkfs.ext4 -q -L "Data" "${DATA_DEV}" || die "mkfs.ext4 failed" + fi + mount -t ext4 -o defaults,noatime "${DATA_DEV}" "${MOUNT_POINT}" \ + || die "mount ${MOUNT_POINT} failed" + log "${MOUNT_POINT} is ready (unencrypted)" + exit 0 +} + +if [ "${OTP_READ_OK}" = "0" ] || [ -z "$(echo "${KEY_HEX}" | tr -d '0')" ]; then + log "OTP key is all-zeros or unreadable — mounting ${DATA_DEV} unencrypted" + mount_unencrypted +fi + +# --- Encrypted path --- +luks_format() { + log "Formatting /dev/mmcblk0p3 with LUKS2" + dd if=/dev/zero of="${DATA_DEV}" bs=1M count=4 status=none 2>/dev/null || true + cryptsetup luksFormat \ + --batch-mode \ + --type luks2 \ + --key-file "${KEY_FILE}" \ + --key-size 512 \ + --cipher aes-xts-plain64 \ + --hash sha256 \ + --pbkdf pbkdf2 \ + "${DATA_DEV}" \ + || die "luksFormat failed" + cryptsetup luksOpen "${DATA_DEV}" "${MAPPER_NAME}" \ + --key-file "${KEY_FILE}" \ + || die "luksOpen after format failed" + log "Creating ext4 filesystem inside encrypted container" + mkfs.ext4 -q -L "DataEnc" "${MAPPER_DEV}" \ + || die "mkfs.ext4 failed" +} + +if cryptsetup isLuks "${DATA_DEV}" 2>/dev/null; then + log "Opening existing LUKS2 container on ${DATA_DEV}" + if ! cryptsetup luksOpen "${DATA_DEV}" "${MAPPER_NAME}" \ + --key-file "${KEY_FILE}" 2>/dev/null; then + log "WARNING: luksOpen failed (stale header or wrong key) — re-formatting" + luks_format + fi +else + log "No LUKS header on ${DATA_DEV} — formatting with LUKS2 (first boot)" + luks_format +fi + +log "Mounting ${MAPPER_DEV} at ${MOUNT_POINT}" +mount -t ext4 -o defaults,noatime "${MAPPER_DEV}" "${MOUNT_POINT}" \ + || die "mount ${MOUNT_POINT} failed" + +log "${MOUNT_POINT} is ready (encrypted)" diff --git a/configs/beacon_cm4_rauc_defconfig b/configs/beacon_cm4_rauc_defconfig index a2a8560..e36eab3 100644 --- a/configs/beacon_cm4_rauc_defconfig +++ b/configs/beacon_cm4_rauc_defconfig @@ -40,6 +40,8 @@ BR2_PACKAGE_RAUC_NETWORK=y BR2_PACKAGE_RAUC_JSON=y BR2_PACKAGE_DROPBEAR=y BR2_PACKAGE_CRYPTSETUP=y +BR2_PACKAGE_E2FSPROGS=y +BR2_PACKAGE_BEACON_OTP=y BR2_PACKAGE_UTIL_LINUX_WDCTL=y BR2_TARGET_ROOTFS_EXT2=y BR2_TARGET_ROOTFS_EXT2_4=y diff --git a/package/beacon-otp/Config.in b/package/beacon-otp/Config.in new file mode 100644 index 0000000..0cc6a56 --- /dev/null +++ b/package/beacon-otp/Config.in @@ -0,0 +1,6 @@ +config BR2_PACKAGE_BEACON_OTP + bool "beacon-otp" + help + Reads the device-specific 256-bit private key from RPi OTP + via the VideoCore mailbox (/dev/vcio). Used by the + beacon-encrypt-data service to unlock the LUKS2 data partition. diff --git a/package/beacon-otp/beacon-otp.mk b/package/beacon-otp/beacon-otp.mk new file mode 100644 index 0000000..b796f84 --- /dev/null +++ b/package/beacon-otp/beacon-otp.mk @@ -0,0 +1,22 @@ +################################################################################ +# +# beacon-otp +# +################################################################################ + +BEACON_OTP_VERSION = local +BEACON_OTP_SITE = $(BR2_EXTERNAL_BEACON_PATH)/package/beacon-otp/src +BEACON_OTP_SITE_METHOD = local +BEACON_OTP_LICENSE = MIT + +define BEACON_OTP_BUILD_CMDS + $(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \ + -o $(@D)/beacon-otp-key $(@D)/beacon-otp-key.c +endef + +define BEACON_OTP_INSTALL_TARGET_CMDS + $(INSTALL) -D -m 0750 $(@D)/beacon-otp-key \ + $(TARGET_DIR)/usr/sbin/beacon-otp-key +endef + +$(eval $(generic-package)) diff --git a/package/beacon-otp/src/beacon-otp-key.c b/package/beacon-otp/src/beacon-otp-key.c new file mode 100644 index 0000000..7c9f858 --- /dev/null +++ b/package/beacon-otp/src/beacon-otp-key.c @@ -0,0 +1,84 @@ +/* beacon-otp-key.c + * Read the device-specific private key from RPi OTP via the VideoCore mailbox. + * Usage: beacon-otp-key [-b] + * (no args) print 64-char hex string + newline + * -b write 32 raw bytes to stdout (for use as a key-file) + */ +#include +#include +#include +#include +#include +#include + +#define IOCTL_MBOX_PROPERTY _IOWR(100, 0, char *) +#define TAG_GET_PRIVATE_KEY 0x00030081u +#define KEY_WORDS 8 /* 8 x 32-bit = 256-bit key */ + +int main(int argc, char *argv[]) +{ + int binary = (argc > 1 && strcmp(argv[1], "-b") == 0); + + /* + * Mailbox property buffer layout (uint32_t words): + * [0] total message size in bytes + * [1] process-request code (0) + * [2] tag id + * [3] value-buffer size in bytes = (2 + KEY_WORDS) * 4 + * [4] request/response indicator (0 = request) + * [5] offset into OTP keystore (0) + * [6] number of words to read + * [7 .. 6+KEY_WORDS] key data (output) + * [7+KEY_WORDS] end tag (0) + */ + uint32_t buf[7 + KEY_WORDS + 1]; + memset(buf, 0, sizeof(buf)); + buf[0] = (uint32_t)sizeof(buf); + buf[1] = 0x00000000; + buf[2] = TAG_GET_PRIVATE_KEY; + buf[3] = (2 + KEY_WORDS) * 4; + buf[4] = 0; + buf[5] = 0; + buf[6] = KEY_WORDS; + buf[7 + KEY_WORDS] = 0; + + int fd = open("/dev/vcio", O_RDWR); + if (fd < 0) { + perror("beacon-otp-key: open /dev/vcio"); + return 1; + } + + if (ioctl(fd, IOCTL_MBOX_PROPERTY, buf) < 0) { + perror("beacon-otp-key: ioctl MBOX_PROPERTY"); + close(fd); + return 1; + } + close(fd); + + if (buf[1] != 0x80000000u) { + fprintf(stderr, "beacon-otp-key: mailbox error 0x%08x\n", buf[1]); + return 1; + } + + for (int i = 0; i < KEY_WORDS; i++) { + uint32_t w = buf[7 + i]; + if (binary) { + uint8_t b[4] = { + (uint8_t)(w & 0xff), + (uint8_t)((w >> 8) & 0xff), + (uint8_t)((w >>16) & 0xff), + (uint8_t)((w >>24) & 0xff) + }; + if (fwrite(b, 1, 4, stdout) != 4) { + perror("beacon-otp-key: fwrite"); + return 1; + } + } else { + printf("%08x", w); + } + } + if (!binary) + printf("\n"); + fflush(stdout); + return 0; +} diff --git a/scripts/flash-cm4-sb.sh b/scripts/flash-cm4-sb.sh new file mode 100755 index 0000000..4399cff --- /dev/null +++ b/scripts/flash-cm4-sb.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# flash-cm4.sh - Flash CM4 eMMC while EMMC_DISABLE jumper is bridged +# Usage: ./scripts/flash-cm4.sh [/dev/sdX] +# If no device given, auto-detects the CM4 USB mass storage device. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BEACON_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO_ROOT="$(cd "$BEACON_DIR/.." && pwd)" +USBBOOT_DIR="$REPO_ROOT/usbboot" +MSD_DIR="$USBBOOT_DIR/secure-boot-msd" +IMAGE="$REPO_ROOT/output/images/sdcard.img.xz" +PRIVATE_KEY="$REPO_ROOT/private.pem" + +# Build rpiboot from source if not already compiled +if [ ! -x "$USBBOOT_DIR/rpiboot" ]; then + echo "==> Building rpiboot from source..." + make -C "$USBBOOT_DIR" +fi + +# Ensure secure-boot-msd boot.img is signed (required for secure-boot-locked CM4) +if [ ! -f "$MSD_DIR/boot.sig" ] || [ "$MSD_DIR/boot.img" -nt "$MSD_DIR/boot.sig" ]; then + echo "==> Signing secure-boot-msd/boot.img with private.pem..." + "$USBBOOT_DIR/tools/rpi-eeprom-digest" -i "$MSD_DIR/boot.img" \ + -o "$MSD_DIR/boot.sig" -k "$PRIVATE_KEY" +fi + +find_removable_sd() { + lsblk -dno NAME,RM | awk '$2==1{print $1}' | grep '^sd' | head -1 +} + +# Step 1: Expose CM4 eMMC as USB mass storage (skip if device already present) +if [ -n "${1:-}" ] && [ -b "${1}" ]; then + echo "==> $1 already present — skipping rpiboot." +elif [ -z "${1:-}" ] && [ -n "$(find_removable_sd)" ]; then + echo "==> Removable block device already present — skipping rpiboot." +else + # NOTE: mass-storage-gadget64 is rejected by secure-boot-locked CM4s. + # Use secure-boot-msd (signed boot.img) instead. + echo "==> Running rpiboot to expose CM4 eMMC (EMMC_DISABLE jumper must be bridged)..." + sudo "$USBBOOT_DIR/rpiboot" -d "$MSD_DIR" + echo "==> rpiboot done." +fi + +# Step 2: Find the device (explicit arg or auto-detect) +if [ -n "${1:-}" ]; then + DEVICE="$1" + echo "==> Using device: $DEVICE — waiting for it to appear..." + for i in $(seq 1 30); do + [ -b "$DEVICE" ] && break + sleep 1 + printf " waiting for %s... (%ds)\r" "$DEVICE" "$i" + done + if [ ! -b "$DEVICE" ]; then + echo "ERROR: $DEVICE did not appear as a block device within 30s." + exit 1 + fi +else + DEVICE="" + echo "==> Waiting for removable block device..." + for i in $(seq 1 30); do + DEV=$(find_removable_sd) + if [ -n "$DEV" ]; then + DEVICE="/dev/$DEV" + break + fi + sleep 1 + printf " waiting... (%ds)\r" "$i" + done + if [ -z "$DEVICE" ]; then + echo "ERROR: No removable block device found within 30s." + echo " Run 'lsblk' to find it, then re-run: $0 /dev/sdX" + exit 1 + fi + echo "==> Auto-detected CM4 eMMC at $DEVICE" +fi + +# Step 3: Safety check - refuse to flash the host NVMe disk +if echo "$DEVICE" | grep -qE '^/dev/nvme'; then + echo "ERROR: $DEVICE looks like the host NVMe. Aborting." + exit 1 +fi + +# Step 4: Unmount any auto-mounted partitions +echo "==> Unmounting $DEVICE partitions..." +sudo umount "${DEVICE}"?* 2>/dev/null || true +sudo umount "${DEVICE}"[0-9]* 2>/dev/null || true + +# Step 5: Flash via bmaptool +echo "==> Flashing $IMAGE -> $DEVICE ..." +sudo bmaptool copy "$IMAGE" "$DEVICE" +sudo sync + +echo "" +echo "==> Flash complete!" +echo " Remove the EMMC_DISABLE jumper, then power-cycle the CM4." diff --git a/scripts/flash-cm4.sh b/scripts/flash-cm4.sh deleted file mode 100755 index 2378cb4..0000000 --- a/scripts/flash-cm4.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash -# flash-cm4.sh - Flash CM4 eMMC while EMMC_DISABLE jumper is bridged -# Usage: ./scripts/flash-cm4.sh [/dev/sdX] -# If no device given, auto-detects the CM4 USB mass storage device. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -BEACON_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -REPO_ROOT="$(cd "$BEACON_DIR/.." && pwd)" -USBBOOT_DIR="$REPO_ROOT/usbboot" -IMAGE="$REPO_ROOT/output/images/sdcard.img.xz" - -# Build rpiboot from source if not already compiled -if [ ! -x "$USBBOOT_DIR/rpiboot" ]; then - echo "==> Building rpiboot from source..." - make -C "$USBBOOT_DIR" -fi - -# Step 1: Expose CM4 eMMC as USB mass storage -echo "==> Running rpiboot to expose CM4 eMMC (EMMC_DISABLE jumper must be bridged)..." -sudo "$USBBOOT_DIR/rpiboot" -d "$USBBOOT_DIR/mass-storage-gadget64" -echo "==> rpiboot done, waiting for block device..." - -# Step 2: Find the device (explicit arg or auto-detect USB disk ~8 GiB) -if [ -n "${1:-}" ]; then - DEVICE="$1" - echo "==> Using specified device: $DEVICE" -else - DEVICE="" - for i in $(seq 1 30); do - sleep 1 - # Detect USB block device of 7-8 GiB (CM4 eMMC) - DEVICE=$(lsblk -dno NAME,TRAN,SIZE \ - | awk '$2=="usb" && ($3~/^7\.[0-9]+G$/ || $3~/^8\.[0-9]+G$/) {print "/dev/"$1}' \ - | head -1) - [ -n "$DEVICE" ] && break - printf " waiting... (%ds)\r" "$i" - done - if [ -z "$DEVICE" ]; then - echo "ERROR: CM4 eMMC did not appear as a USB block device within 30s." - echo " Run 'lsblk' manually and re-run with explicit device: $0 /dev/sdX" - exit 1 - fi - echo "==> Auto-detected CM4 eMMC at $DEVICE" -fi - -# Step 3: Safety check - refuse to flash the host nvme/sata disk -if echo "$DEVICE" | grep -qE '^/dev/(nvme|sd[a-z]{2,}|sda$)'; then - lsblk -dno TRAN "$DEVICE" | grep -qx usb || { - echo "ERROR: $DEVICE does not appear to be a USB device. Aborting." - exit 1 - } -fi - -# Step 4: Unmount any auto-mounted partitions -echo "==> Unmounting $DEVICE partitions..." -sudo umount "${DEVICE}"?* 2>/dev/null || true -sudo umount "${DEVICE}"[0-9]* 2>/dev/null || true - -# Step 5: Flash via bmaptool -echo "==> Flashing $IMAGE -> $DEVICE ..." -sudo bmaptool copy "$IMAGE" "$DEVICE" -sudo sync - -echo "" -echo "==> Flash complete!" -echo " Remove the EMMC_DISABLE jumper, then power-cycle the CM4."