A Raspberry Pi secured inside a briefcase with dual-sensor tamper detection (Hall effect primary + light secondary), a 120-second password challenge on unauthorized open, cryptographic drive wipe on failure, and a quantum-safe encrypted recovery USB for reimaging.
Implementation: packages/tamper_briefcase/
Secure physical transport for a field-deployable agent terminal. The briefcase provides the physical security layer for operating AI agents in untrusted environments -- extending the digital security model (LUKS, split-privilege services) into the physical domain (tamper detection, sensor hardening, cryptographic wipe).
+------------------------------------------------------------------+
| BRIEFCASE (Pelican 1490 or similar hard case) |
| |
| +------------+ |
| | Neodymium | <- lid-mounted magnet (epoxied near latch) |
| | Magnet | |
| +------------+ |
| - - - - - - - - - - lid / base hinge line - - - - - - - - - |
| |
| +------------+ +----------+ GPIO +--------------------+ |
| | Hall-effect| | BH1750 |<-------->| Raspberry Pi 5 | |
| | sensor | | Light | I2C + | | |
| | (SS49E / | | Sensor | GPIO | +==============+ | |
| | A3144) | +----------+ | | LUKS2 Root | | |
| +------------+ | | LUKS2 Data | | |
| ^ aligned with lid magnet | +==============+ | |
| | | |
| +------------------+ USB-C | Recovery USB -> | |
| | USB-C Power |<------->internal | (port, serial- | |
| | Bank (cradle) | lead USB-C | locked) | |
| | >=10,000 mAh | +--------------------+ |
| | (slides out | |
| | for charging) | |
| +------------------+ |
+------------------------------------------------------------------+
A light sensor alone is unreliable as a primary trigger. It suffers from seam leakage in bright environments, dark-current drift and temperature variation, and the "briefcase moved under a lamp" problem. Debouncing helps with noise but cannot distinguish "lid opened" from "ambient conditions changed."
The fix: a Hall effect sensor as the authoritative lid-state indicator and a light sensor as secondary confirmation and anti-tamper evidence.
| Sensor | Role | Failure Mode | Consequence |
|---|---|---|---|
| Hall effect (A3144) | Primary: authoritative lid open/close detection | Magnet removed/shielded = permanent "open" reading | Triggers challenge (fail-safe) |
| BH1750 light sensor | Secondary: confirms physical exposure, detects bypass attempts | Sensor covered or blinded | Hall still triggers independently |
Hall says CLOSED + Light LOW -> Normal (armed, sleeping)
Hall says CLOSED + Light HIGH -> Suspicious (log anomaly, possible bypass attempt)
Hall says OPEN + Light HIGH -> Confirmed tamper (trigger challenge)
Hall says OPEN + Light LOW -> Tamper in dark environment (still trigger -- Hall is authoritative)
Key principle: Hall alone is sufficient to trigger. Light alone never triggers -- it adds confidence and detects sensor-bypass scenarios.
Closing the briefcase does not instantly arm the system. The lid must remain continuously closed (Hall reading "closed") for a configurable delay (default 15 seconds) before transitioning from DISARMED to ARMED. This prevents false triggers while adjusting the latch or contents.
Hall Effect Sensor (A3144 -- digital latch, recommended for simplicity)
---------------------------------------------------------------------
VCC ----------> Pin 1 (3.3V)
GND ----------> Pin 9 (GND)
OUT ----------> Pin 7 (GPIO 4)
+-- 10k pull-up to 3.3V
Magnet present -> OUT = LOW (lid closed)
Magnet absent -> OUT = HIGH (lid open)
BH1750 Light Sensor (I2C)
-------------------------
VCC ----------> Pin 1 (3.3V)
GND ----------> Pin 6 (GND)
SDA ----------> Pin 3 (GPIO 2 / SDA1)
SCL ----------> Pin 5 (GPIO 3 / SCL1)
ADDR ----------> GND (sets I2C address to 0x23)
Neodymium Magnet (12mm disc, N52 grade)
---------------------------------------
Epoxied to inside of lid, aligned directly above Hall sensor
when lid is closed. 5-10mm gap is fine for A3144.
Mount the Hall sensor on the base of the briefcase near the latch mechanism. Epoxy a small neodymium disc magnet to the corresponding spot on the lid interior. When closed, the magnet sits directly above the Hall sensor (within ~10mm). The BH1750 mounts nearby, oriented upward toward the lid seam -- the first entry point for light.
| Component | Recommended Model | Purpose | Est. Cost |
|---|---|---|---|
| Raspberry Pi | Pi 5 (4GB+) | Main compute | $60-80 |
| MicroSD Card | Samsung PRO Endurance 128GB | OS + data (high-endurance) | $20 |
| Hall Effect Sensor | A3144 (digital latch) | Primary lid-state detection | $1-2 |
| Neodymium Magnet | 12mm N52 disc | Hall sensor trigger, lid-mounted | $2-3 |
| Light Sensor | BH1750 (I2C) | Secondary tamper confirmation | $3-5 |
| USB-C Power Bank | Anker 325 or similar (10,000mAh+) | Removable, rechargeable power | $25-35 |
| USB-C cable (short) | 15cm USB-C to USB-C, right-angle | Internal Pi-to-bank connection | $5 |
| Briefcase | Pelican 1490 Laptop Case | Hardened, light-sealed enclosure | $100-150 |
| Recovery USB | IronKey or Samsung T7 Shield | Quantum-safe recovery media | $50-80 |
| Pull-up Resistor | 10k (for A3144) | Hall sensor signal conditioning | $0.10 |
| Mounting | Brass standoffs, M2.5 screws, velcro, foam | Secure all components inside case | $10-15 |
A standard USB-C power bank in a foam cradle. No proprietary UPS HATs.
The bank sits in a snug foam cutout inside the briefcase, connected to the Pi via a short (15cm) USB-C cable with strain relief. To charge, unplug the cable and slide the bank out. To restore power, slide it back in and reconnect.
Benefits over HAT-based UPS solutions: no proprietary APIs, no pogo pin alignment issues, easy to swap/upgrade the bank, and you can charge it anywhere with any USB-C charger. A 10,000mAh bank will run a Pi 5 for roughly 4-6 hours depending on workload.
Optional enhancement: A small always-on MCU (e.g., ATtiny85 or RP2040) powered by the bank with negligible draw (~1mA) can monitor the Hall sensor even when the Pi is off, and trigger a wake signal or audible alarm. This covers the "power bank disconnected while case is closed" attack vector.
The system splits into three systemd services enforcing least privilege:
+-------------------------------------------------------------+
| SERVICE ARCHITECTURE |
| |
| +----------------------+ |
| | tamper-sensor.service| <- runs as unprivileged "tamper" |
| | (reads Hall + Light) | <- no device writes, no crypto |
| | Emits: LidOpened | <- writes JSON to root-owned |
| | LidClosed | FIFO |
| | LightAnomaly | |
| +----------+-----------+ |
| | FIFO |
| +----------v-----------+ |
| | tamper-gate.service | <- runs as root (minimal) |
| | (orchestrator) | <- receives sensor events |
| | Manages: arming FSM | <- launches challenge subprocess|
| | challenge | <- decides on wipe |
| | lock/unlock | |
| +----------+-----------+ |
| | only on confirmed failure |
| +----------v-----------+ |
| | tamper-wipe.service | <- ConditionPathExists guard |
| | (one-shot) | <- requires explicit trigger |
| | Irreversible action | file to start |
| +-----------------------+ |
+-------------------------------------------------------------+
This gives you:
- Least privilege: The sensor daemon has zero write access to block devices or crypto subsystems.
- Explicit gating: The wipe can only execute when a trigger file exists, created only by the gate service after a confirmed challenge failure.
- Audit trail: Each service logs independently. The wipe service's activation is a distinct systemd event.
- Reduced blast radius: A bug in the sensor code cannot accidentally trigger a wipe.
All application code is in Rust. See packages/tamper_briefcase/ for the full implementation:
| Crate | Binary | Role |
|---|---|---|
tamper-common |
(library) | Shared types: TamperEvent, SystemState, Config |
tamper-sensor |
tamper-sensor |
GPIO + I2C sensor reading, FIFO event emission |
tamper-gate |
tamper-gate |
Arming FSM, challenge dispatch, wipe authorization |
tamper-challenge |
tamper-challenge |
Interactive password prompt with scrypt verification |
tamper-recovery |
tamper-recovery |
PQC key generation, wrapping, signing, verification |
Bash scripts handle raw system operations:
| Script | Role |
|---|---|
scripts/wipe_drive.sh |
LUKS header destruction via dd/cryptsetup |
scripts/recovery_launcher.sh |
Live USB recovery orchestration |
/dev/mmcblk0
+-- p1 (256MB) boot -- FAT32, unencrypted (kernel + initramfs)
+-- p2 (8GB) rootfs -- ext4, LUKS2 encrypted
+-- p3 (rest) data -- ext4, LUKS2 encrypted <- PRIMARY WIPE TARGET
| Secret | Protects | Stored Where | Backed Up How |
|---|---|---|---|
| Root passphrase | LUKS on /dev/mmcblk0p2 (OS) | Entered at boot (or keyfile in initramfs) | Paper backup in secure location |
| Data passphrase | LUKS on /dev/mmcblk0p3 (user data) | Entered after boot / keyfile | Paper backup + recovery USB (wrapped) |
| Tamper password | Challenge on lid-open | /etc/tamper/password.hash (scrypt) |
You remember it |
During recovery, you need the root passphrase to unlock the freshly flashed OS and the data passphrase to recreate the data volume. The recovery USB stores the data passphrase in a quantum-safe wrapped form.
# Encrypted root (entered at boot via initramfs prompt)
sudo cryptsetup luksFormat --type luks2 \
--cipher aes-xts-plain64 \
--key-size 512 \
--hash sha512 \
--iter-time 5000 \
/dev/mmcblk0p2
# Encrypted data partition
sudo cryptsetup luksFormat --type luks2 \
--cipher aes-xts-plain64 \
--key-size 512 \
--hash sha512 \
--iter-time 5000 \
/dev/mmcblk0p3
# Open and format data
sudo cryptsetup open /dev/mmcblk0p3 data_crypt
sudo mkfs.ext4 -L SECURE_DATA /dev/mapper/data_cryptAES-256-XTS is already quantum-resistant for symmetric data-at-rest encryption. The LUKS container does not need PQC. Where PQC matters is in how you protect and distribute the recovery key material -- specifically, public-key operations like key encapsulation and digital signatures.
The threat model: an adversary who captures your recovery USB today and stores the encrypted data, hoping to break the public-key wrapping layer with a future quantum computer. By using a hybrid classical+PQ scheme for key wrapping, you ensure the recovery secret remains protected even against harvest-now-decrypt-later attacks.
Recovery USB (32GB+)
+-- Partition 1 (512MB) -- FAT32, unencrypted
| +-- recovery_public.json <- PQ public keys + wrapped blob metadata
| +-- wrapped_secret.bin <- Hybrid-encrypted recovery secret
|
+-- Partition 2 (rest) -- LUKS2 encrypted
| (passphrase derived from unwrapped recovery secret)
+-- base_image.img.zst <- Compressed Pi OS image
+-- base_image.img.zst.sig <- ML-DSA-87 detached signature
+-- tamper_config/ <- /etc/tamper/* files
| +-- password.hash
| +-- salt
+-- device_secrets.json.enc <- Wrapped root + data passphrases
+-- manifest.json <- Image version, SHA-256 checksums
SETUP (on air-gapped workstation):
===================================
1. Generate a 64-byte random RECOVERY SECRET
+-- This is the master secret. Everything derives from it.
2. Derive the LUKS passphrase for USB Partition 2 via KDF:
+-- luks_passphrase = HKDF-SHA512(recovery_secret, salt, "usb-luks")
3. Derive a wrapping key for the device secrets bundle:
+-- wrap_key = HKDF-SHA512(recovery_secret, salt, "device-secrets-wrap")
4. Encrypt device_secrets.json with wrap_key (AES-256-GCM)
+-- Contains: root_passphrase, data_passphrase
5. Wrap the RECOVERY SECRET using hybrid key encapsulation:
+-- Classical: X25519 ECDH -> shared_secret_classical
+-- Post-Quantum: ML-KEM-1024 -> shared_secret_pq
+-- Combined: SHA-512(shared_secret_classical || shared_secret_pq) -> wrapping_key
+-- AES-256-GCM-encrypt(recovery_secret, wrapping_key) -> wrapped_blob
6. Sign the disk image with ML-DSA-87
7. Store on USB:
+-- Partition 1 (clear): public keys, wrapped_blob, salt, signature
+-- Partition 2 (LUKS): image, device_secrets.json.enc, config
8. Store OFFLINE (paper / air-gapped):
+-- Classical private key (X25519)
+-- PQ private key (ML-KEM-1024)
+-- Signing private key (ML-DSA-87)
+-- Recovery secret (emergency backup)
RECOVERY (on target Pi, booted from live USB):
===============================================
1. User enters recovery password at prompt
+-- This password decrypts the offline-stored private keys
2. Unwrap: Use private keys to decapsulate -> recover wrapping_key
+-- Decrypt wrapped_blob -> RECOVERY SECRET
3. Derive luks_passphrase from recovery secret
+-- Open USB Partition 2
4. Verify image signature (ML-DSA-87)
5. Flash image to SD card
6. Derive wrap_key, decrypt device_secrets.json.enc
+-- Retrieve root_passphrase + data_passphrase
7. Configure the freshly flashed system:
+-- Set up LUKS on root with root_passphrase
+-- Set up LUKS on data with data_passphrase
+-- Restore tamper config from tamper_config/
Key generation and unwrapping are implemented in the tamper-recovery Rust crate:
# Generate recovery material (air-gapped workstation)
tamper-recovery generate --output-dir ./recovery_keys
# Sign a disk image
tamper-recovery sign --image base_image.img.zst \
--private-key-file ./recovery_keys/private/recovery_private.json \
--output base_image.img.zst.sig
# Verify image signature (recovery environment)
tamper-recovery verify --image base_image.img.zst \
--signature base_image.img.zst.sig \
--public-meta-file recovery_public.json
# Unwrap device secrets (recovery environment)
tamper-recovery unwrap \
--private-key-file /media/token/recovery_private.json \
--public-meta-file /mnt/recovery_meta/recovery_public.json \
--wrapped-secret-file /mnt/recovery_meta/wrapped_secret.bin \
--encrypted-secrets-file /mnt/recovery/device_secrets.json.enc+------------------------------------------------------+
| LID INTERIOR |
| |
| +----------+ |
| | Neodymium| <- 12mm disc, epoxied, aligned above |
| | Magnet | Hall sensor when lid is closed |
| +----------+ |
| |
|---- hinge -------------------------------------------|
| BASE |
| |
| +-----+ +---------+ |
| |Hall | | BH1750 | <- both near latch edge |
| |A3144| | (facing | |
| | | | up) | |
| +--+--+ +----+----+ |
| | | jumper wires |
| +--+----------+----------+ +-------------------+ |
| | | | | |
| | Raspberry Pi 5 | | USB-C Power Bank | |
| | (brass standoffs | | (foam cradle, | |
| | on acrylic plate) | | slides out) | |
| | | | | |
| +------------------------+ +-------------------+ |
| |
| +------------------------------------------------+ |
| | Foam padding / weatherstripping around edges | |
| +------------------------------------------------+ |
+------------------------------------------------------+
-
Hall sensor + magnet alignment: Most critical mechanical detail. The A3144 needs the magnet within ~10mm when closed. Test alignment before permanently mounting -- use blu-tack first, mark positions, then epoxy.
-
Light sealing: Line the briefcase seam with adhesive foam weatherstripping. Aim for < 1 lux when sealed for huge margin before false triggers matter (since light is secondary).
-
Power bank cradle: Cut a foam block to snugly hold the bank. Use a short right-angle USB-C cable with strain relief (zip-tied to the mounting plate) so repeated insertion does not stress the Pi's port.
-
Ventilation: A Pi 5 under load can thermal-throttle in a sealed case. If running anything heavier than idle monitoring, drill small ventilation holes in the mounting plate or add a slim 5V fan. For a mostly-sleeping tamper monitor, passive cooling is fine.
Run the consolidated setup script on the Pi:
cd packages/tamper_briefcase
cargo build --release
./deploy/setup.sh# 1. Flash Raspberry Pi OS Lite (64-bit) to SD card
sudo rpi-imager
# 2. Create tamper user
sudo useradd --system --no-create-home --shell /usr/sbin/nologin tamper
sudo usermod -aG i2c,gpio tamper
# 3. Enable I2C
sudo raspi-config # Interface Options -> I2C -> Enable
# 4. Install dependencies
sudo apt update && sudo apt install -y cryptsetup zstd i2c-tools
# 5. Verify sensors
i2cdetect -y 1 # Should show 0x23 (BH1750)
# 6. Set tamper password
sudo tamper-challenge setup
# 7. Install binaries
sudo cp target/release/tamper-sensor /usr/local/bin/
sudo cp target/release/tamper-gate /usr/local/bin/
sudo cp target/release/tamper-challenge /usr/local/bin/
sudo cp target/release/tamper-recovery /usr/local/bin/
sudo cp scripts/wipe_drive.sh /usr/local/bin/
sudo chmod +x /usr/local/bin/wipe_drive.sh
# 8. Install and enable services
sudo cp systemd/*.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable tamper-sensor tamper-gate
sudo systemctl start tamper-sensor tamper-gate
# 9. Create LUKS partitions (see Disk Encryption section)
# 10. Build recovery USB on air-gapped workstation (see Recovery section)Test 1 -- Sensor reads:
Close case, verify Hall reads LOW, lux < 1.
Open case, verify Hall reads HIGH, lux jumps.
Test 2 -- Arming delay:
Close case, watch journal. Confirm ARMING state for 15s, then ARMED.
Reopen during arming -- confirm return to DISARMED.
Test 3 -- Challenge (non-destructive):
Temporarily replace wipe_drive.sh with a no-op script.
Arm the system, open the case, verify challenge prompt appears.
Enter correct password -- verify DISARMED.
Enter wrong password 3x -- verify wipe trigger file is created.
Test 4 -- Wipe (sacrificial SD card):
Use a throwaway SD card with a LUKS partition.
Run the full wipe flow. Verify LUKS header is destroyed
(cryptsetup isLuks should fail afterward).
Test 5 -- Recovery:
Use the recovery USB to reimage the wiped card.
Verify LUKS is reconfigured and tamper services start on boot.
Test 6 -- Anomaly detection:
With system armed, shine a flashlight through seams while
holding magnet in place (simulating Hall bypass). Verify
LIGHT_ANOMALY events logged and escalation after threshold.
| Threat | Mitigation |
|---|---|
| Cold boot attack | Enable encrypted swap; consider kernel memory encryption |
| SD card physical removal | LUKS2 FDE -- data at rest is encrypted |
| Light sensor bypass (tape/cover) | Hall is primary and independent; sustained light anomaly escalates |
| Hall sensor bypass (external magnet) | Light sensor detects exposure; anomaly counter triggers challenge |
| Both sensors bypassed simultaneously | Requires precise physical access; consider potting sensors in epoxy |
| Power cut to prevent wipe | Battery bank provides independent power |
| Power bank removed while closed | Optional: always-on MCU monitors Hall independently |
| USB device injection | Lock USB ports by serial number; only allow recovery stick |
| Recovery stick theft | Hybrid PQ+classical encryption; private keys stored separately offline |
| Shoulder surfing | Consider OLED display instead of HDMI for password entry |
| Wipe triggered accidentally | Split-service architecture with explicit trigger file guard |
Paired audio device connected through the briefcase. Voice-command disconnect triggers a disarm/re-arm cycle. Reconnection requires:
- Opening the briefcase (triggers tamper challenge)
- Authenticating via password challenge
- Disarming the wipe protocol
- Re-pairing the Bluetooth audio device
This ensures the headset cannot be silently reassociated without passing through the full tamper response chain.
- Network alerting: Send Signal/Telegram/webhook on tamper before countdown starts
- Dead man's switch: Server expects heartbeat every N hours; triggers remote wipe on silence
- Decoy partition: Wrong password shows fake desktop; real data stays in hidden LUKS volume
- TPM 2.0 HAT: Bind LUKS keys to hardware state (detects SD card moved to different Pi)
- Accelerometer (MPU6050): Detect motion/tilt as additional tamper signal
- GPS geofencing: Alert or trigger if briefcase leaves defined geographic area
- Faraday cage lining: Copper mesh inside case to block RF (WiFi/Bluetooth attacks)
- Always-on MCU co-processor: ATtiny85/RP2040 on battery, monitors Hall even when Pi is off, triggers piezo alarm