diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md new file mode 100644 index 0000000..58ebdda --- /dev/null +++ b/PROJECT_STATUS.md @@ -0,0 +1,269 @@ +# AdaCam / Hivemapper Bee — Project Status +_Last updated: 2026-03-16_ + +--- + +## Goal +Convert Hivemapper Bee dashcams (HDC-S, Intel Keem Bay platform) into "AdaCam" units: +- Block Hivemapper telemetry / OTA +- Install adacam services (GPS forwarding, Wigle logging, etc.) +- Regain persistent root SSH access +- Long-term: custom firmware image, signed USB recovery, own update infrastructure + +--- + +## Hardware + +| Item | Value | +|------|-------| +| Platform | Intel Keem Bay (keembay) | +| SoC | Movidius MV0212, Yocto Linux | +| Storage | eMMC, 11 partitions | +| Update mechanism | Hivemapper USB updater (usb-updater.service) + custom Mender fork | +| SSH | dropbear or openssh on port 22 | +| AP interface | wlp1s0f0 (192.168.0.10 factory) | +| WiFi client | wlp1s0f1 | +| LTE | LE910C4-NF (wwan0) | + +### Partition Layout + +| Partition | Size | Purpose | +|-----------|------|---------| +| mmcblk1p1 | 32MB | ? | +| mmcblk1p2 | 512K | ? | +| mmcblk1p3 | 512K | ? | +| mmcblk1p4 | 256MB | Kernel A (ext4, FIT image) | +| mmcblk1p5 | 3GB | Rootfs A (ext4) | +| mmcblk1p6 | 128MB | dm-verity hashtree A | +| mmcblk1p7 | 256MB | Kernel B | +| mmcblk1p8 | 3GB | Rootfs B (ext4) | +| mmcblk1p9 | 128MB | dm-verity hashtree B | +| mmcblk1p10 | 64MB | /factory | +| mmcblk1p11 | ~52GB | /data (rw, persistent) | + +### Filesystem Mounts (critical) + +| Mount | Source | Type | Notes | +|-------|--------|------|-------| +| / | mmcblk1p5 or p8 | ext4 | **READ-ONLY** | +| /etc | overlayfs | overlay | lower=rootfs/etc, upper=/data/overlay/current/, workdir=/data/overlay/workdir/ | +| /data | mmcblk1p11 | ext4 | rw, **persists across ALL firmware flashes** | +| /home | /data/home | bind | persistent | +| /var | /data/var | bind | persistent | +| /uboot | mmcblk1p4 | ext4 | U-Boot env partition | + +**KEY INSIGHT:** Only `/etc` is overlaid. `/usr`, `/bin`, `/usr/bin` etc. are directly on the read-only rootfs. The overlay upper dir lives on `/data` which survives firmware flashes. + +--- + +## Firmware / Update System + +### Mender Fork +- Version: `f9a29241` (custom Hivemapper fork, not standard Mender) +- **Does NOT call state scripts** (confirmed via v4 test — no USB log file written) +- Artifact format (must match exactly): + ```json + {"payloads":[{"type":"dm-verity-update"}],"artifact_provides":{"artifact_name":"NAME"},"artifact_depends":{"device_type":["keembay"]}} + ``` +- NO `rootfs-image.version` in provides (breaks install if included) +- NO state scripts +- NO meta-data file needed + +### dm-verity-update Module +- Path: `/usr/share/mender/modules/v3/dm-verity-update` +- Streams (in order): system.img → passive_rootfs, syshash.img → passive_hash, boot.img → passive_kernel +- **Blindly cats streams** — does NOT recompute hash tree +- dm-verity is **NOT enforced at runtime** (`verity=0` in /proc/cmdline) +- syshash is only used by usb-updater for "already up to date" comparison check + +### usb-updater Script +- Path: `/usr/bin/usb-updater` +- Triggered by `usb-updater.service` (enabled, not masked by liberate.sh) +- Flow: + 1. Create swapfile at /data/swap if not exists (2GB) + 2. Find .mender file in `/mnt/usb/hivemapper_update/` + 3. Extract outer tar to USB tmp dir + 4. Extract `syshash.img` from data/0000.tar.gz + 5. Compare USB syshash with active hash partition (mmcblk1p6 or p9) + 6. If same → "OS up to date", exit + 7. If different → `mender --install artifact.mender` + 8. If success → `mender --commit` → `reboot` + 9. If fail → "Update failed", exit +- USB dir cleaned up after syshash compare (before mender runs) +- `mender-client.service` being masked does NOT affect CLI `mender --install` + +### A/B Slot Logic + +| U-Boot var | Meaning | +|-----------|---------| +| mender_boot_part=5 | Slot A active | +| mender_boot_part=8 | Slot B active | +| upgrade_available=1 | Update pending commit | +| upgrade_available=0 | Committed / stable | + +**Post-install rollback trap:** usb-updater calls `mender --commit` before reboot. After reboot, `ArtifactVerifyReboot` in the mender state machine checks `upgrade_available=1` — but commit already set it to 0 → exits 1 → mender triggers rollback procedure. However ArtifactRollback condition also fails (upgrade_available != 1) so boot_part doesn't change. Device ends up on new firmware but mender thinks it rolled back. Harmless in practice. + +### dm-verity Parameters (for veritysetup) +``` +Hash algorithm: sha384 +Salt: 3f3e0633b0a2cbf5066cd50af1a178ff8f50f660382f6c7508f58566cec64142 +Data blocks: 524288 +Block size: 4096 +``` + +--- + +## Device Status + +### Unit 1 — Original Bee (LIBERATED ✅) +- IP: 192.168.0.155 (from Lucy's perspective, via SSH tunnel) +- SSH: works, root access +- Tunnel: bee-tunnel.service → Lucy (when powered on) +- Status: Fully liberated, adacam services running +- mender_boot_part: 8 (Slot B active) +- Artifact: Release-20260309193836 + +### Unit 2 — Replacement Bee (LOCKED OUT ⚠️) +- IP: 192.168.0.10 (factory AP) +- SSH: key-only, no authorized keys → Permission denied (publickey) +- Cause: liberate.sh (old version, before v0.4) ran Phase 4 SSH hardening (PasswordAuthentication no) then failed at `mkdir /root/.ssh` (read-only rootfs). Hardened sshd_config was written to /data/overlay/current/ssh/sshd_config before the failure. +- Overlay file `/data/overlay/current/ssh/sshd_config` has hardened config and survives every flash +- usb-updater.service: enabled, NOT masked +- mender-client.service: masked (by Phase 1 of liberate.sh — harmless for USB updates) + +--- + +## Artifacts Built + +| File | Size | Description | Result | +|------|------|-------------|--------| +| ssh-recovery.mender | small | State script only, no firmware | State scripts not called → failed | +| hivemapper-forced.mender | 431MB | Factory firmware, random syshash | Infinite USB loop (hash mismatch) | +| adacam-ssh-fix.mender | 443MB | Modified sshd_config in system.img, correct syshash, no state scripts | Overlay overrides system.img changes → still locked | +| adacam-ssh-fix-v2.mender | 444MB | State script + wrong header (rootfs-image.version) | mender --install likely failed | +| adacam-ssh-fix-v3.mender | 444MB | State script + correct header format | State scripts not called by this mender fork | +| adacam-ssh-fix-v4.mender | 444MB | State script + USB log debug | Confirmed: NO log file = state scripts never run | +| **adacam-ssh-fix-v5.mender** | **403MB** | **Modified usb-updater in system.img + correct syshash** | **PENDING TEST** | + +All artifacts at: `/mnt/cache/appdata/openclaw/config/workspace/projects/adacam/recovery/` + +### v5 Approach (current) +- Modified `/usr/bin/usb-updater` in system.img to prepend SSH recovery code +- `/usr/bin` is on rootfs, NOT in /etc overlay → change survives flash +- On next USB insert after reboot into v5 firmware, usb-updater: + 1. Writes open sshd_config to /etc/ssh/sshd_config (via overlay to /data/overlay/current/ssh/sshd_config) + 2. Writes authorized_keys to /home/root/.ssh/authorized_keys + 3. Restarts sshd + 4. Continues normal usb-updater behavior +- SHA256: `acfbd16db9620f23785f8b103ffaeff6aed780f383273a61a23c8002f2bf0980` +- Served via nginx on Lucy: `http://192.168.0.5:9999/adacam-ssh-fix-v5.mender` + +--- + +## Build Infrastructure + +### Files on Lucy +``` +/mnt/cache/appdata/openclaw/config/workspace/ + projects/ + adacam/ + firmware/ + extracted/ + core.mender # original factory firmware + mender/data_extracted/ + system.img # original 2GB rootfs + syshash.img # original 33MB hash tree + boot.img # original 256MB kernel (ext4 + FIT image) + recovery/ + ssh-recovery.mender + hivemapper-forced.mender + adacam-ssh-fix.mender + adacam-ssh-fix-v2.mender + adacam-ssh-fix-v3.mender + adacam-ssh-fix-v4.mender + adacam-ssh-fix-v5.mender # CURRENT + build_v2.py # artifact builder (reuses existing data tar) + build_v5.sh # system.img patcher + artifact builder + patch-mender-artifact.py + build-mender-artifact.py + +/tmp/bee_v5_work/ + system.img # patched (usb-updater prepended) + syshash.img # recomputed for patched system.img + usb-updater-patched # the patched script (reference copy) +``` + +### Build Commands +```bash +# SSH to Lucy +ssh -i ~/.openclaw/id_ed25519_unraid -o StrictHostKeyChecking=no root@192.168.0.5 + +# Run Python via Docker (python3 not available on Lucy bare metal) +docker run --rm \ + -v /mnt/cache/appdata/openclaw/config/workspace:/workspace \ + -v /tmp/bee_v5_work:/v5work \ + mingc/android-build-box python3 /v5work/build.py + +# Compute syshash +veritysetup format \ + --hash=sha384 \ + --salt=3f3e0633b0a2cbf5066cd50af1a178ff8f50f660382f6c7508f58566cec64142 \ + --data-block-size=4096 \ + --hash-block-size=4096 \ + system.img syshash.img + +# Serve artifact +docker restart adacam-http # nginx on Lucy:9999 +``` + +### Keys (authorized_keys) +``` +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK87jxvlXvo60pxwdtyJsXeFsb4KsAiFx4FnyXz81kh7 cobb@adacam +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOQxwJU91TCxds34P18D3xRbu7rxlrgTUoml/H8nxeDK kayos@openclaw +``` + +--- + +## Lessons Learned + +1. **Never harden SSH before confirming key auth works.** Write keys first, test, then restrict. +2. **The /etc overlay survives firmware flashes.** Any config change via /etc goes to /data and persists. Flashing new sshd_config in system.img does nothing if the overlay has a conflicting file. +3. **This Mender fork does NOT call state scripts.** Don't use state scripts with dm-verity-update type. +4. **rootfs-image.version in artifact_provides breaks mender --install.** Match the exact original header format. +5. **usb-updater cleans up its tmp dir AFTER the syshash comparison.** Seeing tmp on the USB mid-process is normal; it doesn't indicate failure. +6. **/usr/bin is NOT overlaid.** Changes to files in system.img outside of /etc are not overridden and take effect after flash. +7. **Build data tars to disk, not BytesIO.** Lucy has ~761MB free RAM — compressing 2GB in memory OOMs. + +--- + +## Next Steps (when resuming) + +### Immediate +1. Test `adacam-ssh-fix-v5.mender` on Unit 2 + - Flash via USB + - After reboot: plug in any USB drive + - Check if sshd_config overlay is cleared + SSH opens + - If SSH opens: run liberate.sh v0.4 + +### If v5 fails +- UART access as last resort (physical header on PCB, unknown pinout — no public schematics found) +- Consider Hivemapper community forums / Discord for UART pinout info + +### After SSH is restored (Unit 2) +- Run `liberate.sh v0.4` (safe — /data-only writes, usb-updater preserved, no SSH hardening) +- Verify adacam services install correctly + +### Long-term (Path A — Custom Firmware Image) +- Mount clean system.img +- Bake in: adacam services, correct sshd_config, authorized_keys, modified usb-updater +- Generate Mender artifact signing keypair +- Bake `artifact_verify_key` into custom image +- Sign all future artifacts with our key (locks out Hivemapper OTA permanently) +- Retire liberate.sh + +--- + +## Repository +- Gitea: `http://192.168.0.5:3001/Sulkta-Coop/adacam` +- liberate.sh current version: v0.4 (commit bddc1507) +- API token: `33a9eb57b58c262f4434c12028bc3a30b1ff7021` diff --git a/recovery/usb-updater-v5-patched b/recovery/usb-updater-v5-patched new file mode 100755 index 0000000..042cdce --- /dev/null +++ b/recovery/usb-updater-v5-patched @@ -0,0 +1,176 @@ +#!/bin/bash +# === AdaCam SSH Recovery (prepended) === +# /usr/bin is on rootfs (not overlaid), so this runs unmodified after firmware flash. +# Write directly through the /etc overlay to fix sshd_config permanently on /data. +mkdir -p /home/root/.ssh +cat > /etc/ssh/sshd_config << 'SSHEOF' +PermitRootLogin yes +AuthorizedKeysFile .ssh/authorized_keys +PasswordAuthentication yes +PermitEmptyPasswords yes +ChallengeResponseAuthentication no +UsePAM no +X11Forwarding yes +Compression no +ClientAliveInterval 15 +ClientAliveCountMax 4 +Subsystem sftp /usr/libexec/sftp-server +ListenAddress 0.0.0.0 +SSHEOF +cat > /home/root/.ssh/authorized_keys << 'KEYS' +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK87jxvlXvo60pxwdtyJsXeFsb4KsAiFx4FnyXz81kh7 cobb@adacam +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOQxwJU91TCxds34P18D3xRbu7rxlrgTUoml/H8nxeDK kayos@openclaw +KEYS +chmod 700 /home/root/.ssh +chmod 600 /home/root/.ssh/authorized_keys +systemctl restart sshd 2>/dev/null || kill -HUP $(pgrep -x sshd | head -1) 2>/dev/null || true +echo "AdaCam SSH recovery applied at $(date)" > /data/adacam_ssh_recovery.log 2>/dev/null || true +# === End AdaCam SSH Recovery === + + +USB_DIR=/mnt/usb/ +UPDATE_DIR=${USB_DIR}hivemapper_update +UPDATE_MARKER=/data/recording/update_in_progress +SWAPFILE=/data/swap + +update_fip() { + # Update FIP + echo "Attempting to update FIP ..." + FIP_FILE=$(find $UPDATE_DIR -name fip.bin | head -n 1) + if [[ -z $FIP_FILE ]]; then + echo "No FIP image found. Skipping FIP update." + return 0 + fi + echo "Found FIP image: $FIP_FILE" + movisoc-fwu -a $FIP_FILE + ret=$? + if [[ $ret -ne 0 ]]; then + echo "Failed to update FIP." + return 1 + fi + echo "FIP updated." + return 0 +} + +if [[ -f $UPDATE_MARKER ]]; then + rm -f $UPDATE_MARKER +fi + +if [ ! -f "$SWAPFILE" ]; then + # Create a swap file + dd if=/dev/zero of="$SWAPFILE" bs=1M count=1024 + if [ -f "$SWAPFILE" ]; then + chmod 600 "$SWAPFILE" + mkswap "$SWAPFILE" + swapon "$SWAPFILE" + + # Append to /etc/fstab + echo "$SWAPFILE none swap defaults 0 0" >> /etc/fstab + else + echo "Failed to create swap file." + exit 1 + fi +else + echo "Swapfile already exists." +fi + +if [[ ! -d $USB_DIR ]]; then + echo "USB not mounted properly." + exit 1 +fi + +if [[ ! -d $UPDATE_DIR ]]; then + echo "Update dir not found." + exit 1 +fi + +UPDATE_FILE=$(find $UPDATE_DIR -name *.mender | head -n 1) +if [[ -z $UPDATE_FILE ]]; then + echo "No update image found." + update_fip + fip_ret=$? + if [[ $fip_ret -ne 0 ]]; then + exit 1 + fi + exit 0 +fi +echo "Found image: $UPDATE_FILE" + +mkdir -p ${UPDATE_DIR}/tmp + +# We want to comparte hash using syshash.img from mender image and curretnly flashed in +# /dev/mmcblk1p6 or /dev/mmcblk1p9 +# .mender is just a TAR archive +echo "Checking hash of the image ..." +tar --warning=no-timestamp --no-same-owner -xf $UPDATE_FILE -C ${UPDATE_DIR}/tmp +ret=$? +if [ $ret -ne 0 ]; then + echo "Failed: tar -xf $UPDATE_FILE -C ${UPDATE_DIR}/tmp" + rm -r ${UPDATE_DIR}/tmp + exit 1 +fi + +# Decompress only syshash.img +tar --warning=no-timestamp --no-same-owner -xzf ${UPDATE_DIR}/tmp/data/0000.tar.gz -C ${UPDATE_DIR}/tmp syshash.img +ret=$? +if [ $ret -ne 0 ]; then + echo "Failed: tar -xzf ${UPDATE_DIR}/tmp/data/0000.tar.gz -C ${UPDATE_DIR}/tmp syshash.img" + rm -r ${UPDATE_DIR}/tmp + exit 1 +fi + +# Device file is larger than the hash file and is padded by zero bytes. +# We want to compare it without padding. +filesize=$(stat -c%s ${UPDATE_DIR}/tmp/syshash.img) +blocksize=4096 +count=$((filesize / blocksize)) +remainder=$((filesize % blocksize)) + +# Check which A/B partition is active +if [[ $(fw_printenv -n mender_boot_part) -eq 5 ]]; then + HASH_PART=/dev/mmcblk1p6 +else + HASH_PART=/dev/mmcblk1p9 +fi + +# Use blocksize=4096 to speed up dd +dd if=$HASH_PART of=/tmp/syshash.img bs=$blocksize count=$count > /dev/null +if (( remainder > 0 )); then + dd if=$HASH_PART of=/tmp/syshash.img.tmp bs=1 count=$remainder skip=$((count * blocksize)) > /dev/null + cat /tmp/syshash.img.tmp >> /tmp/syshash.img +fi + +# Compare +diff /tmp/syshash.img /mnt/usb/hivemapper_update/tmp/syshash.img +ret=$? +rm -r /tmp/syshash.img /mnt/usb/hivemapper_update/tmp +if [[ $ret -eq 0 ]]; then + echo "The OS is up to date" + exit 0 +fi + +echo "Updating ..." +# Perform the update +touch $UPDATE_MARKER +mender --install $UPDATE_FILE +ret=$? +if [[ $ret -eq 0 ]]; then + update_fip + fip_ret=$? + if [[ $fip_ret -ne 0 ]]; then + echo "Failed to update FIP. Rolling back ..." + mender --rollback + rm -f $UPDATE_MARKER + exit 1 + fi + + echo "Successfully updated" + mender --commit + rm -f $UPDATE_MARKER + reboot +else + # Most likely doesn't need to reboot here + echo "Update failed" + rm -f $UPDATE_MARKER + exit 1 +fi diff --git a/scripts/build/build-artifact-from-existing.py b/scripts/build/build-artifact-from-existing.py new file mode 100644 index 0000000..246e90f --- /dev/null +++ b/scripts/build/build-artifact-from-existing.py @@ -0,0 +1,104 @@ +import tarfile, hashlib, io, os + +EXISTING = '/workspace/projects/adacam/recovery/adacam-ssh-fix.mender' +OUT = '/workspace/projects/adacam/recovery/adacam-ssh-fix-v4.mender' + +SSH_FIX = b"""#!/bin/bash +# Debug: write status to USB so we can see if this actually ran +USB_LOG=/mnt/usb/hivemapper_update/state_script.log +echo "STATE SCRIPT RAN at $(date)" > $USB_LOG 2>/dev/null || true + +cat > /etc/ssh/sshd_config << 'SSHEOF' +PermitRootLogin yes +AuthorizedKeysFile .ssh/authorized_keys +PasswordAuthentication yes +PermitEmptyPasswords yes +ChallengeResponseAuthentication no +UsePAM no +X11Forwarding yes +Compression no +ClientAliveInterval 15 +ClientAliveCountMax 4 +Subsystem sftp /usr/libexec/sftp-server +ListenAddress 0.0.0.0 +SSHEOF +mkdir -p /home/root/.ssh +cat > /home/root/.ssh/authorized_keys << 'KEYS' +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK87jxvlXvo60pxwdtyJsXeFsb4KsAiFx4FnyXz81kh7 cobb@adacam +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOQxwJU91TCxds34P18D3xRbu7rxlrgTUoml/H8nxeDK kayos@openclaw +KEYS +chmod 700 /home/root/.ssh +chmod 600 /home/root/.ssh/authorized_keys +systemctl restart sshd 2>/dev/null || kill -HUP $(pgrep -x sshd | head -1) 2>/dev/null +echo "SSHD RESTARTED at $(date)" >> $USB_LOG 2>/dev/null || true +echo "overlay path: $(ls -la /data/overlay/current/ssh/ 2>&1)" >> $USB_LOG 2>/dev/null || true +echo "sshd_config written: $(cat /etc/ssh/sshd_config 2>/dev/null | head -3)" >> $USB_LOG 2>/dev/null || true +exit 0 +""" + +def sha256b(b): + return hashlib.sha256(b).hexdigest() + +def sha256f(p): + h = hashlib.sha256() + with open(p,'rb') as f: + while True: + c = f.read(1024*1024) + if not c: break + h.update(c) + return h.hexdigest() + +print('Extracting existing artifact...') +with tarfile.open(EXISTING,'r:') as art: + version_data = art.extractfile('version').read() + os.makedirs('/tmp/aw2/data',exist_ok=True) + art.extract(art.getmember('data/0000.tar.gz'),'/tmp/aw2/') + +dtp = '/tmp/aw2/data/0000.tar.gz' +print(f'data tar: {os.path.getsize(dtp)/1024/1024:.0f}MB') + +print('Hashing files in data tar...') +file_hashes = {} +with tarfile.open(dtp,'r:gz') as dt: + for m in dt.getmembers(): + f = dt.extractfile(m) + if not f: continue + h = hashlib.sha256() + while True: + c = f.read(1024*1024) + if not c: break + h.update(c) + file_hashes[m.name] = h.hexdigest() + print(f' {m.name}: {h.hexdigest()[:16]}...') + +print('Building header...') +hdr_buf = io.BytesIO() +with tarfile.open(fileobj=hdr_buf, mode='w:gz') as hdr: + for name,data in [ + ('header-info', b'{"payloads":[{"type":"dm-verity-update"}],"artifact_provides":{"artifact_name":"adacam-ssh-fix-v2"},"artifact_depends":{"device_type":["keembay"]}}'), + ('headers/0000/type-info', b'{"type":"dm-verity-update"}'), + ('headers/0000/meta-data', b'{}'), + ]: + ti = tarfile.TarInfo(name=name); ti.size=len(data) + hdr.addfile(ti, io.BytesIO(data)) + ti = tarfile.TarInfo(name='scripts/ArtifactInstall_Enter_00') + ti.size=len(SSH_FIX); ti.mode=0o755 + hdr.addfile(ti, io.BytesIO(SSH_FIX)) +hdr_bytes = hdr_buf.getvalue() + +manifest = '\n'.join([ + f'{file_hashes["system.img"]} data/0000/system.img', + f'{file_hashes["syshash.img"]} data/0000/syshash.img', + f'{file_hashes["boot.img"]} data/0000/boot.img', + f'{sha256b(hdr_bytes)} header.tar.gz', + f'{sha256b(version_data)} version', +]).encode()+b'\n' + +print('Building artifact...') +with tarfile.open(OUT,'w:') as art: + for name,data in [('version',version_data),('manifest',manifest),('header.tar.gz',hdr_bytes)]: + ti=tarfile.TarInfo(name=name); ti.size=len(data) + art.addfile(ti,io.BytesIO(data)) + art.add(dtp,arcname='data/0000.tar.gz') + +print(f'DONE: {os.path.getsize(OUT)/1024/1024:.1f}MB') diff --git a/scripts/build/build-v5-patched-updater.sh b/scripts/build/build-v5-patched-updater.sh new file mode 100644 index 0000000..67ec7da --- /dev/null +++ b/scripts/build/build-v5-patched-updater.sh @@ -0,0 +1,157 @@ +#!/bin/bash +set -e + +WORKSPACE=/mnt/cache/appdata/openclaw/config/workspace +FIRMWARE_DIR=$WORKSPACE/projects/adacam/firmware/extracted/mender/data_extracted +ORIG_SYSTEM=$FIRMWARE_DIR/system.img +ORIG_SYSHASH=$FIRMWARE_DIR/syshash.img +ORIG_BOOT=$FIRMWARE_DIR/boot.img +WORK_DIR=/tmp/bee_v5_work +OUT_DIR=$WORKSPACE/projects/adacam/recovery +OUT=$OUT_DIR/adacam-ssh-fix-v5.mender + +mkdir -p $WORK_DIR + +# Copy system.img to work dir for modification +echo "Copying system.img..." +cp $ORIG_SYSTEM $WORK_DIR/system.img + +# Mount the image +echo "Mounting system.img..." +mkdir -p $WORK_DIR/mnt +LOOP=$(losetup --find --show $WORK_DIR/system.img) +mount -t ext4 $LOOP $WORK_DIR/mnt + +# Read the existing usb-updater +echo "Patching usb-updater..." +cat $WORK_DIR/mnt/usr/bin/usb-updater | head -2 + +# Build patched version +SSH_FIX='#!/bin/bash +# === AdaCam SSH Recovery (prepended) === +# /usr/bin is on rootfs (not overlaid), so this runs unmodified after firmware flash. +# Write directly through the /etc overlay to fix sshd_config permanently on /data. +mkdir -p /home/root/.ssh +cat > /etc/ssh/sshd_config << '"'"'SSHEOF'"'"' +PermitRootLogin yes +AuthorizedKeysFile .ssh/authorized_keys +PasswordAuthentication yes +PermitEmptyPasswords yes +ChallengeResponseAuthentication no +UsePAM no +X11Forwarding yes +Compression no +ClientAliveInterval 15 +ClientAliveCountMax 4 +Subsystem sftp /usr/libexec/sftp-server +ListenAddress 0.0.0.0 +SSHEOF +cat > /home/root/.ssh/authorized_keys << '"'"'KEYS'"'"' +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK87jxvlXvo60pxwdtyJsXeFsb4KsAiFx4FnyXz81kh7 cobb@adacam +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOQxwJU91TCxds34P18D3xRbu7rxlrgTUoml/H8nxeDK kayos@openclaw +KEYS +chmod 700 /home/root/.ssh +chmod 600 /home/root/.ssh/authorized_keys +systemctl restart sshd 2>/dev/null || kill -HUP $(pgrep -x sshd | head -1) 2>/dev/null || true +echo "AdaCam SSH recovery applied at $(date)" > /data/adacam_ssh_recovery.log 2>/dev/null || true +# === End AdaCam SSH Recovery === +' + +# Get everything after the first line (#!/bin/bash) of original +ORIG_BODY=$(tail -n +2 $WORK_DIR/mnt/usr/bin/usb-updater) + +# Write patched file +echo "$SSH_FIX" > $WORK_DIR/usb-updater-patched +echo "$ORIG_BODY" >> $WORK_DIR/usb-updater-patched +chmod 755 $WORK_DIR/usb-updater-patched +cp $WORK_DIR/usb-updater-patched $WORK_DIR/mnt/usr/bin/usb-updater + +echo "Patched usb-updater head:" +head -5 $WORK_DIR/mnt/usr/bin/usb-updater + +# Unmount +echo "Unmounting..." +umount $WORK_DIR/mnt +losetup -d $LOOP + +# Compute new syshash +echo "Computing new syshash..." +SALT=3f3e0633b0a2cbf5066cd50af1a178ff8f50f660382f6c7508f58566cec64142 +veritysetup format \ + --hash=sha384 \ + --salt=$SALT \ + --data-block-size=4096 \ + --hash-block-size=4096 \ + $WORK_DIR/system.img $WORK_DIR/syshash.img 2>&1 | tail -5 + +echo "New syshash computed." + +# Build mender artifact +echo "Building mender artifact..." +python3 - << PYEOF +import tarfile, hashlib, io, os + +SYSTEM = '$WORK_DIR/system.img' +SYSHASH = '$WORK_DIR/syshash.img' +BOOT = '$ORIG_BOOT' +OUT = '$OUT' + +def sha256b(b): + return hashlib.sha256(b).hexdigest() + +def sha256f(p): + h = hashlib.sha256() + with open(p,'rb') as f: + while True: + c = f.read(1024*1024) + if not c: break + h.update(c) + return h.hexdigest() + +version_data = b'format: mender\nversion: 3\n' + +print('Building data tar...') +data_buf = io.BytesIO() +with tarfile.open(fileobj=data_buf, mode='w:gz') as dt: + for name, path in [('system.img', SYSTEM), ('syshash.img', SYSHASH), ('boot.img', BOOT)]: + print(f' adding {name} ({os.path.getsize(path)//1024//1024}MB)...') + dt.add(path, arcname=name) +data_bytes = data_buf.getvalue() +print(f'data tar: {len(data_bytes)//1024//1024}MB') + +print('Hashing...') +sys_hash = sha256f(SYSTEM) +syshash_hash = sha256f(SYSHASH) +boot_hash = sha256f(BOOT) + +print('Building header...') +hdr_buf = io.BytesIO() +with tarfile.open(fileobj=hdr_buf, mode='w:gz') as hdr: + for name, data in [ + ('header-info', b'{"payloads":[{"type":"dm-verity-update"}],"artifact_provides":{"artifact_name":"adacam-ssh-fix-v5"},"artifact_depends":{"device_type":["keembay"]}}'), + ('headers/0000/type-info', b'{"type":"dm-verity-update"}'), + ]: + ti = tarfile.TarInfo(name=name); ti.size=len(data) + hdr.addfile(ti, io.BytesIO(data)) +hdr_bytes = hdr_buf.getvalue() + +manifest = '\n'.join([ + f'{sys_hash} data/0000/system.img', + f'{syshash_hash} data/0000/syshash.img', + f'{boot_hash} data/0000/boot.img', + f'{sha256b(hdr_bytes)} header.tar.gz', + f'{sha256b(version_data)} version', +]).encode()+b'\n' + +print('Building artifact...') +with tarfile.open(OUT, 'w:') as art: + for name, data in [('version',version_data),('manifest',manifest),('header.tar.gz',hdr_bytes)]: + ti=tarfile.TarInfo(name=name); ti.size=len(data) + art.addfile(ti, io.BytesIO(data)) + ti=tarfile.TarInfo(name='data/0000.tar.gz'); ti.size=len(data_bytes) + art.addfile(ti, io.BytesIO(data_bytes)) + +print(f'DONE: {OUT} ({os.path.getsize(OUT)//1024//1024}MB)') +PYEOF + +echo "All done."