Bose SoundTouch Toolkit

Documentation for controlling and preserving Bose SoundTouch devices

View the Project on GitHub gesellix/Bose-SoundTouch

Capture Device Pairing Traffic

Step-by-step runbook for factory-resetting a SoundTouch speaker, pairing it to a Bose cloud account, and capturing every cloud request via mitmproxy. Tested on Apple Silicon Mac.

Goal: obtain a full .mitm recording of the account-pairing flow (streaming.bose.com) triggered by the official Android app.


Overview

Phase 0  Pre-flight checks
Phase 1  Factory reset speaker
Phase 2  Provision speaker Wi-Fi (AP mode, console)
Phase 3  Start mitmproxy + emulator + Frida
Phase 4  Pair speaker via Bose app (adb-driven)
Phase 5  Save & inspect recording

The Android emulator does not have Bluetooth, so the standard BLE setup path is unavailable. Instead:

  1. Provision Wi-Fi directly over the speaker’s AP web server (Phase 2).
  2. Once the speaker is on the LAN, the Bose app discovers it via mDNS — no BLE needed.

Phase 0 — Pre-flight

The emulator setup is fully scripted. Run once per machine:

# Place the Bose APK at scripts/android/bose.apk first (see BOSE-APP-ADB-Emulator.md § 1)
# Run mitmweb once to generate the CA: mitmweb --listen-port 8080 (Ctrl-C after it starts)

scripts/android/setup-mitm-avd.sh

This installs the system image, creates an AVD named bose-mitm, installs the mitmproxy cert and Bose APK, and saves an emulator snapshot mitm-ready — so subsequent sessions never repeat the cert/reboot cycle.

For subsequent sessions, Phase 3 below is replaced by a single command:

scripts/android/start-mitm-session.sh

Manual steps are only needed if you want to understand the internals; see BOSE-APP-ADB-Emulator.md for the full manual walkthrough.


Phase 1 — Factory Reset Speaker

Perform the reset for your model (see DEVICE-INITIAL-SETUP.md § 5 for full table):

Model Sequence
SoundTouch 10 Power on; hold Preset 1 + Vol − ~10 s → solid amber Wi-Fi LED
SoundTouch 20 Power on; hold Preset 1 + Vol − ~10 s → lights blink L→R, amber
SoundTouch 20/30 Series III Hold Preset 1 + Preset 6 ~10 s
SoundTouch 300 Hold Vol − ~15 s until light bar blinks

Wait until the white LED sweep / restart animation completes (~30 s). The speaker is now in setup mode and broadcasting its own Wi-Fi AP.


Phase 2 — Provision Speaker Wi-Fi (AP Mode)

2.1 Connect Mac to Speaker AP

# Find the SSID — use System Settings → Wi-Fi or:
#   sudo wdutil info   (macOS Sequoia+, airport command removed)

# Connect (replace SSID with actual value)
SPEAKER_SSID="Bose SoundTouch XXXX"
networksetup -setairportnetwork en0 "$SPEAKER_SSID"

# Confirm: speaker web UI reachable at 192.0.2.1 (client gets 192.0.2.2)
curl -s --connect-timeout 5 http://192.0.2.1/ | head -3

2.2 Push Home Wi-Fi Credentials

The setup UI at http://192.0.2.1/ uses the SoundTouch API on port 8090. Push credentials directly:

HOME_SSID="MyHomeNetwork"
HOME_PASS="MyPassword"

# Optional: trigger a site survey first so the speaker finds your SSID
curl -s -X POST http://192.0.2.1:8090/performWirelessSiteSurvey \
  -H 'Content-Type: text/xml' \
  --data-raw '<PerformWirelessSiteSurvey timeout="5"/>'

curl -s -X POST http://192.0.2.1:8090/addWirelessProfile \
  -H 'Content-Type: text/xml' \
  --data-raw "<AddWirelessProfile><profile ssid=\"${HOME_SSID}\" password=\"${HOME_PASS}\" securityType=\"wpa_or_wpa2\" /></AddWirelessProfile>"

Expected response: <AddWirelessProfileResponse />

2.3 Reconnect Mac to Home Network

HOME_SSID="MyHomeNetwork"
HOME_PASS="MyPassword"

networksetup -setairportnetwork en0 "$HOME_SSID" "$HOME_PASS"

2.4 Wait for Speaker to Join LAN

# Poll mDNS until the speaker appears (~15-30 s)
echo "Waiting for speaker on LAN..."
until dns-sd -B _soundtouch._tcp local 2>&1 | grep -m1 "Add"; do sleep 2; done
echo "Speaker is online"

