docs+scripts: project status, build scripts, patched usb-updater v5

- PROJECT_STATUS.md: full project log (hardware, partitions, artifacts, lessons learned, next steps)
- scripts/build/build-artifact-from-existing.py: rebuild artifact from existing data tar with new header
- scripts/build/build-v5-patched-updater.sh: patch system.img usb-updater + build artifact
- recovery/usb-updater-v5-patched: patched usb-updater with SSH recovery prepended

adacam-ssh-fix-v5.mender: 403MB, SHA256 acfbd16db9620f23785f8b103ffaeff6aed780f383273a61a23c8002f2bf0980
Status: PENDING TEST on replacement Bee (192.168.0.10)
This commit is contained in:
Kayos 2026-03-16 09:58:45 -07:00
parent fa38d03cc6
commit ed7ae5ba57
4 changed files with 706 additions and 0 deletions

269
PROJECT_STATUS.md Normal file
View file

@ -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`

176
recovery/usb-updater-v5-patched Executable file
View file

@ -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

View file

@ -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')

View file

@ -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."