# Resolve its IP
dns-sd -L "$(dns-sd -B _soundtouch._tcp local 2>&1 | grep Add | awk '{print $7}')" \
  _soundtouch._tcp local 2>&1 | grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}'

Or just scan for open port 8090:

# Quick scan of your /24 subnet for port 8090
SUBNET="192.168.1"   # adjust to your local subnet
for i in $(seq 1 254); do
  (ping -c1 -W1 ${SUBNET}.$i &>/dev/null && \
   nc -z -w1 ${SUBNET}.$i 8090 2>/dev/null && \
   echo "${SUBNET}.$i") &
done
wait

Phase 3 — Start mitmproxy, Emulator, Frida

Run each block in a separate terminal tab.

3.1 Start mitmweb (native macOS app)

Note: Docker mitmproxy does not work here — its NAT layer prevents the emulator from reaching it. Use the native macOS app instead (download: https://downloads.mitmproxy.org/12.2.2/mitmproxy-12.2.2-macos-arm64.tar.gz).

CAPTURE="bose-pairing-$(date +%Y%m%d-%H%M%S).mitm"
/Applications/mitmproxy.app/Contents/MacOS/mitmweb \
  --web-host 0.0.0.0 --listen-port 8080 --mode regular \
  --set web_password=bose \
  -w "scripts/android/captures/${CAPTURE}"
# Captures → scripts/android/captures/
# Web UI   → http://127.0.0.1:8081/?token=bose

3.2 Start Emulator

~/Library/Android/sdk/emulator/emulator -avd Pixel_6_API33 -writable-system &
echo "Waiting for emulator boot..."
adb wait-for-device
adb -s emulator-5554 wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done'
echo "Emulator ready"

Enable root and install certificate (only needed once per emulator session):

adb -s emulator-5554 root
adb -s emulator-5554 shell avbctl disable-verification
adb -s emulator-5554 reboot
adb -s emulator-5554 wait-for-device
adb -s emulator-5554 root

HASH=$(openssl x509 -inform PEM -subject_hash_old \
  -in ~/.mitmproxy/mitmproxy-ca-cert.pem | head -1)

adb -s emulator-5554 push ~/.mitmproxy/mitmproxy-ca-cert.pem /data/local/tmp/mitmproxy.pem
adb -s emulator-5554 shell su 0 mkdir -p /data/misc/user/0/cacerts-added
adb -s emulator-5554 shell su 0 \
  cp /data/local/tmp/mitmproxy.pem /data/misc/user/0/cacerts-added/${HASH}.0
adb -s emulator-5554 shell su 0 \
  chmod 644 /data/misc/user/0/cacerts-added/${HASH}.0

Set proxy to Mac IP:

MAC_IP=$(ipconfig getifaddr en0)
adb -s emulator-5554 shell settings put global http_proxy "${MAC_IP}:8080"
echo "Proxy set to ${MAC_IP}:8080"

Confirm emulator can reach the speaker:

SPEAKER_IP=192.168.1.50   # adjust to your speaker's LAN IP
adb -s emulator-5554 shell ping -c 3 "$SPEAKER_IP"

3.3 Start frida-server

adb -s emulator-5554 push scripts/android/frida-server /data/local/tmp/frida-server
adb -s emulator-5554 shell su 0 chmod 755 /data/local/tmp/frida-server
adb -s emulator-5554 shell su 0 "nohup /data/local/tmp/frida-server > /dev/null 2>&1 &"
sleep 2
echo "frida-server running"

3.4 Configure config.js

start-mitm-session.sh patches scripts/android/frida/config.js automatically with the current Mac IP and mitmproxy cert. No manual step needed.

3.5 Launch App with SSL Unpinning

scripts/android/frida-venv/bin/frida \
  -U \
  -f com.bose.soundtouch \
  -l scripts/android/frida/config.js \
  -l scripts/android/frida/native-connect-hook.js \
  -l scripts/android/frida/android/android-system-certificate-injection.js \
  -l scripts/android/frida/android/android-proxy-override.js \
  -l scripts/android/frida/android/android-certificate-unpinning.js \
  -l scripts/android/frida/android/android-certificate-unpinning-fallback.js

native-connect-hook.js is required — the Bose app uses native networking that bypasses Java proxy settings.

Expected Frida output:

== System certificate trust injected ==
== Proxy system configuration overridden to <IP>:8080 ==
== Proxy configuration overridden to <IP>:8080 ==
== Certificate unpinning completed ==
== Unpinning fallback auto-patcher installed ==

Phase 4 — Pair Speaker via App

The Bose app should now be running in the emulator with all traffic going through mitmproxy.

4.1 Inspect UI to Find Interactive Elements

# Dump current screen
adb -s emulator-5554 shell uiautomator dump /sdcard/ui.xml
adb -s emulator-5554 pull /sdcard/ui.xml /tmp/ui.xml

# Helper: list all clickable elements with their text + bounds
grep -o 'text="[^"]*" resource-id="[^"]*" \.\.\. clickable="true"[^/]*' /tmp/ui.xml \
  || python3 -c "
import xml.etree.ElementTree as ET
tree = ET.parse('/tmp/ui.xml')
for n in tree.iter('node'):
    if n.get('clickable') == 'true' and n.get('text'):
        print(n.get('bounds'), n.get('resource-id'), repr(n.get('text')))
"

4.2 Navigate Setup Flow (adb)

# Take a screenshot at any point to see current state
adb -s emulator-5554 shell screencap /sdcard/screen.png
adb -s emulator-5554 pull /sdcard/screen.png /tmp/screen.png
open /tmp/screen.png

# Tap by resource-id (find IDs from ui.xml dump)
adb -s emulator-5554 shell uiautomator runtest ... # or input tap

# Tap by screen coordinates
adb -s emulator-5554 shell input tap X Y

# Type into the focused field
adb -s emulator-5554 shell input text "your@email.com"

# Press Enter / Next
adb -s emulator-5554 shell input keyevent 66

4.3 Expected Setup Steps in the App

Follow the on-screen flow; mitmproxy captures everything automatically.

  1. Sign in — enter email + password → triggers POST /streaming/account/login
  2. Add speaker — tap “Set Up a New Speaker” or equivalent
  3. App discovers speaker via mDNS on LAN (no BLE required)
  4. Wi-Fi already configured — app skips the Wi-Fi step since speaker is online
  5. Name speaker — type a name → WebSocket name message to speaker port 8080
  6. Pairing — app sends setMargeAccount WebSocket to speaker → speaker POSTs to streaming.bose.com/{accountId}/devices

All cloud requests (steps 1, 6) will appear in mitmweb at http://127.0.0.1:8081.


Phase 5 — Save & Inspect Recording

# Stop mitmweb (Ctrl-C in its terminal) — file is already written continuously

# Inspect offline in the web UI
mitmweb -r "$CAPTURE"

# Filter to streaming.bose.com only
mitmdump -r "$CAPTURE" --flow-filter '~u streaming.bose.com' -w bose-cloud-only.mitm

# Quick text summary
mitmdump -r "$CAPTURE" --flow-filter '~u streaming.bose.com' 2>/dev/null \
  | grep -E "POST|GET" | head -30

Convert to .http files (IntelliJ-compatible)

Use scripts/convert_mitm_script.py to extract each flow as a .http file, organized by path:

NAME=$(basename "$CAPTURE" .mitm)
OUT="scripts/android/mitm/${NAME}"

/Applications/mitmproxy.app/Contents/MacOS/mitmdump \
  -n -r "$CAPTURE" \
  -s scripts/convert_mitm_script.py \
  --set out_dir="${OUT}"

Output lands in scripts/android/mitm/<name>/mirror/ as numbered .http files plus *-websocket/ subdirectories for WebSocket frames. The directory is gitignored.


Cleanup

# Remove proxy from emulator
adb -s emulator-5554 shell settings delete global http_proxy

# Kill emulator
adb -s emulator-5554 emu kill

Frida artefacts live in scripts/android/ (gitignored) and persist between sessions — no cleanup needed unless you want to force a fresh setup.


Troubleshooting

Symptom Likely cause Fix
curl http://192.0.2.1 times out Mac not on speaker AP Re-run networksetup -setairportnetwork; speaker AP gateway is 192.0.2.1
addWirelessProfile returns error Speaker not in AP mode or wrong IP Confirm speaker AP is active; use http://192.0.2.1:8090/addWirelessProfile
Speaker not found via mDNS Speaker still on AP (not home LAN yet) Wait ~30 s, retry; check router DHCP leases
Emulator can’t ping speaker Different subnet or emulator proxy misconfigured adb shell ping the Mac IP first; check proxy setting
App shows “No speakers found” App not detecting mDNS Ensure emulator is on same /24 as speaker; disable emulator Wi-Fi and re-enable
No traffic in mitmweb Frida not running or cert mismatch Check Frida output for == Certificate unpinning ==; verify issuer in config.js

See Also


Session Trace (2026-05-02, ST10)

Raw log of the first interactive run. To be cleaned up into the runbook above.

Setup

Factory Reset (ST10)

Pre-reset note

Wi-Fi Provisioning (AP mode)

Wi-Fi Provisioning Result

MITM Session Start

Capture Working